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 one emphasis device for load-bearing ideas: GitHub-style alert
callouts. The prompt teaches only > [!IMPORTANT] (one device, one
meaning — an unconditional truth laid down, a payoff landed, a hinge
definition) with the restraint law: everything else stays in prose, if
everything is boxed nothing is. The renderer accepts all five GitHub
markers and boxes them identically — a stray [!NOTE] still meant 'this
deserves emphasis', and a marker line must never leak as text.
Rendering (owner rulings): SHARP corners, white stroke, no background,
no label, no icon — just the outline — and the box hugs its text
(width: fit-content capped at the column) instead of stretching
full-width into empty space. Same grammar as the plan blocks: this is
structure, not chrome.
Seam: the Streamdown blockquote override in md-blocks. parseAlertMarker
is pure and exported — Streamdown wraps blocks in its own styled
components, so detection reads the hast node's tagName, and a standalone
marker drops its <br> hard-break artifact along with the empty
paragraph. Plain blockquotes pass through untouched.
Rider (owner, same round): tables hug their contents — width:
max-content on the data-streamdown table (width:auto would still
stretch, the classic table trap); wide ones keep the md-blocks
overflow-x scroller. Graceful in exports: a callout is still a
blockquote there.
6 render pins: the box mounts with white stroke + fit-content + no
rounding, all five markers box, standalone-marker drops the empty
paragraph, multi-paragraph structure kept, plain quotes untouched.
- bridge measures the BODY (scrollHeight + rect + margins) instead of the
viewport-clamped documentElement — documentElement.scrollHeight latches
at the parent's own height, so demos could grow but never shrink on
window resize (owner-reported); smoke: 1036 → 2386 → 1036 px
- invert now PERSISTS as details.ui.inverted on the steer record, written
by the silent artifact-ui route (dismissal pattern: no wake, no SSE) and
seeded through the projection (SubagentArtifactData.ui); survives
navigation + reload, never enters teacher context or exports
- invert restyled as the house Toggle switch (settings controls) on both
cards; demo header is the caption grammar: 'INTERACTIVE DEMO — title'
left (title normal-case, same size/tracking), switch + download far
right; figure header: label left, switch/download/open far right
1439 tests green (route + pure-seam pins added); live smoke passed
The owner speaks in display values: "labels until zoom" shows ratio/0.5,
so the old default displayed 8× — and "default one" means display 1× =
ratio 0.5, one stop tighter than the round-1 value. Both ladders are
re-centered so the new defaults sit dead-center again (stop #5):
labelZoom: [0.125, 0.175, 0.25, 0.35, 0.45, 0.5, 0.75, 1, 1.5, 2, 3]
— default 0.5 (displays 1×); finer steps below the default, the old
familiar run above; old stored 4/24 snap to 3.
blur: [10, 15, 20, 25, 30, 40, 50, 60, 80, 100, 120]
— default 40 dead-center, min 10 per the owner's suggestion, old
ceiling kept; old 4/5/7.5 snap to the new floor.
The dead-center ladder law is restored with no exceptions; migration
pins updated (blur 8.1 → 10, labelZoom 0.5 now a member).
1428 tests, tsc, lint, build green.
Four placeholders rewritten per the owner's set (2026-10-07): the
thinking fields are an invitation to stream-of-consciousness transfer,
and everything else is implied by the field's existence — no "the
teacher reads all of this", no mechanism, no em dashes, nothing that
wraps. The reading field's placeholder no longer breaks to a second
line.
quiz: "Why this answer? Whatever comes to mind."
reading: "Whatever comes to mind as you read."
practice: "Work here, think out loud."
pause: "Ask anything, or press Enter for the next one."
The answer-field placeholders ("Type your answer…") stay: brief and
functional already (owner ruling). Quiz placeholder pin updated.
1428 tests, tsc, lint, build green.
The first cut's flat 6.8px/char table was calibrated for a generic 12px
sans; the plan titles actually render in Source Sans 3 at 11px — much
narrower — so every box carried ~10px of invisible slack per side (the
owner's "a lot of empty space left and right of the text"). The class
rates are now least-squares-fitted against getComputedTextLength
measurements of the live plan titles (narrow 3.1, wide 9.2, caps+digits
6.0, default 5.4): residuals land within ±4px per line, and PAD_X stays
16 — equal to the vertical (44−11)/2. Smoke-verified: per-side padding
now reads 14.4–19.1px across all 12 nodes (was 22.4–28.6, 10/12 over
25). And per the owner's follow-up: the in-transcript boxes keep their
WHITE outlines (gray card fill good, box edges stay white).
1428 tests, tsc, lint, build green.
The plan skin drops its fixed 160px slabs: every box now sizes to its
wrapped text (px-per-char estimate + symmetric 12px padding, floored at
96px), the wrap limit rises 20→28 chars — the early-wrap fix, boxes
grow so lines stop breaking with room to spare — and every title line
anchors to its box midline (textAnchor middle, was a x=12 left inset).
The in-transcript snapshot block speaks the dock language now: boxes
fill-card stroke-border like the composer shells, text fill-foreground,
retiring the white-on-transparent mono look. The popup and the margin
diagram keep their status colors. Mermaid proper is untouched (owner:
it's fine as is).
Character-count estimation, not canvas measurement: the layout path
stays deterministic, synchronous, DOM-free — SSR and exact render pins
keep working. Verified live on the Relational-databases plan (12 nodes,
widths 174–215 tracking the longest line, no overflow, glow intact).
1428 tests, tsc, lint, build green.
The smoke found it: Tab from body on home preventDefault'd but never
focused — home does not render the shared Composer component; its
composer is the bespoke topic textarea, and the selector matched
nothing. The chain is now topicInputRef itself.
Also verified live this round: the one-Esc law on plan/chains/settings
(exactly one action, nothing stacked), Enter-submits from the Other
input (a real session filed the custom answer immediately), and
Shift+Tab staying the thinking-level cycle everywhere.
1427 tests, tsc, lint, build green.
Owner ruling (keyboard round 2026-10-06): Esc (or any key) does ONE
action, on the topmost active layer. The plan-open-while-dictating
double-act was the symptom: the plan popup listened at WINDOW level
with no claim, and same-node window listeners all fire — the global
chain (registered first) cancelled the dictation while the plan's own
listener closed the plan. One Esc, two actions.
The law (now the house pattern, documented at the chain):
- field-level handlers claim at their element (focus in a field: the
field's action) — unchanged, already correct
- every SURFACED layer claims at the DOCUMENT level with preventDefault
+ stopPropagation — document fires before the window chain, so the
chain stays silent under any surface
- the window chain is the bottom-up fallback: dictation (a live capture,
NOT a surface — it survives the Esc that closes a plan or overlay),
then abort (escAbortTarget layers its own targets)
Converted to the claim: plan popup, session overlay, chain table,
settings dialog, setup dialog, the delete/abort dialogs (window-level
stopPropagation was never enough — same-node listeners all fire), and
the stats popover. Popovers (popover-dismiss), menus, model picker, and
the sidebar context menu already claimed correctly.
1427 tests, tsc, lint, build green.
Plain Tab now navigates instead of being pure dead weight: the global
handler dispatches lh:kbd-cycle-fields and each composer-bearing
surface answers with its own chain, gathered from live DOM at event
time (the docks mount and unmount — refs would go stale):
- chat: reading-thinking → the active dock's field → the composer,
visual order, wrap; Tab from anywhere else focuses the first live
field (navigate in)
- new-session + home: the composer is the whole chain — Tab navigates
into it
Shift+Tab is untouched (the thinking-level cycle keeps it — owner
ruling); the quiz dock's rows↔thinking Tab claim stops propagation at
document level, so a claimed Tab never reaches the dispatch — no
double act.
1427 tests, tsc, lint, build green.
Slice 1 — the ask's custom-answer field: Enter submits directly. A
single-line input with no newline to protect, and typing has already
selected the row — every Enter (shift or not) rides the same submit()
seam as the button, dictation finish-first included. No more the
Esc-then-Enter dance to file a custom answer.
Slice 2 — Enter opens the waiting check (owner, keyboard round
2026-10-06): while a question waits with the dock CLOSED (behind its
reveal invitation, or collapsed by scroll) Enter is never dead. Any
live dictation finishes FIRST — the flush lands in the reading-thinking
field, the live claim, per the nothing-gets-lost law — then the check
reveals/expands. Two seams: the reading-thinking field's own Enter
(plain or ⌘/Ctrl; Shift+Enter keeps the newline convention — the dump
convention survives for deliberate breaks, and Enter stays a newline
once the check is OPEN), and a document-level listener for plain-focus
Enter — gated to skip typing surfaces and focused buttons, which own
their own Enter.
1427 tests, tsc, lint, build green.
Owner ruling (release polish round, 2026-10-06): the compaction
summary dialog is gone. The content is model-facing plumbing — anyone
who wants to inspect it reads their provider logs. Both dividers
survive as INERT markers: the moment divider's 'Context compacted'
note and the cut divider's 'HISTORY COMPACTED · 152k tok' seam chip
render as plain text; no button, no dialog, no state. The marker parts
and the server fold are untouched — this is a pure render-side
removal.
Pins: dividers render without <button> or cursor affordances, and the
marker's summary text never reaches the DOM. 1427 tests, tsc, lint,
build green.
Owner round (2026-10-06): markdown export of sessions, actioned from
the sidebar context menu — "Export as Markdown…" sits with the
management actions, above Delete (the destructive action stays last).
- GET /api/chats/:id/markdown (404 unknown): the whole session as one
attachment — text/markdown; charset=utf-8, filename from the slugified
title ("relational-databases-joins-normal-forms.md", non-ascii titles
fall back to "session.md"). Both session kinds export.
- The client is a same-origin anchor download; the route's
Content-Disposition names the file.
- sessionMarkdown's user-facing debut — minted ANCHORLESS (message-id
anchors are extraction provenance, not learner-facing). The engine
joins the core barrel (pure, browser-safe) for its first consumer.
The practice join (owner-approved mid-round): practice tool results are
bare acks — statement, outcome, verdict, and the model solution all
live in the assistant's call arguments. The engine now folds them the
same way the view seam folds cards: Problem blocks render the pose's
statement (solution never leaks at pose time); Concluded blocks render
the outcome as prose ("Solved with hints.") + verdict, then the
pose-committed model solution as a blockquoted reveal via
problemInputForConclude (clean-poses-only, its refusal law). A
dismissed problem concludes as "The learner skipped this problem." —
no reveal. Errored poses render nothing (the failed-tool law).
1427 tests (route pins: headers, slug, anchorless body, 404; engine
pins: pose-time statement, the reveal, the dismissal refusal, the
errored-pose silence); tsc, lint, build green.
Owner report (Electron): the Chains and Import-notes buttons were not
clickable — clicking dragged the window. Root cause: the chrome column
lacked lh-no-drag. The drag mask resolves by DOM order, and the graph
chrome's column carries the carve exactly for this reason (the
2026-09-17 mask law). The minimal home's column now carries the same
class on the same shape — byte-for-byte the graph chrome's law — and
the pointer-events juggling is gone (it was browser-thinking; Electron
needs the region carve, not the pointer filter).
Summary refinement (owner, same round): every count on its OWN line —
no dot separators; the two blocks (propositions/chains, teaching/
practice sessions) part with one empty line (a dedicated h-4 spacer);
imported notes render as a COUNT ("4 imported notes"), never the file
list. KnowledgeSummary's contract changes sources: string[] →
importedNotes: number.
1419 tests (own-line pins: no " · " anywhere, h-4 parting, count-not-
paths, singulars); tsc, lint, build green.
The chains-as-home experiment is retired (owner, 2026-10-06): dragging
the table out of its overlay home caused every mess we saw — checkboxes
by default, the bar/pill collision, the clearance band. The eventual
plan is the DATA VIEW as home; until that is designed and built, the
rendering-off home is deliberately minimal:
- The SAME composer blob home already floats — teach/practice switcher
and pick pill untouched.
- The graph chrome's survivors in the graph view's OWN places: the
top-right column — actions row (Chains + Import, the same 36px band
pills) with the cost pill BELOW it. Import always renders (the
entry-point law). No node search, no render gear (canvas things).
- A quiet centered summary (KnowledgeSummary, a pure body — pinned by
the render suite): propositions · chains, teaching · practice session
counts, the imported-sources list as truncated muted rows; the empty
knowledge base says so and names the import entry point. Biased up
off the composer (pb-40), pointer-events-none.
- NO recent sessions (the sidebar owns that, owner ruling).
The chain table returns to overlay-only duty: ChainTableBody's
onClose-optional and topBarExtra experiment hooks are reverted, and
components/chains-home.tsx is deleted. MinimalHome renders the STANDARD
ChainTableOverlay behind its Chains button — the pick flow mirrors
GraphView's contract verbatim (chainSelectOpen raises the overlay in
select mode; Done reports the lifted pick set down).
Smoke-verified live, twice: the summary shows real counts (262 props ·
160 chains, 195 teaching · 17 practice, 4 source paths); overlay
open/close via button + X; pick flow — cluster check → bar "23 nodes →
17 chains · Clear · Done" → uncheck 2 props → Done → chip "21
propositions selected"; practice session starts from the composer;
settings restore returns the canvas home. (First run's "select mode
missing" was a tester artifact: the bar is selection-gated and prop
checkboxes render only when expanded — both confirmed working.)
1417 tests (new KnowledgeSummary pins); tsc, lint, build green.
Owner refinement on the chains home: the floating top-right chrome row
sat ON TOP of the propositions table — covering content reads badly.
The two survivors (Import notes — always, the entry-point law; the
cost pill when any run exists) now ride the shared TopBar's right
cluster, left of the search input, carved out of the drag band with
lh-no-drag — consistent with the graph view, where they already live in
the top band.
ChainTableBody gains topBarExtra?: React.ReactNode (absent on the
overlay — nothing changes there); ChainsHome's floating chrome row is
deleted; the empty state keeps its own centered import button.
Parked for the next design pass (owner, this round): (1) the pick flow
reads messy — checkboxes show by default, so the pill's select mode is
near-invisible and after Done the Clear/Delete bar + the practice pill
coexist with two Clear buttons; the original vision was checkboxes
appear ONLY when picking. (2) the composer's clearance band has no
outline — the docked area needs a visual edge.
1415 tests (new pin: topBarExtra renders in the bar with the drag
carve; overlay variant injects nothing); tsc, lint, build green.
The settings Graph section splits into two switches (owner, 2026-10-06):
- EXTRACTION (was the lone "Graph" row): the DATA channel — pipeline,
auditor, prober graph tools. Key unchanged (graph.enabled), label now
names its job; off = the graph stops growing, nothing is deleted.
- GRAPH RENDERING (new, graph.rendering, default on): display-only.
Off = the home's main view is the CHAINS VIEW instead of the canvas;
data, reasoning-chain model, and the pipeline keep working untouched.
The chains home (components/chains-home.tsx): ChainTableBody in flow —
same body as the overlay, same powers (query + sort, selection, the
delete fence with its confirm dialog), but onClose is absent so the
top-bar X does not render (the table IS the page; ChainTableBody now
takes onClose optionally). The chrome survivors float top-right below
the top bar: Import notes (ALWAYS — the entry-point law) + the cost
pill (GraphCostControl, now exported). The composer blob persists over
everything; the table scrolls CLEAR of it via a fixed pb-40 band (text
under a floating box is noise — unlike the canvas, which runs under).
Pick flow mirrors the overlay contract exactly (home lifts the pick
set; selectMode swaps the bar's confirm to Done). The chip relabels to
"Pick propositions" (no "from the map" — there is no map); while
picking, the chip row hides so the floating bar above the composer is
the one voice. Settings resolve rendering through the one core
resolver + renderingEnabled(); read server-side per navigation, so the
switch applies on the next visit home.
Smoke-verified live (headless browser): chains home renders without the
X, import + search + composer clearance confirmed, pick flow checked 2
propositions -> Done -> chip count -> practice session started with the
graph context carried (2 props, 2 chains), settings restored.
Known follow-ups (flagged, not built): after Done the bar morphs to the
table's Clear/Delete management bar while the chip also shows the count
(two voices, same set — the table's own editor law); /chat/new's pick
chip still says "from the map".
Tests: rendering in factory/sanitize/parse/resolve pins, the GraphSection
render pins (Extraction label + rendering row), the home-variant
chain-table pin (no close X). 1414 pass; tsc, lint, build green.
Owner report 2026-10-06: practice sessions 'open and close instantly'
while teaching morphs. Live instrumented run (Element.prototype.animate
wrapped, three transitions recorded) proved the pose/conclude morphs
fire identically in practice (54px↔327.5px, 120ms, same easing) — the
asymmetry was the FOLD: the shell's height-morph key read
`mode:${pending ? expanded : "-"}` and pending (the question's tool
call) is null in practice, so a practice dock's bar↔form toggle never
changed the key — no height morph, instant jump, only the 160ms content
fade. The ternary predates practice sessions (aeef28d, August); the
remembered Oct-5 fix (ec8315b) added the fade only. Round 16's pause
flow raised fold exposure until the seam surfaced.
Fix: dockStateKey(mode, docked, expanded) — components/dock-state-key.ts
— widened to !!pending || !!practice (the fold machine's own ownership
rule). Typing growth keeps the key (never animates); the fold rides the
exact branch teaching has used for months. Pinned in
tests/dock-state-key.test.ts.
Diagnosed: the transcript's MessageContent boxes carry overflow-hidden
(ai-elements/message.tsx) — the absolutely-positioned box rendered fine
over its own message block's content (the closing-words text under the
conclude card) and clipped the moment it crossed that block's bottom
edge onto the next message. Exactly the reported cut-off.
Fix: the box portals to document.body in fixed position (the dialogs'
law) — placed from the trigger's viewport rect at open time, below the
trigger, left-clamped to the viewport; z-50. Any-scroll-close means
there is nothing to track.
Owner corrections (2026-10-06): the chevron is gone (Metadata text
alone is the trigger); the data reverts to the original small register
(text-xs rows, mono values — the 15px pass was the misread: the font
question was about the metadata label, not the data); the box opens
BELOW the trigger, always.
Owner corrections (2026-10-06): no expand/collapse; a quiet METADATA
trigger sits at the card's bottom (no divider — it just sits there);
click opens the info box in the graph-view node card's exact register
(rounded-xl border bg-card p-4 shadow-lg, w-80, floating ABOVE the
trigger so the transcript scroller never clips it); ANY scroll
(capture-phase — every scroller in the tree) or click outside closes
it, plus Esc claimed from the layered chain (usePopoverDismiss, the
popover law). Values match the model solution's font size
(text-[15px], verified — the drawer's text-xs did not); labels keep
the eyebrow register.
Owner ask (2026-10-06): reveal everything the agent filled in when
writing the problem and concluding it, at the very bottom of the
conclude card, collapsible, arriving collapsed.
The fields, both calls:
- problem: mode (recall/discrimination/composition), target
proposition ids, statement, modelSolution (the last two ARE the
visible card — the drawer shows the metadata only)
- conclude: outcome, hint rung (R0–R5/null), miss class
(cue/discrimination/execution/careless/structural/null), retest.
Seams: problemInputForConclude (native-messages) walks the reveal's
walk and folds the WHOLE problem-call input onto the conclude part
(projectView join; same refused-pose law — an unacked pose folds
nothing); ArtifactPartProps grows problemInput. Targets resolve to
proposition NAMES at render (one lazy module-cached /api/graph fetch;
failure degrades to raw ids — observability never breaks the card).
The drawer: divider + chevron 'AGENT'S DATA' toggle (mono-caps
eyebrow register), definition rows with mono values, 'untagged'/
'none' honest empties, absent entirely when the calls filled nothing
(the dismissal line, legacy bare records).
The docks' useEnterSubmits pattern, mounted for the pause in ChatView:
window-level Enter (typing surfaces and buttons excluded, so the
focused composer/button keep their own lane — no double fire) calls
sendNext, which self-guards on nextReady (inert outside the pause;
the dock listeners are unmounted by then — the two never overlap).
Owner's keyboard-first directive: body focus, a scroll, anywhere.
Owner correction (2026-10-06): the floating chip clipped inside the
composer band. Replaced with the button morph: an EMPTY composer in
the between-problems pause renders a wide green 'Next problem' button
(bg-green-600 — the answered-card correct register, mono-caps label,
arrow glyph); typing anything returns the regular white send; busy
(steer/stop) outranks the morph. Click and Enter share the one
act() lane, so the button also teaches the Enter-on-empty behavior.
The wrapper div + chip are gone from chat.tsx; inputAppends widens
its record param to { messages; kind? } (the practice gate reads it
without a cast).
Owner ruling (2026-10-06): every concluded problem opens a pause. The
agent may speak its closing words about the learner's last thinking
after the conclude result, then ENDS ITS TURN — a system-prompt law
deliberately, per owner correction, NOT tool-level terminate. The next
problem is never posed until the learner explicitly moves on; the pause
is the session's teaching space (repairs, clarifications, follow-ups —
still surgical, never re-teaching).
The move-on signal is a first-class input: { type: "next" } appends a
practice-next marker (content = single-sourced NEXT_PROBLEM_TEXT — the
model's unambiguous go-ahead: pose per the selection rules or wrap if
the arc is complete). Honest 400s: practice sessions only, nothing
active (mid-problem input rides the dock), riding after any pending
dismissal synthesis exactly like text.
The fast path (owner approved: composer-band chip + Enter-on-empty):
- Composer.enterAction gains "next" — Enter on an EMPTY composer in
the pause fires the signal; typing anything makes Enter a send again;
busy (steer/stop) outranks the pause.
- The Next chip hovers at the composer band's top-right (pick-pill
chrome register) whenever the pause is live and a problem has ever
been posed; the placeholder becomes the hint ("Ask about this
problem — or press Enter for the next one…", exact);
- the transcript renders the signal as a compact centered NEXT PROBLEM
line (mono-caps chrome); session-core appends it optimistically.
CONCLUDE_ACK_TEXT rewritten to carry the law; doctrine §VIII gains the
pause law (owner-sanctioned edit #3); 00-base's conclude/dismissal
bullets rewritten + the between-problems section added.
The relational-databases incident, diagnosed and fixed:
TRIGGER — pi's estimator law, adopted exactly: anchor on the last
assistant message's provider-reported usage (totalTokens || the field
sum) and chars/4-estimate ONLY what was appended after it. The anchor
scan skips aborted/zero-usage assistants and STOPS at the newest
compaction marker (pre-cut usage described context the next request
will not carry — pi's distrust rule). The old estimator
(JSON.stringify of every message /4) tripled the true context
(measured: 84,412 'tokens' estimated vs 38,799 provider-reported on
the incident record) — scaffolding, escaped LaTeX, thinking blocks
and app-side details all counted. estimateMessageTokens now counts
wire text only (text + thinking + toolCall name/args; content text
for user/toolResult/custom). The unanchored fallback runs the plain
heuristic over the CUT-HONORED projection plus the additive prompt
term; the anchored path adds nothing (usage already billed prompt
and tools).
OBSERVABILITY — compaction is a visible session-wide state (owner
spec): the transcript-bottom placeholder register gains COMPACTING
CONTEXT… (BrailleSpinner + Shimmer, the THINKING placeholder's twin,
mutually exclusive by construction — the gate precedes beginTurn),
and the sidebar row shows 'Compacting context · <ago>' with the
spinner (sessionMeta outranks busy; compacting-changed joins
turn-started/turn-settled as refresh triggers). Liveness rides the
API like turn-busy: ChatListItem.compacting + compactionInFlight on
both GET /api/chats and GET /api/chats/[id] (initiallyCompacting
covers the navigate-back-mid-compaction mount — the event already
fired), exported through the server barrel.
Also: the relational-databases record rolled back to pre-compaction
(marker, post-marker turns, tree reset; backup in /tmp) for a clean
retest. The summarizer-usage 'gap' was a misread — steps[] was
populated all along.
- settings: compaction.thresholdTokens row on the Agent tab (owner pick)
in a Context group — CommitInput mirroring the graph budget row, hint
resolved through the SAME core resolver the trigger reads; PUT rejects
below the 32k floor with the anti-churn rationale; own section
(compaction) in SETTINGS_SECTIONS + the settings-signal union
- view fold: one marker → two dividers — the minimal MOMENT divider at
the marker (retrying-message register, no rule lines) and the longer
CUT divider emitted immediately before the first kept message
(phase-divider geometry, chip carries '· Nk tok'); both carry the full
summary payload; full history stays visible to the user
- components/compaction-divider.tsx: the dividers + summary dialog
(pure exported body for SSR pins, the delete-dialog pattern; Esc +
Close dismiss, backdrop does not)
- chat.tsx: compaction-changed SSE flips the running state — composer
canSend, onSend/doSend/doSteerOrSend, and both docks' submit + hint
('compacting session…') gate on it (the server's registry mutex is the
structural gate; this is the affordance); dialog mounts beside the
abort dialog
- pins: settings-body (4), view fold (2), divider SSR (4)
The filter's mb-2 stacked with the session list's built-in 8px top
padding, doubling that seam to 16px while the others read 8. The
column now runs 12 / 8 / 8 / 8 — the list's own padding carries the
last seam.
Both /chat/new drafts lose their TopBar — home's register now: drag
mask first in the DOM, floating sidebar-reopen toggle as the only
pointer-events-auto spot, the empty state carrying the orientation.
The chain picker's own bar then sits at the very top of the column,
exactly like the graph view — no stacked bars, no special-casing.
The chain table drops its 'Chains' title with it: the serif title
register is RESERVED — it always names a session; the table names
nothing, so its bar carries counts + search + close only.
The stack read 12/6/4/4/14 — the search→filter seam was the tightest
gap in the column and the filter→list seam 3.5× larger (its mb-1.5
stacked with the list's 8px top padding). One rhythm now: mb-2 on the
create block, the Sessions+search row, and the filter; the list's p-2
top padding IS the rhythm. The 6px inside the create block stays —
intra-group spacing, a different layer.
The overlay is absolute inset-0 — it resolves against its nearest
positioned ancestor. Mounted at the NewSession root it spanned the
whole window, over the sidebar and the traffic lights. It now hosts
inside the draft content column's relative container (the overlay
slot on DraftShell) — the same confinement the graph view gives it:
right of the sidebar, under the top bar, inside the session's section.
The sticky composer wrapper is a pointer-events-none mask; the pill
row sat outside the composer box's re-enable, so it rendered dead —
no hover, no click. One class, the 953b22b pattern the CTA row and
composer box already follow.
The SAME pill the home screen carries — above the composer, dashed
'Pick propositions from the map' when empty, count + Clear when picked
— now lives on the /chat/new?mode=practice draft. The draft has no
graph behind it, so the pill opens the standalone ChainTableOverlay
(select mode, controlled selection, Done bar) over the /api/graph
snapshot, fetched lazily on first open. The first send carries the
picks as graphContext: the created session is born WITH its ids-first
context marker (target validation's truth set) instead of pickless —
the same contract as the home screen's practice send. Teach drafts
render no pill; practice with no picks stays lean on the wire.
It scopes what the list shows, so it sits directly above it as a
caption row; search stays the top muscle-memory target. Column now
reads: LH band → New Teaching/Practice (stacked) → Sessions+search →
Teaching · Practice · All → list.
- SESSION SEARCH goes fuzzy on the SAME fuzzyScore the model pickers
use (de0c286's util — one scoring across every search surface):
ranked by score instead of raw substring; empty query keeps order.
- SIDEBAR: New Teaching / New Practice STACK (full-width rows); the
kind filter reads Teaching · Practice · All — the real modes first,
All as the escape hatch, mirroring the stack above. Home stays in the
LH band (owner-confirmed).
- TEACH DIVIDER, the stacked-dividers bug: the plan presentation and
the first tagged quiz call usually share ONE assistant message, so
the message-level seam drew Planning+Teaching back-to-back. New pure
teachStepIndex(parts) finds the STEP boundary governing the flip
call; the divider renders there (replacing that boundary's plain
hairline — a labeled rule supersedes it), prepending before the whole
message only when the flip is in its first step. The fold (chip,
sidebar state) is untouched.
- JOBS SUITE, flake round 3: the sweep scans the SHARED jobs dir, so
parallel test FILES' running records respawn into this file's
counters — the assertions go jurisdictional (own-job ids collected;
foreign respawns are not this test's business). 1331/1331.
- ZOOM: the ⌘±/⌘0 ladder is owned, not stock roles — applyZoom sets the
factor and broadcasts 'lh:zoom'; the renderer publishes --lh-zoom; the
gutter lengths (.lh-lights-gutter / .lh-closed-toggle) divide by it so
their RENDERED size is a physical constant at every zoom — the graph
button stays glued next to the lights, nothing slides under them.
before-input-event catches ⌘=/layout variants the menu's Plus may not
claim; did-finish-load re-pushes on reload; getZoom covers first paint.
The lights themselves stay at construction position — Electron 44 has
NO setTrafficLightPosition (options-only; the first run crashed on
it). HIG-acceptable: native lights hug the corner; the web chrome
compensates around them.
- JOBS SUITE FLAKE, actually fixed this time: the seams are
module-global and the sweep scans every record — a timed-out async
test left a running orphan + late wake that poisoned its NEIGHBORS
(respawned 3 !== 0). New afterEach fence: settle every tracked
running job, unregister aborts, seams reset to inert no-ops — every
test starts from identical state regardless of upstream flake.
Verified: two consecutive full-suite runs 1326/1326.
- ENTER SUBMITS REGARDLESS OF FOCUS (owner): dictation means the dock
fields are rarely focused. New useEnterSubmits hook — a window-level
listener inside both docks (mounted only while a problem/question is
active). Plain Enter anywhere outside a typing surface submits the
practice thinking / the teaching answer; typing surfaces keep their
own handlers, buttons keep their click-activation (the collapsed
Problem bar still expands on Enter), modified Enters untouched.
Teaching sessions get the same law as practice — one standard.
- MODEL DASHES: the round-5 join("-") edit never landed (the batch
script aborted on an earlier assert before reaching it) — the source
still joined with spaces while the commit message claimed otherwise.
Now actually joined with dashes: 'glm-5.3-flash' everywhere (picker,
setup dialog, stats bar); pins updated to match.
- SIDEBAR: the practice dumbbell moves to the far right of the session
row — titles sit flush with teaching rows, cleaner list.
- 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).
The round-4 batch threw on an earlier replace and never reached
reasoning.tsx — the trigger row kept text-sm. Now text-xs font-medium
uppercase tracking-wide, matching YOUR THINKING and the card eyebrows;
chevrons align through the 1ch rail. Plus one missed prettyModel pin
(gpt 5.2, no auto-caps).
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 H: the Nudge/Commit two-lane buttons
are gone. One Send; Enter sends, Shift+Enter is a newline. Whether a
submission claims an answer is apparent from the thinking itself — the
agent reads it (hint ladder for traces, moves+verdict for claims).
- The thinking input loses the commit field end-to-end: ChatInput,
parseChatInput, newThinkingMessage (details and the model-facing label
both collapse to the single 'Your thinking'), the optimistic marker.
- Legacy records' commit flags are deliberately unread.
- Doctrine: the two labeled channels become one read instruction; 'answer
commits' section retitled 'Claimed answers' — a claim is not a
progress signal.
- ThinkingCard: one label, collapsed arrival (the unified row).
- The dock placeholder no longer explains lanes that no longer exist.
Owner round 2 (2026-10-05), Slice G: 'Solved' alone over unlabeled prose
didn't read as the model solution. The hero now carries its own 'Model
solution' section label (the quiz cards' register), and the verdict
section mirrors it — label muted, text at the same 50% register as quiz
explanations.
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), Slice D — the dock mechanics:
- INTENT-GATED FOLD: a ResizeObserver on the scroller's content stamps
the existing 400ms mute whenever the transcript GROWS — growth frames
(a live turn streaming under stick-to-bottom's chase) can never fold
the dock; and a fold now needs the distance AND stick-to-bottom's
isAtBottom agreeing the learner left. Fixes the generation churn
(dock disappearing and reappearing while the agent works). Shrinks
stamp nothing (the fold's own morph must not self-mute re-expand).
- MEASURED HEIGHT CAPS: the question (55dvh) and problem (40vh) areas
now cap at the available column (scroller clientHeight − 2rem,
re-measured on resize) via --dock-max-h — questions stretch to the
top bar with sensible padding and scroll internally only when
genuinely huge.
- The practice dock's statement area carries the 'Problem' eyebrow —
the quiz cards' label row, mirrored (dock and transcript card read
identically).
- The fold's CONTENT fade: a 160ms WAAPI fade/rise on the bar↔form
toggle — no remount, so the question form's state survives; the
shell height morph was already animated.
Owner round (2026-10-05), Slice C:
- ProblemCard: ALWAYS OPEN — the answered card's container + eyebrow
discipline ('Problem' label, lh-prose statement), no chevron, no
collapsed bar. The dock mirrors it in Slice D.
- ConcludeCard: the reveal is the modelSolution committed at POSE time,
shown verbatim as the hero — never re-authored (the slim schema's
law). The verdict rides below in the quiz-explanation register
(small, muted 'Verdict:' row). Nothing collapsible. Legacy inputs
with their own solutionReveal fall back to it.
- The reveal join: modelSolutionForConclude (native-messages.ts) — the
newest cleanly-posed problem before the conclude call; refused poses
reveal nothing. projectView enriches tool-conclude parts with
part.reveal at projection; ArtifactPartProps carries the field
(contracts/chat.ts).
Pinned: always-open problem card; hero-first conclude layout with the
verdict after it; legacy fallback; the join (clean pose, refused pose,
multi-problem records) and the view enrichment.