Skip to content

Add Live mode: real Copilot chat (app + canvas) with React parity#3

Merged
MSFT-TKENDRICK merged 5 commits into
msft-tkendrick/tokenizer-canvas-extensionfrom
msft-tkendrick/copilot-sdk-live-mode
Jun 15, 2026
Merged

Add Live mode: real Copilot chat (app + canvas) with React parity#3
MSFT-TKENDRICK merged 5 commits into
msft-tkendrick/tokenizer-canvas-extensionfrom
msft-tkendrick/copilot-sdk-live-mode

Conversation

@MSFT-TKENDRICK

Copy link
Copy Markdown
Owner

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

  • Live mode (app + canvas): real GitHub Copilot chat via the Copilot SDK, gated behind the existing Simulated/Live toggle. Ambient auth; streaming responses; usage filtering; tool lockdown; lifecycle/abort/reset handling.
  • Canvas parity: the extension now builds and serves the published React app (src/App.tsx) from web-ui/ (relative-base, no service worker) via the extension loopback server — canvas and webapp are one app (maximal code reuse).
  • Install/open fix: resolves the extension failing to install and open.
  • Warm-up hardening: bounded handshake with timeout + cooldown and a distinct ENGINE_STATUS.error, so Live degrades to "Live unavailable…" instead of hanging when the Copilot runtime can't initialize; Simulated keeps working.
  • Docs: evidence provenance repointed at the built web-ui/ React surface.

Validation

  • npm test90/90 (incl. Live service/httpHandler/protocol-parity + new warm-up timeout test)
  • npm run typecheck → clean
  • npm run build:pages → builds; canvas + webapp build green
  • e2e 15/15, a11y 4/4, canvas visual 3/3 (from prior runs)

Note: a true end-to-end Live round-trip can't be exercised on the dev host (Windows ARM64 vs a win32-x64 Copilot CLI package with no arm64 native addon) — an environment/packaging limitation, not a code defect. Live wiring is covered by unit tests against a fake SDK plus a documented standalone SDK probe.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

MSFT-TKENDRICK and others added 5 commits June 15, 2026 07:28
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>
@MSFT-TKENDRICK MSFT-TKENDRICK merged commit 06f0864 into msft-tkendrick/tokenizer-canvas-extension Jun 15, 2026
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.

1 participant