agentHost: add a built-in Todos canvas showing the Copilot session's plan and todos by spencrr · Pull Request #343133 · microsoft/vscode · GitHub
Skip to content

agentHost: add a built-in Todos canvas showing the Copilot session's plan and todos - #343133

Draft
Spencer Schoenberg (spencrr) wants to merge 2 commits into
microsoft:mainfrom
spencrr:dev/spencrr/todos-canvas
Draft

Spencer Schoenberg (spencrr) wants to merge 2 commits into
microsoft:mainfrom
spencrr:dev/spencrr/todos-canvas

Conversation

@spencrr

Copy link
Copy Markdown

Fixes #343132. Related: github/copilot-cli#1724.

Copilot sessions keep their plan in plan.md and their work items in the todos / todo_deps SQLite tables, and the agent host already reads both for Mission Control's extensions/getPlan. The Agents window never shows them. This adds a host-provided Todos canvas so the user can follow the agent's progress, using the SDK's canvasProvider mechanism for host-declared canvases (the same pattern the Copilot app uses for its Plan canvas). No SDK or protocol changes.

Changes

  • copilotBuiltinCanvases.ts: declares the todos canvas with createCanvas under the stable provider id vscode:agent-host.
  • copilotTodosCanvasServer.ts: ICopilotTodosCanvasServer, a LoopbackProxyServer subclass. Binds on the first presentation and closes with the last one. Serves /<token>/ (page), plan.json, and an SSE events stream; 404 for unknown tokens or extra path segments, 405 for non-GET, no-store / nosniff / no-referrer headers.
  • copilotTodosCanvasPage.ts: self-contained page with a nonce CSP. Groups todos into In Progress / Ready / Blocked / Other / Done, derives "Blocked by …" from unmet todo_deps, shows a progress bar, and renders plan.md with a small Markdown subset. Agent content only reaches the DOM through textContent. Light/dark via prefers-color-scheme.
  • copilotSessionLauncher.ts: passes canvasProvider and canvases to the SDK when canvases are enabled. Ephemeral sessions are unchanged.
  • copilotAgentSession.ts: the runtime adapter implements openBuiltinCanvas / closeBuiltinCanvas (one served page per session, synchronous teardown on close or dispose, retry after a failed bind), forwards session.todos_changed / session.plan_changed to the page, and tags the canvas with the new host provenance in listings and agentHost.canvasOpened telemetry.
  • host added to the extensionSource unions in agent.ts, canvas.ts, and session.ts so isOpenableExtensionCanvasDefinition admits it to + › Canvas.
  • AGENT_HOST_SESSIONS_PROVIDER.md and PERFORMANCE.md updated for the host-provided canvas and the new telemetry value.

How to test

  1. Set chat.canvases.enabled to true and open the Agents window.
  2. Start a local Copilot chat and ask the agent to track a few steps with its sql todo tools, or ask it to open the Todos canvas directly.
  3. Open + › Canvas › Todos above the right-hand editor area. The page should show the todos grouped by status, with blocked items listing what they wait on, and the plan below. It should update as the agent writes to todos / todo_deps or edits the plan.
  4. Close the tab and reopen it from + › Canvas; close it from the canvas pill and confirm the served page is gone (the previous URL returns 404).
  5. Disable chat.canvases.enabled, start a new session, and confirm no Todos entry appears.

Validation

  • npm run typecheck-client ✅
  • npm run eslint -- <changed files> ✅
  • scripts/test.bat --run src/vs/platform/agentHost/test/node/copilotTodosCanvasServer.test.ts --run src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts --run src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts --run src/vs/platform/agentHost/test/node/copilotAgent.test.ts --run src/vs/platform/agentHost/test/node/agentHostServices.test.ts --run src/vs/workbench/contrib/canvases/test/common/canvasDefinitionStore.test.ts ✅ (2,332 passing)
  • Served the compiled page with sample data and checked it in the integrated browser in light and dark: grouping, dependencies, live SSE refresh, and <script>-looking titles rendered as text.
  • Not run: the live-runtime canvas integration test. The runtime listing host-declared canvases in rpc.canvas.list() is asserted by the SDK's canvasProvider docs but not exercised here; step 3 above covers it.

