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
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,17 @@ an `- id: cctui` config override in the profile's `cordis.patch.yml`: `provider`

- **Conversation loop**: streamed deltas render live; reasoning behind Ctrl+O/Ctrl+R density
toggles; busy verbs and spinners; Esc interrupts (Ctrl+C never kills the app).
- **Tools**: Claude-style `⏺ Tool(args)` / `⎿ result` trail; write/edit diffs render as
structured diff cards; sandbox-escalation approvals pop the approval box (`1` approve /
`2`+Enter deny); todo lists pin under the busy line.
- **Tools**: Claude-style `⏺ Tool(args)` / `⎿ result` trail, each result summarised the way
the original does it — `Read 3 lines`, `Found 37 files`, `Received 1.2KB (200 OK)`,
`Did 1 search in 2s` — with a failed call carrying the tool's own message and a running one
its own clock; write/edit diffs render as structured diff cards; sandbox-escalation
approvals pop the approval box (`1` approve / `2`+Enter deny); todo lists pin under the
busy line.
- **Subagents**: a delegation reads `⏺ Subagent(Review the diff)` / `⎿ Done (2 tool uses ·
1.2k tokens · 11s)`, and its children stream into `/agents` — goal, live tool calls,
tokens, spawn tree.
- **Sessions**: `/sessions` (Ctrl+X) lists live and persisted sessions; `/resume <id>`
replays a persisted transcript; `/new`, `/title`, `/rename`.
replays a persisted transcript, tool results and all; `/new`, `/title`, `/rename`.
- **Commands**: every harness `ctx.commands` entry (e.g. `/plan`, `/goal`) appears in the
completion menu and dispatches through the harness; `/model` opens the picker backed by
the llm catalog; `/effort`, `/context`, `/usage`, `/help`, `/status`.
Expand Down
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.15",
"version": "0.3.16",
"description": "Claude-Code-style terminal UI for deepseek-harness, ported from clawcodex ui-tui",
"license": "MIT",
"repository": {
Expand Down
24 changes: 23 additions & 1 deletion scripts/tool-gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
'PROBE bash {"command": "echo escalated", "description": "an escalating echo",'
' "sandbox_permissions": "danger-full-access", "justification": "the gallery needs the approval box"}'
),
# a failure whose output overruns the row's safety cap
"bash_fail_long": (
'PROBE bash {"command": "seq 1 40 >&2; exit 1", "description": "a noisy failure"}'
),
"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 Expand Up @@ -98,6 +102,12 @@
'PROBE workflow {"script": "log(\\"hello from the workflow\\")\\nreturn { ok: true }",'
' "meta": {"name": "gap-probe", "description": "a one-line workflow that returns immediately"}}'
),
"subagent_fork": (
'PROBE subagent_fork {"description": "Check the fork path", "prompt": "Say ok and stop."}'
),
"job_kill": 'PROBE job_kill {"job_id": "no-such-job", "reason": "gallery probe"}',
"interrupt_agent": 'PROBE interrupt_agent {}',
"update_goal": 'PROBE update_goal {"goal_id": "no-such-goal", "revision": 1, "action": "complete"}',
"skill": 'PROBE skill {"name": "nonexistent-skill"}',
"read_image": 'PROBE read_image {"file_path": "e2e-scratch/gallery.png"}',
# this one insists on an absolute path
Expand Down Expand Up @@ -128,6 +138,12 @@
}


# Scenarios that deliberately park on a prompt — the approval box, the plan
# review — never settle on their own, so a no-argument sweep would sit through
# the idle timeout for each. Name them explicitly (with --live) to capture them.
LIVE_ONLY = {"bash_escalate", "exit_plan_mode"}


def bootstrap() -> None:
# the mutation scenarios must CREATE their file, not find yesterday's
for leftover in (ROOT / "e2e-scratch").glob("gallery-*.txt"):
Expand Down Expand Up @@ -299,6 +315,12 @@ def capture(scenario: str, expand: bool, live: float | None = None) -> str | Non
for index, prompt in enumerate(prompts):
session.send(prompt.encode())
session.pump(0.8)
if prompt.startswith("/"):
# a slash word opens the completion menu, whose Enter ACCEPTS
# rather than submits; Esc dismisses it first (nothing is
# running yet, so Esc cannot interrupt a turn here)
session.send(b"\x1b")
session.pump(0.4)
session.send(b"\r")
session.wait_for(TOOL_GLYPH, 60)
if live is not None and index == len(prompts) - 1:
Expand Down Expand Up @@ -334,7 +356,7 @@ def main() -> int:
outdir.mkdir(parents=True, exist_ok=True)
bootstrap()

