The teaching agent no longer runs on pi. Stack:
- ToolLoopAgent (ai@7) + OpenRouter (OPENROUTER_API_KEY), model via LH_MODEL,
default anthropic/claude-sonnet-4.5 with reasoning tokens
- quiz/ask_user_question = client-side tools (no server execute): the call
lands as a typed tool part in input-available state; the card grades via
src/core/question-core.ts; addToolOutput sends the verdict; the teacher's
reaction continues the same assistant message
- resume-mid-quiz is free: the pending tool part persists in the transcript
- persistence: one JSON per chat (data/chats/), full UIMessage transcripts,
saved onFinish (also on abort)
- UI: AI Elements (owned copy-paste components, pruned to the five in use)
+ button-based QuizCard/AnsweredCard (ask answers never graded)
- pi fully removed: TUI, extensions, Fastify+WS server, bridge, custom
protocol all deleted (recoverable from git history)
- tests: question-core units carry over; chat-flow integration drives
handleChatRequest with MockLanguageModelV4 (48 tests, zero tokens);
render smoke tests guard the cards (renderToStaticMarkup)
Carried over unchanged: teacher system prompt, question-core, schema/
ledger/derive/extract pipeline, data model.
- Markdown.tsx never passed text as children to ReactMarkdown (valid JSX,
compiled clean, rendered nothing) — the 'thinking shows, text doesn't' bug
- toolResult entries (quiz/ask dispatch instructions for the LLM) leaked
into the transcript as visible text; now never rendered
- render smoke tests (renderToStaticMarkup, no DOM): pin markdown content,
KaTeX, button-based quiz options, ask answers not graded, toolResult and
[Quiz answer] hiding — negative-tested against the actual bugs
- React default import in all .tsx: tsx test runner uses classic JSX
transform (ignores tsconfig jsx option), Vite uses automatic runtime;
explicit import works under both
- katex css moved to main.tsx (tests can't import css)
- verified live in browser: all assistant messages render, ask card has no
grading marks, pending quiz re-arms on resume
Root causes of the v1 browser bugs (all fixed + covered by deterministic tests):
- duplicated messages: per-socket snapshot + global broadcast marking raced;
replaced with a single append-only entry log (exactly-once per socket,
structurally) — covered by tests/server.test.ts
- delayed/missing echo of own messages: optimistic append with sending/failed
status, reconciled by text when the real entry arrives
- no thinking shown: thinking deltas streamed live (kind: thinking) and
persisted in assistant entries so they survive reloads
- ask answers graded as wrong: AnsweredCard no longer applies quiz grading to
ask answers (no ✓/✗, no verdict)
- broken option selection: native radio/checkbox inputs fighting React state
replaced with button-based options (single source of truth, one-click select)
- no URLs: History API routing — / home, /s/<file> per session (deep links,
refresh, back button); session view resumes its file on mount
- lost connection = dead UI: exponential-backoff reconnect with full resync
- stick-to-bottom transcript with jump-to-latest pill
Server: startServer() with injectable runtime factory (scripted model in
tests); createSession returns file for routing; scripted model gained thinking
turns. 51 deterministic tests green.