diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 35fd5dc088..ec6a2b49f6 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3841,7 +3841,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash-persistent` - `inject`: `tools` · `terminals` · `workingDirectory` -- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts) +- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent Bash tool. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index a5161531c2..4f56ac804d 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3843,7 +3843,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-bash-persistent` - `inject`: `tools` · `terminals` · `workingDirectory` -- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts) +- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts) ```ts config-catalog /** Configuration for the persistent Bash tool. */ diff --git a/packages/shell/tool-bash-persistent/README.i18n.yaml b/packages/shell/tool-bash-persistent/README.i18n.yaml index ab0611f17b..cc6225da8c 100644 --- a/packages/shell/tool-bash-persistent/README.i18n.yaml +++ b/packages/shell/tool-bash-persistent/README.i18n.yaml @@ -39,8 +39,8 @@ en: 857e036f41c913ac zh: e9f383f7ee384c59 /deepseek-ai-dsh-tool-bash-persistent/understand-the-implementation/command-flow: - en: 3b7bf7c20b266c31 - zh: 85a325ef65fa7ac7 + en: c511f75009ba3743 + zh: b3b722e00b2910b1 /deepseek-ai-dsh-tool-bash-persistent/further-exploration: en: 42c387ccaab7e64c zh: e58ec4ea60cf7711 @@ -72,8 +72,8 @@ en: ba6a48888c3361e5 zh: 3a8f648053555dc9 /deepseek-ai-dsh-tool-bash-persistent/known-limitations-and-deferred-work: - en: dc5a908f2b2da040 - zh: 3afceaabcf324771 + en: b12111850d752f0b + zh: 92b855387ec187ff /deepseek-ai-dsh-tool-bash-persistent/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/shell/tool-bash-persistent/README.md b/packages/shell/tool-bash-persistent/README.md index 22ea00bed1..4bbea6bf1a 100644 --- a/packages/shell/tool-bash-persistent/README.md +++ b/packages/shell/tool-bash-persistent/README.md @@ -85,7 +85,7 @@ This section explains the design decisions behind the tool and points at the cod ### Command flow -A first command spawns the shell through `ctx.terminals.spawn`, disables input echo (`stty -echo`), and waits for readiness. Each command is then wrapped into one physical line — a printf of the start marker, the command body escaped with `$'…'`, and a printf of the end marker plus `$?` — so embedded newlines cannot leak terminal prompts into the result. The tool polls the scrollback in 1,000-line pages until the end marker appears, extracts the span, and renders it with any status marker. A timeout aborts the deadline, captures the partial output, and resets the shell. +A first command spawns the shell through `ctx.terminals.spawn`, disables input echo and history expansion (`stty -echo; set +H`), and waits for readiness. Each command is then wrapped into one physical line — a printf of the start marker, the command body escaped with `$'…'`, and a printf of the end marker plus `$?` — so embedded newlines cannot leak terminal prompts into the result. With history expansion off, `!` sequences reach bash unchanged instead of being rewritten or rejecting the whole line. The tool polls the scrollback in 1,000-line pages until the end marker appears, extracts the span, and renders it with any status marker. A timeout aborts the deadline, captures the partial output, and resets the shell. @@ -148,6 +148,7 @@ These limits define when the tool is a poor fit or needs special care. They are - **An interactive foreground child returns early with partial output only where the subprocess provider proves its stdin wait** — elsewhere the call runs to `timeoutMs`. - **Explicit `exit` and timeout discard shell state** — cancellation also resets and discards the result, even when a complete status marker is already observable; the next call starts a fresh shell. - **Environment facts such as network access and package mirrors belong in the configured `description`** — not this package's default. +- **Raw control characters other than newline and carriage return reach readline as typed keys** — a literal TAB, ESC, or DEL in the command is consumed as an editing key, so content that needs them, such as a Makefile heredoc, should be written with a file tool or produced by `printf` escapes. ### Dev Note diff --git a/packages/shell/tool-bash-persistent/README.zh.md b/packages/shell/tool-bash-persistent/README.zh.md index c51bb4a00f..6b4877e177 100644 --- a/packages/shell/tool-bash-persistent/README.zh.md +++ b/packages/shell/tool-bash-persistent/README.zh.md @@ -85,7 +85,7 @@ kind: "package-reference" ### 命令流程 -首条命令通过 `ctx.terminals.spawn` 生成 shell,禁用输入回显(`stty -echo`),并等待就绪。随后每条命令都包装成一行物理文本——printf 起始标记、用 `$'…'` 转义的命令体、printf 结束标记加 `$?`——因此内嵌换行无法把终端提示符泄漏进结果。工具以 1,000 行一页轮询 scrollback,直到出现结束标记,提取区间并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。 +首条命令通过 `ctx.terminals.spawn` 生成 shell,禁用输入回显和历史展开(`stty -echo; set +H`),并等待就绪。随后每条命令都包装成一行物理文本——printf 起始标记、用 `$'…'` 转义的命令体、printf 结束标记加 `$?`——因此内嵌换行无法把终端提示符泄漏进结果。关闭历史展开后,`!` 序列原样到达 bash,不会被改写,也不会使整行被拒绝。工具以 1,000 行一页轮询 scrollback,直到出现结束标记,提取区间并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。 @@ -148,6 +148,7 @@ kind: "package-reference" - **交互式前台子进程只在子进程提供方证明其 stdin 等待时才提前返回部分输出**——否则调用一直运行到 `timeoutMs`。 - **显式 `exit` 与超时会丢弃 shell 状态**——取消同样重置并丢弃结果,即使完整状态标记已经可观察;下一次调用启动全新 shell。 - **网络访问与包镜像等环境事实属于配置的 `description`**——而不是本包的默认描述。 +- **除换行与回车外,原始控制字符会作为按键到达 readline**——命令中的字面 TAB、ESC 或 DEL 会被当作编辑按键消耗,因此需要这些字符的内容(例如 Makefile heredoc)应通过文件工具写入,或用 `printf` 转义生成。 ### 开发备注 diff --git a/packages/shell/tool-bash-persistent/src/index.ts b/packages/shell/tool-bash-persistent/src/index.ts index 4f5d65736e..94de7ccf09 100644 --- a/packages/shell/tool-bash-persistent/src/index.ts +++ b/packages/shell/tool-bash-persistent/src/index.ts @@ -267,10 +267,12 @@ function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShell live.delete(owner) }, 'tool-bash-persistent owner cache cleanup') } - // Echo suppression only: the prompt stays the backend's own, so the - // backend's prompt-based readiness detection keeps working. + // Suppress input echo and history expansion, which would otherwise + // rewrite `!` sequences or discard the whole wrapper line before bash + // parses it. The prompt stays the backend's own, so the backend's + // prompt-based readiness detection keeps working. const setup = ctx.terminals.startSend(owner, spawned.sessionId, { - text: 'stty -echo', + text: 'stty -echo; set +H', submit: true, signal: combinedSignal, }) diff --git a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts index d69bd9d9ba..16e9124d23 100644 --- a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts @@ -165,6 +165,14 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => { )) expect(heredoc).toBe('alpha\nbeta\n[Command finished with exit code 0]') + // Interactive bash applies history expansion to typed input before parsing; + // it must neither rewrite `!` sequences nor drop the whole wrapper line. + const history = text(await execute( + 'history-expansion', + "echo '' > page.html; cat page.html; echo 'it'; echo hi!!", + )) + expect(history).toBe('\nit\nhi!!\n[Command finished with exit code 0]') + const pipeline = text(await execute( 'pipeline', '{ sleep 0.1; printf "delayed\\n"; } | cat',