feat(ui): drop the slash and the argument echo from the command row

The web command row renders `title · summary` from one logged command
lifecycle pair, and the two halves were written without knowing about each
other: the title was the dispatched line rebuilt from `command/run` and the
summary was `command/done`'s verbatim text, so every Access-chip pick read
`/permission workspace-write · Permission preset: workspace-write.` — the
command name twice and its argument twice.

The title is now the bare command name (no `/`, no arguments — the summary
already says what the command did), and a command handler's settlement text
never repeats the command's own name, so `/permission` returns `preset
workspace-write`. The row reads `permission · preset workspace-write`, and
the TUI notice still names the preset that now applies. The log is
unchanged: `command/run` keeps its structured name/args split for a richer
registered row.
This commit is contained in:
creatixchu
2026-07-30 17:34:03 +08:00
parent 6686f47945
commit d9cb19f276
11 changed files with 163 additions and 18 deletions

View File

@@ -3,7 +3,9 @@
// else covers: sidebar cold listing, the implicit resume/attach inside the
// history RPC, history-page tool views, and the client fold of historical
// events — with ZERO model calls in replay (no replay fixture; a stray stream
// fails loud on the open llm seam). The seed is a recorded fixture under the
// fails loud on the open llm seam). The cold session also carries the one
// keyless command-row surface: an Access-chip pick runs `/permission` on the
// host, so the settled row's copy has a golden here. The seed is a recorded fixture under the
// same record discipline as every other: DSH_SNAPSHOT=record drives the turn
// live through the composer (real read tool against seeded workspace files)
// and harvests seed.jsonl; replay/refresh seed it cold and only render.
@@ -22,6 +24,9 @@ import { newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/seeded-history', import.meta.url))
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/ui.expected.md', import.meta.url))
// The command-row golden: the same conversation after one /permission switch,
// which is the only surface that shows a settled command row's copy.
const COMMAND_ROW_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/command-row.expected.md', import.meta.url))
const MODE = webSnapshotMode()
const SEED_ID = 'seeded-history-web-e2e'
@@ -147,11 +152,30 @@ describe('web e2e: seeded history renders through cold resume', () => {
await expect.poll(() => page.getByText('a.txt', { exact: false }).count(), { timeout: 5_000 }).toBeGreaterThan(0)
})
it.skipIf(MODE === 'record')('an Access-chip switch lands one command row: bare name, non-repeating settlement text', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-command-row'))
// The Access chip submits `/permission <preset>` — a host command with no
// model call, so the settled row renders keylessly over this cold history.
// The row copy is the assertion: `permission · preset workspace-write`,
// where neither half repeats the other (the dispatched `/` and its
// argument stay out of the title, and the settlement text never restates
// the command's own name).
await page.getByRole('button', { name: 'Access mode, current: Danger Full Access' }).click()
await page.getByRole('menuitem', { name: 'Workspace Write' }).click()
await page.getByRole('button', { name: 'Access mode, current: Workspace Write' }).waitFor({ timeout: 10_000 })
await expect.poll(() => page.getByText('preset workspace-write', { exact: true }).count(), { timeout: 10_000 }).toBe(1)
expect(await page.getByText('permission', { exact: true }).count()).toBe(1)
expect(await page.getByText('/permission workspace-write', { exact: true }).count()).toBe(0)
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(COMMAND_ROW_EXPECTED, snapshot, MODE)
}, 60_000)
it.skipIf(MODE === 'record')('issued zero model calls and stayed clean', async () => {
// No replay fixture was installed and the llm seam is open — any stray
// stream would have failed the turn loudly. Cleanliness pins the wire.
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['seed.jsonl', 'ui.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, ['command-row.expected.md', 'seed.jsonl', 'ui.expected.md'])
})
})

View File

@@ -0,0 +1,45 @@
- banner:
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
- button "复制":
- img
- button "在新对话中分支":
- img
- button "编辑":
- img
- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.":
- img
- img
- text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel.
- img
- text: Read
- button "a.txt"
- img
- text: Read
- button "b.txt"
- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed.":
- img
- img
- text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed.
- paragraph: DONE
- button "复制":
- img
- button "在新对话中分支":
- img
- text: {{clock}}
- img
- text: permission preset workspace-write
- textbox "Message the agent"
- button "Add attachment":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Plan mode off, press to turn on": Plan off
- button "Select model, current deepseek-v4-flash":
- text: deepseek-v4-flash
- img
- button "Send message" [disabled]
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 98% Input 15.8K tok · Output 135 tok