Notes for reviewers

  • Theming is light/dark only, like every canvas; a native pane would need a URL-less canvas kind.
  • After Separate Copilot extension and Canvas enablement #342369 lands, this sits on the chat.canvases.enabled (rendering) side of the split and does not depend on chat.copilotExtensions.enabled. The description of chat.canvases.enabled should then mention built-in canvases alongside extension ones.
  • Auto-opening the canvas when the agent first writes a todo is intentionally left out.

Copilot AI balanced review requested due to automatic review settings October 21, 2026 04:58
Copilot sessions keep their plan in plan.md and their work items in the
todos/todo_deps SQL tables. The agent host already reads both for
Mission Control's extensions/getPlan, but the Agents window never shows
them, and the runtime declares no canvases of its own.

Declare a "todos" canvas on the SDK control connection through the
canvasProvider mechanism, under the stable id vscode:agent-host, and
serve its page from a loopback server that re-reads on
session.todos_changed and session.plan_changed. Canvas listings tag it
with a new "host" provenance so it appears under + > Canvas and the
agent can open it with open_canvas. Gated by chat.canvases.enabled like
every other canvas; no SDK or protocol changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

5 open issues
What changed in this PR

Adds a host-provided Todos canvas for Copilot agent sessions so users can view the session plan (plan.md) and SQL todos/dependencies live in the Agents window, without SDK/protocol changes.

Changes:

  • Introduces a loopback HTTP server + self-contained HTML page for the built-in Todos canvas (plan JSON + SSE change events).
  • Wires built-in canvases into session launch/runtime (canvasProvider, canvases, open/close handling) and forwards plan/todos change events.
  • Extends canvas provenance unions and telemetry/docs/tests to support a new host extension source.
File Description
src/​vs/​workbench/​contrib/​canvases/​test/​common/​canvasDefinitionStore.test.ts Adjusts store tests to include the new host-provided todos canvas in listings.
src/​vs/​workbench/​contrib/​canvases/​common/​canvas.ts Adds host to the canvas contribution provenance union.
src/​vs/​sessions/​services/​sessions/​common/​session.ts Adds host to session canvas provenance union for session-level canvas definitions.
src/​vs/​sessions/​contrib/​providers/​agentHost/​AGENT_HOST_SESSIONS_PROVIDER.md Documents the agent-host-declared built-in canvas provider and todos canvas behavior.
src/​vs/​platform/​agentHost/​test/​node/​copilotTodosCanvasServer.test.ts Adds dedicated tests for serving, routing, SSE behavior, and lifecycle of the Todos canvas server.
src/​vs/​platform/​agentHost/​test/​node/​copilotSessionLauncher.test.ts Ensures launcher passes built-in canvas provider + canvases for non-ephemeral sessions only.
src/​vs/​platform/​agentHost/​test/​node/​copilotAgentSession.test.ts Adds tests for host provenance in listings and open/close/retry behavior for the Todos canvas.
src/​vs/​platform/​agentHost/​test/​node/​copilotAgent.test.ts Registers the Todos canvas server in agent test service wiring.
src/​vs/​platform/​agentHost/​node/​copilot/​copilotTodosCanvasServer.ts New loopback server implementation serving HTML, plan.json, and SSE events.
src/​vs/​platform/​agentHost/​node/​copilot/​copilotTodosCanvasPage.ts New self-contained HTML/CSS/JS page (CSP nonce) rendering todos + plan markdown subset.
src/​vs/​platform/​agentHost/​node/​copilot/​copilotSessionWrapper.ts Plumbs session.plan_changed and session.todos_changed events from the SDK session.
src/​vs/​platform/​agentHost/​node/​copilot/​copilotSessionLauncher.ts Plumbs canvasProvider + built-in canvases into SDK session create/resume config.
src/​vs/​platform/​agentHost/​node/​copilot/​copilotBuiltinCanvases.ts Declares the stable built-in canvas provider identity and the todos built-in canvas.
src/​vs/​platform/​agentHost/​node/​copilot/​copilotAgentSession.ts Implements built-in canvas open/close and forwards plan/todo changes to the served page.
src/​vs/​platform/​agentHost/​node/​agentHostTelemetryReporter.ts Adds host to the bounded telemetry classification and mapping for agentHost.canvasOpened.
src/​vs/​platform/​agentHost/​node/​agentHostServices.ts Registers ICopilotTodosCanvasServer as a host service.
src/​vs/​platform/​agentHost/​common/​agent.ts Adds host provenance to the agent-level canvas info type.
src/​vs/​platform/​agentHost/​PERFORMANCE.md Documents the new host telemetry dimension usage for agentHost.canvasOpened.

