Skip to content

Tokenizer Copilot canvas extension with Live mode + shared token-auth chat - #4

Open
MSFT-TKENDRICK wants to merge 10 commits into
mainfrom
msft-tkendrick/copilot-sdk-live-mode
Open

MSFT-TKENDRICK wants to merge 10 commits into
mainfrom
msft-tkendrick/copilot-sdk-live-mode

Conversation

@MSFT-TKENDRICK

Copy link
Copy Markdown
Owner

Why

The tokenizer web app only ever simulated Copilot chat. This stack adds a real GitHub Copilot Desktop canvas extension for the tokenizer, wires up an actual Copilot SDK-backed "Live" chat mode alongside the existing simulation, and (most recently) lets the published GitHub Pages site unlock that same Live experience via a user-supplied GitHub token, since the static site has no ambient/loopback Copilot engine the way the extension does.

What's here

  • Tokenizer as a Copilot canvas extension (spec 002): .github/extensions/tokenizer/ renders the same published React app inside a Copilot Desktop canvas panel, backed by a loopback HTTP server plumbed through the extension SDK.
  • Live mode (real Copilot chat): a Simulated | Live toggle extends the existing chat UI (no redesign). Live mode streams real responses via the Copilot SDK over the extension's ambient auth, with lifecycle hardening (abort, reset, warm-up polling, toggle gating) added after review passes.
  • Shared, reusable Live UX: the Live control surface (mode toggle, status badge, streaming message view) is extracted into shared components (LiveControls, LiveModeToggle, LiveStatusBadge, LiveMessageStream) and a shared LiveClient/useLiveChat transport contract, so the webapp and canvas render identical UX instead of duplicating it.
  • Website token-auth Live chat: since GitHub Pages has no ambient engine, a new LiveTokenForm lets a visitor paste a GitHub token, validated and used by a browser-direct createTokenLiveClient (GitHub Models API, OpenAI-compatible SSE) that implements the same LiveClient contract as the ambient client. The token lives only in the client's closure (never React state, never persisted, never logged). This path is gated behind a build-time capability flag (__BROWSER_TOKEN_AUTH__, true only for the Pages build) and loaded via a flag-checked dynamic import, so it is tree-shaken entirely out of the canvas/extension bundle -- the extension's ambient auth is completely unchanged. The Pages build also ships a connect-src CSP scoping outbound requests (and the token) to api.github.com / models.github.ai.
  • Agentic SDLC scaffolding: an sdlc/ package plus docs/sdlc/** (specs, facets, ADRs, BDD features, eval configs, evidence, and squad manifests) documents and grounds this work through a spec-to-evidence pipeline, including a separate model-cost-comparison spec/squad.

Notable design points for reviewers

  • Generation-guarded hook: useLiveChat(client) takes an injected transport and tracks a client generation + send id, so swapping between the ambient client and a token client (or vice versa) can never let a stale transport's status probe or in-flight stream write into the new one's state.
  • Transactional history: the token client only commits a turn to its in-memory conversation history after it fully succeeds, so an aborted or failed turn never corrupts the next request's context.
  • Capability gating, not runtime detection: whether the token-auth path exists is decided at build time (vite.config.ts sets the flag true only for build:pages; vite.canvas.config.ts forces it false), which is a stronger and more auditable guarantee than trying to infer "static site" at runtime.
  • Verified empirically: on a from-scratch static file server that 404s the extension's /copilot/live/* endpoints (faithfully reproducing GitHub Pages), the token form renders correctly in Live mode with zero CSP violations or page errors, and the rebuilt canvas bundle contains zero references to the GitHub Models transport.

Validation

  • npm run typecheck clean
  • npm test -- 105 vitest tests passing (including 15 new tests for the token client covering status probing, SSE streaming/usage/CRLF framing, multi-turn history, abort semantics, and model-id mapping)
  • npm run test:e2e / npm run test:a11y -- 15 Playwright specs passing (DOM contract for the toggle/status/composer preserved through the component extraction)
  • npm run test:visual -- 6 visual scenarios captured
  • Canvas visual specs (playwright.canvas.config.ts) -- 3 specs passing against the built canvas bundle
  • npm run build:pages and npm run build:canvas both succeed; manual diffing of the two bundles confirms the token-auth transport is present only in the Pages build

MSFT-TKENDRICK and others added 10 commits June 12, 2026 12:54
Introduce an agentic SDLC workflow that turns a SpecKit spec into a
provably spec-grounded squad of Copilot custom agents with evidence for
every requirement.

Toolkit (sdlc/, @tokenizer/sdlc):
- SpecKit parser + ambiguity detector (interview agenda)
- micro-spec facet + ADR managers
- squad role derivation + forge with a grounding proof
  (100% requirement coverage, no ungrounded members, no dangling refs)
- Copilot SDK inference + fleet dispatch (deterministic offline fallback)
- ASSERT eval_config.yaml generation (squad-grounding + per-capability)
- BDD Gherkin generation, guarded TDD red/green/refactor loop
- evidence collector (coverage, screenshots, video, Playwright)
- CLI (run/interview/ground/fleet) + full pipeline orchestrator
- 53 vitest tests, 94.5% statement coverage, offline-deterministic

Agents (.github/agents/):
- sdlc orchestrator + spec-interviewer, facet-curator, evidence-auditor
- generated, grounded squad members under squad/

Docs + worked example:
- docs/sdlc/sdlc-agentic-workflow.md methodology
- specs/001-model-cost-comparison SpecKit spec (spec/plan/tasks)
- generated bundle (interview, grounding 13/13, 6 evals, 5 features /
  13 scenarios, TDD loop, evidence) + ADR-001 / resolved MSF-001
- sdlc_assert_targets.py ASSERT callable adapter

Root: add sdlc convenience scripts; exclude sdlc/** from app vitest.
No app regressions (typecheck + 12 tests green).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make the dev inner-loop / outer-loop relationship explicit across the
orchestrator agent, methodology doc, and facet-curator:

- Outer loop owns spec meaning (interview -> facets/ADRs -> grounded
  squad -> evals); inner loop owns implementation (BDD/TDD/evidence).
- The inner loop feeds the outer loop: implementation discoveries that
  reveal ambiguity or require a requirement change escalate rather than
  guess, via derivative facet specs and targeted user re-elicitation.
- A requirement revision records an ADR, updates the SpecKit spec, and
  re-forges/re-grounds the squad with affected evals/features/TDD
  entries regenerated before the inner loop resumes.
- Updated the mermaid diagram to show the inner->outer feedback edge.

Docs only; no code changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the tokenizer as a GitHub Copilot desktop canvas extension,
driven end-to-end by the agentic SDLC orchestrator over
specs/002-tokenizer-canvas.

Canvas (.github/extensions/tokenizer/):
- extension.mjs: SDK canvas wiring - per-instance loopback http.Server,
  static asset + /state persistence routes, SSE /events, and
  set_text / get_summary actions. Never writes stdout (JSON-RPC safe).
- web/{index.html,styles.css,app.js}: iframe UI (live summary, color +
  non-color token categories, sortable 23-model cost table) on app theme
  tokens.
- web/{tokenizer.mjs,models.mjs}: 1:1 ESM ports of src/lib/tokenizer.ts
  and src/lib/copilotModels.ts, shared by iframe and Node actions.
- Session-scoped persistence keyed by sessionId under $COPILOT_HOME,
  outside the repo tree (ADR-002 / MSF-006).

SDLC artifacts (spec 002):
- specs/002-tokenizer-canvas/spec.md (FR-009 clarification resolved).
- docs/sdlc/002-tokenizer-canvas: interview, GROUNDED proof (13/13),
  squad manifest, 8 ASSERT evals, 7 BDD features, TDD loop, evidence.
- docs/sdlc/facets/MSF-006..010 + docs/sdlc/adr/ADR-002.
- .github/agents/squad/tokenizer-canvas-*: 7 spec-grounded members.
- tests/sdlc/tokenizer-canvas: TDD tests, Playwright visual spec, static
  server. Evidence screenshots committed; regenerable videos/traces/
  coverage HTML are gitignored.

Gates: npm test 66/66, typecheck clean, build OK, design:lint 0/0.
Canvas runtime-validated (reload->ready, open, set_text/get_summary,
/state persistence, input-schema + reserved-name guards).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
Extract the Live chat control surface into reusable components shared by the
webapp and canvas, and add a protected GitHub-token connector that unlocks the
same Simulated|Live toggle on the static GitHub Pages site (which has no
ambient/loopback engine). The extension's ambient auth is left untouched.

- Add createTokenLiveClient: a browser-direct GitHub Models transport
  (OpenAI-compatible SSE) behind the shared LiveClient contract, with in-memory
  multi-turn history committed only on success, CRLF-robust SSE framing, and
  scrubbed HTTP errors. The raw token lives only in the client closure -- never
  React state, never persisted, never logged.
- Extract a shared liveTransport.ts contract; refactor useLiveChat to accept an
  injected client with generation + send-id guards so an ambient<->token swap
  can never cross streams or status writes.
- Add shared components LiveModeToggle / LiveStatusBadge / LiveTokenForm /
  LiveControls; App.tsx renders LiveControls on both surfaces, preserving the
  exact DOM the e2e/a11y/visual specs assert.
- Gate the token path on a build-time capability flag (__BROWSER_TOKEN_AUTH__,
  Pages build only) via a flag-checked dynamic import, so the GitHub Models
  transport is tree-shaken entirely out of the canvas/extension bundle.
- Ship a connect-src CSP on the Pages build confining outbound requests (and
  thus the token) to api.github.com / models.github.ai.

Validation: typecheck clean; 105 vitest (incl. 15 new token-client tests);
15 e2e + a11y; 6 visual; 3 canvas visual specs. Empirically verified on a
faithful static host that the token form renders in Live mode with zero CSP
violations, and that the rebuilt canvas bundle ships no GitHub Models transport.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover a malformed JSON SSE frame (must not crash the stream, valid deltas
still finalize) and a 429 rate-limit response (clear message, no token
leakage in the thrown error).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@MSFT-TKENDRICK
MSFT-TKENDRICK marked this pull request as ready for review September 1, 2026 13:58
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