diff --git a/package-lock.json b/package-lock.json index 9b62689..85d1123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dsh-cctui", - "version": "0.3.8", + "version": "0.3.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsh-cctui", - "version": "0.3.8", + "version": "0.3.9", "license": "MIT", "dependencies": { "@dsh-cctui/ink": "file:./packages/dsh-cctui-ink", diff --git a/package.json b/package.json index 700ee56..55630cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-cctui", - "version": "0.3.8", + "version": "0.3.9", "description": "Claude-Code-style terminal UI for deepseek-harness, ported from clawcodex ui-tui", "license": "MIT", "repository": { diff --git a/scripts/tool-gallery.py b/scripts/tool-gallery.py index e7216b3..47b1c3b 100755 --- a/scripts/tool-gallery.py +++ b/scripts/tool-gallery.py @@ -82,8 +82,27 @@ 'PROBE subagent {"description": "Audit the tests", "prompt": "Summarise the test layout and stop."}' ), "skill": 'PROBE skill {"name": "nonexistent-skill"}', + "read_image": 'PROBE read_image {"file_path": "e2e-scratch/gallery.png"}', + "str_replace_editor": 'PROBE str_replace_editor {"command": "view", "path": "src/domain/usage.ts"}', + "job_output": 'PROBE job_output {"job_id": "no-such-job"}', + "send_message": 'PROBE send_message {"subagent_id": "no-such-agent", "message": "hello"}', + "get_goal": 'PROBE get_goal {}', + # plan mode has to be on before the tool will run; pair with --live to catch + # the review prompt while it is up + "exit_plan_mode": [ + '/plan', + 'PROBE exit_plan_mode {"plan": "# Close the tool-result gaps\\n\\n- read cards\\n- search cards"}' + ], "job_list": 'PROBE job_list {}', "list_agents": 'PROBE list_agents {}', + # a RUN of calls, which is the only way the collapsed brief reads right + "mixed_run": ( + 'PROBE read {"file_path": "src/domain/usage.ts"}' + ' ;; PROBE read {"file_path": "src/domain/roles.ts"}' + ' ;; PROBE glob {"pattern": "src/domain/*.ts"}' + ' ;; PROBE bash {"command": "ls -1 src | head -3", "description": "peek"}' + ' ;; PROBE bash {"command": "echo hi", "description": "echo"}' + ), "todo": ( 'PROBE todo_write {"todos": [{"content": "first task", "status": "in_progress"},' ' {"content": "second task", "status": "pending"}]}' diff --git a/src/__tests__/toolArgs.test.ts b/src/__tests__/toolArgs.test.ts index 63c7f16..d9dd547 100644 --- a/src/__tests__/toolArgs.test.ts +++ b/src/__tests__/toolArgs.test.ts @@ -48,10 +48,18 @@ describe('toolArgsPreview', () => { expect(toolArgsPreview(raw)).toBe('Review the diff') }) - it('falls back to the unfiltered projection when every argument is bulk', () => { - expect(toolArgsPreview('{"todos":[{"content":"a","status":"pending"}]}')).toBe( - 'todos: [{"content":"a","status":"pending"}]' - ) + it('does not restate a plan the review box is already showing', () => { + const raw = '{"plan":"# Close the gaps\\n\\n- read cards\\n- search cards"}' + + // the plan renders in full right below the row; squeezing it into 64 + // columns of parens shows it twice, once badly + expect(toolArgsPreview(raw)).toBe('') + }) + + it('renders nothing when every argument is bulk, leaving a bare label', () => { + // `⏺ Todo Write` with the checklist below it, the way upstream renders it — + // formatToolCall drops the parens entirely for an empty preview + expect(toolArgsPreview('{"todos":[{"content":"a","status":"pending"}]}')).toBe('') }) it('renders non-string values as JSON', () => { diff --git a/src/domain/toolArgs.ts b/src/domain/toolArgs.ts index 2938f93..9e75eb9 100644 --- a/src/domain/toolArgs.ts +++ b/src/domain/toolArgs.ts @@ -28,7 +28,8 @@ /** * Arguments that never identify a call. Bulk payloads (a file body, an edit's - * before/after, a checklist, a delegation's brief) and policy/qualifier fields + * before/after, a checklist, a delegation's brief, a plan under review) and + * policy/qualifier fields * (why a command may escalate, which window of a file to read) both make the * row longer and less distinguishable, which is the opposite of the point. */ @@ -41,6 +42,7 @@ const NOISE_KEYS: ReadonlySet = new Set([ 'new_string', 'old_str', 'old_string', + 'plan', 'prompt', 'todos', // policy / qualifiers @@ -117,10 +119,10 @@ export const toolArgsPreview = (raw: string): string => { return project(salient) } - const labels = entries.filter(([key]) => LABEL_KEYS.has(key)) - - // A call whose every argument is bulk (a bare `todo_write`) still deserves a - // row that says something; falling back to the unfiltered projection beats - // rendering `⏺ Todo Write()`. - return project(labels.length ? labels : entries) + // A call whose every argument is bulk or policy has nothing to put in the + // parens, and that is the right answer, not a failure: `formatToolCall` + // renders a preview-less call as a bare label, which is exactly what the + // original shows for `⏺ TodoWrite` and `⏺ ExitPlanMode` — both of which draw + // their real content (the checklist, the plan under review) below the row. + return project(entries.filter(([key]) => LABEL_KEYS.has(key))) } diff --git a/test/e2e/probe-llm.mjs b/test/e2e/probe-llm.mjs index 10410af..8794617 100644 --- a/test/e2e/probe-llm.mjs +++ b/test/e2e/probe-llm.mjs @@ -32,17 +32,27 @@ class ProbeAdapter extends LlmAdapter { .slice(lastUserIndex(msgs) + 1) .some(m => Array.isArray(m.content) && m.content.some(b => b.type === 'tool-result')) - const m = /^PROBE\s+(\S+)\s+([\s\S]*)$/.exec(user.trim()) + // `PROBE `, or several separated by ` ;; ` to exercise the + // collapsed brief, which only reads right across a RUN of calls. + const calls = user + .trim() + .split(' ;; ') + .map(part => /^PROBE\s+(\S+)\s*([\s\S]*)$/.exec(part.trim())) + .filter(Boolean) + + if (calls.length && !hasToolResult) { + for (const [index, m] of calls.entries()) { + const name = m[1] + const args = m[2].trim() || '{}' + const id = `probe-${++seq}` + yield { blockType: 'tool-call', index, type: 'block-start' } + yield { argumentsDelta: args, id, name, type: 'tool-call-delta', index } + yield { block: { arguments: args, id, name, type: 'tool-call' }, index, type: 'block-end' } + } - if (m && !hasToolResult) { - const name = m[1] - const args = m[2].trim() || '{}' - const id = `probe-${++seq}` - yield { blockType: 'tool-call', index: 0, type: 'block-start' } - yield { argumentsDelta: args, id, name, type: 'tool-call-delta', index: 0 } - yield { block: { arguments: args, id, name, type: 'tool-call' }, index: 0, type: 'block-end' } yield { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } } yield { reason: { kind: 'tool-calls' }, type: 'finish' } + return }