Review effort: Lite

Comment on lines +73 to +77
const changeListener = source.onDidChange(() => {
for (const stream of streams) {
stream.write('event: change\ndata: {}\n\n');
}
});

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4cedf21: the change listener skips streams that are already destroyed/writableEnded and drops them from the set; dispose does the same before end(). Added a test that disconnects one subscriber and fires changes before and after the server sees the close, asserting no uncaughtException and that the remaining stream still receives both events.

Comment on lines +108 to +109
const { pathname } = new URL(req.url ?? '/', 'http://127.0.0.1');
const [, token, route, ...rest] = pathname.split('/');

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4cedf21. For the record the base class already catches throws from handleRequest and returns 500, so this could not kill the listener, but 500 was the wrong status for junk input. Unparsable URLs now take the same 404 path as unknown tokens so probes learn nothing either way.


export const COPILOT_TODOS_CANVAS_ID = 'todos';

const todosCanvasDescription = 'Shows the session plan document together with the todo items tracked in the `todos` and `todo_deps` SQL tables, grouped by status with their blocking dependencies. Open it when planning or implementing multi-step work so the user can follow progress.';

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally not localized. description is never rendered in VS Code UI (no consumer in the canvas picker, Add Tab menu, or pill reads it); it is the SDK's "short description shown to the agent in canvas catalogs", i.e. model-facing prompt text, and we keep those in English like tool descriptions so model behavior does not vary by UI locale. displayName is the user-facing one. Added a code comment saying so.

Comment on lines +103 to +107
if (req.method !== 'GET') {
res.writeHead(405, { 'Allow': 'GET', 'Cache-Control': 'no-store' });
res.end();
return;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4cedf21; the 405 now spreads securityHeaders alongside Allow, and the test asserts nosniff/no-store on it.

Comment on lines +44 to +46
loadError: localize('todosCanvas.loadError', "Couldn't load todos: {0}", '{0}'),
blockedBy: localize('todosCanvas.blockedBy', "Blocked by: {0}", '{0}'),
summaryDone: localize('todosCanvas.summaryDone', "{0} of {1} done", '{0}', '{1}'),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the explicit args. They are identity substitutions so the formatted result is still a {0}/{1} template that the page script fills at render time. Dropping them yields the same output today only because _format returns the message untouched when args.length === 0; the explicit form does not depend on that detail and makes the two-stage formatting visible. Added a comment at the call site.

… bad URLs

Skip SSE streams that already ended before writing a change event, so a
peer that disconnects between its last write and the close event cannot
raise an unhandled stream error. Treat unparsable request URLs like
unknown tokens (404 instead of 500), and send the same security headers
on 405 responses.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +73 to +82
const changeListener = source.onDidChange(() => {
for (const stream of streams) {
// A peer can disconnect between its last write and the close event; writing then would emit an unhandled error.
if (stream.destroyed || stream.writableEnded) {
streams.delete(stream);
} else {
stream.write('event: change\ndata: {}\n\n');
}
}
});
});
res.write(': connected\n\n');
entry.streams.add(res);
const heartbeat = setInterval(() => res.write(': heartbeat\n\n'), SSE_HEARTBEAT_INTERVAL_MS);
Comment on lines +312 to +320
var progress = el('div', 'progress');
progress.setAttribute('role', 'progressbar');
progress.setAttribute('aria-valuemin', '0');
progress.setAttribute('aria-valuemax', String(todos.length));
progress.setAttribute('aria-valuenow', String(groups.done.length));
var fill = el('div');
fill.style.width = Math.round(groups.done.length / todos.length * 100) + '%';
progress.appendChild(fill);
fragment.appendChild(progress);
const strings = getStrings();
const nonce = escapeHtml(scriptNonce);
return `<!DOCTYPE html>
<html lang="en">
Comment on lines +22 to +23

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agents window: show the Copilot session's plan and todos in a canvas

3 participants