Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions scripts/tool-gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions src/__tests__/harnessClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GatewayEvent, { type: 'approval.request' }>

// 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()

Expand Down
3 changes: 2 additions & 1 deletion src/components/prompts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -135,7 +136,7 @@ export function ApprovalPrompt({ cols = 80, onChoice, req, t }: ApprovalPromptPr

return (
<Box borderColor={t.color.warn} borderStyle="round" flexDirection="column" paddingX={1}>
<Text bold color={t.color.warn}>{req.toolName} command</Text>
<Text bold color={t.color.warn}>{toolTrailLabel(req.toolName)} command</Text>

<Box flexDirection="column" paddingLeft={1}>
{shown.map((line, i) => (
Expand Down
10 changes: 8 additions & 2 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>()
private callRawArgs = new Map<string, string>()
/** Live subagents, keyed by the child's own session id. */
Expand Down Expand Up @@ -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))) {
Expand Down Expand Up @@ -1008,7 +1009,12 @@ export class HarnessGatewayClient extends GatewayClient {

private parkApproval(req: ApprovalRequest): Promise<ApprovalOutcome> {
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<ApprovalOutcome>(resolve => {
this.gateApproval = { resolve }
Expand Down
Loading