From 14a667c1551c911925c6dbbde555964aefb94974 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Wed, 19 Aug 2026 07:59:16 -0700 Subject: [PATCH] fix(approval): ask about the command, not the paperwork around it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The approval box is the one screen where a misleading line has a cost, and it was showing the whole argument object — with the command it is asking about crowded off the end: ╭──────────────────────────────────────────────────────────────────────╮ │ bash command │ │ command: echo escalated, description: an escalating echo, sandbox_… │ │ the gallery needs the approval box │ │ ⚠ escalate sandbox to danger-full-access: the gallery needs the … │ The escalation fields are already the warning row's job, stated right below with their justification, so the command line repeated them and then truncated mid-word. Now it carries the salient argument — the same projection the `⏺ Tool(args)` row uses, which for a shell call is exactly the command: ╭──────────────────────────────────────────────────────────────────────╮ │ Bash command │ │ echo escalated │ │ ⚠ escalate sandbox to danger-full-access: the gallery needs the … │ │ Do you want to proceed? │ │ ❯ 1. Yes │ │ 2. No │ The header takes the display label too — `Bash command`, not `bash command` — the same `toolTrailLabel` the trail already applies to the wire name everywhere else. Co-Authored-By: Claude Opus 5 --- package-lock.json | 4 ++-- package.json | 2 +- scripts/tool-gallery.py | 5 +++++ src/__tests__/harnessClient.test.ts | 32 +++++++++++++++++++++++++++++ src/components/prompts.tsx | 3 ++- src/harness/client.ts | 10 +++++++-- 6 files changed, 50 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0c617b..1319ed9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dsh-cctui", - "version": "0.3.12", + "version": "0.3.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsh-cctui", - "version": "0.3.12", + "version": "0.3.13", "license": "MIT", "dependencies": { "@dsh-cctui/ink": "file:./packages/dsh-cctui-ink", diff --git a/package.json b/package.json index 605d331..7004d28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-cctui", - "version": "0.3.12", + "version": "0.3.13", "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 aed5df8..dfacd52 100755 --- a/scripts/tool-gallery.py +++ b/scripts/tool-gallery.py @@ -62,6 +62,11 @@ 'PROBE bash {"command": "for i in 1 2 3 4 5 6 7 8; do echo line-$i; sleep 1; done",' ' "description": "print 8 lines with a 1s delay"}' ), + # a sandbox escalation, which pops the approval box (pair with --live) + "bash_escalate": ( + 'PROBE bash {"command": "echo escalated", "description": "an escalating echo",' + ' "sandbox_permissions": "danger-full-access", "justification": "the gallery needs the approval box"}' + ), "bash_fail": 'PROBE bash {"command": "ls /no/such/dir", "description": "failing ls"}', "write": 'PROBE write {"file_path": "e2e-scratch/gallery-write.txt", "content": "alpha\\nbeta\\ngamma\\n"}', # the harness refuses to edit a file this session has not observed, so the diff --git a/src/__tests__/harnessClient.test.ts b/src/__tests__/harnessClient.test.ts index c8e6032..370c23d 100644 --- a/src/__tests__/harnessClient.test.ts +++ b/src/__tests__/harnessClient.test.ts @@ -804,6 +804,38 @@ describe('HarnessGatewayClient', () => { await expect(denied).resolves.toBe('rejected') }) + it('asks about the command, not the escalation paperwork around it', async () => { + const w = makeWorld() + + w.client.start() + await settle() + w.fire('turn/start', { turn: 1 }) + w.fire('tool/call', { + arguments: + '{"command":"echo escalated","description":"an escalating echo","sandbox_permissions":"danger-full-access","justification":"the gallery needs the approval box"}', + callId: 'c7', + name: 'bash', + step: 1, + turn: 1 + }) + + const handler = (w.listeners.get('approval/request') ?? [])[0]! + + void handler( + { agent: w.agent, callId: 'c7', reason: 'escalate sandbox', toolName: 'bash' }, + () => Promise.resolve('unavailable') + ) + await settle() + + const ask = w.events.at(-1) as Extract + + // this line is what the user reads before saying yes; the escalation and + // its justification already have their own warning row below it + expect(ask.payload.command).toBe('echo escalated') + + await w.client.request('approval.respond', { choice: 'deny' }) + }) + it('delegates approvals for other agents down the chain', async () => { const w = makeWorld() diff --git a/src/components/prompts.tsx b/src/components/prompts.tsx index 0012e78..ce08a4e 100644 --- a/src/components/prompts.tsx +++ b/src/components/prompts.tsx @@ -2,6 +2,7 @@ import { Box, Text, useInput, wrapAnsi } from '@dsh-cctui/ink' import { useState } from 'react' import { isMac } from '../lib/platform.js' +import { toolTrailLabel } from '../lib/text.js' import type { Theme } from '../theme.js' import type { ApprovalReq, ClarifyReq, ConfirmReq, PlanApprovalReq } from '../types.js' @@ -135,7 +136,7 @@ export function ApprovalPrompt({ cols = 80, onChoice, req, t }: ApprovalPromptPr return ( - {req.toolName} command + {toolTrailLabel(req.toolName)} command {shown.map((line, i) => ( diff --git a/src/harness/client.ts b/src/harness/client.ts index 78bad23..34a2cdd 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -327,6 +327,7 @@ export class HarnessGatewayClient extends GatewayClient { private usageTotals: Usage = { calls: 0, input: 0, output: 0, total: 0 } private turnCount = 0 private permissionMode = 'default' + /** Per-call argument preview, for the approval box's command line. */ private callArgs = new Map() private callRawArgs = new Map() /** Live subagents, keyed by the child's own session id. */ @@ -829,7 +830,7 @@ export class HarnessGatewayClient extends GatewayClient { this.callNames.set(id, name) this.callStarted.set(id, Date.now()) - this.callArgs.set(id, prettyArgs(rawArgs)) + this.callArgs.set(id, toolArgsPreview(rawArgs)) this.callRawArgs.set(id, rawArgs) if (isDelegationCall(toolTrailLabel(name))) { @@ -1008,7 +1009,12 @@ export class HarnessGatewayClient extends GatewayClient { private parkApproval(req: ApprovalRequest): Promise { const id = req.callId ? String(req.callId) : '' - const command = (id ? this.callArgs.get(id) : undefined) ?? req.reason ?? req.toolName + // The SALIENT argument, not the whole argument object. This line is what + // the user actually reads before saying yes, and a shell call's escalation + // fields (`sandbox_permissions`, `justification`) crowded the command it is + // asking about off the end of it — while the warning line below already + // states the escalation and its justification. + const command = (id ? this.callArgs.get(id) : undefined) || req.reason || req.toolName return new Promise(resolve => { this.gateApproval = { resolve }