Tokenizer Copilot canvas extension with Live mode + shared token-auth chat - #4
Open
MSFT-TKENDRICK wants to merge 10 commits into
Open
MSFT-TKENDRICK wants to merge 10 commits into
MSFT-TKENDRICK wants to merge 10 commits into
Conversation
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
marked this pull request as ready for review
September 1, 2026 13:58
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.
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
.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.Simulated | Livetoggle 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.LiveControls,LiveModeToggle,LiveStatusBadge,LiveMessageStream) and a sharedLiveClient/useLiveChattransport contract, so the webapp and canvas render identical UX instead of duplicating it.LiveTokenFormlets a visitor paste a GitHub token, validated and used by a browser-directcreateTokenLiveClient(GitHub Models API, OpenAI-compatible SSE) that implements the sameLiveClientcontract 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 aconnect-srcCSP scoping outbound requests (and the token) toapi.github.com/models.github.ai.sdlc/package plusdocs/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
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.vite.config.tssets the flag true only forbuild:pages;vite.canvas.config.tsforces it false), which is a stronger and more auditable guarantee than trying to infer "static site" at runtime./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 typecheckcleannpm 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 capturedplaywright.canvas.config.ts) -- 3 specs passing against the built canvas bundlenpm run build:pagesandnpm run build:canvasboth succeed; manual diffing of the two bundles confirms the token-auth transport is present only in the Pages build