Plan canvas (owner report: arrows clipped at the viewBox edge in dist —
measured 14px/20px cuts in the owner's real plans): blockLayout now
unions node boxes with every edge path's control-point hull (a bezier
never leaves it; the layout calls edgePathD with the renderer's exact
inputs). Dagre's node extent stays the floor, so ordinary plans render
byte-identical; the viewBox origin shifts only when ink bulges past
zero. Verified on both real chats; pins use the owner's real
linear-algebra tree (golden-real-data).
Keyboard-first (owner's law: Enter always submits, arrows always cycle;
text fields the only exception):
- Enter guard split (enter-submits): a focused BUTTON keeps native
Enter only when keyboard-walked (:focus-visible) or inside an open
overlay (menus/dialogs); pointer-clicked transcript buttons submit,
preventDefault suppressing the re-activation underneath.
- Focus hygiene (focus-hygiene): pointer clicks on transient controls
(agent/user thinking toggles, copy, mermaid expand, plan chip) blur
to body — the state clicking empty space produces.
- Session search owns the walk: Up/Down wrap-cycle the filtered rows,
Enter opens the walked row or the first match (quick-open), then
blurs and keeps the query. The Alt-arrow session cycle is RETIRED.
- Registry: model-menu walk/pick entries removed (obvious in-field
navigation, not settings material); question-dock keys stay
displayOnly per owner ruling.
Gates: 1489/1489, typecheck, lint. Verified live end-to-end.
Owner verdict on the accent step: way too light - in-card thinking
drops the band entirely (rail only, identical heights). The rail
narrows again: 14px gap inside the 3ch inset (rail calc(3ch-14px),
about 8px) - the wide rail was distracting. Total inset still 3ch,
chevron law intact.
Owner round four on the user rows: (1) the rail thins - the gap inside
the 3ch inset grows 5px -> 8px, so the rail is calc(3ch-8px); total
inset still 3ch, chevron law intact. (2) The band padding drops again,
5px -> 4px (py-1). (3) The in-card thinking band steps to bg-accent -
the same ONE-STEP lift the chat band gets (background->card on the page,
card->accent on the card); card-color on card-color was invisible, the
accent gives the card rows a visible band with identical heights. (4)
pr-4 on the band rows - the expanded text wraps EARLY instead of hugging
the band's clear right edge.
Owner round three on the user rows: (1) the band padding halves,
py-2.5 -> py-[5px] — subtle, not tall; same for the messages. (2) The
in-card thinking joins the band: identical styling and heights for
collapsed thinking in the chat and inside question cards (card-on-card
is quiet by design; revert if it reads too light). (3) The left gap
MATCHES the new 5px padding, so the rail fills the rest of the 3ch
inset - border-l-[calc(3ch-5px)] pl-[5px]: a thick muted rail, total
inset still 3ch (the chevron-alignment law survives). The banded prop
dies - one geometry, cards included.
Owner round two on user-vs-agent clarity (the chat-comeback was tried
and set aside): we go all in on documents. No chat sides. User messages
and every transcript user-thinking row are full-width CARD-COLOR BANDS
- the band is the authorship mark against the agent's transparent
prose. No rounded corners, no borders besides the rail.
The rail keeps its muted color but is now 6px wide (was 2px): the
artificially wide 3ch inset (the chevron-alignment law with the agent's
flush rows) is re-split - rail eats the air, the rail-to-text gap drops
from 3ch-2px to a natural 3ch-6px, and the total inset stays 3ch so the
chevrons still align. The 'You' label stays on messages - one register
with the thinking labels.
In-card thinking (inside answered cards) takes the rail but NO band:
card-color on card-color would be invisible; geometry alone carries it.
Transcript rows arrive expanded (clearly sent) - the reading lift now
opens on arrival too; the in-card row stays collapsed. Cmd+T still
rules all rows.
Also this round: the answered-card last-child rhythm law
([&>*:last-child]:mb-0) - on ask cards the thinking row's my-3 stacked
with the card's p-4 into a 28px void below (12px above); the card's own
padding is the bottom rhythm when a row ends the card.
Blockquotes: the callout override had displaced Streamdown's own styled
blockquote component, so plain quotes rendered as bare unstyled text
since that commit. The pass-through branch now restores Streamdown's
verbatim classes (border-l-4, muted italic).
Explanations tucked for ALL questions (owner): the prediction's Why /
Hide-why toggle is universal — every quiz answered card sits its
explanation behind the same collapsed button. Same pattern, one law.
The thinking row's rhythm: my-1 -> my-3. The 4px margin hugged the
options on non-graded answered cards (owner report); 12px matches the
card's mt-2/mt-3 spacing grammar everywhere the row renders.
Side-based authorship (owner round, 2026-10-07): the document shape
blurred user into agent — three left-rail idioms (user text, user
thinking, quoted prose) collided once quotes rendered. User rows go
RIGHT: 'You' messages and practice thinking cards hug the right edge at
85% width with the rail mirrored (border-r), text still reading
left-to-right. In-card thinking (inside the answered card, the agent's
context) stays left. Transcript thinking rows arrive EXPANDED — the
thinking was clearly sent. Cmd+T (toggle-user-thinking, chat scope,
cheat-sheet row) sets every user-thinking row to one state via a module
store: explicit sets stomp individual toggles, late mounts honor the
last set (the state initializer reads the store, so static render
reflects it too).
- THE 3CH RAIL: 'AGENT’S THINKING' (16ch, flush) − 'YOUR THINKING' (13ch)
= 3ch of mono — the rail widens to 3ch (border + padding), keeping the
chevrons aligned while giving the quote room to breathe (the 1ch rail
read cramped). YOU row follows.
- THE STANDARD CHORD everywhere: Enter submits, Shift+Enter is a
newline, ⌘/Ctrl+Enter works too — the teaching thinking field drops
the ⌘-only Gmail convention; the hint reads '↵ submits · ⇧↵ newline'
in both docks, shown in text mode too (it Enter-submits as well).
- The transcript-end optimistic placeholder joins the register: small
mono caps THINKING… (was text-sm sentence case).
- Model names keep their dashes: prettyModel renders 'glm-5.3-flash'
exactly as written (no auto-caps, no dash→space join).
Owner round 4 (2026-10-05):
- THE RAIL IS THE MISSING CHARACTER: rail + padding = exactly 1ch of
the mono heading font. 'YOUR THINKING' (13ch) + rail == 'AGENT
THINKING' (14ch) flush — the chevrons line up vertically, no fixed
columns. The YOU heading joins the rail (label inside, like the
thinking rows) — one quote geometry everywhere.
- HEADING REGISTER: YOUR THINKING / AGENT THINKING / YOU all use the
card eyebrows' small mono caps (text-xs = the 11px rung, font-medium
uppercase tracking-wide) — the app chrome is JetBrains Mono.
- SUBMIT THINKING: the practice button is now literally the question
dock's Button (h-9, px-3.5) in one p-1 row with the hint left
('↵ submits · ⇧↵ newline') — the keyboard hints are back. Corner
radii concentric in both docks: shell 16 − 6 = field 10 − 4 = button 6
(rounded-[10px] fields, rounded-[6px] buttons — the old rounded-xl/12
was the 'too large' culprit).
- MODEL NAMES: prettyModel no longer auto-capitalizes — names render
exactly as their providers write them ('glm 5.3 flash').
- Quiz explanations pulled closer (mt-3 → mt-2).
Owner round 3 (2026-10-05):
- CONCLUDE CARD: the stacked same-weight headings are gone. Plain
'Concluded' eyebrow + the outcome in the quiz cards' EXACT chip
register. Colors per outcome: solved green (the 'correct' twin),
solvedWithHints amber (succeeded with cost), surrendered red (the
problem won), movedOn muted (the agent's call, not a verdict against
the learner). Verdict as plain 50% prose under the heading — the
'Verdict:' label is gone — then a divider, then the labeled model
solution.
- ALL-CAPS: 'YOUR THINKING' and 'AGENT THINKING' — display strings only
(the model-facing marker label stays sentence-case). The two rows are
equal-length over the shared rail, so the chevrons align vertically.
Dock field labels match.
Owner correction (2026-10-05): the collapsed trigger row carried no rail,
so ownership was only visible once expanded. The rail now wraps the whole
block — label (trigger) and, on expand, the italic 50% text — so the
quote reads as one object and WHICH thinking belongs to whom is clear
from the rail alone, collapsed or expanded. Same shape as the user text
('You') row.
Owner round 2 (2026-10-05), Slice F:
- USER TEXT: the right-aligned bubble is gone. User messages are
full-width document rows — muted 'You' label, muted quote rail,
full-opacity text. Learner words are content, not a chat-app tell.
- USER THINKING (everywhere): arrives COLLAPSED as a trigger row in the
exact register of the agent's reasoning rows (muted label + chevron +
hover affordance); expanding opens the quote block — rail, 50%
opacity, italics. The inner-voice signature. The ThinkingBlock loses
its shell/border: pure document material.
- QUIZ CARDS: the answer's thinking is the SAME collapsible row, same
placement, just inside the container; the explanation drops to 50%
opacity (upright — italics stays the thinking signature).
- AGENT'S THINKING: the duration bookkeeping is gone (it was live-turn
state that died to 'a few seconds' on remount anyway) — the settled
row reads static 'Agent's thinking', pairing with 'Your thinking'.
Spinner + shimmer stay the working signal while streaming.
- Glance law: user thinking = rail + 50% italic; agent thinking = no
rail; user text = upright, full opacity, 'You' label.
Owner round (2026-10-05): the same act (a human thinking in text) had two
visual languages — the quiz cards' quote block vs the practice container
with a collapsing chevron and raw pre-wrap text that read oversized.
- components/thinking-block.tsx: the quiz quote-block recipe verbatim
(rail + uppercase label + lh-prose at foreground/90) carried by a
faint user-side shell — ml-auto, max-w-[95%] mirroring the user
bubble's geometry, px-4/py-3 rhythm. Never collapsible. Rails reuse
the existing vocabulary: primary = committed register, muted =
working register.
- ThinkingCard becomes the practice adapter: 'Your solution' (primary)
/ 'Your thinking' (muted) — one component, two registers.
- The reading channel LIFTS out of the answered card: chronologically
prior to the question, it renders above it as its own user-side
block under the plain 'Your thinking' label; the in-card inset and
the 'while reading' phrasing are gone.
- lh-card-enter (globals.css): the one-shot 150ms fade+rise mount,
the app's motion register; honors reduced-motion.