From 0125f9019e6171d05842aa90beab85a7905ad66d Mon Sep 17 00:00:00 2001
From: 07akioni <07akioni2@gmail.com>
Date: Mon, 7 Sep 2026 13:08:11 +0800
Subject: [PATCH] revert: remove pwsh changes from desktop PR
---
docs/config-catalog.i18n.yaml | 4 +-
docs/config-catalog.md | 2 +-
docs/config-catalog.zh.md | 2 +-
.../tool-pwsh-persistent/README.i18n.yaml | 4 +-
packages/shell/tool-pwsh-persistent/README.md | 6 +--
.../shell/tool-pwsh-persistent/README.zh.md | 6 +--
.../shell/tool-pwsh-persistent/src/index.ts | 22 +++++++++--
.../tests/loader-composition.spec.ts | 5 +--
.../tool-pwsh-persistent/tests/tools.spec.ts | 37 ++++++++++++++-----
9 files changed, 59 insertions(+), 29 deletions(-)
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 720265389c..81fb2e6ee1 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.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 docs/config-catalog.md
-config-catalog.md: ef3a549df0a06d0c2e46a25c077b758e15cb74c3
-config-catalog.zh.md: 6c1a190d4cfc1a7a352d3b5d11333dc2543a632d
+config-catalog.md: 51bca4bda1f2b1b69fc8d6cbb04d545b9fd78b20
+config-catalog.zh.md: 4572fcd8a9a6e5bbbbccdfb3eb7e64e9465e81c0
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index ef3a549df0..51bca4bda1 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -2830,7 +2830,7 @@ export interface Config {
}
```
-Source: [`packages/shell/tool-pwsh-persistent/src/index.ts:456`](../packages/shell/tool-pwsh-persistent/src/index.ts)
+Source: [`packages/shell/tool-pwsh-persistent/src/index.ts:472`](../packages/shell/tool-pwsh-persistent/src/index.ts)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 6c1a190d4c..4572fcd8a9 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -2832,7 +2832,7 @@ export interface Config {
}
```
-来源:[`packages/shell/tool-pwsh-persistent/src/index.ts:456`](../packages/shell/tool-pwsh-persistent/src/index.ts)
+来源:[`packages/shell/tool-pwsh-persistent/src/index.ts:472`](../packages/shell/tool-pwsh-persistent/src/index.ts)
diff --git a/packages/shell/tool-pwsh-persistent/README.i18n.yaml b/packages/shell/tool-pwsh-persistent/README.i18n.yaml
index a3f202e0ce..927b854832 100644
--- a/packages/shell/tool-pwsh-persistent/README.i18n.yaml
+++ b/packages/shell/tool-pwsh-persistent/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/shell/tool-pwsh-persistent/README.md
-README.md: ed5e0c340a0a9a0af53d4c9d80e2516e1a2568a8
-README.zh.md: 28c700e7e5f693df48b433737d49c3f95e4d7305
+README.md: a23bd1ca677d00edd83d9f07f8520d12ce2b8e6c
+README.zh.md: 1eb2ae252c2baeca7dc6cb5dfdaca18e10abd93b
diff --git a/packages/shell/tool-pwsh-persistent/README.md b/packages/shell/tool-pwsh-persistent/README.md
index ed5e0c340a..a23bd1ca67 100644
--- a/packages/shell/tool-pwsh-persistent/README.md
+++ b/packages/shell/tool-pwsh-persistent/README.md
@@ -73,7 +73,7 @@ This section explains the design decisions behind the tool and points at the cod
### Design philosophy
- **A deliberate twin of `dsh-tool-bash-persistent`.** The session registry, polling loop, and reset contract mirror the persistent bash tool by design ([pwsh persistent PTY Agent Note](../../../.agents/notes/archived/architecture/2026-08-11-pwsh-persistent-pty.md)).
-- **Prompt-function readiness.** The pwsh terminal backend owns prompt bootstrap and publishes the session only after its controlled `prompt` function is ready. The tool uses that existing prompt instead of submitting a second definition. Its BEL-terminated OSC marker and printable `dsh> ` tail provide the fast readiness path; the silence tier still settles a completed command when the host cannot accept that prompt evidence. A model redefinition of `prompt` also degrades readiness to the silence tier.
+- **Prompt-function readiness.** The tool installs its own `prompt` function that prints a BEL-terminated OSC marker plus a printable prompt; the OSC marker carries the last exit code and the printable prompt settles every command, so a model redefinition of `prompt` degrades readiness to the silence tier.
- **PSReadLine echo stripped by anchoring.** PowerShell renders submitted input back into the stream; the marker-anchored extraction and a wrapper-source strip remove the echo, and a wrapper that wraps across the terminal width may leave a partial echo in partial-output results.
- **Reset, never repair.** Any uncertain state — an explicit `exit`, a timeout, a send failure, an abort — closes the shell and starts the next call fresh.
@@ -81,12 +81,12 @@ This section explains the design decisions behind the tool and points at the cod
| File | Role |
|---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: shell registry, command wrapping, scrollback polling, extraction and rendering |
+| [`src/index.ts`](src/index.ts) | Plugin entry: shell registry, prompt setup, command wrapping, scrollback polling, extraction and rendering |
| — | No runtime invariant companion is published; the adapter's private owner-to-shell cache has no observable event or data relation. Lifecycle tests prove its cleanup without adding a public API solely for an invariant. |
### Command flow
-A first command spawns the shell through `ctx.terminals.spawn` and receives it only after the selected terminal backend has completed prompt bootstrap. Each command is wrapped into one physical line — `Write-Output` of the start marker, the body escaped with backtick escapes into a double-quoted string, and `Write-Output` of the end marker plus the exit status — so PSReadLine's echo of a wrapped line cannot fabricate completion. The tool polls the scrollback in 1,000-line pages until the end marker or a completed prompt appears, extracts the span, strips the echoed wrapper and prompts, 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`, installs the `prompt` override, and waits for readiness. Each command is wrapped into one physical line — `Write-Output` of the start marker, the body escaped with backtick escapes into a double-quoted string, and `Write-Output` of the end marker plus the exit status — so PSReadLine's echo of a wrapped line cannot fabricate completion. The tool polls the scrollback in 1,000-line pages until the end marker or a completed prompt appears, extracts the span, strips the echoed wrapper and prompts, and renders it with any status marker. A timeout aborts the deadline, captures the partial output, and resets the shell.
diff --git a/packages/shell/tool-pwsh-persistent/README.zh.md b/packages/shell/tool-pwsh-persistent/README.zh.md
index 28c700e7e5..1eb2ae252c 100644
--- a/packages/shell/tool-pwsh-persistent/README.zh.md
+++ b/packages/shell/tool-pwsh-persistent/README.zh.md
@@ -73,7 +73,7 @@ kind: "package-reference"
### 设计理念
- **`dsh-tool-bash-persistent` 的刻意孪生。** 会话注册表、轮询循环与重置约定按设计镜像持久 bash 工具([pwsh 持久 PTY Agent Note](../../../.agents/notes/archived/architecture/2026-08-11-pwsh-persistent-pty.md))。
-- **prompt 函数就绪。** pwsh terminal 后端负责 prompt 引导,并且只在其受控 `prompt` 函数就绪后发布会话。工具直接使用现有 prompt,不再提交第二次定义。BEL 结尾的 OSC 标记与可打印的 `dsh> ` 尾部提供快速就绪路径;当宿主无法接受该 prompt 证据时,静默层级仍会结算已完成的命令。模型重定义 `prompt` 也会把就绪降级到静默层级。
+- **prompt 函数就绪。** 工具安装自己的 `prompt` 函数,打印 BEL 结尾的 OSC 标记加可打印提示词;OSC 标记携带最后的退出码,可打印提示词让每条命令都能结算,因此模型重定义 `prompt` 会把就绪降级到静默层级。
- **PSReadLine 回显靠锚定剥离。** PowerShell 会把提交的输入渲染回流中;标记锚定提取与包装源码剥离移除回显,而跨终端宽度换行的包装可能在部分输出结果中留下部分回显。
- **重置,而非修复。** 任何不确定状态——显式 `exit`、超时、发送失败、中止——都会关闭 shell 并让下一次调用从全新状态开始。
@@ -81,12 +81,12 @@ kind: "package-reference"
| 文件 | 职责 |
|---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:shell 注册表、命令包装、scrollback 轮询、提取与渲染 |
+| [`src/index.ts`](src/index.ts) | 插件入口:shell 注册表、prompt 设置、命令包装、scrollback 轮询、提取与渲染 |
| — | 不发布运行时不变式伴生入口;shell 复用可通过工具执行观察。 |
### 命令流程
-首条命令通过 `ctx.terminals.spawn` 生成 shell,并且只在所选 terminal 后端完成 prompt 引导后取得会话。随后每条命令都包装成一行物理文本——`Write-Output` 起始标记、用反引号转义进双引号字符串的命令体、`Write-Output` 结束标记加退出状态——因此 PSReadLine 对换行包装的回显无法伪造完成。工具以 1,000 行一页轮询 scrollback,直到出现结束标记或完成的提示词,提取区间、剥离回显的包装与提示词,并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。
+首条命令通过 `ctx.terminals.spawn` 生成 shell,安装 `prompt` 覆盖,并等待就绪。随后每条命令都包装成一行物理文本——`Write-Output` 起始标记、用反引号转义进双引号字符串的命令体、`Write-Output` 结束标记加退出状态——因此 PSReadLine 对换行包装的回显无法伪造完成。工具以 1,000 行一页轮询 scrollback,直到出现结束标记或完成的提示词,提取区间、剥离回显的包装与提示词,并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。
diff --git a/packages/shell/tool-pwsh-persistent/src/index.ts b/packages/shell/tool-pwsh-persistent/src/index.ts
index 5e7ea23d62..f0a575363e 100644
--- a/packages/shell/tool-pwsh-persistent/src/index.ts
+++ b/packages/shell/tool-pwsh-persistent/src/index.ts
@@ -17,7 +17,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
const TRUNCATED_MESSAGE = 'To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with Select-String in order to find the line numbers of what you are looking for.'
const LOST_PREFIX_MESSAGE = 'The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.\n'
const SHELL_RESET_MESSAGE = 'The persistent pwsh shell was reset; the next pwsh call starts from the workspace with a fresh current directory and environment.'
-const SHELL_PROMPT = 'dsh> '
+const SHELL_PROMPT = '__DSH_PERSISTENT_PWSH_PROMPT__ '
const TIMEOUT_CODE = 'PERSISTENT_PWSH_TIMEOUT'
// One page is enough to find a just-emitted completion marker; the full
// scrollback is assembled only when a command settles or needs partial output.
@@ -252,6 +252,15 @@ async function respondToSessionExit(
].filter(part => part.length > 0).join('\n')
}
+/**
+ * The pwsh prompt function that overrides the backend bootstrap value with
+ * this tool's own prompt. `[char]27`/`[char]7` build the OSC bytes at runtime
+ * because raw ESC characters in submitted input are unreliable under
+ * PSReadLine.
+ */
+const PWSH_PROMPT_SETUP =
+ "function prompt { [Console]::Write([char]27 + ']133;D;' + [int]$LASTEXITCODE + [char]7); '" + SHELL_PROMPT + "' }"
+
function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShells {
const pending = new WeakMap>()
const live = new Map()
@@ -298,8 +307,15 @@ function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShell
live.delete(owner)
}, 'tool-pwsh-persistent owner cache cleanup')
}
- // The selected terminal backend owns bootstrap and publishes only a
- // ready session.
+ const setup = ctx.terminals.startSend(owner, spawned.sessionId, {
+ text: PWSH_PROMPT_SETUP,
+ submit: true,
+ signal: combinedSignal,
+ })
+ const result = await setup.done
+ if (result.sessionStatus.kind === 'exited' || result.waitReason === 'timeout') {
+ throw new Error('persistent pwsh shell did not accept initialization')
+ }
return spawned.sessionId
} catch (error: unknown) {
await reset(owner, 'persistent pwsh initialization failed')
diff --git a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts
index 9a98a15d0d..74bc8c0514 100644
--- a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts
+++ b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts
@@ -150,10 +150,7 @@ describe.skipIf(!hasPwsh)('persistent pwsh through a real cordis.yml Loader comp
})
expect(context.tools.schemas().map(schema => schema.name)).toEqual(['pwsh'])
- expect(text(await execute(
- 'state',
- '$env:KEEP = "loader"; New-Item -ItemType Directory -Force -Path nested | Out-Null; Set-Location nested',
- ))).toBe('')
+ await execute('state', '$env:KEEP = "loader"; New-Item -ItemType Directory -Force -Path nested | Out-Null; Set-Location nested')
const observed = text(await execute('observe', 'Write-Output "cwd=$PWD keep=$env:KEEP"'))
expect(observed).toContain(`cwd=${join(root, 'nested')} keep=loader`)
expect(observed).not.toContain('DSH_PERSISTENT_PWSH')
diff --git a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
index 9f15ddd6fc..4190bdb604 100644
--- a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
+++ b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
@@ -91,6 +91,8 @@ type StubMode =
| 'torn-status'
| 'finish-torn-status'
| 'end-only'
+ | 'init-exit'
+ | 'init-timeout'
| 'spawn-error'
| 'send-error'
| 'prompt-after-idle'
@@ -105,14 +107,13 @@ const START_PATTERN = /__DSH_PERSISTENT_PWSH_START_[^_]+(?:-[^_]+)*__/
const END_PATTERN = /__DSH_PERSISTENT_PWSH_END_[^:]+:/
class StubTerminalSession implements TerminalBackendSession {
- readonly motd = 'dsh> '
+ readonly motd = '__DSH_PERSISTENT_PWSH_PROMPT__ '
readonly pid = 123
statusValue: TerminalSessionStatus = { kind: 'running' }
scrollback = this.motd
closed: string[] = []
mode: StubMode
sends = 0
- requests: TerminalSendRequest[] = []
pendingText = ''
historyTruncated = false
throwOnSend = false
@@ -123,7 +124,16 @@ class StubTerminalSession implements TerminalBackendSession {
startSend(request: TerminalSendRequest): TerminalSendOperation {
this.sends += 1
- this.requests.push(request)
+ if (request.text.startsWith('function prompt')) {
+ if (this.mode === 'init-exit') {
+ this.statusValue = { kind: 'exited', exitCode: 1, signal: null }
+ return this.operation(Promise.resolve(this.result('', 'session_exit')))
+ }
+ if (this.mode === 'init-timeout') {
+ return this.operation(Promise.resolve(this.result('', 'timeout')))
+ }
+ return this.operation(Promise.resolve(this.result(this.motd, 'stdin_read')))
+ }
if (this.mode === 'send-error') throw new Error('stub send failed')
if (this.throwOnSend) throw new Error('PTY session has exited')
if (this.mode === 'wait-for-abort' || this.mode === 'end-on-abort') {
@@ -334,9 +344,7 @@ describe('tool-pwsh-persistent', () => {
expect(text(await call(ctx, owner, 'Write-Output one'))).toBe('hello from stub')
expect(text(await call(ctx, owner, 'Write-Output two'))).toBe('hello from stub')
expect(stub.sessions).toHaveLength(1)
- expect(stub.sessions[0]?.sends).toBe(2)
- expect(stub.sessions[0]?.requests[0]?.text).toContain('__DSH_PERSISTENT_PWSH_START_')
- expect(stub.sessions[0]?.requests[0]?.text).not.toContain('function prompt')
+ expect(stub.sessions[0]?.sends).toBe(3)
const ownerWithoutCwd = agent(ctx, undefined)
expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub')
@@ -358,7 +366,7 @@ describe('tool-pwsh-persistent', () => {
expect(result).not.toContain('Invoke-Expression')
})
- it('preserves command output that equals the controlled shell prompt', async () => {
+ it('preserves command output that equals the private shell prompt', async () => {
const { ctx, owner, stub } = await setup({ backendType: 'stub' })
await call(ctx, owner, 'warm up')
const session = stub.sessions[0]!
@@ -400,13 +408,13 @@ describe('tool-pwsh-persistent', () => {
session.mode = 'prompt-only'
const promptFallback = text(await call(ctx, owner, 'bad {'))
expect(promptFallback).toContain('pwsh: synt')
- expect(promptFallback).not.toContain(session.motd)
+ expect(promptFallback).not.toContain('DSH_PERSISTENT_PWSH_PROMPT')
session.mode = 'prompt-crlf'
session.scrollback = ''
const crlfPromptFallback = text(await call(ctx, owner, 'bad {'))
expect(crlfPromptFallback).toContain('pwsh: synt')
- expect(crlfPromptFallback).not.toContain(session.motd)
+ expect(crlfPromptFallback).not.toContain('DSH_PERSISTENT_PWSH_PROMPT')
session.mode = 'end-only'
session.scrollback = ''
@@ -501,7 +509,7 @@ describe('tool-pwsh-persistent', () => {
const result = text(await call(ctx, owner, 'bad {'))
expect(result).toContain('partial syntax output')
expect(result).toContain('pwsh: syntax error')
- expect(result).not.toContain(session.motd)
+ expect(result).not.toContain('DSH_PERSISTENT_PWSH_PROMPT')
expect(result).not.toContain('DSH_PERSISTENT_PWSH_START')
})
@@ -546,6 +554,15 @@ describe('tool-pwsh-persistent', () => {
},
)
+ it.each(['init-exit', 'init-timeout'] as const)(
+ 'fails initialization and closes the unusable shell for %s',
+ async (mode) => {
+ const { ctx, owner, stub } = await setup({ backendType: 'stub' }, mode)
+ expect((await call(ctx, owner, 'pwd')).isError).toBe(true)
+ expect(stub.sessions[0]?.closed).toContain('persistent pwsh initialization failed')
+ },
+ )
+
it('clears a failed spawn without trying to close an unpublished shell', async () => {
const { ctx, owner, stub } = await setup({ backendType: 'stub' }, 'spawn-error')
expect((await call(ctx, owner, 'pwd')).isError).toBe(true)