Skip to content
Merged
11 changes: 11 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,25 @@ These are the same eleven the browser edition allows.
- **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.
- **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.

### The agent shows its plan, and you can keep typing while it works

**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.

- **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.
- **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.
- **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.

## Not in this release

<!-- TODO -->
- **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.

## Test coverage

<!-- TODO measure with scripts/draft-release-notes.mjs (counted cases only). -->
- `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.
- `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.
- `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.
- `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.

**Full changelog:** https://github.com/levelcodeai/levelcode/compare/v1.4.0...vX.Y.Z
212 changes: 212 additions & 0 deletions docs/PLAN-PROGRESS.md

Large diffs are not rendered by default.

153 changes: 132 additions & 21 deletions extensions/levelcode-ai/agent.js

Large diffs are not rendered by default.

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

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

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

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

/**
* "Send now" on a queued message: hand it to the running agent at its next step. With no run going there is
* nothing to hand it to, so it goes straight back to the chat's queue — which sends it as a normal message.
*/
function acceptSteer(msg) {
const id = typeof msg.id === 'string' ? msg.id : '';
const text = typeof msg.text === 'string' ? msg.text.trim() : '';
if (!id) { return; }
if (!activeAgentRun || !text) { post({ type: 'steersReturned', ids: [id] }); return; }
steerInbox.push({ id, text });
dbg('steer.waiting', { chars: text.length, waiting: steerInbox.length });
}

let lastAgentGoal = null; // remembered so the response bar's Retry can re-run it
/**
* The goal as the person typed it, for `remember` and `forget` to quote: '' unless `opts.typed`. A Retry of a
Expand Down Expand Up @@ -2436,9 +2461,13 @@ async function agentFlow(text, imageBlocks, opts) {
}
const diagBaseline = verifyCfg.enabled ? snapshotDiagnostics() : new Map();
dbg('verify.config', { enabled: verifyCfg.enabled, hasCommand: !!verifyCfg.command, maxRounds: verifyCfg.maxRounds, includeWarnings: verifyCfg.includeWarnings });
const runToken = {};
activeAgentRun = runToken;
try {
await runAgent({
messages: withImages(agentMessages), // persists across runs → the agent remembers the session
plan: agentPlan, // the conversation's task list (update_plan / wrap_up keep it)
takeSteers: () => steerInbox.splice(0), // "Send now" messages, taken at the loop's next step
providerId: req.providerId, // Anthropic native, or an OpenAI-shaped provider via translation (P2)
baseURL: req.baseURL, // for the custom / Ollama endpoints
label: req.label, // route name for error attribution — "LevelCode Cloud" on the gateway,
Expand Down Expand Up @@ -2521,6 +2550,7 @@ async function agentFlow(text, imageBlocks, opts) {
signal: abort.signal
});
} finally {
if (activeAgentRun === runToken) { activeAgentRun = null; }
// Everything below writes to state a teardown may already have replaced. runAgent holds
// `agentMessages` BY REFERENCE, so a teardown that rebinds the global leaves this run pushing
// into an orphaned array — and then `agentMessages.slice(sessTurnStart)` would slice the NEW,
Expand Down Expand Up @@ -2720,12 +2750,18 @@ function withImages(msgs) {
return touched ? out : msgs;
}

async function handleSend(text, images) {
/**
* A message from the chat. `opts.reply` marks a click on a reply button in the next-step box: words the agent
* wrote (the person saw a short label), so the goal is not recorded as typed — not for the Ask When Risky
* reviewer, not for `remember` to quote.
*/
async function handleSend(text, images, opts) {
const reply = !!(opts && opts.reply);
const imageBlocks = storeImages(images);
if ((!text || !text.trim()) && !imageBlocks.length) { return; }
text = text || '';
if (ctx) { ctx.globalState.update('levelcode.ai.hasSentMessage', true); } // user engaged → stop auto-revealing the panel on launch
if (agentMode) { await agentFlow(text, imageBlocks, { typed: true }); return; }
if (agentMode) { await agentFlow(text, imageBlocks, { typed: !reply }); return; }
const cfg = aiConfig();
const providerId = currentProviderId();
dbg('chat.send', { provider: providerId, model: activeModel(cfg, providerId), chars: text.length, history: conversation.length });
Expand Down Expand Up @@ -3305,12 +3341,13 @@ class ChatViewProvider {
case 'setMode': agentMode = !!msg.agent; post({ type: 'mode', agent: agentMode }); break;
case 'setApprovals': setApprovalMode(String(msg.mode || '')); break;
case 'setAutopilot': setApprovalMode(msg.on ? 'autopilot' : 'ask'); break; // the two-option menu's message
case 'send': await handleSend(msg.text, msg.images); break;
case 'send': await handleSend(msg.text, msg.images, { reply: msg.reply === true }); break;
// One surface for "that could not be attached" — VS Code's own, not a second one
// invented inside the transcript.
case 'notice': if (msg.text) { vscode.window.showWarningMessage(String(msg.text)); } break;
case 'pickImages': await pickImages(); break;
case 'attachImagePaths': await attachImagePaths(msg.paths); break;
case 'steer': acceptSteer(msg); break;
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;
case 'stopCommand': { dbg('stopCommand', { id: msg.id }); const s = commandStops.get(msg.id); if (s) { try { s(); } catch (e) { /* gone */ } } break; }
case 'approvalResponse': {
Expand Down
2 changes: 1 addition & 1 deletion extensions/levelcode-ai/mcpConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const crypto = require('crypto');

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

Expand Down
Loading
Loading