Repository navigation
agentHost: add a built-in Todos canvas showing the Copilot session's plan and todos - #343133
Spencer Schoenberg (spencrr) wants to merge 2 commits into
Conversation
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.
790dbb6 to
39181ec
Compare
There was a problem hiding this comment.
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
ServerResponse.write()can throw (e.g.,ERR_STREAM_WRITE_AFTER_END) if a response is… · Newnew URL(req.url, ...)can throw on malformed/invalid request URLs, which would surface as an… · New This description appears user-facing (surfaced via canvas listings), but it is not localized while… · New The 405 response currently omits the other security headers you apply elsewhere… · New Passing'{0}'/'{1}'as arguments tolocalizeis redundant and obscures intent (when no args… · New
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 forwardsplan/todoschange events. - Extends canvas provenance unions and telemetry/docs/tests to support a new
hostextension source.
Review effort: Lite
| const changeListener = source.onDidChange(() => { | ||
| for (const stream of streams) { | ||
| stream.write('event: change\ndata: {}\n\n'); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
| const { pathname } = new URL(req.url ?? '/', 'http://127.0.0.1'); | ||
| const [, token, route, ...rest] = pathname.split('/'); |
There was a problem hiding this comment.
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.'; |
There was a problem hiding this comment.
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.
| if (req.method !== 'GET') { | ||
| res.writeHead(405, { 'Allow': 'GET', 'Cache-Control': 'no-store' }); | ||
| res.end(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Fixed in 4cedf21; the 405 now spreads securityHeaders alongside Allow, and the test asserts nosniff/no-store on it.
| 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}'), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
10 open issues
There’s still a race where the client disconnects between thedestroyed/writableEndedcheck and… · New There’s still a race where the client disconnects between thedestroyed/writableEndedcheck and… · Newnew URL(req.url, ...)can throw on malformed/invalid request URLs, which would surface as an…ServerResponse.write()can throw (e.g.,ERR_STREAM_WRITE_AFTER_END) if a response is… The progressbar has values but no accessible name/label, so many screen readers will announce it… · New The page is localized vialocalize(...), but the document language is hard-coded toen. This… · New The 405 response currently omits the other security headers you apply elsewhere… This description appears user-facing (surfaced via canvas listings), but it is not localized while… The comment claims this description is not shown in VS Code UI, but it is part of the canvas… · New Passing'{0}'/'{1}'as arguments tolocalizeis redundant and obscures intent (when no args…
Review effort: Lite
| 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); |
| 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"> |




Fixes #343132. Related: github/copilot-cli#1724.
Copilot sessions keep their plan in
plan.mdand their work items in thetodos/todo_depsSQLite tables, and the agent host already reads both for Mission Control'sextensions/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'scanvasProvidermechanism for host-declared canvases (the same pattern the Copilot app uses for its Plan canvas). No SDK or protocol changes.Changes
copilotBuiltinCanvases.ts: declares thetodoscanvas withcreateCanvasunder the stable provider idvscode:agent-host.copilotTodosCanvasServer.ts:ICopilotTodosCanvasServer, aLoopbackProxyServersubclass. Binds on the first presentation and closes with the last one. Serves/<token>/(page),plan.json, and an SSEeventsstream; 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 unmettodo_deps, shows a progress bar, and rendersplan.mdwith a small Markdown subset. Agent content only reaches the DOM throughtextContent. Light/dark viaprefers-color-scheme.copilotSessionLauncher.ts: passescanvasProviderandcanvasesto the SDK when canvases are enabled. Ephemeral sessions are unchanged.copilotAgentSession.ts: the runtime adapter implementsopenBuiltinCanvas/closeBuiltinCanvas(one served page per session, synchronous teardown on close or dispose, retry after a failed bind), forwardssession.todos_changed/session.plan_changedto the page, and tags the canvas with the newhostprovenance in listings andagentHost.canvasOpenedtelemetry.hostadded to theextensionSourceunions inagent.ts,canvas.ts, andsession.tssoisOpenableExtensionCanvasDefinitionadmits it to + › Canvas.AGENT_HOST_SESSIONS_PROVIDER.mdandPERFORMANCE.mdupdated for the host-provided canvas and the new telemetry value.How to test
chat.canvases.enabledtotrueand open the Agents window.sqltodo tools, or ask it to open the Todos canvas directly.todos/todo_depsor edits the plan.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)<script>-looking titles rendered as text.rpc.canvas.list()is asserted by the SDK'scanvasProviderdocs but not exercised here; step 3 above covers it.Notes for reviewers
chat.canvases.enabled(rendering) side of the split and does not depend onchat.copilotExtensions.enabled. The description ofchat.canvases.enabledshould then mention built-in canvases alongside extension ones.