Skip to content

Commit ed77319

Browse files
authored
Merge pull request #115 from levelcodeai/feat/plan-progress
feat(agent): a task list, a hand-back box, and Enter that queues — plan and progress like Claude desktop
2 parents 2824011 + 0b41f0c commit ed77319

16 files changed

Lines changed: 2366 additions & 132 deletions

‎RELEASE-NOTES.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,25 @@ These are the same eleven the browser edition allows.
2626
- **They are now shared by all your profiles.** VS Code keeps settings like these in the Default profile. A value written in another profile's `settings.json` is ignored, and changing one from another profile writes it to the Default profile for you.
2727
- **Nothing changes if you only ever set them in your user settings**, which is where the Settings editor and the chat's own menus put them.
2828

29+
### The agent shows its plan, and you can keep typing while it works
30+
31+
**Enter no longer stops the agent.** Until now, pressing Enter while the agent was working stopped the run. Now what you type waits just above the message box and is sent as your next message when the run finishes. To stop a run, use ■ with the message box empty.
32+
33+
- **A task list under the latest message.** When a job has more than one step, the agent keeps a short task list as the last thing in the conversation. The step it is on spins (it stops spinning while the agent waits for you to approve something), finished steps turn grey, and a long list shows the steps around the current one with "earlier steps" and "more to go" counts. The list goes away when the run ends. It replaces the "Plan 2/5" bar above Keep all / Undo all.
34+
- **A clear hand-back.** When a run ends with a decision for you, the agent asks one question with one or two answers to click ("Run the tests now?"). Clicking an answer sends it as your next message. Before that box appears, the files the agent changed are checked (the editor's diagnostics, and your verify command if you set one); if the check fails, the agent fixes the problems first.
35+
- **Messages that wait their turn.** One waiting message shows as a single row; several fold into "3 messages queued · Next: …". **Send now** hands a message to the agent at its next step without stopping the run. You can also edit a waiting message in the message box, reorder several, or remove them. If a run is stopped or fails, anything still waiting comes back into the message box instead of being sent.
36+
2937
## Not in this release
3038

3139
<!-- TODO -->
40+
- **Send now is for text.** A waiting message with an image is sent when the run ends, not mid-run. And a message sent with Send now is read after the step the agent is on: it does not interrupt a command that is already running, and it does not hold back the agent's next edit while it waits.
3241

3342
## Test coverage
3443

3544
<!-- TODO measure with scripts/draft-release-notes.mjs (counted cases only). -->
3645
- `settingsScope` (4 cases): every LevelCode AI setting is user-only unless it is on the list above; every scope is one the editor knows, because it reads a misspelt one as a setting a folder can set; and the desktop and the browser edition allow the same list.
46+
- `turnPlan` (20 cases), `agentPlanProgress` (11) and `planProgressUi` (24): how the task list and the hand-back are read and moved; the agent loop ending on a hand-back with a question without another model call, checking its edits there, and delivering Send now at the next step (or handing it back when the run stops); and the chat page's list, box and queue, down to Enter queueing instead of stopping.
47+
- `scripts/plan-progress-editor-check.js` runs a scripted agent in the real editor (17 checks, light and dark), and `web/test/e2e-plan-progress.mjs` does the same in the browser edition (9 checks). Neither is part of the gate.
3748
- `scripts/folder-settings-editor-check.js` opens the real editor on a project that tries all of the above and checks that none of it takes effect. It is not part of the gate.
3849

3950
**Full changelog:** https://github.com/levelcodeai/levelcode/compare/v1.4.0...vX.Y.Z

‎docs/PLAN-PROGRESS.md‎

Lines changed: 212 additions & 0 deletions
Large diffs are not rendered by default.

‎extensions/levelcode-ai/agent.js‎

Lines changed: 132 additions & 21 deletions
Large diffs are not rendered by default.

‎extensions/levelcode-ai/extension.js‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@ let review;
114114
/** Persistent agent transcript for the session (tool calls + results), so follow-up goals
115115
* remember prior runs. Reset by New Chat. */
116116
let agentMessages = [];
117+
/** The agent's task list for this conversation (docs/PLAN-PROGRESS.md). agent.js keeps `steps` current through
118+
* update_plan and wrap_up, and every run of the conversation gets the same object, so an {"advance": true}
119+
* moves the list an earlier run left. A new conversation, and a resumed one, start with none. */
120+
let agentPlan = { steps: null };
121+
/** Messages the user pressed "Send now" on while the agent works. The running loop takes them at its next
122+
* step (ctx.takeSteers); what it never reaches it hands back to the chat's queue when the run ends. */
123+
let steerInbox = [];
124+
/** The agent run going now, as a token only its own agentFlow holds: a late finally from a run that was torn
125+
* down cannot mark a newer run as over. */
126+
let activeAgentRun = null;
117127

118128
function post(msg) { if (activeWebview) { activeWebview.postMessage(msg); } }
119129

@@ -1794,6 +1804,7 @@ async function resumeSession(id) {
17941804
if (!r) { post({ type: 'agentError', message: 'That session could not be opened — it may have been deleted.' }); return; }
17951805

17961806
agentMessages = Array.isArray(r.messages) ? r.messages.slice() : []; // what the model resumes with
1807+
agentPlan = { steps: null }; steerInbox = []; // the list is drawn live, never replayed
17971808
// The Ask When Risky reviewer counts only goals typed since this point as the person's word. A session
17981809
// file is read back from `sessions.dir`, which a workspace setting can point anywhere, so its earlier
17991810
// goals go to the reviewer as activity. That errs toward asking, never toward running.
@@ -1888,6 +1899,7 @@ function resetConversationState() {
18881899
clearQuestions();
18891900
conversation = [];
18901901
agentMessages = [];
1902+
agentPlan = { steps: null }; steerInbox = [];
18911903
humanGoals = []; humanAnswers = [];
18921904
diagrams.reset(); // the next conversation starts at d-1, owing nothing
18931905
diagramsStubbed = false;
@@ -2361,6 +2373,19 @@ async function compactAgentMemory() {
23612373
return { ok: true, beforeMsgTokens, afterMsgTokens };
23622374
}
23632375

2376+
/**
2377+
* "Send now" on a queued message: hand it to the running agent at its next step. With no run going there is
2378+
* nothing to hand it to, so it goes straight back to the chat's queue — which sends it as a normal message.
2379+
*/
2380+
function acceptSteer(msg) {
2381+
const id = typeof msg.id === 'string' ? msg.id : '';
2382+
const text = typeof msg.text === 'string' ? msg.text.trim() : '';
2383+
if (!id) { return; }
2384+
if (!activeAgentRun || !text) { post({ type: 'steersReturned', ids: [id] }); return; }
2385+
steerInbox.push({ id, text });
2386+
dbg('steer.waiting', { chars: text.length, waiting: steerInbox.length });
2387+
}
2388+
23642389
let lastAgentGoal = null; // remembered so the response bar's Retry can re-run it
23652390
/**
23662391
* The goal as the person typed it, for `remember` and `forget` to quote: '' unless `opts.typed`. A Retry of a
@@ -2436,9 +2461,13 @@ async function agentFlow(text, imageBlocks, opts) {
24362461
}
24372462
const diagBaseline = verifyCfg.enabled ? snapshotDiagnostics() : new Map();
24382463
dbg('verify.config', { enabled: verifyCfg.enabled, hasCommand: !!verifyCfg.command, maxRounds: verifyCfg.maxRounds, includeWarnings: verifyCfg.includeWarnings });
2464+
const runToken = {};
2465+
activeAgentRun = runToken;
24392466
try {
24402467
await runAgent({
24412468
messages: withImages(agentMessages), // persists across runs → the agent remembers the session
2469+
plan: agentPlan, // the conversation's task list (update_plan / wrap_up keep it)
2470+
takeSteers: () => steerInbox.splice(0), // "Send now" messages, taken at the loop's next step
24422471
providerId: req.providerId, // Anthropic native, or an OpenAI-shaped provider via translation (P2)
24432472
baseURL: req.baseURL, // for the custom / Ollama endpoints
24442473
label: req.label, // route name for error attribution — "LevelCode Cloud" on the gateway,
@@ -2521,6 +2550,7 @@ async function agentFlow(text, imageBlocks, opts) {
25212550
signal: abort.signal
25222551
});
25232552
} finally {
2553+
if (activeAgentRun === runToken) { activeAgentRun = null; }
25242554
// Everything below writes to state a teardown may already have replaced. runAgent holds
25252555
// `agentMessages` BY REFERENCE, so a teardown that rebinds the global leaves this run pushing
25262556
// into an orphaned array — and then `agentMessages.slice(sessTurnStart)` would slice the NEW,
@@ -2720,12 +2750,18 @@ function withImages(msgs) {
27202750
return touched ? out : msgs;
27212751
}
27222752

2723-
async function handleSend(text, images) {
2753+
/**
2754+
* A message from the chat. `opts.reply` marks a click on a reply button in the next-step box: words the agent
2755+
* wrote (the person saw a short label), so the goal is not recorded as typed — not for the Ask When Risky
2756+
* reviewer, not for `remember` to quote.
2757+
*/
2758+
async function handleSend(text, images, opts) {
2759+
const reply = !!(opts && opts.reply);
27242760
const imageBlocks = storeImages(images);
27252761
if ((!text || !text.trim()) && !imageBlocks.length) { return; }
27262762
text = text || '';
27272763
if (ctx) { ctx.globalState.update('levelcode.ai.hasSentMessage', true); } // user engaged → stop auto-revealing the panel on launch
2728-
if (agentMode) { await agentFlow(text, imageBlocks, { typed: true }); return; }
2764+
if (agentMode) { await agentFlow(text, imageBlocks, { typed: !reply }); return; }
27292765
const cfg = aiConfig();
27302766
const providerId = currentProviderId();
27312767
dbg('chat.send', { provider: providerId, model: activeModel(cfg, providerId), chars: text.length, history: conversation.length });
@@ -3305,12 +3341,13 @@ class ChatViewProvider {
33053341
case 'setMode': agentMode = !!msg.agent; post({ type: 'mode', agent: agentMode }); break;
33063342
case 'setApprovals': setApprovalMode(String(msg.mode || '')); break;
33073343
case 'setAutopilot': setApprovalMode(msg.on ? 'autopilot' : 'ask'); break; // the two-option menu's message
3308-
case 'send': await handleSend(msg.text, msg.images); break;
3344+
case 'send': await handleSend(msg.text, msg.images, { reply: msg.reply === true }); break;
33093345
// One surface for "that could not be attached" — VS Code's own, not a second one
33103346
// invented inside the transcript.
33113347
case 'notice': if (msg.text) { vscode.window.showWarningMessage(String(msg.text)); } break;
33123348
case 'pickImages': await pickImages(); break;
33133349
case 'attachImagePaths': await attachImagePaths(msg.paths); break;
3350+
case 'steer': acceptSteer(msg); break;
33143351
case 'stop': dbg('stop.clicked', { running: commandStops.size }); for (const [, stop] of commandStops) { try { stop(); } catch (e) { /* gone */ } } if (abort) { abort.abort(); } clearApprovals(); clearQuestions(); break;
33153352
case 'stopCommand': { dbg('stopCommand', { id: msg.id }); const s = commandStops.get(msg.id); if (s) { try { s(); } catch (e) { /* gone */ } } break; }
33163353
case 'approvalResponse': {

‎extensions/levelcode-ai/mcpConfig.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const crypto = require('crypto');
3232

3333
// The agent's built-in tools (agent.js TOOLS). An MCP tool may never shadow one of these.
3434
const BUILTIN_TOOL_NAMES = [
35-
'list_files', 'read_file', 'search', 'update_plan', 'edit_file', 'write_file',
35+
'list_files', 'read_file', 'search', 'update_plan', 'wrap_up', 'edit_file', 'write_file',
3636
'delete_file', 'run_command', 'read_command_output', 'ask_user', 'use_skill'
3737
];
3838

0 commit comments

Comments
 (0)