diff --git a/packages/subprocess/subprocess-local/README.i18n.yaml b/packages/subprocess/subprocess-local/README.i18n.yaml
index 76150e77d5..972689a0ea 100644
--- a/packages/subprocess/subprocess-local/README.i18n.yaml
+++ b/packages/subprocess/subprocess-local/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md
-README.md: dd9edbc99578f5411fad993cf88f93429dc9cab2
-README.zh.md: 7bb43f7bbce5ef6b128edc9ef2104d35a5887845
+README.md: b0c96119d6e390afb30a6339d45d4ce186cd67de
+README.zh.md: 817e6ae149ea8d6c6062c7262ec374e259e530ef
diff --git a/packages/subprocess/subprocess-local/README.md b/packages/subprocess/subprocess-local/README.md
index dd9edbc995..b0c96119d6 100644
--- a/packages/subprocess/subprocess-local/README.md
+++ b/packages/subprocess/subprocess-local/README.md
@@ -25,7 +25,7 @@ Mount `dsh-subprocess-local` in any composition that runs child processes on the
## Use this package
-Mount the provider beside its consumers and start processes exactly as the subprocess service specifies; this package decides only how those processes run on the host.
+Mount the provider beside its consumers and start processes exactly as the subprocess service specifies; this package decides only how those processes run on the host. On Windows, non-terminal children start with their console windows hidden so background commands do not take focus.
### Mounting the provider
diff --git a/packages/subprocess/subprocess-local/README.zh.md b/packages/subprocess/subprocess-local/README.zh.md
index 7bb43f7bbc..817e6ae149 100644
--- a/packages/subprocess/subprocess-local/README.zh.md
+++ b/packages/subprocess/subprocess-local/README.zh.md
@@ -25,7 +25,7 @@ kind: "package-reference"
## 使用本包
-把提供方与它的消费方挂载在同一组合中,并完全按子进程服务的规定启动进程;本包只决定这些进程在宿主机上如何运行。
+把提供方与它的消费方挂载在同一组合中,并完全按子进程服务的规定启动进程;本包只决定这些进程在宿主机上如何运行。在 Windows 上,非终端子进程会隐藏其控制台窗口,因此后台命令不会抢占焦点。
### 挂载提供方
diff --git a/packages/subprocess/subprocess-local/src/spawn.ts b/packages/subprocess/subprocess-local/src/spawn.ts
index 433ba01791..bec0a0c2d2 100644
--- a/packages/subprocess/subprocess-local/src/spawn.ts
+++ b/packages/subprocess/subprocess-local/src/spawn.ts
@@ -7,7 +7,7 @@
* @module dsh-subprocess-local/spawn
*/
-import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
+import { type ChildProcess, type SpawnOptions, spawn, spawnSync } from 'node:child_process'
import type { Readable } from 'node:stream'
import { randomBytes } from 'node:crypto'
import { closeSync, mkdtempSync, openSync, unlinkSync, writeSync } from 'node:fs'
@@ -26,6 +26,12 @@ import type {
} from '@deepseek-ai/dsh-subprocess'
import { linuxProcessGroupHasLiveMembers } from './process-inspector.ts'
+type SpawnProcess = (
+ program: string,
+ args: readonly string[],
+ options: SpawnOptions,
+) => ChildProcess
+
/**
* Build a child environment: explicit caller entries override the scrubbed
* parent base using the target platform's environment-key semantics. A string
@@ -48,6 +54,8 @@ export function childEnv(extra?: Readonly): NodeJS.ProcessEnv
/** Injectable knobs so tests can exercise spill and platform behavior deterministically. */
export interface SpawnInternals {
+ /** Node process spawner used by focused option tests. */
+ spawn?: SpawnProcess
/** Directory for spill files (defaults to the OS temp dir). */
spillDir?: string
/** Windows tree-termination runner (defaults to `taskkill /PID /T /F`). */
@@ -329,6 +337,7 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
}
const spillDir = internals.spillDir ?? privateSpillDir()
const platform = internals.platform ?? process.platform
+ const spawnProcess = internals.spawn ?? spawn
const taskkill = internals.taskkill ?? taskkillProcessTree
const linuxGroupHasLiveMembers = internals.linuxProcessGroupHasLiveMembers ?? linuxProcessGroupHasLiveMembers
@@ -347,7 +356,7 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
const stdinMode = spec.stdio.stdin
const env = childEnv(spec.env)
- const child = spawn(program, args, {
+ const child = spawnProcess(program, args, {
cwd: spec.cwd,
env,
stdio: [
@@ -358,6 +367,7 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
// `detached` gives teardown a tree root on POSIX (its own process group);
// Windows terminates by root pid through taskkill /T instead.
detached: platform !== 'win32',
+ windowsHide: platform === 'win32',
})
const collectStream = (mode: SubprocessOutputMode, stream: Readable | null, label: string): OutputCollector | undefined => {
diff --git a/packages/subprocess/subprocess-local/tests/spawn.spec.ts b/packages/subprocess/subprocess-local/tests/spawn.spec.ts
index 1f21dfaaa1..038fe1c392 100644
--- a/packages/subprocess/subprocess-local/tests/spawn.spec.ts
+++ b/packages/subprocess/subprocess-local/tests/spawn.spec.ts
@@ -1,3 +1,4 @@
+import { spawn as nodeSpawn } from 'node:child_process'
import { mkdtempSync, readFileSync, statSync, unlinkSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
@@ -631,6 +632,30 @@ describe('stdio dispositions', () => {
})
describe('windows tree semantics (injected platform)', () => {
+ it('hides the child window without changing output, exit, stdio, or tree-root options', async () => {
+ let options: Parameters[2]
+ const result = await finish(spawnSubprocess(spec('echo hello'), {
+ spillDir,
+ platform: 'win32',
+ spawn: (program, args, spawnOptions) => {
+ options = spawnOptions
+ return nodeSpawn(program, args, spawnOptions)
+ },
+ }))
+
+ expect(options!).toMatchObject({
+ windowsHide: true,
+ detached: false,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ })
+ expect(result).toMatchObject({
+ exitCode: 0,
+ signal: null,
+ stdout: { text: 'hello\n', truncated: false },
+ stderr: { text: '', truncated: false },
+ })
+ })
+
it('host-exit termination routes through taskkill immediately', async () => {
const killed: number[] = []
const running = spawnSubprocess(spec('exec sleep 60', { graceMs: 60_000 }), {