for scenario in args.scenarios or list(SCENARIOS):
for scenario in args.scenarios or [name for name in SCENARIOS if name not in LIVE_ONLY]:
shot = capture(scenario, args.expand, args.live)
if shot is None:
continue
Expand Down
13 changes: 11 additions & 2 deletions src/__tests__/text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,17 @@ describe('buildToolTrailLine', () => {
const line = buildToolTrailLine('Bash', 'x', false, big)
const detail = parseToolTrailResultLine(line)!.detail

expect(detail.split('\n')).toHaveLength(13) // 12 + trailing …
expect(detail.endsWith('…')).toBe(true)
// 12 kept + a marker that says how much went, not a bare … that reads as
// "a bit more" whether three lines were dropped or three hundred
expect(detail.split('\n')).toHaveLength(13)
expect(detail.endsWith('… +28 lines')).toBe(true)
})

it('reads the truncation marker in the singular when one line went', () => {
const big = Array.from({ length: 13 }, (_, i) => `l${i}`).join('\n')
const detail = parseToolTrailResultLine(buildToolTrailLine('Bash', 'x', false, big))!.detail

expect(detail.endsWith('… +1 line')).toBe(true)
})

it('anchors the call/detail split so args containing :: cannot mis-split', () => {
Expand Down
12 changes: 8 additions & 4 deletions src/domain/toolArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* ⏺ Search(pattern: "presentResult", path: "src")
* ⏺ Write(notes.txt)
*
* Two rules produce all four shapes. Render the arguments as `key: value`
* One rule produces all four shapes: render the arguments as `key: value`
* pairs, and collapse to the bare value when only one pair survives. What
* "survives" is the interesting part: an argument carrying BULK (a file's new
* content, an edit's replacement text, a todo list) or POLICY (a sandbox
Expand All @@ -20,6 +20,10 @@
* `bash(command, description, sandbox_permissions, justification)` reduces to
* `Bash(ls -1 src)` — no per-tool table, no tool names hard-coded here.
*
* Two refinements keep that rule honest against real argument shapes: a
* `description` is held back until nothing sharper survives (see LABEL_KEYS),
* and an object argument is identified by its own `name` (see identify).
*
* Deliberately derived from the raw arguments rather than the harness's own
* `presentCall` title: the title restates the tool's verb ("Read src/a.ts"),
* which the row already prints, and a shell command that happens to start with
Expand All @@ -29,9 +33,9 @@
/**
* Arguments that never identify a call. Bulk payloads (a file body, an edit's
* before/after, a checklist, a delegation's brief, a plan under review, a
* workflow's script) 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.
* workflow's script) 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.
*/
const NOISE_KEYS: ReadonlySet<string> = new Set([
// bulk payloads
Expand Down
11 changes: 9 additions & 2 deletions src/lib/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,15 @@ export const buildToolTrailLine = (
const raw = (note ?? '').trim()
const lines = raw.split('\n')

const detail = (lines.length > TRAIL_DETAIL_MAX_LINES
? [...lines.slice(0, TRAIL_DETAIL_MAX_LINES), '…']
// A bare `…` says something was cut without saying how much, which reads as
// "a bit more" whether three lines went or three hundred. Upstream's own
// truncation marker counts what it dropped, and so does the verbose block
// below; this now matches them. Deliberately NOT "(ctrl+o to expand)": a
// result the gateway retained no raw copy of expands to the same truncated
// text, and pointing at a key that changes nothing is worse than silence.
const dropped = lines.length - TRAIL_DETAIL_MAX_LINES
const detail = (dropped > 0
? [...lines.slice(0, TRAIL_DETAIL_MAX_LINES), `… +${fmtK(dropped)} ${dropped === 1 ? 'line' : 'lines'}`]
: lines
)
.join('\n')
Expand Down
Loading