feat(ai): calm transcript — narrative voice, grouped activity, and chat-UI polish#32
Conversation
SYSTEM_BASE gains the pair-working register: narrate briefly before a tool batch and interpret results after (always acting in the SAME turn — narration never replaces action, so the anti-stall rules keep their teeth), failures as findings with a one-line Correction: pattern, ask_user options ordered with the recommendation first and the why in its description, and a Done: line followed by a short wrap-up for substantial runs (the finish regex already accepts it). run_command's explanation becomes effectively mandatory (prompt rule + schema description): 5-10 imperative words that become the action's label in the activity view — the raw command stays behind the expandable card.
Consecutive tool actions now fold into ONE collapsible card (docs/CALM-TRANSCRIPT.md).
While the group is open its header shows the LIVE step in present-progressive
("Verifying the edit and checking repo state") with a fixed vertical footprint —
completed members fold away instead of scrolling the log. When narration, an
approval, questions, an error, or run-end closes the group, the header flips to a
past-tense aggregate ("Ran 2 commands, read and edited PLAN.md +56 -0", counts
merged per category, same-file read+edit folded, summed diffstat).
The reducer lives entirely in the webview — the extension host posts the same
events as ever. Safety rules (D4): approvals and ask_user questions render at top
level, never inside a group; a failing member (nonzero exit, timeout, verify fail)
marks the group failed and auto-expands it — collapse is for success; a user-stop
is a choice, not a failure. A running member's Stop is delegated to a header
button so a collapsed group never hides the control. Single-member groups unwrap
to the bare card. Background commands that exit after their group closed
re-finalize it in place.
Labels: file ops derive ("Read PLAN.md", "Edit path" → tensed); commands use the
model's explanation (imperative, per the S1 prompt) with the first command
segment as fallback. tenseLabel converts verbs at the start and after and/then/
comma from a small irregular-verb map — no grammar engine.
levelcode.ai.chat.groupActivity (default on) rides the config message; off
restores the flat timeline exactly.
test/narrativeUi.test.js pins the grammar via the shHighlight extraction pattern
(19 tests, including compiling chat.html's whole script to catch syntax slips).
Full extension gate: 19 suites green.
There was a problem hiding this comment.
Pull request overview
This PR implements the “calm transcript” spec (docs/CALM-TRANSCRIPT.md) for LevelCode AI agent runs: a more narrative agent voice (system prompt changes) plus a webview-only activity reducer that groups consecutive tool actions into a single collapsible “activity” card with live/progressive headers while running and aggregate/past-tense summaries when finished.
Changes:
- Updates the agent system prompt in
agent.jsto enforce a short narrative register and makerun_command.explanationeffectively mandatory. - Adds grouped-activity rendering + reducer logic to the chat webview (
media/chat.html), with a new toggle setting exposed via config plumbing. - Introduces unit tests that extract and validate the new pure UI functions directly from
chat.html, and documents the full design/spec indocs/CALM-TRANSCRIPT.md.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/CALM-TRANSCRIPT.md | Adds the detailed spec and rationale for calm transcript voice + grouped activity UI. |
| extensions/levelcode-ai/agent.js | Revises the agent system prompt to narrate actions and require command explanations. |
| extensions/levelcode-ai/extension.js | Sends the new groupActivity config flag to the webview. |
| extensions/levelcode-ai/media/chat.html | Implements grouped-activity UI (CSS + reducer + labeling/aggregation helpers) and config toggle handling. |
| extensions/levelcode-ai/package.json | Adds levelcode.ai.chat.groupActivity setting (default on) with description. |
| extensions/levelcode-ai/test/narrativeUi.test.js | Adds extraction-pattern unit tests for chat.html grouping/labeling functions and script compilation. |
Comments suppressed due to low confidence (1)
extensions/levelcode-ai/media/chat.html:1851
- If verifyCards[m.id] is missing (e.g. after checkpoint restore/pruning), verifyDoneFinish currently returns early and leaves verifySteps[m.id] behind. Clean up verifySteps even when the DOM card is gone to avoid leaking state across long sessions.
// Group bookkeeping: ok and unrunnable (a config issue, not the agent's code) close clean;
// a real verify failure marks the group failed and auto-expands it.
groupStepDone(verifySteps[m.id], !(m.ok || m.unrunnable));
delete verifySteps[m.id];
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| lastModelId = m.modelId || m.model || ''; // gateway sends modelId (the real id); BYOK model IS the id | ||
| if (typeof m.groupActivity === 'boolean'){ groupsOn = m.groupActivity; } // calm-transcript toggle | ||
| renderRouting(); |
There was a problem hiding this comment.
Fixed in b655f95. Setting groupActivity false now calls closeGroup() first, so any open group is finalized and the timeline returns fully flat immediately.
| let curGroup = null; // open group state, or null | ||
| const termSteps = {}; // run id → step (a command's exit updates its group later) | ||
| const editSteps = {}; // edit id → step (re-renders update the counts in place) | ||
| const verifySteps = {}; // verify id → step |
There was a problem hiding this comment.
Fixed in b655f95. editSteps[id] is now released in resolveEditCard alongside editCards[id], and all three step maps (edit/term/verify) are pruned on checkpoint restore next to their card maps.
| // Group bookkeeping: a user-stop is a choice, not a failure — only timeouts and nonzero exits | ||
| // mark the group failed (and auto-expand it). | ||
| groupStepDone(termSteps[m.id], m.how === 'timeout' || (m.how !== 'stopped' && m.code !== 0)); | ||
| delete termSteps[m.id]; |
There was a problem hiding this comment.
Fixed in b655f95. termExitFinish's early-return path now settles the group step and delete termSteps[id] before returning, so no step state is left behind when the card is gone.
…er 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).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
extensions/levelcode-ai/agent.js:51
run_command’sexplanationis described as “ALWAYS provide…”, but it’s not included in the tool schema’srequiredlist. If the model omits it, the command row/group header loses its semantic label (a key part of calm transcript).
{ name: 'run_command', description: 'Run a shell command in the workspace root (or a named workspace folder via "folder" in multi-root workspaces). Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string', description: 'ALWAYS provide: 5-10 words, active voice, imperative — what this command does ("Run the extension unit tests", "Find the insertion point in section 10"). Shown to the user as this action\'s label.' }, folder: { type: 'string', description: 'multi-root workspaces only: the workspace folder NAME to run in; defaults to the first folder' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command'] } },
| { 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'] } }, | ||
| { 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'] } }, |
There was a problem hiding this comment.
Fixed in b655f95. explanation added to required for both read_file and search (and run_command).
| function collapseMember(el){ | ||
| if (!el || !el.classList){ return; } | ||
| if (el.classList.contains('tl-cmd')){ el.classList.add('collapsed'); return; } | ||
| const det = el.querySelector && el.querySelector('details.diffwrap'); | ||
| if (det){ det.open = false; } | ||
| } |
There was a problem hiding this comment.
Fixed in b655f95. groupAppend no longer collapses a running member — it folds only non-running members on insert; groupStepDone folds a member once it completes. Verified in a rendered webview with an actually-running command: the running npm test card stays expanded with its streaming output while finished members fold to one-line rows.
| // Group bookkeeping: a user-stop is a choice, not a failure — only timeouts and nonzero exits | ||
| // mark the group failed (and auto-expand it). | ||
| groupStepDone(termSteps[m.id], m.how === 'timeout' || (m.how !== 'stopped' && m.code !== 0)); | ||
| delete termSteps[m.id]; |
There was a problem hiding this comment.
Fixed in b655f95. Collapse-on-success now happens at completion (groupStepDone, success branch), not at insertion — so groups stay compact but the active command remains readable while it runs.
| g.closed = true; | ||
| g.el.classList.remove('running'); | ||
| g.label.textContent = groupAggregate(g.steps); | ||
| g.counts.innerHTML = groupCountsHtml(g); | ||
| g.state.innerHTML = codicon(g.failed ? 'circle-slash' : 'check'); | ||
| g.state.className = 'cmdstate groupstate ' + (g.failed ? 'bad' : 'ok'); | ||
| g.stopBtn.hidden = true; | ||
| if (g.failed){ g.el.classList.remove('collapsed'); } |
There was a problem hiding this comment.
Fixed in b655f95. finalizeGroup now detects a still-running member: it keeps the live header and the Stop control and returns without settling; the member's later groupStepDone re-enters and completes the group. So a running background command is never shown as done and never loses its Stop button. Added a reducer test for this.
… one speaker label
From a real run: the grouping worked but the surrounding chrome undid it.
- A finished group never folded. `.tl-group.collapsed .groupbody{display:none}`
existed but finalizeGroup never applied it, so a closed group showed its
aggregate header AND every finished row — N+1 lines where the reference shows
one. Successful groups now fold to the summary line; failures stay open, and a
header the user clicked keeps the state they chose.
- A finished group kept wearing its spinner: the rail node stayed on the `sync`
glyph while a second check sat at the end of the header. The rail now carries
the outcome and the trailing state clears on success — one glyph, not three.
- Tool rows were monospace. That was right when they read `read src/agent.js`;
now they carry the model's own sentence, and prose in the editor font read
like a command line.
- "LEVELCODE AI" was stamped over every narration block — four times in one
turn, chopping a single train of thought into separate announcements. One
label per turn; continuations flow as prose with a tightened gap.
groupReducer covers all of it (14 tests): fold-on-success, userToggled respected,
failure stays open, rail glyph swap, label-once-per-turn, re-armed by a new user
message. Full gate green (20 suites).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
extensions/levelcode-ai/media/chat.html:1565
groupAppend()always callscollapseMember(el), andcollapseMember()collapses every.tl-cmdcard. SinceaddTermRun()creates command cards as.tl.tl-cmd.running, this hides the running command's.cmdbox/streamed output while the group is open (CSS:.tl-cmd.collapsed .cmdbox { display: none; }). That contradicts the spec’s “live step output stays readable” behavior and makes it hard to monitor long-running commands without extra clicks.
function groupAppend(el, step){
if (!groupsOn){ log.appendChild(el); return; }
const g = curGroup || openGroup();
collapseMember(el);
g.body.appendChild(el);
extensions/levelcode-ai/media/chat.html:1619
- After expanding a running command card (so output is visible), successful completion should re-collapse it back to a one-line row so the group footprint stays calm. Right now only failures auto-expand; success never folds members back down, so a finished command can remain expanded inside the group.
function groupStepDone(step, failed){
if (!step){ return; }
step.status = failed ? 'fail' : 'done';
const g = step.group; if (!g){ return; }
if (failed){
extensions/levelcode-ai/agent.js:34
- The new “explanation” guidance is internally inconsistent: SYSTEM_BASE requires 3–10 words for
run_command,read_file, andsearch, but the tool schemas specify 3–8 forread_file/searchand 5–10 forrun_command. Aligning these avoids confusing the model about what format is required.
'- 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.',
Spotted in a real run: a red stop button sat beside a finished group, offering
to stop a command that had already exited.
The reducer was right — finalizeGroup does set `stopBtn.hidden = true`. The CSS
was wrong: `.tl-group .groupstop { display: inline-flex }` outranks the UA
stylesheet's `[hidden] { display: none }`, so the attribute was inert and the
button had been painted since the group opened, running or not. chat.html
already carried four hand-written escapes for this same trap (#workbar,
.modewrap, .modemenu, .st-approvals); this was the missing fifth.
No DOM test could have caught it — the fake DOM has no stylesheet, so it
faithfully reported a hidden attribute the browser then ignored. So the guard is
static: webviewCss.test.js collects every element that ships the `hidden`
attribute and fails if a class rule forces `display` without a matching
`[hidden]` escape. Verified against the pre-fix file, where it flags .groupstop
and nothing else.
Full gate green (21 suites).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
extensions/levelcode-ai/agent.js:34
- SYSTEM_BASE says every run_command/read_file/search explanation should be “3-10 words”, but the tool schemas below specify 3–8 for read_file/search and 5–10 for run_command. This mismatch can confuse the model and makes the requirement harder to follow consistently; align the wording in one place (SYSTEM_BASE is the most visible).
'- 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.',
extensions/levelcode-ai/media/chat.html:1436
- The new step registries (termSteps/editSteps/verifySteps) appear to be never cleared when an edit is resolved (resolveEditCard deletes editCards[m.id] but not editSteps[m.id]) and they’re also not cleared on a chat reset. Over a long session this can retain stale step objects (and detached DOM nodes via step.card/group), and late exit messages can still try to update state that the UI has discarded.
const termSteps = {}; // run id → step (a command's exit updates its group later)
const editSteps = {}; // edit id → step (re-renders update the counts in place)
const verifySteps = {}; // verify id → step
Several questions rendered stacked in one card under a single "Send answers" button. Answering the first and missing the rest was easy — and the agent then proceeded on defaults the user never chose, which is worse than not asking. The card now shows one question at a time: - an `n of N` counter, Back to revise, and an advance button; - NOTHING is posted before the last question — the button advances until then; - skipping is deliberate: with nothing picked the button reads "Skip this one" (quiet style) and that answer posts as empty rather than silently defaulted; - on send the card collapses to "Answers recorded — click to review", unfolding to every question with what was picked; - one question keeps the plain card, no step chrome. Also fixes a specificity trap in the new CSS: `.questions.wizard .qblock.cur` (4 classes) outranked `.questions.collapsed .qblock` (3), so collapsing the card mid-wizard would have left the current question on screen. The override matches at equal weight and comes later; webviewCss pins the source order. askWizard.test.js (12 tests) clicks the real function through a fake DOM: no early post, skip-records-empty, Back preserves and revises, ordered payload, multiSelect, single-question path, and the collapsed record. Gate green (22).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
extensions/levelcode-ai/agent.js:45
read_fileis documented as requiring anexplanation(and downstream UI uses it as a label), but the tool schema still only requirespath. Makingexplanationrequired will better enforce the new contract across providers/models.
{ 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'] } },
extensions/levelcode-ai/agent.js:46
searchis documented as requiring anexplanation(and downstream UI uses it as a label), but the tool schema still only requiresquery. Makingexplanationrequired will better enforce the new contract across providers/models.
{ 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'] } },
| d.innerHTML = '<div class="tl-rail"><span class="tl-node">' + (IC[icon] ? codicon(icon) : esc(icon || '•')) + '</span></div>' | ||
| + '<div class="tl-body"><span class="toolt">' + esc(text || '') + '</span></div>'; | ||
| log.appendChild(d); scrollIfStuck(); | ||
| + '<div class="tl-body"><span class="toolt" title="' + esc(text || '') + '">' + esc(shown) + '</span></div>'; |
There was a problem hiding this comment.
Fixed in b655f95. The title now uses escAttr() (which escapes "), so search "foo" no longer breaks the attribute. Pinned with a test.
| { 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'] } }, | ||
| { name: 'delete_file', description: 'Delete an EXISTING workspace file (e.g. removing a file during a refactor). Applied immediately; the user reviews it with Keep/Undo, and the per-turn checkpoint can restore it. To RENAME or move a file: write_file the new path, then delete_file the old one.', input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } }, | ||
| { name: 'run_command', description: 'Run a shell command in the workspace root (or a named workspace folder via "folder" in multi-root workspaces). Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string' }, folder: { type: 'string', description: 'multi-root workspaces only: the workspace folder NAME to run in; defaults to the first folder' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command'] } }, | ||
| { name: 'run_command', description: 'Run a shell command in the workspace root (or a named workspace folder via "folder" in multi-root workspaces). Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string', description: 'ALWAYS provide: 5-10 words, active voice, imperative — what this command does ("Run the extension unit tests", "Find the insertion point in section 10"). Shown to the user as this action\'s label.' }, folder: { type: 'string', description: 'multi-root workspaces only: the workspace folder NAME to run in; defaults to the first folder' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command'] } }, |
There was a problem hiding this comment.
Fixed in b655f95. explanation is now in run_command's required.
…shed-group node Two polish issues from a real transcript: - File-reference chips (the "MD REPLIES.md" pills in prose) sat below the text baseline. `vertical-align: -2px` pushed them down and `line-height: 1.7` inflated their box; a line with a chip was also ~4px taller than its neighbours. Now `vertical-align: middle` with a trimmed box — measured centered to 1px on the text line, and 20px tall against a 24px line so it no longer stretches the line. - A finished group's rail node showed a blue check inside the accent-tinted circle — the accent fill is the *running* identity, and every other done tick (Kept, verified, command-ok) is a quiet green. The node now recolours on close: success speaks that same green in a neutral ring (.tl-group.gok), failure the error colour (.tl-group.gfailed). No coloured circle competing with the glyph. Verified in a dark-theme render of the real webview: chip midpoint 47px vs the text line's 46px, node colour rgb(78,201,122) = #4ec97a not the accent blue. groupReducer pins the gok/gfailed state classes. Gate green (22 suites).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (4)
extensions/levelcode-ai/media/chat.html:1599
- groupAppend() collapses every appended command card via collapseMember(), including currently-running commands/verify cards. Since .tl-cmd.collapsed hides .cmdbox, this prevents users from seeing live output while a step runs, contradicting the “running step stays readable” requirement.
function groupAppend(el, step){
if (!groupsOn){ log.appendChild(el); return; }
const g = curGroup || openGroup();
collapseMember(el);
g.body.appendChild(el);
extensions/levelcode-ai/media/chat.html:1654
- Successful running members are never collapsed when they finish. With the fix to keep running steps expanded, a command/verify card will stay expanded after completion, so the group won’t settle back into one-line rows. Collapsing successful members on completion keeps the group’s vertical footprint calm while still auto-expanding failures.
function groupStepDone(step, failed){
if (!step){ return; }
step.status = failed ? 'fail' : 'done';
const g = step.group; if (!g){ return; }
if (failed){
extensions/levelcode-ai/agent.js:46
- The system prompt says read_file/search MUST include an explanation (used as the activity label), but the tool input schemas don’t require it. Marking explanation as required makes the “calm transcript” labeling invariant enforceable at the schema level.
{ 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'] } },
{ 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'] } },
extensions/levelcode-ai/agent.js:51
- run_command’s explanation is described as “ALWAYS provide”, but the schema still only requires command. If explanation is required for the activity label, it should be required in the schema as well to prevent unlabeled command cards.
{ name: 'run_command', description: 'Run a shell command in the workspace root (or a named workspace folder via "folder" in multi-root workspaces). Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string', description: 'ALWAYS provide: 5-10 words, active voice, imperative — what this command does ("Run the extension unit tests", "Find the insertion point in section 10"). Shown to the user as this action\'s label.' }, folder: { type: 'string', description: 'multi-root workspaces only: the workspace folder NAME to run in; defaults to the first folder' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command'] } },
| if (!card){ | ||
| card = document.createElement('div'); card.className = 'editcard'; | ||
| editCards[m.id] = card; log.appendChild(card); | ||
| editCards[m.id] = card; | ||
| const st = { | ||
| kind: m.deleted ? 'delete' : (m.exists ? 'edit' : 'create'), |
There was a problem hiding this comment.
Fixed in b655f95. New grouped edit cards now default the diff closed (wasOpen = !groupsOn), so they render as a one-line row on first paint. Verified in a rendered webview: an editApplied inside a group renders with details.diffwrap closed.
An undocumented composer command. `/rme` renders a single reflective line as a calm remind-me card (serif italic behind a soft accent rule). Handled locally in doSend alongside /skills — no LLM call, no network. The words themselves are kept out of the source: stored base64 and decoded (UTF-8 aware, the text is Cyrillic) only when the command runs, so they surface in the app and not to anyone scanning the file. Not listed in any command help or autocomplete — it's meant to be found, not advertised. Verified in a rendered webview: /rme echoes the command and prints the exact line; the plaintext appears nowhere in the repo. Compile + full gate green.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
extensions/levelcode-ai/media/chat.html:1611
- Inside grouped activity, collapseMember() collapses every .tl-cmd immediately, including running commands. That defeats the intended behavior (and the nearby comment in addTermRun) that live command output is visible by default while the step runs.
function collapseMember(el){
if (!el || !el.classList){ return; }
if (el.classList.contains('tl-cmd')){ el.classList.add('collapsed'); return; }
const det = el.querySelector && el.querySelector('details.diffwrap');
if (det){ det.open = false; }
| const cmds = steps.filter((s) => s.kind === 'cmd').length; | ||
| const searches = steps.filter((s) => s.kind === 'search' || s.kind === 'list').length; | ||
| const reads = of('read'), edits = of('edit'), creates = of('create'), dels = of('delete'); | ||
| // File work leads, commands trail ("Read and edited extension.js, ran a command") — the reference | ||
| // ordering: what changed matters more than how many shells it took. | ||
| const parts = []; | ||
| const readOnly = reads.filter((p) => edits.indexOf(p) < 0 && creates.indexOf(p) < 0); | ||
| const readEdited = edits.filter((p) => reads.indexOf(p) >= 0); | ||
| if (readEdited.length && !readOnly.length && readEdited.length === edits.length){ | ||
| parts.push('read and edited ' + files(readEdited)); | ||
| } else { | ||
| if (readOnly.length){ parts.push('read ' + files(readOnly)); } | ||
| if (edits.length){ parts.push('edited ' + files(edits)); } | ||
| } | ||
| if (creates.length){ parts.push('created ' + files(creates)); } | ||
| if (dels.length){ parts.push('deleted ' + files(dels)); } | ||
| if (searches){ parts.push('searched the workspace'); } | ||
| if (cmds){ parts.push('ran ' + (cmds === 1 ? 'a command' : cmds + ' commands')); } |
There was a problem hiding this comment.
Fixed in b655f95. groupAggregate now counts list steps separately from search and renders "listed files" rather than folding them into "searched the workspace". Added a test.
New `check-circle` icon — a thin ring with a rounded tick, drawn as strokes (fill:none overrides the svg-level fill=currentColor) so it reads as the outline mark from the design. currentColor, so it inherits green in "done" contexts and the accent in the menu. Swapped in at every status tick: Kept / Deleted-fallback, verified — no problems, Answers recorded (title + verdict), a command's ok exit, an approved command, plan step done, the account "Synced" line, and the Copied confirmation. The approvals + mode menus' selected mark now paints the same glyph instead of a literal ✓ (data-ico="check-circle", filled by the broadened [data-ico] pass). Left alone on purpose: the group rail node, whose .tl-node already IS the ring — a glyph-ring inside it would double up. Verified in a dark-theme render: menu tick is the ring-check in the accent colour, Kept/verified/exit all show the green ring-check, and exactly one bare check (the node) remains. webviewCss pins the glyph shape, the menu wiring, and that no other bare check survives. Gate green (22 suites).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
extensions/levelcode-ai/media/chat.html:2101
- In grouped-activity mode, new edit cards default
wasOpen = true, butgroupAppend(card, st)runs beforecard.innerHTMLis populated. That meanscollapseMember()can’t close the<details.diffwrap>, and the edit diff renders expanded inside the activity group, defeating the “collapsed member rows” invariant.
let wasOpen = true;
extensions/levelcode-ai/media/chat.html:2162
editStepsentries are created for each edit card (for in-place count updates), but they’re never cleaned up when an edit card is resolved. Over long sessions this can leak step objects and keep references alive unnecessarily.
card.classList.add('resolved');
delete editCards[m.id];
}
| // A remind-me line. The words are kept out of the source as base64 and decoded only here, so | ||
| // they surface only when someone runs /rme — not to anyone scanning the file. UTF-8 aware decode | ||
| // (the text is Cyrillic), so atob's byte string is run back through TextDecoder. | ||
| var RME_B64 = '0KHQsNC80L7QtNC+0YHRgtCw0YLQvdGW0YHRgtGMLCDQt9Cx0YPQtNC+0LLQsNC90LAsINGJ0L7QsSDRg9C90LjQutC90YPRgtC4INC/0L7QutC40L3Rg9GC0L7RgdGC0ZYsIOKAlCDRhtC1INGB0LDQvNC1INGC0LUsINGJ0L4g0YLRgNC40LzQsNGUINC70Y7QtNC10Lkg0L3QsCDQstGW0LTRgdGC0LDQvdGWLg=='; | ||
| function rmeText(){ | ||
| try { return new TextDecoder().decode(Uint8Array.from(atob(RME_B64), function(c){ return c.charCodeAt(0); })); } | ||
| catch (e) { return ''; } | ||
| } |
There was a problem hiding this comment.
Keeping this intentionally — the product owner explicitly requested /rme and that its text stay out of the source (it's a private easter-egg reminder, not user-facing product copy). It's handled locally with no LLM/network call, isn't advertised in any command list, and the source carries a comment explaining the encoding. Happy to move the string to a resource file if you'd prefer, but it stays in this PR at the owner's request.
… .html files Two follow-ups to the circle-check work. - The finished group's rail node was a bare check inside .tl-node's own gray ring — a check in a circle, but a *double* ring once the glyph is itself a ring-check. The done node now paints check-circle (or circle-slash on failure) and drops its own border (border-color: transparent), so the glyph's ring is the only ring; the node keeps its background to mask the rail line, and the glyph grows to 18px to fill it. One clean green ring-check. - .html/.htm files wear the HTML5 shield instead of the "HTML" text pill — a self-contained two-tone-orange shield with the white "5", no pill chrome around it. Other extensions keep their lettered badges. Verified the mark renders as the real HTML5 logo at size. groupReducer + webviewCss updated: the node paints the ring-check and drops its border. Gate green (22 suites).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
extensions/levelcode-ai/media/chat.html:1470
- The /rme reminder text is intentionally obfuscated as base64 and decoded at runtime. Shipping obfuscated user-visible strings in the webview makes the code harder to audit, localize, and review for unwanted content, and can trigger security review concerns. Prefer keeping the reminder text as plain source (or as a localized/resource constant) and avoid atob/TextDecoder here unless there’s a strong, documented reason.
// A remind-me line. The words are kept out of the source as base64 and decoded only here, so
// they surface only when someone runs /rme — not to anyone scanning the file. UTF-8 aware decode
// (the text is Cyrillic), so atob's byte string is run back through TextDecoder.
var RME_B64 = '0KHQsNC80L7QtNC+0YHRgtCw0YLQvdGW0YHRgtGMLCDQt9Cx0YPQtNC+0LLQsNC90LAsINGJ0L7QsSDRg9C90LjQutC90YPRgtC4INC/0L7QutC40L3Rg9GC0L7RgdGC0ZYsIOKAlCDRhtC1INGB0LDQvNC1INGC0LUsINGJ0L4g0YLRgNC40LzQsNGUINC70Y7QtNC10Lkg0L3QsCDQstGW0LTRgdGC0LDQvdGWLg==';
function rmeText(){
try { return new TextDecoder().decode(Uint8Array.from(atob(RME_B64), function(c){ return c.charCodeAt(0); })); }
catch (e) { return ''; }
extensions/levelcode-ai/media/chat.html:2504
- This change introduces a new local slash command
/rmeand its associated UI output, but the PR description/spec for calm transcript doesn’t mention adding new commands. If/rmeis intended to ship, it should be documented (discoverability + maintenance); otherwise consider removing it from this PR to keep scope focused on transcript voice/grouping.
// Slash commands — handled locally (deterministic, no LLM call).
if (/^\/skills\b/i.test(t)){ add('user', esc(t)); forceStick(); input.value = ''; auto(); vscode.postMessage({ type: 'listSkills' }); return; }
if (/^\/rme\b/i.test(t)){ add('user', esc(t)); forceStick(); input.value = ''; auto(); addReminder(); return; }
add('user', render(t)); forceStick(); input.value = ''; auto();
extensions/levelcode-ai/agent.js:46
- The system prompt says read_file/search MUST include an "explanation" that becomes the action label, but the tool input_schemas don’t require it. Marking explanation as required here makes the constraint enforceable at the tool-schema level (and improves model compliance) instead of relying only on prompt wording.
{ 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'] } },
{ 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'] } },
extensions/levelcode-ai/agent.js:51
- Same as read_file/search: the prompt and schema text say run_command explanation is ALWAYS provided and it drives the user-visible activity label, but the JSON schema doesn’t require it. Adding it to required makes the tool contract match the "MUST" wording and reduces unlabeled command cards.
{ name: 'run_command', description: 'Run a shell command in the workspace root (or a named workspace folder via "folder" in multi-root workspaces). Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string', description: 'ALWAYS provide: 5-10 words, active voice, imperative — what this command does ("Run the extension unit tests", "Find the insertion point in section 10"). Shown to the user as this action\'s label.' }, folder: { type: 'string', description: 'multi-root workspaces only: the workspace folder NAME to run in; defaults to the first folder' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command'] } },
…ema, leaks Copilot review of the calm-transcript PR surfaced several real issues: - **Running members were collapsed on insertion**, hiding the live command's output behind a chevron — the opposite of the intended fixed-footprint header (finished members fold, the running one stays readable). groupAppend now folds only non-running members; groupStepDone folds a member when it completes successfully. This is the behavior the original screenshots asked for. - **A group could claim to be finished while a background command still ran.** finalizeGroup now detects a still-running member, keeps the live header and the Stop control, and lets that member's exit re-enter and settle the group — so a running command is never misrepresented as done nor loses its only Stop button. - **Grouped edit cards rendered with the full diff open** (groupAppend ran before the card's innerHTML existed, so collapseMember couldn't close it). New grouped edit cards now default the diff closed — a one-line row. - **The tooltip attribute wasn't quote-safe.** search chips carry `search "foo"`; esc() doesn't escape quotes, so the raw " closed the title="…" attribute. Uses escAttr now. - **`explanation` is now schema-required** on run_command / read_file / search, matching the "MUST include" contract (was prose-only; schema allowed omission). - **Step-map leaks**: editSteps was never cleared (now released in resolveEditCard and pruned on checkpoint restore alongside its card); termSteps leaked on termExitFinish's early return (now settled + deleted there too). - **Aggregate accuracy**: listing files was folded into "searched the workspace"; it's now its own "listed files" clause. - **groupActivity off mid-run** now closes any open group so the timeline returns fully flat immediately. Kept intentionally (product owner's explicit request): the `/rme` command and its base64 payload — see the reply on that thread. groupReducer +3 tests (running-stays-expanded, still-running-close, quote-escape), narrativeUi +1 (list ≠ search). Every change verified in a rendered webview with an actually-running command. Gate green (22 suites).
|
Addressed the review in b655f95. Summary: Real bugs fixed
Kept intentionally
Tests: groupReducer +3, narrativeUi +1. Full extension gate green (22 suites). |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
extensions/levelcode-ai/agent.js:51
- The SYSTEM_BASE rules describe explanation as "3–10 words" for run_command/read_file/search, but the run_command schema currently says "5–10 words". Aligning these avoids conflicting constraints in the prompt+schema.
{ name: 'run_command', description: 'Run a shell command in the workspace root (or a named workspace folder via "folder" in multi-root workspaces). Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string', description: 'REQUIRED: 5-10 words, active voice, imperative — what this command does ("Run the extension unit tests", "Find the insertion point in section 10"). Shown to the user as this action\'s label.' }, folder: { type: 'string', description: 'multi-root workspaces only: the workspace folder NAME to run in; defaults to the first folder' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command', 'explanation'] } },
Second review pass flagged doc drift and a word-count mismatch: - The `explanation` length was three different numbers — SYSTEM_BASE said 3-10, run_command's schema 5-10, read_file/search 3-8, the doc 5-10. Unified to **3-8 words** everywhere (prompt, all three schemas, D6). Every example given already fits that range. - D3 claimed "no event schema changes" — but the `agentTool` payload gained `label`/`kind`/`path` (types unchanged, fields added, filled by agent.js). Reworded to say exactly that. - "Not doing (yet)" still listed model-written labels for reads/searches as unbuilt; this PR implements them (required `explanation`, used as the row label). Removed that bullet and updated §1's baseline note to point at D6/S1. Docs + prompt only; no behavior change. Gate green.
…t restore Review caught that the new calm-transcript state outlives a log wipe: - The `reset` handler cleared the log HTML and the card/step maps but left `curGroup` pointing at the now-detached `.groupbody` and `turnLabeled` set. The next run would append into disconnected DOM (nothing shows), and the first assistant line would render as an unlabeled continuation. Reset both now. - `applyCheckpointRestored` removes this turn's (and later turns') DOM and pruned only the card maps — the step maps and a possibly-stranded `curGroup` were left behind. It now prunes editSteps/termSteps/verifySteps too and drops the group if its element was removed. (This also completes the earlier request to prune step maps on checkpoint restore, not just on reset.) - `pruneDetached` is now dual-mode: it follows `.card` for step-map values, so the same helper works for both card and step maps. webviewCss +2 static guards. Gate green (22 suites).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
extensions/levelcode-ai/media/chat.html:1647
- When a group is closed while a member is still running,
closeGroup()setscurGroup = nulland thenfinalizeGroup()callsrefreshGroupHead(). BecauserefreshGroupHead()always reads fromcurGroup, that call becomes a no-op and the closed-but-still-running group header won’t refresh (label/counts/stop wiring) as the underlying step state changes.
function refreshGroupHead(){
const g = curGroup; if (!g){ return; }
extensions/levelcode-ai/media/chat.html:1679
finalizeGroup()tries to keep the header live when a member is still running, but it callsrefreshGroupHead()aftercloseGroup()has already nulledcurGroup, so the refresh is skipped. After updatingrefreshGroupHeadto accept an explicit group, pass the group here so the closed group’s header stays in sync until the running member exits.
if (g.steps.some((s) => s.status === 'running')){
g.el.classList.remove('collapsed'); // never hide a command that is still going
refreshGroupHead();
return;
Implements docs/CALM-TRANSCRIPT.md (first commit): make an agent run read like a colleague narrating their work — short prose between actions, consecutive actions folded into one expandable card. Reference behavior: Claude Code's transcript.
S1 — the voice (
agent.js)SYSTEM_BASEgains the pair-working register:Done:sentinel, the anti-stall nudge, and the max_tokens machinery keep working untouched;Correction:pattern;ask_useroptions ordered with the recommendation first;Done:line + a short wrap-up for substantial runs;run_command/read_file/searcheach carry a requiredexplanation(5–10 imperative words) that becomes the action's label; the raw tool text drops to the tooltip.S2–S4 — grouped activity (
chat.html, webview-only reducer)levelcode.ai.chat.groupActivity(default on); off restores the flat timeline exactly.Polish (from live testing)
.groupstop { display:inline-flex }outranked the UA[hidden]rule, so it sat beside finished groups offering to stop an exited command. Fixed with the[hidden]escape; a static guard (webviewCss.test.js) now catches this class of bug across every runtime-toggled element.ask_userasks one question at a time —n of N, Back, an advance button; nothing posts before the last question; skipping is deliberate ("Skip this one", recorded as empty); the card collapses to "Answers recorded" and unfolds to the full record./rme— an undocumented composer command that prints a single reflective line; the words are base64 in source, decoded only when it runs.check-circle) replaces every bare checkmark — Kept, verified, answers recorded, ok-exit, approved, plan-step-done, Synced, Copied, and the menu's selected tick — one outline mark,currentColor(green when done, accent in the menu). The finished group node paints it too and drops its own border so there's a single ring..html/.htmfiles in place of the "HTML" text pill.Tests
Zero-dependency Node suites, extraction-pattern (same as
shHighlight.test.js) — they exercise the shippedchat.html, not copies:narrativeUi.test.js— grammar: tense conversion, the aggregate sentence (clause order, same-file merge, fallbacks), chip→step, command labels.groupReducer.test.js— DOM behavior against a fake DOM: live→aggregate header, member collapse, failure re-open, unwrap, late background exit, fold-on-success, one-label-per-turn.askWizard.test.js— the one-question-at-a-time flow clicked through: no early post, skip-records-empty, Back preserves/revises, ordered payload, collapse-on-send.webviewCss.test.js— static source invariants no DOM test can see:[hidden]never defeated bydisplay, wizard/collapse specificity, the check-circle shape + reach.Full extension gate green (22 suites). Every visual change verified in a rendered webview (dark theme), not just asserted.