Skip to content

Commit 8697dbe

Browse files
committed
fix(levelcode-ai): correct the group card's live state, aggregate order and labels
Four defects found by re-reading the reference transcript against what the grouping actually did: 1. The aggregate read its clauses backwards ("Ran 2 commands, read and edited PLAN.md"). Files lead, commands trail; a lone command is "a command", not "1 command". 2. Groups were created collapsed, which hid the output of the step the header claims to be running — the opposite of the fixed-footprint behavior it was meant to give. The group body now stays open and each member collapses to a one-line row as it finishes (collapseMember), so only finished work folds. A failing step re-opens its own row and the group; the single-member unwrap hands the card back expanded. 3. read_file and search derived their labels from arguments ("Read src/x.js") where the reference shows the model's own sentence. Both now take a required explanation, same as run_command; it titles the row and the raw tool text drops to the tooltip. 4. None of the DOM behavior was executed by a test — only the pure grammar functions and a syntax compile, which is why 2 shipped. test/groupReducer.js extracts the real reducer functions and drives them through a fake DOM: live header, member collapse, failure re-open, user-stop-is-not-failure, unwrap, late background exit, idempotent close, grouping-off, label/tooltip. Full extension gate green (20 suites).
1 parent a1ea803 commit 8697dbe

5 files changed

Lines changed: 334 additions & 52 deletions

File tree

docs/CALM-TRANSCRIPT.md

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# LevelCode — Calm narrative transcript (voice + grouped activity) — scope & plan
22

