feat(tools): run_code description param + native-parity dispatch logging + web code-mode seam

run_code gains a required bash-style description parameter: presentCall
titles the card with it and moves the program to rawInput, so every
surface gets a readable label. tool/code-dispatch now logs each
sub-call's complete content/isError (the tool/result vocabulary),
replacing the bounded resultSummary and deleting the summarize/cwd
machinery — a UI renders sub-calls through the identical path as native
results. The dsh config tree mounts the worker code runtime and reads
DSH_TOOLS_MODE (temporary seam until per-session mode selection lands).

Session format stays v0 (pre-release churn). Code-mode ACP/TUI fixtures
re-recorded; TUI presenter pin refreshed; catalogs regenerated. Keyless
web smoke pins the code-mode wire contract (tools=[run_code] + SDK
prompt section).
This commit is contained in:
Tianyi Cui
2026-07-26 02:43:34 +08:00
parent 6f50208ea3
commit d60dea9f55
40 changed files with 1312 additions and 1291 deletions

View File

@@ -115,7 +115,7 @@ Returning `undefined` selects generic fallback. Presenters depend only on their
Under `code` or `both`, the registry exposes the reserved `run_code` transport and a deterministic TypeScript SDK for the current scope; only the program's outer logs and return value re-enter model context. The SDK declares exact `ToolArgsMap` and `ToolOutputMap` entries for every visible tool, and each binding resolves to the tool's canonical JSON value. Each lossless-JSON binding call re-enters the complete tool pipeline sequentially with logged correlation to the outer call. Denials and other failed results reject with the real program-visible `ToolCallError` carrying only `toolName` and `message`; Native content and internal error codes stay outside the Code contract. Ordinary side effects are not rolled back, and sub-call `additionalContexts` are deferred through the parent result to preserve call/result adjacency. Run settlement aborts and drains outstanding bindings; runtime failures surface as `CodeRunFailedError`. See the [Code Mode foundation](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md), [typed-return contract](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md), and [code-runtime seam](../../code-runtime/README.md). Try `pnpm run demo:code-mode`.
- **The SDK section** (`tools:sdk`, order 150): a lazy prompt section regenerating, at each assembly, `JsonValue`, exact `ToolArgsMap` / `ToolOutputMap`, `ToolName`, the `ToolCallError` declaration, and a mapped `tools` namespace for the calling scope's visible end capabilities (exotic names via quoted keys), plus fixed usage instructions. Deterministic — lexicographic tool order, byte-identical text for an unchanged tool set (prefix-cache-friendly). The codegen (`jsonSchemaToTs`, exported) handles every unified schema construct and degrades unsupported raw constructs to `unknown`, never throwing during prompt assembly.
- **The dispatch bridge** (`run_code`'s execute): every binding call is snapshotted as lossless JSON before dispatch (`undefined`, `BigInt`, cycles, sparse arrays, `-0`, and exotic objects reject that one call), serialized through a per-run queue (even `Promise.all` executes underlying calls one at a time in submission order), given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A success returns the final canonical value after policy; a failure reaches the worker as one message and becomes `ToolCallError(toolName, message)`. Each sub-call is logged as a `tool/code-dispatch` session event with deterministic id `<parent>:code:<n>` and a bounded Native-content summary; `deriveMessages()` does not surface that event or persist the value. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and retaining each source/meta even when the program later fails.
- **The dispatch bridge** (`run_code`'s execute): every binding call is snapshotted as lossless JSON before dispatch (`undefined`, `BigInt`, cycles, sparse arrays, `-0`, and exotic objects reject that one call), serialized through a per-run queue (even `Promise.all` executes underlying calls one at a time in submission order), given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A success returns the final canonical value after policy; a failure reaches the worker as one message and becomes `ToolCallError(toolName, message)`. Each sub-call is logged as a `tool/code-dispatch` session event with deterministic id `<parent>:code:<n>` and the complete model-facing `content`/`isError` outcome (the `tool/result` vocabulary, so UIs render sub-calls through the native path); `deriveMessages()` does not surface that event or persist the canonical value. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and retaining each source/meta even when the program later fails.
- **Settlement discipline**: the bridge owns a run-scoped abort that follows the outer signal in and fires when the run settles for any reason, so a budget expiry aborts an in-flight sub-tool instead of orphaning it; the bridge then drains its queue BEFORE returning, so every `tool/code-dispatch` lands inside the open turn. A failed run throws `CodeRunFailedError` (`code: 'CODE_RUN_FAILED'`, message = the failure kind + captured logs), which the pipeline converts to a structured `isError` the model self-corrects from.
- **Result boundary**: intermediate binding values cross the worker boundary whole and have no per-binding byte cap. `run_code` returns canonical `{ logs: string[], result?: JsonValue }`; strings render raw, every other present JSON root renders through a stack-safe pretty JSON traversal whose total indentation is capped at ten characters (deeper subtrees stay compact), `null` remains explicit, and absent `result` means the program returned `undefined`. The worker's configurable `maxOutputBytes` (default 64 MiB) applies only to the combined serialized outer log-array, completion-value, or failure-message payloads; fixed result-envelope syntax and presentation whitespace are outside that ledger. Invalid and over-limit completions fail explicitly, and only this outer result is eligible for ordinary spill.
@@ -189,5 +189,5 @@ Append-only; newly visible content follows the reusable request prefix and does
- **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper.
- **Code Mode is TypeScript-only and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`; scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only.
- **Code Mode intermediate values are execution-local and unbounded by bytes** — they cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap.
- **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The rendered `content` of every sub-call IS logged verbatim on `tool/code-dispatch`, uncapped and outside spill policy, so programs that read huge files grow the session log by the same bytes (spill integration for the logged copy is deferred work).
- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md).

View File

@@ -5,7 +5,6 @@
* @module @deepseek-ai/dsh-tools/src/code-mode
*/
import { parse } from 'node:path'
import { CallId, HarnessError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
@@ -21,17 +20,16 @@ declare module '@deepseek-ai/dsh-session' {
* `run_code` call id, the deterministic sub-call id
* (`<parent>:code:<n>`), the tool `name` with its JSON-normalized
* `arguments` — the exact value dispatched, normalized BEFORE dispatch,
* so this append can never fail on payload shape — whether the sub-call
* errored, and a bounded `resultSummary` of its model-facing text. Before
* bounding, occurrences of a non-root session workspace path are
* normalized to `.` so host-specific absolute path lengths cannot change
* the summary.
* so this append can never fail on payload shape — and the sub-call's
* complete model-facing outcome in `tool/result`'s own vocabulary
* (`content` + `isError`), so UIs render a sub-call through the exact
* code path that renders a native call.
* Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
* model context; persistence and UIs get every call. Appended inside the
* parent `run_code`'s execution (the bridge drains its queue before
* returning), so the turn-enclosure invariant holds by construction.
*/
'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; resultSummary: string }
'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; content: ContentBlock[] }
}
}
@@ -55,35 +53,6 @@ export class CodeRunFailedError extends HarnessError {
}
}
/**
* Cap for a `tool/code-dispatch` event's `resultSummary`. A log-ergonomics
* constant, not config: the full result already flows to the program; the
* summary exists so log readers see what a sub-call returned at a glance.
*/
const SUMMARY_MAX_CHARS = 200
/** Join Native content for the bounded durable sub-dispatch summary; non-text blocks become diagnostic placeholders. */
function textOf(content: ContentBlock[]): string {
return content
.map((block) => {
switch (block.type) {
case 'text': return block.text
// ContentBlockMap is merge-extensible — future block kinds land here
// deliberately (no assertNever on merge-extensible unions).
default: return `[${block.type} content]`
}
})
.join('\n')
}
/** Normalize workspace paths, then bound a sub-call's model-facing text for its durable log summary. */
function summarize(text: string, cwd: string | undefined): string {
const stableText = cwd === undefined || cwd === parse(cwd).root
? text
: text.replaceAll(cwd, '.')
return stableText.length > SUMMARY_MAX_CHARS ? `${stableText.slice(0, SUMMARY_MAX_CHARS)}` : stableText
}
/**
* Snapshot one binding call's argument as lossless JSON, then snapshot that
* detached value again so dispatch and logging stay independent without
@@ -221,6 +190,13 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
+ 'Only what you print or return comes back — curate it.',
parameters: {
code: { type: 'string', required: true, description: 'The program: the body of an async TypeScript function.' },
description: {
type: 'string',
required: true,
description: 'Clear, concise description of what this program does in active voice, '
+ '5-10 words (shown in the UI). Examples: "Count TODO markers across packages"; '
+ '"Read failing test and its fixture"; "Rename config key in every cordis.yml".',
},
},
output: {
schema: {
@@ -238,6 +214,9 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
},
},
async execute(args, exec): Promise<RunCodeOutput> {
if (args.description.trim().length === 0) {
throw new Error('invalid description: expected a non-empty string')
}
const runtime = requireRuntime()
// The run-scoped abort: follows the outer signal in, and fires when the
@@ -288,7 +267,6 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
for (const context of result.additionalContexts ?? []) {
exec.deferContext(context)
}
const text = textOf(result.content)
exec.agent?.session.append('tool/code-dispatch', {
parentCallId: exec.callId,
subCallId,
@@ -298,7 +276,9 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
// this record from what it actually received.
arguments: normalized.logged,
isError: result.isError,
resultSummary: summarize(text, exec.agent.session.header.cwd),
// The registry deep-froze this projection at result finalization;
// append snapshots it again, so the log copy stays detached.
content: result.content,
})
return result.isError
? { isError: true as const, message: result.error.message }
@@ -363,10 +343,11 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
exec.signal.removeEventListener('abort', onOuterAbort)
}
},
// The program is the call's always-visible UI label.
// The model-authored description is the call's always-visible UI label
// (the bash `description` precedent); the program itself rides rawInput.
presentCall: args => ({
card: 'generic',
title: args.code,
title: args.description,
kind: 'execute',
rawInput: args.code,
}),

View File

@@ -87,11 +87,11 @@ function registerEcho(ctx: Context, name = 'echo'): unknown[] {
}
/** A structural fake of the owning agent: captures session appends. */
function fakeAgent(options: { cwd?: string } = { cwd: '/workspace' }): { agent: Agent; events: { type: string; data: unknown }[] } {
function fakeAgent(): { agent: Agent; events: { type: string; data: unknown }[] } {
const events: { type: string; data: unknown }[] = []
const agent = {
session: {
header: options.cwd === undefined ? {} : { cwd: options.cwd },
header: { cwd: '/workspace' },
append: (type: string, data: unknown) => { events.push({ type, data }) },
},
} as unknown as Agent
@@ -99,12 +99,16 @@ function fakeAgent(options: { cwd?: string } = { cwd: '/workspace' }): { agent:
}
/** Dispatch run_code through the registry pipeline, as the loop would. */
async function runCode(ctx: Context, code: string, extras: { agent?: Agent; signal?: AbortSignal } = {}): Promise<ToolExecutionResult> {
async function runCode(
ctx: Context,
code: string,
extras: { agent?: Agent; signal?: AbortSignal; description?: string } = {},
): Promise<ToolExecutionResult> {
return ctx.tools.execute({
signal: testToolSignal,
callId: CallId('call-1'),
name: RUN_CODE_NAME,
arguments: { code },
arguments: { code, description: extras.description ?? 'Run the test program' },
...extras.agent ? { agent: extras.agent } : {},
...extras.signal ? { signal: extras.signal } : {},
})
@@ -374,8 +378,14 @@ describe('the run_code dispatch bridge', () => {
expect(calls).toEqual([{ value: 'one' }, { value: 'two' }])
const dispatches = events.filter(event => event.type === 'tool/code-dispatch')
expect(dispatches.map(event => event.data)).toEqual([
{ parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo', arguments: { value: 'one' }, isError: false, resultSummary: 'echo:one' },
{ parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo', arguments: { value: 'two' }, isError: false, resultSummary: 'echo:two' },
{
parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo',
arguments: { value: 'one' }, isError: false, content: [{ type: 'text', text: 'echo:one' }],
},
{
parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo',
arguments: { value: 'two' }, isError: false, content: [{ type: 'text', text: 'echo:two' }],
},
])
expect(result.meta).toBeUndefined()
})
@@ -693,19 +703,26 @@ describe('the run_code dispatch bridge', () => {
expect((result.content[0] as { text: string }).text).toContain('requires a code runtime')
})
it('presents the program as the execute-card title', async () => {
it('presents the model-authored description as the execute-card title over the program input', async () => {
const { ctx } = await setup({ mode: 'code' })
const tool = ctx.tools.get(RUN_CODE_NAME)!
// The program is the title, mirroring how command tools label their cards
// with the command while retaining the same value in the expanded input.
expect(tool.presentCall?.({ code: 'return 1' })).toEqual({
// The description labels the card (the bash description precedent); the
// program itself remains the expanded raw input.
expect(tool.presentCall?.({ code: 'return 1', description: 'Return the constant one' })).toEqual({
card: 'generic',
title: 'return 1',
title: 'Return the constant one',
kind: 'execute',
rawInput: 'return 1',
})
})
it('rejects a whitespace-only description with a structured isError', async () => {
const { ctx } = await setup({ mode: 'code' })
const result = await runCode(ctx, 'return 1', { description: ' ' })
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('invalid description')
})
it.each([
['logs only', { logs: ['printed'] }, 'printed'],
['result only', { logs: [], value: 'returned' }, 'returned'],
@@ -759,7 +776,7 @@ describe('the run_code dispatch bridge', () => {
expect('presentResult' in tool).toBe(false)
})
it('renders non-text sub-result blocks as placeholders and truncates long event summaries', async () => {
it('logs the complete sub-result content verbatim, non-text blocks and long text included', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const { agent, events } = fakeAgent()
const long = 'x'.repeat(300)
@@ -786,58 +803,10 @@ describe('the run_code dispatch bridge', () => {
expect(result.isError).toBe(false)
expect((result.content[0] as { text: string }).text).toBe('mixed-value')
const dispatch = events.find(event => event.type === 'tool/code-dispatch')?.data as SessionEventMap['tool/code-dispatch']
expect(dispatch.resultSummary.length).toBe(201)
expect(dispatch.resultSummary.endsWith('…')).toBe(true)
})
it('normalizes the session workspace root before bounding durable result summaries', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
ctx.tools.register(defineTool({
name: 'workspace_path',
description: 'Return a path beneath the session workspace.',
parameters: {},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
execute(_args, exec) {
const cwd = exec.agent?.session.header.cwd ?? ''
return Promise.resolve(`<path>${cwd}/nested/task.txt</path>\n${'x'.repeat(240)}`)
},
}))
runtime.behavior = async request => ({
logs: [],
value: await request.bindings[0]!.functions.workspace_path!({}),
})
const short = fakeAgent({ cwd: '/tmp/workspace' })
const long = fakeAgent({ cwd: `/tmp/${'long-segment/'.repeat(30)}workspace` })
const shortResult = await runCode(ctx, 'program', { agent: short.agent })
const longResult = await runCode(ctx, 'program', { agent: long.agent })
const shortDispatch = short.events[0]!.data as SessionEventMap['tool/code-dispatch']
const longDispatch = long.events[0]!.data as SessionEventMap['tool/code-dispatch']
expect(shortResult.content).not.toEqual(longResult.content)
expect(shortDispatch.resultSummary).toBe(longDispatch.resultSummary)
expect(shortDispatch.resultSummary).toHaveLength(201)
expect(shortDispatch.resultSummary).toMatch(/^<path>\.\/nested\/task\.txt<\/path>\n.+…$/)
})
it('leaves result summaries unchanged when a session cwd is absent or is the filesystem root', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
registerEcho(ctx)
runtime.behavior = async request => ({
logs: [],
value: await request.bindings[0]!.functions.echo!({ value: '/workspace/value' }),
})
const absent = fakeAgent({})
const root = fakeAgent({ cwd: '/' })
await runCode(ctx, 'program', { agent: absent.agent })
await runCode(ctx, 'program', { agent: root.agent })
expect((absent.events[0]!.data as SessionEventMap['tool/code-dispatch']).resultSummary).toBe('echo:/workspace/value')
expect((root.events[0]!.data as SessionEventMap['tool/code-dispatch']).resultSummary).toBe('echo:/workspace/value')
expect(dispatch.content).toEqual([
{ type: 'text', text: long },
{ type: 'reasoning', text: 'hidden' },
])
})
it('rejects undefined, getter-throwing, exotic, and unrepresentable binding arguments before dispatch', async () => {
@@ -1067,7 +1036,7 @@ describe('the run_code dispatch bridge', () => {
name: 'echo',
arguments: { value: 'x' },
isError: false,
resultSummary: 'echo:x',
content: [{ type: 'text', text: 'echo:x' }],
})
const derived = session.deriveMessages()
expect(derived).toHaveLength(1)

View File

@@ -752,7 +752,7 @@ describe('exit_plan_mode', () => {
const result = await ctx.tools.execute({
callId: CallId(`call-exit-${++callCounter}`),
name: RUN_CODE_NAME,
arguments: { code: `return await tools.${EXIT_PLAN_MODE}({ plan: ${JSON.stringify(plan)} })` },
arguments: { code: `return await tools.${EXIT_PLAN_MODE}({ plan: ${JSON.stringify(plan)} })`, description: 'Submit the plan for review' },
signal: new AbortController().signal,
agent,
})

View File

@@ -204,6 +204,7 @@ describe('outer Code Mode failure capture', () => {
name: 'run_code',
arguments: {
code: 'console.log("HEAD-" + "x".repeat(300)); console.log("TAIL-" + "y".repeat(300)); return "unreachable";',
description: 'Print oversized head and tail lines',
},
agent: agent as never,
})

View File

@@ -439,7 +439,7 @@ describe('in-process structured output', () => {
it('keeps pure Code Mode at one wire tool and exposes structured capture through the SDK only', async () => {
const { ctx, parent, adapter } = await setup([
toolCallResponse('c1', RUN_CODE_NAME, { code: 'return await tools.structured_output({ answer: 12 })' }),
toolCallResponse('c1', RUN_CODE_NAME, { code: 'return await tools.structured_output({ answer: 12 })', description: 'Capture the structured answer' }),
], {
toolMode: 'code',
codeRun: async (request) => {
@@ -465,7 +465,7 @@ describe('in-process structured output', () => {
it('discards a nested capture when the enclosing run_code execution fails', async () => {
const { ctx, parent, adapter } = await setup([
toolCallResponse('c1', RUN_CODE_NAME, { code: 'await tools.structured_output({ answer: 12 }); throw new Error("boom")' }),
toolCallResponse('c1', RUN_CODE_NAME, { code: 'await tools.structured_output({ answer: 12 }); throw new Error("boom")', description: 'Capture then fail the program' }),
textResponse('outer code failed'),
], {
toolMode: 'code',
@@ -494,7 +494,7 @@ describe('in-process structured output', () => {
it('discards a nested capture when post-policy blocks the enclosing run_code result', async () => {
const { ctx, parent, adapter } = await setup([
toolCallResponse('c1', RUN_CODE_NAME, { code: 'return await tools.structured_output({ answer: 12 })' }),
toolCallResponse('c1', RUN_CODE_NAME, { code: 'return await tools.structured_output({ answer: 12 })', description: 'Capture the structured answer' }),
textResponse('outer code was blocked'),
], {
toolMode: 'code',

View File

@@ -1,7 +1,7 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=13 bufferRow=13
cursor hidden column=1 viewportRow=12 bufferRow=12
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold
@@ -13,30 +13,27 @@ buffer
3| <blank>
4| "▌ "
style 0-0 fg=yellow
5| "▌ ◌ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
5| "▌ ◌ Echo two markers and combine them "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-95 bold
6| "▌ const second = await tools.bas "
style 3-36 bold
6| "▌ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
style 0-0 fg=yellow
style 2-31 bold
7| "▌ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
7| "▌ const second = await tools.bash({ command: 'echo CODE_TWO' }) "
style 0-0 fg=yellow
8| "▌ const second = await tools.bash({ command: 'echo CODE_TWO' }) "
8| "▌ console.log(first, second) "
style 0-0 fg=yellow
9| "▌ console.log(first, second) "
9| "▌ return `${first}+${second}` "
style 0-0 fg=yellow
10| "▌ return `${first}+${second}` "
10| "▌ "
style 0-0 fg=yellow
11| ""
style 0-0 fg=yellow
12| "────────────────────────────────────────────────────────────────────────────────────────────────"
11| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
13| " "
12| " "
style 1-1 inverse
14| "────────────────────────────────────────────────────────────────────────────────────────────────"
13| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
15| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
14| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
style 0-43 dim
style 69-95 dim
16-35| <blank>
15-35| <blank>

View File

@@ -364,6 +364,7 @@ describe('TUI terminal-state snapshots', () => {
name: 'run_code',
arguments: {
code: "const first = await tools.bash({ command: 'echo CODE_ONE' })\nconst second = await tools.bash({ command: 'echo CODE_TWO' })\nconsole.log(first, second)\nreturn `${first}+${second}`",
description: 'Echo two markers and combine them',
},
}
await renderAfter(harness, () => { appendToolCalls(harness.session, [call]) })