feat(pwsh): render pwsh calls as bash-shaped terminal cards in the Web UI
A completed pwsh foreground call now presents as the bash tool's terminal card with the parsed exit-status pill instead of a generic console fence, and the collapsed row classifies as the shell family (Pwsh title). The marker-to-exit-status parse moves from dsh-tool-bash's private render module into the @deepseek-ai/dsh-bash seam so both shell tools share one inverse of the marker contract (the bash-env precedent). The Web UI needs no per-tool code: the terminal-card bridge maps any card:'terminal' view. Coverage: dsh-bash owns the parse edge cases; the tool-pwsh presenter suite mirrors tool-bash's; the client row-model suite pins the Pwsh shell row; the new keyless pwsh-terminal web lane seeds an authored session, presents it through the real tool on replay, and pins the card golden. TUI is out of scope: the TUI package was removed (ed30088adb), so the Web surface is the only UI the gap affected; the roadmap proposal's stage 2 is updated accordingly.
This commit is contained in:
@@ -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/bash/bash/README.md
|
||||
README.md: d7bf746969f52000fe298b65b995b7c631d8001c
|
||||
README.zh.md: a7c0cac0bce2154362c822c213a44f3c507d541c
|
||||
README.md: beb6a1ab5aa1709033a562c482265be9e2256c52
|
||||
README.zh.md: 2c054f7109438b58c3adef733a27fac18a60009c
|
||||
|
||||
@@ -35,6 +35,8 @@ The per-session sandbox-mode override vocabulary (the `'sandbox/mode'` event, th
|
||||
|
||||
`stdin` and ordinary `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload and `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` values. `dshEnv` is a separate trusted overlay restricted by type to managed keys; the exported `DSH_ENV_PREFIX` is the single source for that namespace, its `DshEnvironmentKey` template type, executor scrubbing, registry validation, derived built-in names, and model guidance. Model bash uses the current snapshot collected by `ctx.bashEnv`. Implementations remove inherited managed keys, then merge `dshEnv` after ordinary `env`, so an omitted current fact cannot fall back to stale ambient state and an `env` entry cannot displace a managed value. The model-facing tool exposes none of these as parameters. All three remain optional on the resolved spec; absent means no input/overlay. See [the bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [the session environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
|
||||
|
||||
The exported `parseExitStatus` (with `ParsedExitStatus`) is the shared rendering contract half of the shell tools: the inverse of the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash`'s `renderResult` and `dsh-tool-pwsh`'s `renderPwshResult` append. Both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill; it lives on the seam so the two tools never drift on the marker contract.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `dsh-tool-bash`, which turns executor output and sandbox facts into guidance and retained tool-result tokens.
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
|
||||
`stdin` 与普通 `env` 由同进程插件(hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay;导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.bashEnv` 收集的当前快照。实现会移除继承的受管 key,再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选;缺失表示没有输入/overlay。详见 [bash-stdin-env Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
|
||||
|
||||
导出的 `parseExitStatus`(连同 `ParsedExitStatus`)是 shell 工具共享渲染契约的另一半:`dsh-tool-bash` 的 `renderResult` 与 `dsh-tool-pwsh` 的 `renderPwshResult` 追加的 `[exit code: N]`/`[killed by signal: X]` marker 的逆解析。两个工具的 `presentResult` 都用它把渲染文本拆成 terminal 卡的输出正文与其退出状态 pill;它放在 seam 上,两个工具便永远不会在 marker 契约上漂移。
|
||||
|
||||
## 模型体验
|
||||
|
||||
通过 `dsh-tool-bash` 间接影响;该工具会将执行器输出与沙箱事实转为指引和保留的工具结果 token。
|
||||
|
||||
@@ -22,6 +22,8 @@ export type {
|
||||
DshEnvironment,
|
||||
DshEnvironmentKey,
|
||||
} from './types.ts'
|
||||
export { parseExitStatus } from './render.ts'
|
||||
export type { ParsedExitStatus } from './render.ts'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
|
||||
42
packages/bash/bash/src/render.ts
Normal file
42
packages/bash/bash/src/render.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Shared rendering helpers for the shell tools (`dsh-tool-bash`,
|
||||
* `dsh-tool-pwsh`): the exit-status marker contract the tools' renderers
|
||||
* emit and the presentation layer parses back.
|
||||
* @module @deepseek-ai/dsh-bash/render
|
||||
*/
|
||||
|
||||
/**
|
||||
* The exit status recovered from a rendered result, with the output body that
|
||||
* status was split off from.
|
||||
*/
|
||||
export type ParsedExitStatus =
|
||||
& { body: string }
|
||||
& ({ exitCode: number } | { signal: string })
|
||||
|
||||
/**
|
||||
* Split a rendered shell-tool result string into its output body and the
|
||||
* structured exit status — the inverse of the `[exit code: N]` /
|
||||
* `[killed by signal: X]` markers the shell tools' renderers append. A killed
|
||||
* marker yields `signal`; otherwise a non-zero marker yields `exitCode`;
|
||||
* absent both means a clean exit 0.
|
||||
*
|
||||
* The consumed marker is removed from `body` because a terminal presentation
|
||||
* shows the exit status as its own pill: leaving the marker in the output
|
||||
* would render the exit twice. Other markers (timeout, sandbox denial) carry
|
||||
* facts no pill shows, so they stay in the body.
|
||||
*
|
||||
* Replay only retains the rendered content text, not the original
|
||||
* `BashRunResult`, so terminal presentation must recover the exit pill here.
|
||||
* Requiring a leading newline and the end of the string keeps ordinary output
|
||||
* that merely ends with marker-like text from matching unless the final line
|
||||
* is indistinguishable from a real marker.
|
||||
* @param text - rendered model-facing shell-tool result.
|
||||
* @returns the marker-free body plus the recovered terminal exit code or signal.
|
||||
*/
|
||||
export function parseExitStatus(text: string): ParsedExitStatus {
|
||||
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
|
||||
if (signal?.[1] !== undefined) return { body: text.slice(0, signal.index), signal: signal[1] }
|
||||
const exit = /\n\[exit code: (\d+)\]$/.exec(text)
|
||||
if (exit?.[1] !== undefined) return { body: text.slice(0, exit.index), exitCode: Number(exit[1]) }
|
||||
return { body: text, exitCode: 0 }
|
||||
}
|
||||
36
packages/bash/bash/tests/render.spec.ts
Normal file
36
packages/bash/bash/tests/render.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Shared exit-status parse contract: the inverse of the `[exit code: N]` /
|
||||
* `[killed by signal: X]` markers `dsh-tool-bash` and `dsh-tool-pwsh` append.
|
||||
* Both tools' presenter suites round-trip their own renderers through this
|
||||
* parse; this spec pins the parse's own edges (marker-like output, body
|
||||
* slicing) once, at the seam that owns it.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseExitStatus } from '../src/render.ts'
|
||||
|
||||
describe('parseExitStatus', () => {
|
||||
it('recovers a clean exit 0 with the body verbatim when no marker is present', () => {
|
||||
expect(parseExitStatus('hi\n\n')).toEqual({ body: 'hi\n\n', exitCode: 0 })
|
||||
expect(parseExitStatus('')).toEqual({ body: '', exitCode: 0 })
|
||||
})
|
||||
|
||||
it('recovers a non-zero exit and strips only its marker from the body', () => {
|
||||
expect(parseExitStatus('oops\n[exit code: 3]')).toEqual({ body: 'oops', exitCode: 3 })
|
||||
// The marker needs the leading newline and the end of the string, so a
|
||||
// clean result whose output merely ENDS in marker-like text is not read
|
||||
// as a failure and the text stays in the body.
|
||||
expect(parseExitStatus('[exit code: 5]')).toEqual({ body: '[exit code: 5]', exitCode: 0 })
|
||||
})
|
||||
|
||||
it('recovers a signal kill ahead of any non-zero exit marker', () => {
|
||||
expect(parseExitStatus('gone\n[killed by signal: SIGKILL]')).toEqual({ body: 'gone', signal: 'SIGKILL' })
|
||||
// A fake signal marker with no leading newline is output, not a kill.
|
||||
expect(parseExitStatus('[killed by signal: SIGKILL]')).toEqual({ body: '[killed by signal: SIGKILL]', exitCode: 0 })
|
||||
})
|
||||
|
||||
it('keeps markers no pill shows (timeout) in the body', () => {
|
||||
expect(parseExitStatus('slow\n[timed out after 100ms]\n[exit code: 143]'))
|
||||
.toEqual({ body: 'slow\n[timed out after 100ms]', exitCode: 143 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user