diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index 7068ba34df..21599ff5f7 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.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 .agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: 7f62e99fe152875bfd04bc11c5a1d5dea9014caa -2026-08-23-client-derived-tool-presentation.zh.md: 433d1efb4cded651b39452716399be1ca02c102d +2026-08-23-client-derived-tool-presentation.md: 58a8f23d717580355b852703f448c723d9c3a7ea +2026-08-23-client-derived-tool-presentation.zh.md: 8e1a02eff57cce7967985a22c5bdcec7c18e6059 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index 7f62e99fe1..58a8f23d71 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -51,7 +51,7 @@ The Host `ToolDefinition.presentCall`, `ToolDefinition.presentResult`, `ToolCall | Retained | the Session log format, Remote journal lifecycle, and Conversation identity/topology | | Retained | the existing keyed slot, Generic fallback, and Chat, Details, and Trajectory structure | | Forbidden | a new Client presenter service, parallel registry, or wire renderer id | -| Forbidden | new cards, visual redesign, interaction redesign, or Code Dispatch rich-card enhancements except the separately owned terminal exception below | +| Forbidden | new cards, visual redesign, interaction redesign, or Code Dispatch rich-card enhancements except the [nested terminal-card exception](../bug-fix/2026-09-05-nested-terminal-cards.md) | | Forbidden | compatibility dual-writing, version negotiation, or retention of the old `view` field | ## Terminology @@ -310,7 +310,7 @@ The Client terminal model derives existing `TerminalBlock` props from the tool n | background/error `terminal_send` | Generic result | | Code Dispatch child | same terminal eligibility and fallback rules as a root call | -Standard shell results continue parsing trailing `[exit code: N]` and `[killed by signal: X]` markers. A parsed marker is removed from the body; timeout, sandbox denial, and markers without a pill remain in the body. +Standard shell results parse trailing `[exit code: N]` and `[killed by signal: X]` markers. A final recognized spill-policy notice selects Generic output instead: expandable in shell rows and raw in Details, because the exit marker may be displaced or omitted. A parsed marker is removed from the terminal body; timeout, sandbox denial, and markers without a pill remain in the body. Call `description` remains above the card and overrides the collapsed summary. Workdir continues handling absolute, relative, and missing values. Relative paths resolve against the Session cwd while preserving normalization for `.`, `..`, drive letters, and UNC roots. @@ -634,7 +634,7 @@ An on-demand RPC would turn one page read into N network calls and would still r ### Allow presentation enhancements -Bundling richer Code Dispatch cards, missing-call-head inference, or other historical presentation enhancements with the ownership change would prevent snapshots from proving equivalence. This decision rejects that coupling; the separately owned terminal exception below does not relax nonterminal child restrictions. +Bundling richer Code Dispatch cards, missing-call-head inference, or other historical presentation enhancements with the ownership change would prevent snapshots from proving equivalence. This decision rejects that coupling; the [nested terminal-card exception](../bug-fix/2026-09-05-nested-terminal-cards.md) does not relax nonterminal child restrictions. ### Accept temporary Generic degradation diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 433d1efb4c..8e1a02eff5 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -51,7 +51,7 @@ Host 的 `ToolDefinition.presentCall`、`ToolDefinition.presentResult`、`ToolCa | 保留 | Session 日志格式、Remote journal 生命周期与 Conversation identity/topology | | 保留 | 现有 keyed slot、Generic fallback、Chat、Details 与 Trajectory 结构 | | 禁止 | 新 Client presenter service、平行 registry 或 wire renderer id | -| 禁止 | 新卡片、视觉改版、交互改版或 Code Dispatch rich-card 增强,下述独立决策的 terminal 例外除外 | +| 禁止 | 新卡片、视觉改版、交互改版或 Code Dispatch rich-card 增强,[嵌套 terminal 卡片例外](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)除外 | | 禁止 | 为兼容保留双写、版本协商或旧 `view` 字段 | ## 术语 @@ -310,7 +310,7 @@ Client terminal model 从工具名称、调用参数、结果 content、error | `terminal_send` background/error | Generic 结果 | | Code Dispatch child | 与根调用相同的 terminal 适用规则与 fallback 规则 | -标准 shell 结果继续解析末尾 `[exit code: N]` 与 `[killed by signal: X]`。已解析的 marker 从正文移除;timeout、sandbox denial 与没有 pill 的 marker 留在正文。 +标准 shell 结果解析末尾 `[exit code: N]` 与 `[killed by signal: X]`。末尾已识别的 spill 策略提示会改用 Generic 输出:在 shell 行中可展开,在 Details 中显示原文,因为退出标记可能被移位或省略。已解析的 marker 从 terminal 正文移除;timeout、sandbox denial 与没有 pill 的 marker 留在正文。 调用 `description` 继续显示在 card 上方并覆盖折叠摘要。workdir 继续按绝对、相对和缺失三种情况处理;相对路径基于 Session cwd,且保留 `.`、`..`、盘符与 UNC root 的归一化。 @@ -634,7 +634,7 @@ read 行结构、applied diff、search 分组、web sources 和有效 truncation ### 允许展示增强 -将更丰富的 Code Dispatch 卡片、缺失 call head 的推断或其他历史展示增强与所有权变更捆绑,会使快照无法证明对等。本决定拒绝这种捆绑;下述独立决策的 terminal 例外不放宽非 terminal 子调用限制。 +将更丰富的 Code Dispatch 卡片、缺失 call head 的推断或其他历史展示增强与所有权变更捆绑,会使快照无法证明对等。本决定拒绝这种捆绑;[嵌套 terminal 卡片例外](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)不放宽非 terminal 子调用限制。 ### 接受临时 Generic 退化 diff --git a/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.i18n.yaml index 98a3742c9f..d7fb9e1418 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.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 .agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.md -2026-09-05-nested-terminal-cards.md: 10e0c082650fe5a937aa8fba9f6080896fe8538f -2026-09-05-nested-terminal-cards.zh.md: 2ad94f62657b78b5eaf6fde0033e73abee07997e +2026-09-05-nested-terminal-cards.md: 0e38414e9450be060b0b66c42c4cac8111dd5476 +2026-09-05-nested-terminal-cards.zh.md: 5f167a926771195d0f6b8596d5335aebe57af3e2 diff --git a/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.md b/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.md index 10e0c08265..0e38414e94 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.md +++ b/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.md @@ -14,12 +14,16 @@ A shell command dispatched through `run_code` carries the arguments and rendered This partially supersedes only the terminal child-card prohibition in [Client-derived tool presentation](../architecture/2026-08-23-client-derived-tool-presentation.md). That note remains active for Client presentation ownership and the diff/read/search/web child restrictions. No Host presenter, event, schema, metadata, call-tree, or model-context change is required. The metadata and execution-local value decisions in [canonical tool output](../architecture/2026-07-20-canonical-tool-output-contract.md) and [PTC typed returns](../feature/2026-07-20-ptc-typed-tool-returns.md) remain intact; metadata omission does not prohibit Client-derived terminal cards. +Shell output ending in a recognized spill-policy notice uses generic output: expandable in `BashRow`, raw fallback in Details. The notice can follow or replace the exit marker, so its absence at the end does not justify a successful terminal status. The Client recognizes the final notice without changing Host output or schemas. + ## Alternatives considered **Keep the blanket nested-call rejection.** Rejected because nesting does not remove the raw facts the terminal model already consumes. It hides usable shell output while the same call renders as a terminal at the root. **Enable every nested structured card.** Rejected because other card models have independent metadata requirements and child restrictions. This fix changes only terminal eligibility. +**Parse exit markers around spill suffixes.** Rejected because truncation can remove the real status; conservative generic output avoids guessing success from an incomplete result. + ## Consequences Rows and Details share terminal derivation for nested calls without a second renderer or presentation hint. Generic fallback and settled-persistent behavior remain separate from terminal-card eligibility. The parent-child relationship still controls tree placement, not terminal rendering. diff --git a/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.zh.md b/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.zh.md index 2ad94f6265..5f167a9267 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-09-05-nested-terminal-cards.zh.md @@ -14,12 +14,16 @@ Status: implemented 本文仅部分取代 [Client 派生工具展示](../architecture/2026-08-23-client-derived-tool-presentation.zh.md)中的 terminal 子调用卡片禁令。该文继续负责 Client 展示所有权及 diff/read/search/web 子调用限制。无需更改 Host 展示转换器、事件、schema、元数据、调用树或模型上下文。[规范工具输出](../architecture/2026-07-20-canonical-tool-output-contract.zh.md)与 [PTC 类型化返回值](../feature/2026-07-20-ptc-typed-tool-returns.zh.md)中的元数据和执行期值决策保持不变;省略元数据不禁止 Client 派生 terminal 卡片。 +以已识别的 spill 策略提示结尾的 shell 输出使用通用展示:在 `BashRow` 中可展开,在 Details 中使用原始回退。提示可能位于退出标记之后或取代它,因此末尾缺少退出标记不能作为 terminal 成功状态的依据。Client 识别最终提示,不更改 Host 输出或 schema。 + ## 考虑过的替代方案 **保留对嵌套调用的一律拒绝。** 不予采用,因为嵌套不会移除 terminal model 已消费的原始事实。这会隐藏可用的 shell 输出,而同一调用位于根时却可渲染为 terminal。 **启用所有嵌套结构化卡片。** 不予采用,因为其他 card model 有独立的元数据要求与子调用限制。本修复只改变 terminal 适用性。 +**解析 spill 后缀附近的退出标记。** 不予采用,因为截断可能移除真实状态;保守的通用输出避免从不完整结果猜测成功。 + ## 后果 行与 Details 对嵌套调用共享 terminal 派生,不增加第二个渲染器或展示提示字段。通用回退与已完成持久 shell 的行为仍独立于 terminal 卡片适用性。父子关系仍控制树中的位置,而非 terminal 渲染。 diff --git a/apps/web/tests/ptc-round.e2e.ts b/apps/web/tests/ptc-round.e2e.ts index 8d169dbfdf..66ff93de30 100644 --- a/apps/web/tests/ptc-round.e2e.ts +++ b/apps/web/tests/ptc-round.e2e.ts @@ -135,6 +135,9 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => { it.skipIf(MODE === 'record')('matches the expanded conversation aria golden with stable anchors', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-ptc-aria')) + const row = page.locator('[data-subcalls] [data-sample="bash"]').first() + await expandOwningTurnProcess(page, row) + if (await row.getAttribute('aria-expanded') !== 'true') await row.click() const snapshot = await captureExpandedTurnProcessAria( page, '[class*="centerCol"]', diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index 652aebb5a4..87ea8a67bf 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/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/client/ui-tool/README.md -README.md: 02c403133f7586217ed3397d9f496baedfcfb0a8 -README.zh.md: 0278e7fceca34d0e2b9334cfb33ae1a85fbdc27c +README.md: e9c975fb40f1584149eb9200f43e25ec70379d8f +README.zh.md: f64bc19752b97295ac80b839f56dd39f47ad2dfb diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index 02c403133f..e9c975fb40 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -43,7 +43,7 @@ The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `blo ### Built-in views -This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running and settled foreground `bash`, `pwsh`, and `terminal_send` calls use terminal cards at the root and in Code Dispatch children, subject to the same argument, result, and error checks. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. +This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in Code Dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. ----- diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 0278e7fcec..f64bc19752 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -43,7 +43,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block` ### 内置视图 -本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中与已完成的前台 `bash`、`pwsh` 和 `terminal_send` 调用,无论位于根还是 Code Dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 +本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 Code Dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 ----- diff --git a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts index 618afa6610..c8f68968fc 100644 --- a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts @@ -219,6 +219,24 @@ export function isSettledPersistentShellCall(block: ToolCallBlock): boolean { return shellCall(parsed.name, parsed.args)?.persistent === true } +/** + * Identify a settled foreground shell preview whose spill footer can hide the exit marker. + * @param block - running or settled Tool block. + * @returns whether the shell output must remain generic without an inferred exit status. + */ +export function isSpilledShellCall(block: ToolCallBlock): boolean { + if (!('kind' in block)) return false + const parsed = parsedToolCall(block) + if (parsed === null) return false + const call = shellCall(parsed.name, parsed.args) + if (call === null || call.background) return false + const output = singleResultText(block) + // spill-policy appends this footer after a bounded preview; the process + // status can be displaced or omitted, so absence cannot imply exit zero. + return output !== undefined + && /(?:^|\n\n)\(Omitted \d+ bytes\. Full formatted result stored at: [\s\S]+\)$/.test(output) +} + interface TerminalSendCall { kind: 'terminal-send' text: string @@ -257,8 +275,8 @@ function parseExitStatus(text: string): { output: string; exitCode?: number; sig /** * Derive terminal props for supported shell and terminal-send calls, including * nested Code Dispatch calls. Standard shell results parse their final status - * marker; persistent shell results, background calls, errors, and malformed - * input use the generic path. {@link isSettledPersistentShellCall} lets that generic + * marker; persistent shell results, spill previews, background calls, errors, + * and malformed input use the generic path. {@link isSettledPersistentShellCall} lets that generic * persistent result remain expandable without inventing one process status. * @param block - running or settled Tool block. * @param sessionCwd - session workspace root used to resolve workdir. @@ -289,7 +307,7 @@ export function terminalCardModel( }, } } - if (block.isError || (call.kind === 'shell' && call.persistent)) return null + if (block.isError || (call.kind === 'shell' && call.persistent) || isSpilledShellCall(block)) return null const output = singleResultText(block) if (output === undefined) return null const status = call.kind === 'terminal-send' ? { output } : parseExitStatus(output) diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx index c291a733a1..e5380979b9 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx @@ -8,6 +8,7 @@ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' import { isSettledPersistentShellCall, + isSpilledShellCall, localizeTerminalCardModel, terminalBlockLabels, terminalCardModel, @@ -53,11 +54,10 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: : model.state const status = stateStatus(state, t) const [expanded, setExpanded] = useState(false) - // Execution failures and persistent-shell results have no terminal card. - // Keep their recorded args and complete output reachable through the generic - // body; background acknowledgements and malformed calls remain collapsed. + // Failures, persistent-shell results, and spill previews use a generic body; + // background acknowledgements and malformed calls remain collapsed. const genericBody = terminal === null - && (model.state === 'error' || isSettledPersistentShellCall(block)) + && (model.state === 'error' || isSettledPersistentShellCall(block) || isSpilledShellCall(block)) && (model.bodyRaw !== null || model.output !== null) const expandable = terminal !== null || genericBody const open = expanded && expandable diff --git a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx index be03edabd4..9476de0ce5 100644 --- a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx @@ -225,6 +225,30 @@ describe('terminalCardModel', () => { expect(terminalCardModel(running({ parentCallId: 'parent' }))).toEqual(terminalCardModel(running())) }) + it.each(['bash', 'pwsh'])('keeps nested persistent %s running cards and settled generic results', (name) => { + const argsRaw = JSON.stringify({ command: 'pwd' }) + expect(terminalCardModel(running({ name, argsRaw, parentCallId: 'parent' }))) + .toEqual(terminalCardModel(running({ name, argsRaw }))) + expect(terminalCardModel(settled({ call: { name, argsRaw }, parentCallId: 'parent' }))).toBeNull() + }) + + it.each(['bash', 'pwsh'])('does not infer %s exit status from a spilled preview', (name) => { + const notice = '(Omitted 50000 bytes. Full formatted result stored at: /spill/output.txt. Read the file.)' + for (const parentCallId of [undefined, 'parent']) { + for (const preview of ['failed\n[exit code: 7]', 'killed\n[killed by signal: SIGTERM]', 'partial', '']) { + expect(terminalCardModel(settled({ + ...parentCallId === undefined ? {} : { parentCallId }, + call: { name, argsRaw: ARGS }, + content: [{ type: 'text', text: preview === '' ? notice : `${preview}\n\n${notice}` }], + }))).toBeNull() + } + } + expect(terminalCardModel(settled({ + call: { name, argsRaw: ARGS }, + content: [{ type: 'text', text: `${notice}\nordinary output` }], + }))).not.toBeNull() + }) + it('returns null for background, errors, malformed args, unsupported tools, and non-text results', () => { expect(terminalCardModel(running({ argsRaw: shellArgs({ run_in_background: true }) }))).toBeNull() expect(terminalCardModel(settled({ isError: true }))).toBeNull() @@ -466,6 +490,18 @@ describe('BashRow terminal card', () => { .toBe('a.ts b.ts\nc.ts d.ts\n') }) + it('expands a spilled child result without a successful terminal indicator', () => { + const output = 'failed\n[exit code: 7]\n\n(Omitted 50000 bytes. Full formatted result stored at: /spill/output.txt. Read the file.)' + const view = render() + const row = view.container.querySelector('[data-sample="bash"]')! + expect(row.getAttribute('role')).toBe('button') + fireEvent.click(row) + expect(view.container.querySelector('[data-terminal]')).toBeNull() + expect(view.container.querySelectorAll('[class*="_ioText_"]')[1]?.textContent).toBe(output) + }) + it('a non-terminal bash call (background start) renders the summary row alone', () => { const view = render(