Owner polish pair before the settings milestone:
- Model menu: arrows walk the filtered results (clamped, no wrap) while
focus stays in the search field; Enter picks the walked row or, with no
walk yet, the first result; typing resets the walk; click and Enter
share one pickModel path; walked row gets a muted ring + scrollIntoView;
cheat-sheet display entries added.
- Graph: window resizes re-anchor the camera so the world point under the
pre-resize viewport center stays under the new center — the graph stays
visually still while scaling (was top-left pinned). viewW/H tracked in
syncSize; re-anchor applied through the zoom behavior (persistence and
card-dismiss unchanged); kFit refresh as before. Pixel-verified: 7/7
center samples identical across a 300x200 shrink.
Owner's second live test:
- DEFERRED transcript: finals accumulate in a buffer during the run and
flush into the primary input once, when dictation ends (Stop →
CloseStream → Deepgram's last finals → socket close → one joined
append; the 3s safety net covers a missing close). Nothing textual
renders while recording — the meter is the only live feedback. Live
output may return as a toggleable setting at the settings milestone;
deferred is the default behavior.
- ADAPTIVE meter: cell count derives from the measured strip width (4px
pitch, 8-48 cells, ResizeObserver; cells drop their minimum width) so
narrow rows beside the knowledge-context slider fit — verified in the
dev shell: draft 20 cells/83px, chat 48 cells/273px, no overlap.
Owner's first live mic test (2026-09-17) drove four changes:
- ONE primary transcription input at all times: while a quiz/ask
question waits, finals land in its field (text mode → 'Your answer',
select modes → 'Your thinking') via a module-level claim registry
(useDictationTarget in dictation.tsx); the composer is the fallback.
Esc in a question field also stops dictation (threaded through
DockOverlay → QuestionDock).
- No interim results: live words were distracting and shifted the meter.
Params now match the pi dictate extension exactly (no interim_results,
endpointing=300, no utterance_end_ms); the meter's width stays fixed.
- Optimistic meter: the click enters the meter view immediately (Stop
pill + meter within one frame, verified at 150ms in the dev shell);
token mint and mic acquisition run concurrently behind it.
- Wide idle button: the stats-bar middle is one full-width affordance —
a quiet bordered 'Dictate' button when idle (failure text replaces the
label), Stop pill + fixed meter while live.
Live-verified in the Electron dev shell: wide button renders on home
(167px) and chat (358px), meter + Stop appear at 150ms after click,
flat 2px bars in silence, clean return to 'Dictate' on stop.
Two live-verified Deepgram facts from the first real mic-test round:
- /v1/auth/grant returns { access_token }, not { token } — the route now
parses the right field.
- Minted JWTs authenticate over the WebSocket via the "bearer"
subprotocol; ["token", jwt] is rejected at the handshake (token is
for raw API keys only, as in the pi dictate extension).
Also documents the env-precedence gotcha in docs/research/stt.md: a
DEEPGRAM_API_KEY exported in the shell (e.g. .zshrc) silently beats
.env.local in next dev and the desktop authEnv() seam.
Four research passes (two library fact-checkers, two prior-art explorers)
with all claims verified against shipped package code (npm tarballs of
sigma@3.0.3 and @sigma/layer-webgl@3.0.0), the sigma.js storybook source,
and primary docs. Nothing implemented yet — this is the decision document.
Headline verified facts:
- sigma v3.0.3 ships a PUBLIC createCanvas/createLayer API; our DOM
injection was improvising a worse version of it. Sigma's own
cluster-label storybook demonstrates our exact minimal-fix pattern
(custom layer + afterRender + graphToViewport).
- @sigma/layer-webgl contours render METABALL fields, not hull polygons;
no dashed strokes; one WebGL context per community (~16 browser cap).
Our visual is not achievable with shipped sigma layer programs.
- Nobody in the graph-tool world ships hull regions (Gephi, Gephi Lite,
Obsidian: node coloring only). Aesthetic references: ggforce
geom_mark_hull, Bostock's thick-stroke hull trick, Excalidraw frames.
- Canvas 2D at 338→low-thousands of elements is comfortably within
demonstrated 60fps territory; the cost driver is repaint count x
pixel area, not element count.
- Cluster-first layout: two-level pattern (quotient anchors -> per-cluster
layout -> compose) is well-trodden; LinLog FA2 is the one-afternoon A/B
candidate; no turnkey JS package composes it (~150 lines of glue).
Options scored: sigma + public 2D canvas layer (7/10, ~1d minimal fix),
hand-rolled d3-zoom + single Canvas 2D (8/10, recommended target),
PixiJS v8 (7/10). Full matrix, facts-not-to-rely-on list, and open items
in the document.