Commit Graph
20 Commits
Author SHA1 Message Date
Amos c55bae0de6 Wave 0: shared contracts + extension points
- src/core/contracts.ts: plan tree / graph / ledger / job protocol / artifact
  registry types — the single source of truth for all workstreams
- tools: per-request factory createTeacherTools({chatId}) composing
  questionTools + planTools (stub) + jobTools (stub); quiz gains nodeId
- prompt composition: system-prompt.md + sorted src/agent/prompts/*.md
- ChatRecord.tree + saveTree; pending gate scoped to quiz/ask (+findPendingJobCall)
- chat.tsx: artifact renderer extension point + ChatActionsContext.syncFromServer
2026-08-22 09:06:24 +03:00
Amos c338f468f8 docs: build plan v1 — waves, gates, worker execution model 2026-08-22 08:41:07 +03:00
Amos 64ea13f0a7 docs: graph-as-session-context, sub-agent architecture, not-a-chat deprioritized
- Every session starts with the learner's knowledge graph as base
  context (precision probing); selection problem noted: pedagogical
  relevance != semantic closeness (structural neighborhood, diagnostic
  hotspots, frontier strip); strategies to tune on data.
- Sub-agent architecture: async background workers with live activity
  indicators; demo-builder TESTS code headlessly; visual-maker verifies
  by looking (port of vault svg-maker); mermaid needs no sub-agent;
  researcher tool for fact verification (port of pi web-search/web-fetch
  + interactive-subagents researcher prompt).
- Not-a-chat presentation principle marked LOW PRIORITY per user.
2026-08-22 08:23:42 +03:00
Amos 588897fba6 docs: refine not-a-chat — spine + interjection cards, anchoring as overlay
Margin model flaws (user feedback): fragment placement needs unreliable
routing; scattering kills the receipt moment and chronology; session
value is in the moment, revision is rare, HTML export deprioritized.
Evolved model: one chronological spine; learner blocks are intact typed
'interjection' cards; teacher replies as woven Q&A interludes
(quote-then-answer); anchoring is a link overlay (backlinks), margins
demoted to a computed view. Chat-ness dies by type asymmetry, not
relocation.
2026-08-22 08:05:02 +03:00
Amos c46e8561c8 docs: presentation principle — it is not a chat
Asymmetry of artifact: the teacher writes the document (whiteboard), the
learner writes in the margins (voice/floor). Bubbles die; the unit is the
block; chronology yields to tree structure; Q&A becomes document
furniture; the composer becomes a response field. Presentation-layer
change only — the interaction model stays conversational.
2026-08-22 07:56:33 +03:00
Amos 3c3deef045 docs: demos as artifacts, dictation/notes direction, structural-identity ideation
- Interactive demos: one format (self-contained HTML/JS in sandboxed
  iframe), demo tool contract (resize bridge, error->fix loop, versions),
  persistence via tool parts; Pyodide deferred, server Python rejected.
- Notes as first-class reasoning channel; dictation requirements
  (AudioWorklet PCM, Deepgram token mint, never-lose-a-dictation) ported
  from the pi dictate extension's proven recipe.
- Structural identity ideation: sectioned lesson-document transcript,
  gated progression with learner-chosen branches, typed blocks, session
  state strip, retrieval-first session opens, durable compiled artifact,
  visible tree edits. Gamification explicitly avoided.
- Roadmap: demo tool and structural candidates slotted; dictation later.
2026-08-22 07:46:11 +03:00
Amos 6e7f118190 docs: graph rendering decision — sigma.js v3 + graphology + forceatlas2
Research verdict: ~40KB gz, synchronous run-N-ticks-then-freeze layout,
labelRenderedSizeThreshold = Obsidian's text fade threshold, near-zero
idle CPU. Obsidian internals confirmed: PixiJS WebGL + d3-force-style
custom sim. Rejected: vis-network, cytoscape, react-force-graph-2d,
cosmos.gl. Pre-impl validation: 2k-node synthetic benchmark.
2026-08-22 07:36:00 +03:00
Amos 7c9132d5b4 docs: vision & plans — graph construction, session experience, presentation, roadmap
Captures 2026-08-22 decisions: hybrid live-emission + consolidation graph
construction (reversing the vault doc's teacher-untouched rule), one
substrate merged across sessions, structured plan-tree tools (mermaid
demoted to ad-hoc), probe/teach phase separation with collapsed teach
checks, right-panel tree view, LaTeX delimiter switch plan, and a
dependency-ordered roadmap.
2026-08-22 07:33:05 +03:00
Amos 3c4eac8061 Fix bar-click flicker: suppress collapse while the reveal scroll is in flight
Clicking the collapsed bar set expanded=true and started a smooth
scrollToBottom — but the scroll position was still legitimately inside
the collapse zone (that's why the bar was showing), so the hysteresis
listener re-collapsed the dock on the very next scroll event, then
re-opened it when the scroll arrived: open → snap shut → reopen.

The machine had no notion of intent. Now the bar click sets an
expandIntent flag that suppresses the collapse branch; it clears when
the scrollToBottom promise settles (completion or interruption) or after
a 2s safety net, so a user yanking the scroll away mid-reveal collapses
the dock again instead of wedging it open.

Verified live: click = exactly one transition (bar→open at ~26ms, stays
open, lands at d=1); collapse still healthy afterward; mid-reveal
interrupt re-collapses within ~300ms.
2026-08-22 06:56:39 +03:00
Amos d1d28d6bb4 Session switch: land at the bottom instantly with the question dock open
- Chat now remounts per session (key={chat.id}): fresh StickToBottom,
  fresh DockOverlay (no cross-session morph from the previous shell
  height), fresh useChat.
- Conversation initial="instant" (was "smooth"): the mount scroll to
  bottom is a jump, not a slide.
- DockOverlay skips the pending-question scroll/expand effect for the
  session's INITIAL pending question (dock starts expanded; the initial
  scroll positions us) — previously it fired a second smooth
  scrollToBottom right after the library's initial scroll (the visible
  'pause, then the quiz pops and pushes everything up').
- The dock-variant fade/rise no longer plays for the session's opening
  variant; reserved for mid-session transitions.
Verified live: client-side switch settles at d=1px from the bottom within
one frame of content swap, dock open from the first frame, both
directions (quiz session, plain session).
2026-08-22 06:45:32 +03:00
Amos 939e1e2e5e Scroll behavior: hysteresis collapse, expand-scrolls-to-bottom, streaming follow
- Oscillation fix: collapsing the dock frees ~Δpx of flow height; within
  Δ of the bottom the browser clamps scrollTop to the new max, landing
  the learner AT the bottom, which re-expanded the dock → loop. Now a
  hysteresis state machine on the scroller's scroll events: collapse only
  when distance-from-bottom > Δ+64 (strictly clamp-proof), expand only
  within 16px of the bottom. ~370px dead band; exactly one transition per
  direction verified live. Heights measured live per question.
- Reaching the bottom re-expands the dock AND scrolls to reveal it fully
  (previously it grew over the content and left you short of the bottom).
- Streaming follow fixed: ConversationContent had h-full, so the observed
  content box never grew and use-stick-to-bottom's ResizeObserver never
  fired — no auto-scroll during streaming. min-h-full restores growth.
  Verified: distance-from-bottom ≤70px transient during a 25s stream,
  settles at 1px.
2026-08-22 06:22:40 +03:00
Amos aeef28d4d9 UI iteration 4: dock morph animation, sticky-in-flow dock, scrollbar, indicators
- Animation: the blob's shell morphs height (WAAPI, 180ms) on every
  variant change — chat ↔ question ↔ collapsed bar — plus a quick
  fade/rise (160ms) on the swapped content. Gotcha pinned in code: never
  measure the shell while a morph is in flight (getBoundingClientRect
  reads the ANIMATED value and corrupts the next morph's start height);
  cancel running morphs before measuring, refresh settled height only
  when no morph is running.
- Gradient-over-scrollbar fixed structurally: the dock is no longer an
  absolute overlay on the conversation root — it's sticky bottom-0 INSIDE
  the scroller (in flow). The transcript scrolls fully clear of the dock,
  the fade never touches the scrollbar lane, and the pb-hack clearances
  are gone.
- New question while at the bottom scrolls to reveal the dock; scrolled
  up → collapsed bar, no yank.
- Thin dark native scrollbar app-wide (8px webkit + scrollbar-width:thin,
  dark thumb, transparent track). Note: use-stick-to-bottom sets
  scrollbar-gutter: stable both-edges on its scroller — the lane is
  outside the content flow by design.
- 'Writing a question…' transcript shimmer while a tool call streams its
  input (same slot as 'Thinking…'). Protocol pinned by a test asserting
  tool-input-start precedes tool-input-available; scripted mock now
  streams tool input as deltas like real providers do. (OpenRouter/
  Anthropic often delivers the whole input in ONE delta — then the dock
  appears instantly and no indicator is needed or shown.)

Verified live: grow 54→349px and shrink 349→54px fire with correct
from/to; collapse/expand intact; gradient stops before the gutter;
Thinking… observed in-page; 58/58 tests.
2026-08-22 05:56:52 +03:00
Amos cd789c11b1 UI iteration 3: question dock — pending questions morph the composer blob
- A pending quiz/ask now REPLACES the chat composer: one persistent
  rounded-2xl shell in the transcript overlay swaps contents (composer ↔
  question ↔ collapsed bar), never remounting — the structure a future
  grow-morph animation needs.
- Unified bottom row inside the question: the note field (select modes),
  the answer field (text-mode asks), or the Other text — one field, one
  'Submit answer' button, Enter submits.
- Collapse on scroll: uses useStickToBottomContext — scroll up and the
  dock folds to a slim bar (kind chip + LaTeX-flattened question teaser +
  'tap to answer'); click scrolls to bottom and re-expands. The expanded
  form stays mounted-hidden so a half-made selection survives.
- Pending/streaming tool parts no longer render inline; answered parts
  land in the transcript as graded cards on submit. Question generation
  shows a shimmer in the shell.
- Composer back to pure chat (no mode machinery); quiz-card.tsx and the
  pendingText/registerSubmit bridge deleted; render tests re-pointed at
  the dock (wrapped in StickToBottom).

Verified live: docked quiz submit (note+selection), ask-text Enter
submit, collapse/expand with selection survival, bar teaser text.
2026-08-22 05:02:15 +03:00
Amos 0baa00e418 UI iteration 2: toggleable sidebar, floating composer blob, pending-question morph
Sidebar (replaces the modal drawer):
- Inline, toggleable, no backdrop/focus theft — can stay open while working
- Open state persists across pages (provider in root layout) and reloads
  (localStorage; starts closed server-side to avoid hydration mismatch,
  stored value applied post-mount; default open on first run)
- Hamburger in page headers only when closed; close button in sidebar header

Floating composer blob (per reference screenshot):
- Floats over the transcript (absolute overlay), persists at bottom while
  scrolling, content fades behind it (gradient mask); pb-44 scroll clearance
- Concentric corner radii: blob rounded-2xl(16px) − p-1.5(6px) = send
  rounded-[10px] = 25% of the 40px button; CornerDownLeft icon per reference
- Stop moved from header into the blob (busy → square icon)
- prompt-input.tsx + 7 orphan ui files pruned (attachments/command palette
  unused); hand-rolled Composer is ~100 lines and render-tested

Pending-question morph (one text field on screen, never two):
- Quiz/ask-select pending → blob becomes the note field (send hidden, hint
  line above); card's internal note input removed; Submit consumes it
- Ask free-text pending → blob becomes the answer field; its send submits
  via the card's registered submit api (ref bridge); Enter works
- Chat owns the single text state; card clears it on submit
- canSend for answer mode mirrors the card predicate directly — reading the
  ref during render lagged one commit (found in browser testing)

Widths: one shared column (max-w-3xl + px-4) for transcript, cards, and
composer — all three measured pixel-identical (left 416, width 736).
Assistant Message max-w-full/MessageContent w-full (was max-w-[95%]/w-fit —
the 'cards are a bit shorter' bug).

User messages render plain text again — no math pipeline on learner-authored
text (currency hazard accepted for the model only, guarded by the prompt).

Tests: 58 pass (5 new composer pins; card tests updated to controlled
props). Verified live: note flow, answer flow, sidebar persistence, float,
fade, zero console errors — plus the live model spontaneously honoring the
$ currency escape contract in a continuation.
2026-08-22 04:28:13 +03:00
Amos 46d71380fa UI iteration 1: app shell + full LaTeX pipeline
Layout:
- Home = knowledge-graph placeholder (main view) with new-session input
  docked below; session list moved out of home into a hamburger drawer
- SessionDrawer: slide-over session navigation (backdrop/ESC close, active
  highlight, fetches /api/chats on open) on both home and chat headers
- Chat header loses the back button — the drawer is the navigation

Math (inline $ was silently not rendering):
- @streamdown/math defaults singleDollarTextMath:false — now
  createMathPlugin({ singleDollarTextMath: true }) in message + reasoning
- katex.min.css imported in layout (streamdown does not inject plugin
  styles; display math only looked right via browser MathML fallback)
- QuizCard/AnsweredCard route question/options/explanation through
  MessageResponse; user chat bubbles render markdown+math too
- Currency hazard: micromark-extension-math@3.1 lacks pandoc's
  closing-digit rule, so bare '$5 and $10' parses as math — defense is at
  the source: system prompt now instructs escaped \$5 / 'USD' for money

Hydration fix (React #418):
- QuizCard shuffled options with Math.random() — server/client orders
  diverged. shuffleOptions now takes an injectable RNG; QuizCard passes
  seededRandom(toolCallId) (mulberry32/FNV-1a in question-core). Same
  question → same stable order across reloads; still varies per question.

Tests: 53 pass (5 new math-rendering pins incl. fenced-vs-one-line $$
behavior and the escaped-currency contract). tsc + next build clean.
Verified live in browser: inline/display math in text + cards, drawer
open/navigate/active-highlight/ESC, no console errors, no font 404s.
2026-08-22 03:23:11 +03:00
Amos a72c902d30 v3: AI SDK pivot — pi removed, Next.js 16 + AI SDK 7 + AI Elements teacher app
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.
2026-08-22 02:18:27 +03:00
Amos e127a9bfbd fix: assistant text rendering, toolResult plumbing leak; add render smoke tests
- 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
2026-08-22 01:20:14 +03:00
Amos 296cc1b7b6 v2 web interface: fix delivery races, real URLs, optimistic echo, thinking stream, button-based quiz UI
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.
2026-08-22 00:57:16 +03:00
Amos 25063b2a3e v1 web interface: dispatch-mode quiz/ask, Fastify+WS server, React frontend, deterministic test suite
- extensions/core/question-core.ts: pure quiz/ask logic (normalize/shuffle/resolve/grade/format) shared by both interfaces
- extensions/core/bridge.ts: in-process server<->extension bridge (answer/dismiss/re-arm)
- extensions/quiz-web.ts + ask-web.ts: web dispatch mode — tool returns immediately, graded answer arrives as next user message; sessions resumable mid-question via lh_question/lh_question_answered custom entries (never in LLM context)
- extensions/quiz.ts: refactored onto shared core (terminal blocking mode unchanged)
- src/server/index.ts: Fastify + WebSocket; REST (list/create/resume), entry sync (diff-based, all message kinds), streaming deltas, pending-question push
- web/: Vite + React — home view (graph placeholder, session list, new-session input), session view (markdown+KaTeX+mermaid transcript, quiz cards, streaming)
- src/testing/scripted-model.ts: deterministic fake LLM (scripted model runtime injected via factory overrides)
- tests/: 48 deterministic tests (unit: question-core + bridge; integration: full pi loop with scripted model incl. resume re-arm)
- scripts/e2e.ts: live smoke test (manual, real LLM, never a gate)
- teacher/system-prompt.md: end-your-turn rule for question tools
2026-08-22 00:19:49 +03:00
Amos 9a6af9697f v0 scaffold: teacher runtime (hermetic pi SDK), data model, ledgers, normalizer
- teacher/system-prompt.md: teach methodology + quiz/ask guidelines folded in
- extensions/: quiz.ts + ask-user-question.ts (hermetic copies)
- src/teacher/create-session.ts: isolated agent (noTools builtin, own agentDir/sessions/prompt)
- src/schema.ts + ledger.ts: append-only ledgers, known-vs-understood data model
- src/extract/normalize.ts: session JSONL -> canonical doc (verified on real session)
- extractor/merger/derive/what-next: specified stubs
2026-08-21 21:37:28 +03:00