Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
"./run": "./src/run/index.ts",
"./server-process": "./src/server-process.ts"
},
"imports": {
"#hidden-console": {
"bun": "./src/services/hidden-console.bun.ts",
"node": "./src/services/hidden-console.node.ts",
"default": "./src/services/hidden-console.bun.ts"
}
},
"scripts": {
"build": "bun run script/build.ts",
"build:node": "bun run script/build-node.ts",
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/server-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { PersistentPty } from "@opencode/schema/persistent-pty"
import { HttpServer } from "effect/unstable/http"
import { Env } from "./env"
import { ServiceConfig } from "./services/service-config"
import { HiddenConsole } from "./services/hidden-console"
import { RetainedImage } from "./services/retained-image"
import { ServiceRegistration } from "./services/service-registration"
import { WebUi } from "./services/web-ui"
Expand Down Expand Up @@ -52,7 +53,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
Effect.mapError(() => new Error("Invalid PTY restart handoff")),
)
const global = yield* Global.Service
if (options.mode === "service") yield* Effect.sync(() => process.chdir(global.home))
if (options.mode === "service") {
yield* Effect.sync(() => process.chdir(global.home))
const status = yield* Effect.sync(() => HiddenConsole.attach())
if (status !== undefined) yield* Effect.logWarning("could not attach a hidden console", { status })
}
return yield* Effect.scoped(
Effect.gen(function* () {
const foreground = options.mode === "default"
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/services/hidden-console.bun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { dlopen, ptr } from "bun:ffi"
import os from "node:os"

// STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
const standardHandles = [-10, -11, -12]

/**
* A detached service starts without a console, so Windows opens a visible window for every
* console child spawned without CREATE_NO_WINDOW (plugin code, hooks, npm's git). Allocating a
* windowless console gives those children one to inherit instead.
*
* Returns the failing HRESULT, or undefined when the console was attached or is unsupported.
*/
export function attach() {
if (process.platform !== "win32") return
// AllocConsoleWithOptions requires Windows 11 24H2 / Server 2025 (build 26100).
if (Number(os.release().split(".")[2]) < 26100) return
const kernel32 = dlopen("kernel32.dll", {
AllocConsoleWithOptions: { args: ["ptr", "ptr"], returns: "i32" },
GetStdHandle: { args: ["i32"], returns: "i64" },
SetStdHandle: { args: ["i32", "i64"], returns: "i32" },
})
// Allocation may point the standard handles at the new console; keep the pipes the spawner reads.
const handles = standardHandles.map((id) => [id, kernel32.symbols.GetStdHandle(id)] as const)
// ALLOC_CONSOLE_OPTIONS { mode: ALLOC_CONSOLE_MODE_NO_WINDOW, useShowWindow: FALSE, showWindow: 0 }
const status = kernel32.symbols.AllocConsoleWithOptions(ptr(new Int32Array([2, 0, 0])), null)
handles.forEach(([id, handle]) => kernel32.symbols.SetStdHandle(id, handle))
kernel32.close()
return status === 0 ? undefined : status
}
4 changes: 4 additions & 0 deletions packages/cli/src/services/hidden-console.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// The Node build has no console binding yet, so its service keeps the console-less behavior.
export function attach(): number | undefined {
return undefined
}
1 change: 1 addition & 0 deletions packages/cli/src/services/hidden-console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as HiddenConsole from "#hidden-console"
Loading