33
The goal, in one sentence: make an agent run read like a colleague narrating their work — short
4-
prose between actions, and consecutive actions folded into one collapsed, expandable card whose
5-
header shows the *live* step while running and a past-tense aggregate ("Ran 2 commands, read and
6-
edited PLAN.md +56 −0") when done — instead of a flat scroll of chips and cards.
4+
prose between actions, and consecutive actions folded into one expandable card whose header shows
5+
the *live* step while running and a past-tense aggregate ("Read and edited PLAN.md +56 −0, ran 2
6+
commands") when done — instead of a flat scroll of chips and cards.
7+
8+
The group card itself is never collapsed while it runs: its *members* collapse to one-line rows as
9+
they finish, so the live step's output stays readable at a fixed vertical footprint. Clause order
10+
in the aggregate is files first, commands last, matching the reference transcript.
711

812
Reference behavior: Claude Code's transcript (screenshots in the design discussion, 2026-07-23).
913
Two halves, shipped as separate slices: the **voice** (system prompt) and the **grouping** (webview).
@@ -77,17 +81,19 @@ rebase-safe, and revertable (one function + CSS).
7781

7882
### D5 — Two header states, exactly like the reference.
7983
While the group is open (agent still acting, no closing event yet): header = the **live** step's
80-
label in present-progressive + the working spinner; completed members are folded away — fixed
81-
vertical footprint while running. When the group closes: header flips to the past-tense
82-
**aggregate**: verb-category counts, same-file read+edit merged ("read and edited PLAN.md"),
83-
summed diffstat, fallback "N steps" when the sentence would get awkward. A single-member group
84-
renders the member bare — no group chrome.
85-
86-
### D6 — Labels: derived for file ops, model-written for commands.
87-
File ops derive labels from args + result ("Read PLAN.md", "Edited PLAN.md +56 −0"). For
88-
commands, `explanation` becomes effectively mandatory: schema text specifies "5–10 words, active
84+
label in present-progressive + the working spinner. The *members* fold: each finished step
85+
collapses to a one-line row while the running one keeps its body open, so the footprint stays
86+
fixed without hiding the output the user is watching. The group card itself is never born
87+
collapsed. When the group closes: header flips to the past-tense **aggregate** — file clauses
88+
first, then searches, then commands ("Read and edited PLAN.md, ran 2 commands"), same-file
89+
read+edit merged, summed diffstat, fallback "N steps" when the sentence would get awkward. A
90+
single-member group unwraps and hands the card back **expanded** — no group chrome.
91+
92+
### D6 — Labels: model-written, for every tool that has a story to tell.
93+
`run_command`, `read_file` and `search` all take a required `explanation`: "5–10 words, active
8994
voice, what it does — e.g. 'Find the insertion point in section 10'", and the voice rules require
90-
it. UI falls back to the first command segment when absent (older transcripts, weaker models).
95+
it. That sentence titles the row; the raw tool text (`read src/agent.js`) becomes the tooltip. UI
96+
falls back to deriving a label from the arguments when absent (older transcripts, weaker models).
9197
Tense: a small verb map (Run/Ran/Running, Read, Edit, Verify, Search, Create, Delete, Install,
9298
Check ~a dozen) converts imperative → progressive/past; unknown verbs render as-is. No grammar
9399
engine.
@@ -110,18 +116,19 @@ Rewrite `SYSTEM_BASE`'s communication rules in `agent.js`:
110116
change it — never a decision buried in prose.
111117
- Finish: a `Done:` line followed by a short structured wrap-up (what landed / how verified /
112118
what's next) for substantial runs; one line stays enough for trivial ones.
113-
- `run_command` MUST carry `explanation` (D6 wording).
119+
- `run_command`, `read_file` and `search` MUST carry `explanation` (D6 wording).
114120
Keep verbatim: decisiveness, words-are-not-edits, same-turn action, skills/plan/multi-root/
115-
autopilot blocks. Update `run_command`'s schema `explanation` description (D6).
121+
autopilot blocks. Update those three schemas' `explanation` descriptions (D6).
116122
*Test:* existing suites green (prompt text is data); manual acceptance below.
117123

118124
**S2 — The group reducer (webview).** *(M)*
119-
In `chat.html`: `openGroup()/routeToGroup(el, meta)/closeGroup()`; route `addAgentLine`,
125+
In `chat.html`: `openGroup()/groupAppend(el, step)/closeGroup()`; route `addAgentLine`,
120126
`addTermRun`, `addEditCard`, verify cards through it; close on text bubble / approval / questions
121127
/ error / `agentDone`. Group DOM: `.tl.tl-group` → header (chevron · label · Σdiffstat · state) +
122-
body (existing cards unchanged). D4 rules (breakout + auto-expand-on-failure). CSS for header +
123-
collapsed body.
124-
*Test:* extraction-pattern unit tests for the routing decisions where practical; manual.
128+
body (existing cards unchanged, each collapsed to a row by `collapseMember` once finished).
129+
D4 rules (breakout + auto-expand-on-failure). CSS for header + collapsed member rows.
130+
*Test:* `test/groupReducer.test.js` drives the real reducer functions against a zero-dep fake DOM —
131+
header states, member collapse, failure re-open, unwrap, late background exit, grouping-off.
125132

126133
**S3 — Labels, aggregate, tense (pure functions + wiring).** *(M)*
127134
`stepLabel(meta)`, `groupAggregate(steps)`, `verbForms(label)` as extractable pure functions in
@@ -152,10 +159,11 @@ detail); single-member degenerate case; a settings escape hatch
152159

153160
Run the acceptance task on a real repo (Claude model, agent mode, autopilot on):
154161
"add a section to docs/X.md, verify the edit landed cleanly, and check repo state".
155-
Pass when the transcript reads: short narration → **one collapsed card** titled with the live step
156-
while running ("Verifying the edit and checking repo state ⟳"), flipping on completion to
157-
"Ran 2 commands, read and edited X.md +N −0" → narration continues → a `Done:` wrap-up — and
158-
expanding the card shows the individual rows (semantic command labels, per-file diffstat), with
162+
Pass when the transcript reads: short narration → **one card** titled with the live step while
163+
running ("Verifying the edit and checking repo state ⟳"), its finished members folded to one-line
164+
rows beneath it while the running step's output stays visible, flipping on completion to
165+
"Read and edited X.md +N −0, ran 2 commands" → narration continues → a `Done:` wrap-up — and
166+
expanding a folded row shows its full output (semantic labels, per-file diffstat), with
159167
any approval card rendered outside the group and any failing step auto-expanded.
160168
Then re-run with grouping toggled off and confirm the flat timeline is unchanged.
161169

extensions/levelcode-ai/agent.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const SYSTEM_BASE = [
3131
'- Use delete_file to remove an existing file (e.g. during a refactor). To RENAME/move a file, write_file the new path then delete_file the old one. Deletions are reviewable (Keep/Undo) and restorable from the per-turn checkpoint.',
3232
'- Your file edits are APPLIED IMMEDIATELY and the user reviews them afterward in the editor with Keep/Undo — do NOT wait for approval, and do NOT re-edit a file you just edited. Only run_command still needs approval; if the user skips a command, adapt or stop.',
3333
'- Commands that do NOT exit on their own (dev servers, file watchers, tail -f) MUST be run with run_command background:true — it returns immediately so you keep working instead of hanging. After starting one, call read_command_output with the returned id to watch for a readiness/port line (e.g. "listening on :3000") before you test against it. Use a normal foreground run_command for things that finish (builds, installs, tests, git, curl). This pairs with verification: bring the app up in the background, confirm it serves, fix, repeat.',
34-
'- EVERY run_command MUST include "explanation": 5-10 words, active voice, imperative, saying what the command does ("Run the extension unit tests", "Find the insertion point in section 10"). It becomes this action\'s label in the user\'s activity view — never omit it.',
34+
'- EVERY run_command, read_file and search MUST include "explanation": 3-10 words, active voice, imperative, saying what you are doing and why ("Run the extension unit tests", "Find the insertion point in section 10", "Read the runAgent call site"). It becomes that action\'s label in the user\'s activity view — never omit it.',
3535
'- Paths are relative to the workspace root. In a MULTI-ROOT workspace (several top-level folders), paths from list_files/search are prefixed with the folder name (e.g. "thin.ly/app/models/link.rb") — use them exactly as shown; an unprefixed path resolves against the first folder. To create a file in a specific folder, prefix its name. run_command accepts an optional "folder" to pick which folder it runs in.',
3636
'- For a multi-step goal, call update_plan FIRST with a short checklist (3-8 short items, all "pending"), then call it again to set an item "in_progress" when you start it and "done" when finished. Skip the plan for trivial single-step goals.',
3737
'- If the goal truly depends on a decision only the user can make (tech stack, scope, where to create files, must-have features), call ask_user ONCE with concise multiple-choice questions (a short header + 2-4 concrete options each) INSTEAD of writing the questions as prose. Put your RECOMMENDED option FIRST and use its description to say why — and, when it matters, what would change your mind. Then act on their answers and do not ask again. Do NOT ask about things you can reasonably decide yourself — prefer a sensible default and proceed.',
@@ -42,8 +42,8 @@ const SYSTEM_BASE = [
4242

4343
const TOOLS = [
4444
{ name: 'list_files', description: 'List workspace files (optional glob like "**/*.js"). Excludes node_modules/.git/build dirs.', input_schema: { type: 'object', properties: { glob: { type: 'string' } } } },
45-
{ name: 'read_file', description: 'Read a workspace file (path relative to the workspace root; in a multi-root workspace use the folder-name prefix exactly as list_files shows it).', input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
46-
{ name: 'search', description: 'Search file contents for a literal string. Returns file:line snippets.', input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
45+
{ name: 'read_file', description: 'Read a workspace file (path relative to the workspace root; in a multi-root workspace use the folder-name prefix exactly as list_files shows it).', input_schema: { type: 'object', properties: { path: { type: 'string' }, explanation: { type: 'string', description: 'ALWAYS provide: 3-8 words, active voice, imperative — WHY you are reading this ("Read the runAgent call site", "Read agent.js tool definitions"). Shown to the user as this action\'s label.' } }, required: ['path'] } },
46+
{ name: 'search', description: 'Search file contents for a literal string. Returns file:line snippets.', input_schema: { type: 'object', properties: { query: { type: 'string' }, explanation: { type: 'string', description: 'ALWAYS provide: 3-8 words, active voice, imperative — what you are looking for ("Find every postMessage call site"). Shown to the user as this action\'s label.' } }, required: ['query'] } },
4747
{ name: 'update_plan', description: 'Declare or update your task checklist for a multi-step goal. Pass the FULL list each time, each item with a status. Call it once up front (all pending), then again to mark an item in_progress when you start it and done when finished. Skip for trivial single-step goals.', input_schema: { type: 'object', properties: { todos: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, status: { type: 'string', enum: ['pending', 'in_progress', 'done'] } }, required: ['title', 'status'] } } }, required: ['todos'] } },
4848
{ name: 'edit_file', description: 'Make a targeted edit to an EXISTING file: replace an exact, unique snippet (old_str) with new_str. Applied immediately; the user reviews it with Keep/Undo. old_str must appear exactly once — include enough surrounding context to be unique.', input_schema: { type: 'object', properties: { path: { type: 'string' }, old_str: { type: 'string' }, new_str: { type: 'string' } }, required: ['path', 'old_str', 'new_str'] } },
4949
{ name: 'write_file', description: 'Create a new file (or fully overwrite a short one) with the COMPLETE content. For edits to existing files, prefer edit_file. Applied immediately; the user reviews it with Keep/Undo.', input_schema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
@@ -276,7 +276,9 @@ async function runTool(tu, ctx) {
276276
return uris.map((u) => vscode.workspace.asRelativePath(u)).join('\n') || '(no files)';
277277
}
278278
if (tu.name === 'read_file') {
279-
ctx.post({ type: 'agentTool', icon: 'file', text: 'read ' + input.path });
279+
// label/path ride alongside the legacy `text` so the chat can title this action with the
280+
// model's own words ("Read the runAgent call site") and still attribute it to a file.
281+
ctx.post({ type: 'agentTool', icon: 'file', text: 'read ' + input.path, label: input.explanation || '', path: input.path, kind: 'read' });
280282
const abs = resolveWorkspacePath(input.path || '', { mustExist: true });
281283
if (!abs) { return 'ERROR: file not found: ' + input.path + whereHint(); }
282284
if (isBinaryFile(abs)) { return 'ERROR: ' + input.path + ' looks like a binary file — not reading it as text.'; }
@@ -285,7 +287,7 @@ async function runTool(tu, ctx) {
285287
return body;
286288
}
287289
if (tu.name === 'search') {
288-
ctx.post({ type: 'agentTool', icon: 'search', text: 'search "' + input.query + '"' });
290+
ctx.post({ type: 'agentTool', icon: 'search', text: 'search "' + input.query + '"', label: input.explanation || '', kind: 'search' });
289291
// Multi-root: search EVERY workspace folder, prefixing hits with the folder name so the
290292
// model can hand the paths straight back to read_file/edit_file.
291293
const folders = workspaceFolderList();

0 commit comments

Comments
 (0)