Add Live mode: real Copilot chat (app + canvas) with React parity#3
Merged
MSFT-TKENDRICK merged 5 commits intoJun 15, 2026
Conversation
Add a "Live" response mode that uses the GitHub Copilot SDK with ambient auth to run real Copilot chat instead of the canned simulation, across both the React webapp and the canvas extension, with the heavy logic shared. Shared Node engine (.github/extensions/tokenizer/live/), consumed by BOTH a new Vite dev/preview plugin and the canvas extension (zero duplication): - resolveSdk.mjs: locate the SDK index.js + CLI exe (env overrides + per-OS install dirs); returns null -> Live degrades gracefully. - service.mjs: lazy, locked-down CopilotClient (forStdio, mode:"empty", availableTools:[], deny-all permissions, streaming) with per-conversation sessions, LRU/TTL eviction, abort, and dispose. - httpHandler.mjs: loopback HTTP/SSE adapter; GET status (warming->ready), POST chat (bearer-gated SSE: ready|delta|usage|message|error|done). Browser clients mirror a tiny wire protocol at the forced JS/TS seams, pinned by a deep-equal parity test: web/protocol.mjs + web/liveClient.mjs (iframe) and src/lib/live/protocol.ts + liveClient.ts (app). React app: useLiveChat hook (rAF-coalesced streaming via useSyncExternalStore so deltas don't re-render the invoice), a Simulated/Live segmented toggle, runtime status line, live model-availability gating, editable composer, real assistant text threaded through examples.ts into the transcript and invoice output row, and a separate "SDK-reported tokens" usage readout. Canvas: lazily-mounted isolated Live service under /live/*, a hidden-by- default chat panel revealed only after a successful /live/status probe, and ref-counted dispose on last-instance close. The SDLC static-server 404s /live/* so the visual fixtures stay stable. DESIGN.md gains Live-mode component tokens + prose (lint clean); all using existing accent/hover-highlight/muted/primary tokens (no new competing colors). typecheck clean, 79/79 unit tests, build, app a11y (both themes), and canvas visual specs all pass; verified end-to-end against the real runtime (status->ready, streamed "pong", SDK usage). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review fixes on top of the initial Live-mode feature, reviewed across
multiple model families (Claude, GPT-5.5, Gemini 3.1 Pro) with regression tests.
Concurrency & lifecycle (service.mjs):
- ensureRuntime() checks `disposed` before returning the cached runtime, the
single choke point that guarantees no SDK session/subprocess is spawned once
teardown has begun (even when a late setModel failure re-routes an in-flight
chat into acquireFreshSession). acquireFreshSession guards inside its IIFE.
- dispose() awaits in-flight session creation (pendingSessions) before
snapshotting sessions, so no created session is orphaned.
Client-disconnect abort (httpHandler.mjs):
- A POST/SSE turn's disconnect signal is the response socket closing, so listen
on `res.on("close")` (guarded by `!res.writableEnded`) instead of the
already-consumed `req` stream. Without this a real Copilot turn kept running
after the client navigated away (quota burn / busy session).
Mode switch as a conversation boundary (App.tsx):
- switchMode now clears transcript + invoice state, mints a fresh conversationId,
disposes the live session being left, and sets the draft per target mode, so
simulated and live turns never bleed across a mode change.
Backend session reset wiring:
- New bearer-gated POST /reset endpoint threaded through protocol, handler, both
clients, the hook, and App so resetPrompt/switchMode dispose the old backend
SDK session instead of only clearing local state.
Live toggle gating + non-warming mount probe:
- useLiveChat probes availability once on mount to gate the Live toggle (disabled
with an explanatory title on hosts with no engine), but uses a non-warming
status probe (?warm=0) so simulated-only loads never spawn the Copilot runtime.
Warming stays lazy until the user switches into Live (and the canvas keeps
warming, since its panel is hidden-until-available).
Tests: new httpHandler.unit.test.mjs (abort-on-disconnect, bearer-gated reset,
warm-flag wiring) and additional service.unit.test.mjs race/lifecycle guards;
the e2e empty-transcript case is hardened. Full validation green: typecheck,
vitest 89/89, build, e2e 15/15, a11y 4/4, canvas visual 4/4.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two defects stopped the canvas extension from installing and opening:
Open: the iframe threw "RangeError: maximumFractionDigits value is out of
range" and rendered an empty model-cost table. formatCredits set
minimumFractionDigits=6 while the value<1 branch capped maximumFractionDigits=4,
so any per-model input cost below 1e-4 AI credits (i.e. any short input — the
default open text "hello world" is only 3 tokens) crashed the whole table on
render. Clamp the formatter so the 6-digit minimum also raises the maximum.
The identical bug existed in the React webapp's formatAiCredits (the source this
canvas was ported from), reachable in Live mode with a short draft — fixed there
too for parity.
Install: the extension lacked the copilot-extension.json manifest, so the
gist-based "Install extension" flow refused it. Add the manifest
({ name, version }) the install flow validates.
Adds a canvas visual regression test that types a 1-token input and asserts the
full 23-row table still renders with no page error; verified it fails against the
pre-fix formatter. Validation: typecheck, vitest 89/89, build, e2e 15/15,
a11y 4/4, canvas visual 5/5, plus a live extension reload + open + headless
render check (23 rows, cheapest credit cell "0.000020", no console/page errors).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The canvas extension previously served a bespoke vanilla-JS tokenizer UI that diverged completely from the published GitHub Pages experience. It now builds and serves the same React app (src/App.tsx) - including the existing Simulated | Live chat toggle - so the canvas and webapp are literally one app (maximal code reuse), per the directive to extend the existing simulated chat with a live toggle rather than redesign it. - Add canvas build: src/canvas-entry.tsx (no service worker), canvas.html (relative-base shell that pins __COPILOT_LIVE_BASE__ + theme bootstrap), and vite.canvas.config.ts (base './', no PWA) -> web-ui/. New build:canvas script. - extension.mjs serves the built bundle from web-ui/ and mounts the Live engine at /copilot/live (aligned with the webapp). Remove the 4 bespoke web/ UI files. useLiveChat honors the __COPILOT_LIVE_BASE__ override. - Repoint the canvas evidence harness (static-server.mjs, canvas.visual.spec) at web-ui/ and the React app. Harden Live graceful-degradation (live/service.mjs): bound the runtime handshake with a timeout and add a distinct ENGINE_STATUS.error, so a resolvable-but-broken CLI runtime (e.g. a wrong-arch native addon that never initializes) degrades to "Live unavailable" instead of hanging forever on "Starting Copilot runtime...". Failed warm-ups cool down before re-spawning. App.tsx maps the new error status to the unavailable UX; Simulated is unaffected. New service unit test covers the timeout -> error + cooldown path. Validation: typecheck clean; vitest 90/90; canvas + webapp builds OK; canvas renders the React app with Simulated default + Live toggle; Live degrades to unavailable on this host while Simulated keeps working. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The canvas visual surface is now the built published React app served from web-ui/, not the removed bespoke web/ UI. Update the evidence provenance note accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Live chat mode to the tokenizer experience and makes the desktop canvas render the exact same published React app as the GitHub Pages site (parity), extending the existing Simulated | Live toggle rather than redesigning.
Stacked on top of #2 (
msft-tkendrick/tokenizer-canvas-extension); this PR contains only the Live-mode work.What's in this branch
src/App.tsx) fromweb-ui/(relative-base, no service worker) via the extension loopback server — canvas and webapp are one app (maximal code reuse).ENGINE_STATUS.error, so Live degrades to "Live unavailable…" instead of hanging when the Copilot runtime can't initialize; Simulated keeps working.web-ui/React surface.Validation
npm test→ 90/90 (incl. Live service/httpHandler/protocol-parity + new warm-up timeout test)npm run typecheck→ cleannpm run build:pages→ builds; canvas + webapp build greenCo-authored-by: Copilot 223556219+Copilot@users.noreply.github.com