Repository navigation
feat: talk for the web (talk.pilgrimapp.org) — foundation + U1 spike - #1
Merged
Merged
Conversation
The reviewed requirements doc and the spike-first implementation plan for talk-web (talk.pilgrimapp.org) — the on-device, in-browser voice-reflection web port modeled on meditate-cli/web. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the meditate-cli/web toolchain (vite-plugin-wasm + top-level-await, strict TS, vitest) recolored to talk's rust palette and targeted at talk.pilgrimapp.org. build:wasm wiring points at scripts/build-wasm.sh (added in U3). No CSP meta yet (U10 owns it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wasm-bindgen façade (crates/talk-wasm, deps talk-core + wasm-bindgen=0.2.95 only) exposing the deterministic surface to JS: shapeEntry/deterministicLight, appendEntry (CLI-identical journal/reflect markdown), palette bytes, the Settle live/committing/settled machine + compose/composeClose/composeReleased, and question selection. Panic-safe boundaries, hand-built JSON (no serde_json). build-wasm.sh mirrors meditate (pinned wasm-bindgen, panic=abort, --target web). Cross-compiles to wasm32; 110 talk-core + 16 talk-wasm tests pass; no listen/ sherpa/libc dep leak. The off-record-pairing lift (U3-B) is deferred to Phase C with U6 — it touches the shipped CLI and only feeds the gated browser pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port terminal.ts (xterm + WebGL/DOM fallback), repl.ts (pure line editor) and loop.ts (frameSequence synchronized output + shouldDraw) from meditate-web. New theme.ts sources the rust core/dim/edge tones from talk-wasm palette() as the single source of truth and maps LineKind→tone (Settled=core, Edge/Question=dim, Chrome=edge). main.ts paints a static composed reflect screen via Settle.compose. tsc clean; vite build OK; 14 vitest tests pass (ported repl + theme/WCAG-contrast + compose-render: settled brighter than edge/question). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A throwaway, runnable browser page (web/spike/, isolated from web/src/) to run the U1 device spike that gates Phase C. Instruments all seven measurements: live-edge + finalize RTF, cached footprint (storage.estimate), full-vs-chunked crypto.subtle.digest on the ~199MB file, CSP-violation capture under a draft strict CSP, crossOriginIsolated/SharedArrayBuffer/threads detection, the fetch module-shape log, and a post-model-load egress log. The sherpa-onnx loader is left at clearly-marked WIRE: seams (no fabricated measurements). README carries the asset-setup steps, the desktop+phone checklist, and the go/no-go memo template mapped to the plan's decision tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Façade (talk-wasm): explicit js_name on Settle commit/finalize; selectQuestion guards mismatched parallel-array lengths; documented palette() length contract; added tests for every push_json_string escape arm, upgrade/post-finalize contracts, high-contrast palette, and compose/select JSON round-trips. Web: parseComposed validates shape + try/catch, toneForKind gains a default branch (no silent mis-paint on a new LineKind); style.css derives the accent from the WASM palette via --accent-rgb set in main.ts (kills the hard-coded RGB drift); theme.test null-narrows; new loop.test.ts ports the no-flash invariant; pretest builds wasm so npm test works on a clean checkout. Spike (U1 honesty): LOUD main-thread-only caveat on the egress/module-shape metrics (worker fetches bypass them); memo CSP generated from the enforced meta (fixes the blob' typo); chunked-digest + crossOriginIsolated caveats; capped capturedFrames to ~60s; footprint reports a model-cache delta; dropped dead code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k-wasm (U3-B) Extract the pure Commit/Revise/pause pairing state machine from src/live.rs::apply_event into talk-core::pairing (Pairing/Decision) — no I/O, no crossterm, no threads. live.rs now delegates the drop/disarm/straddle decision to it and keeps only the CLI lexicon+cleanup transform; behavior is preserved (the 5 pairing characterization tests pass post-refactor). Exposed through a talk-wasm Pairing wrapper so the browser pipeline (U6) shares the privacy invariant instead of re-implementing it in TS. Invariant preserved (arm order load-bearing): Done first; Revise gated on !commit_dropped (straddling Revise still upgrades); Commit-while-paused drops + disarms the paired Revise; re-arm only on the next accepted Commit, never on resume; finish-drain carries commit_dropped forward. cargo test --workspace green (binary 91 + integration 21 + privacy 7 + core 121 + wasm 29); wasm32 builds; bindings expose class Pairing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Engine-agnostic model acquisition. integrity.ts: Web Crypto sha256Hex + a chunked incremental SHA-256 path, verify(), and MODEL_FILES — the 7 EXTRACTED per-file pins ported verbatim from src/download/models.rs (re-pin against the served WASM artifacts at hosting time). cache.ts: a CacheBackend seam (real Cache API default, memory backend for tests; OPFS swap is one line) with persist() opt-in, corrupt/missing → null. download.ts: per-file fetch with streamed progress, AbortController cancel, verify-before-commit, refuse+retry on mismatch, file-boundary resume, and the offline states (cached+offline ok, uncached+offline blocked, mid-download paused→resume). Dependencies injected (fetchFn, cache, isOnline) so the logic is unit-tested without a real browser; the real Cache API + network are validated live. 58 vitest tests pass; tsc clean; vite build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
audio.ts: getUserMedia 16kHz mono + AudioWorklet (shipped web/public/mic-worklet.js) with a ScriptProcessor fallback and the iOS resume quirk; the five mic-permission states (pending/granted/denied/denied-suppressed/device-unavailable) each surfaced distinctly. recognizer.ts: the Recognizer seam — WireSherpaRecognizer (the // WIRE: stub where sherpa streaming Zipformer + Whisper base.en plug in, validated live) and MockRecognizer (scripted partials→endpoint→finalize so the pipeline runs/tests now). pipeline.ts: the thin driver — every event routed through the shared wasm Pairing.decide before touching Settle, so off-record commits + paired revises are dropped and a straddling revise still upgrades (privacy invariant lives in Rust, not TS); hallucination gate on noSpeechProb; R3 idle/hangover/end-of-session finalize-or-drop. main.ts now animates the live edge end-to-end via MockRecognizer (one-line swap to the real engine). Recognizer + audio injected → unit-testable without a mic/engine. 86 vitest tests pass; tsc clean; vite build emits the worklet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SessionControls: a DOM-free controller over a SessionActions seam — space=done, p=pause/off-record (drives pipeline pause/resume), u=raw⇄clean, esc=cancel with the confirm-cancel state (ephemeral cancels immediately), matching live.rs::run_loop. mobile.ts ports isTouch + createChipBar and defines the per-mode chip sets (reflect/journal/unburden session + journal-view) so touch carries the same verbs as the keyboard. Clean/raw toggle flows through Settle.compose show_raw; cleanup level per mode (journal=High, reflect/unburden= Light) via cleanupForMode. main.ts wires onData→onKey and mounts the chip bar on touch. Pipeline gains isPaused() to satisfy SessionActions. 104 vitest tests pass (chip↔key parity, confirm-cancel, clean/raw parity with shapeEntry); tsc clean; vite build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ModeRouter owns the three experiences + the between-session picker. Reflect is the front door: selects a curated question via talk-wasm selectQuestion (the questions/*.toml bundled into web/src/questions via Vite ?raw), shown as text (no TTS), with skip/re-roll before answering and thread accumulation by question id. Journal is freeform; unburden is ephemeral — keeps nothing, plays a closure (composeReleased/composeClose), wipes, returns to the picker. Persistence is a clean EntryStore seam (in-memory now; U9 swaps the durable store with zero router change) — unburden never reaches store.keep, so keeps-nothing is structural. main.ts now boots into the reflect door, runs a MockRecognizer session, lands the entry, and offers the picker. 116 vitest tests pass; tsc clean; vite build bundles the TOML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JournalStore: durable EntryStore over a versioned localStorage blob (corrupt-safe load + migration, ported from meditate's store.ts) — entries by local date, reflect threads by question id, full selection state. Drops into U8's EntryStore seam with zero router change. Storage-failure states each surfaced distinctly (quota→export-now, persist-denied→advisory, write-fail→retriable), never swallowed; the in-memory mirror keeps the entry even on durable-write failure. view.ts: journal IA (by date + by thread, empty state, continue-thread). export.ts: markdown via talk-wasm appendEntry — byte-parity with ~/talk files (tested), download + clipboard behind an injected sink with the one-time OS-shared disclosure. Durability honesty: first-keep export prompt (once), private-mode + eviction + shared-device warnings. modes.ts gains continueQuestion. 158 vitest tests pass; tsc clean; vite build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strict CSP via <meta> (default-src 'self'; connect-src 'self' + the one model host; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; img data:; base-uri/object-src 'none') with each relaxation justified; frame-ancestors is a documented Pages residual for U12. Self-host audit clean — no third-party assets, the model CDN is the only egress origin. Up-front one-time privacy assertion (R5) and the taste-it live-edge preview (R10) on the boot path. no-egress.spec.ts (Playwright): asserts ZERO network after the model-cache phase + a LIVE canary probe (a real request to a CSP-permitted, page.route-stubbed origin) so the interceptor is proven to catch — 'or the test proves nothing' — plus a CSP-blocks-external-connect test. Ran green in real chromium (2 passed). tsc clean; 158 vitest; vite build under the CSP ok; CSP present in dist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
boot.ts: a login-style 'last visit … on talk.pilgrimapp.org' banner over a short
MOTD with the soft install funnel (brew install talk), rust-toned, reduced-motion
aware, returned as tone-tagged lines. deeplink.ts: #q=<question-id> only, with the
SAFE_ID (^[a-z0-9-]{1,64}$) + control-byte-strip moat — over-long ids are rejected
(not truncated-then-accepted), unknown/invalid ids fall back to normal selection.
modes.ts gains startWithQuestion + findQuestionInPack so a valid #q opens that
reflect question. og.html is the rust OG-card source; a faithful placeholder
public/og.jpg ships so the OG meta + Pages build resolve (final artwork deferred).
No new external origins — U10's CSP + net-silence canary unaffected.
182 vitest tests pass; tsc clean; vite build copies og.jpg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pages.yml mirrors meditate's Pages deploy, adapted to talk's conventions:
triggers on web/** + crates/talk-{wasm,core}/**; rust 1.82 + wasm32 target;
cargo binstall --locked wasm-bindgen-cli@0.2.95 (kept in sync with
crates/talk-wasm/Cargo.toml, enforced by build-wasm.sh); node 20; npm ci &&
npm run build; upload web/dist; deploy-pages. All actions SHA-pinned (matching
ci.yml/release.yml). No committed CNAME — domain set in Pages settings. Header
documents the user-owned ops (DNS, Pages custom domain, CDN upload + exact-origin
CORS + Range).
web-ci.yml closes the review's no-web-CI gap: on PRs touching web/** or the wasm
crates, runs npm ci + build:wasm + tsc + vitest + the Playwright net-silence
canary. Both workflows parse + pass actionlint. A real Actions run is deferred to
first push.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Privacy/correctness (ASR path): - Pairing.decide() unknown kind now returns drop (was on-record applyPartial); TS adds a toVerdict() guard at the decide() call sites (#1) - hallucination gate snapshots segment stats at endpoint so the heuristic (no_speech_prob-null) path no longer drops every pass-2 upgrade (#2) - download retry gains exponential backoff + jitter (#3); per-file stall timeout (#15) - journal store merges-before-save + a cross-tab storage listener, closing the concurrent-tab lost-update window (#4) Contracts & reliability: - MockRecognizer.reset() no longer emits (bypassed the Pairing guard); on()/reset() contract documented (#5); cache.put() returns a result instead of masquerading as DownloadFailed (#6); EntryStore.keep/noteServed widened to surface storage failures (#9); start()/continueQuestion() end the prior session (deep-link boot leak) (#10); AudioCapture stop-during-wiring guard + resume-suspended surfacing (#13,#14); store element-shape guard + quota→export-prompt honesty (#16,#17); Pipeline.pairing made private behind commitDropped() (#19) Structure: CLOSE_PHRASES moved into talk-core (single source, wasm-exported) (#8); DemoModeSession extracted behind a LiveSessionView interface (#20); dead recordKept() removed (#7) P3: terminal control-byte strip at the wasm→terminal seam (#23); command() exhaustiveness guard (#24); listener/timer teardown on unload (#25,#27); JSON parse-failure warnings (#26); merged import (#22); workflow checkout SHA aligned to ci.yml (#11,#28) Deferred: broad main.ts decomposition (#21); release.yml SHA alignment (out of talk-web scope). cargo test 122+30; 197 vitest; e2e canary green; builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires a real browser-native Whisper engine into the existing Recognizer seam, replacing the mock for the live app — no emsdk, no COOP/COEP (default asyncify single-thread WASM / WebGPU both run on GitHub Pages), models from the HF CDN. - transformers-worker.ts: WhisperForConditionalGeneration + TextStreamer in a Web Worker (Pattern A), WebGPU→WASM fallback, numThreads=1, streamed partials + completed final, HF download progress forwarded. - transformers-recognizer.ts: implements Recognizer — Int16→Float32, energy VAD (700ms endpoint), debounced rolling-window partials → partial/endpoint/finalize, onModelStatus for the loading UI. - live-session.ts: the engine+mic twin of DemoModeSession behind LiveSessionView, so the router/render are unchanged. - main.ts: ?engine=mock|real (default real) + ?model= override; model/mic status on the status line; skips the sherpa U5 download when the real engine is active. - index.html CSP widened to huggingface.co + cdn.jsdelivr.net (SPIKE — production self-hosts both on cdn.pilgrimapp.org and narrows back); canary boots ?engine=mock. The TextStreamer per-token callback maps onto talk's live edge (Whisper streaming = the jitter; completed generate = the settle). transformers code is isolated in the lazy worker chunk (main entry unchanged); engine bytes load on first real session. tsc clean; 211 vitest pass (14 new, fake-worker); vite build ok. Real transcription + perf (esp. mobile) are unverified until in a browser. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The spike was wired but couldn't run end-to-end; fixes the chain that blocked real transcription (validated live: on-device WebGPU Whisper transcribes the mic): - CSP: allow HuggingFace weight-delivery hosts (*.huggingface.co LFS + *.hf.co Xet) — model .onnx files 302-redirect there; connect-src checks the final URL. - worker: omit language/task for English-only Whisper checkpoints (.en) — they throw 'Cannot specify task/language for an English-only model'. - main: bind ANY live session, not just DemoModeSession (the instanceof check left the real LiveModeSession unbound, so keys never reached it). - sessions: gesture-gate the mic — start capture in begin() (called on the first keypress/click), not the constructor; getUserMedia/AudioContext need a user gesture or the AudioContext stays suspended and no frames arrive. - main: a click anywhere also begins (robust when the terminal lacks keyboard focus, e.g. DevTools open). tsc clean; 211 vitest pass; build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… bug) frameSequence homed the cursor and overwrote in place but never erased what the overwrite didn't cover, so a shorter frame (the picker drawn over a long session transcript) or a shorter line left the previous frame's text behind — the garbled, overlapping screen. Erase each row to end-of-line (a shorter line can't keep a longer line's tail) and erase to end-of-screen after the last row (a shorter frame can't keep a taller frame's rows). Still no full clear (\x1b[2J) — EL+ED clear exactly what the in-place overwrite leaves, so it stays flash-free. 5 loop tests pass (the no-2J invariant holds). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…b parity) strip_sound_tags was written and tested but wired into nothing — so the live engine's non-speech annotations ([BLANK_AUDIO], (Keyboard clicking)) settled as kept text: '[BLANK_AUDIO].', '(Keyboard clicking).'. Wire it into the front of deterministic_light, the one cleanup both the CLI (DeterministicFormatter) and the web (wasm deterministicLight) share, so both strip them. A tag-only segment cleans to '' (ensure_terminal leaves empty empty — no lone period). Also extend SOUND_WORDS with the desk/ambient events Whisper emits while you talk at a keyboard (keyboard, clicking, tapping, mouse, rustling, …); the all-words-must-be-sound-words rule still protects real asides like '(I think)'. 123 talk-core tests pass; 16 web pipeline tests pass (incl. wasm-path parity). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A cheat-sheet behind one keystroke: ? or / opens a help overlay listing exactly what the current screen accepts (session / picker / journal / closing) plus the always-available globals and the privacy promise. Any key closes it — and the closing key is swallowed so it never also fires a command underneath (space closes help without landing the entry). Surfaced inline on the picker + journal hints (? help) so it's discoverable. A static reference, not a type-to-run palette — discoverability without adding a new input mode to a calm reflection app. tsc clean; 212 vitest pass; build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
onMicState only updated the status line while state !== 'granted', so the brief
'pending' message ('Waiting for microphone permission…') latched and the granted
transition never cleared it — even when the mic was already granted (it still
passes through 'pending' for one frame). Clear the line on granted; the session
view's own 'ready' / '[ Silence ]' indicator already shows it's listening.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…all, picker Six fixes surfaced by running it live: - boot no longer auto-dismisses on a timer. The 4.2s timer dropped the user into a session whose mic never started (getUserMedia needs a user gesture) — now the 'press any key to begin' gate IS that gesture; boot holds until a key/click. - esc works again. Letters reached onData but a bare Escape did not, so esc-cancel and esc-back silently did nothing. Route Escape through attachCustomKeyEventHandler, context-aware (close help · leave journal · dismiss closing · cancel session). - 'speech model ready (webgpu)' no longer trails across every screen — the status line clears on ready (the header + session indicator already show readiness). - 'downloading speech model…' only on a real first download; a cached load says 'loading…' (a per-browser model-seen flag distinguishes them). - install funnel detects the OS: Homebrew on macOS (corrected to the real tap walktalkmeditate/tap/talk — was the nonexistent 'brew install talk'), the crate elsewhere. Help overlay lists the detected command + both methods. - picker gains a calm one-line description per mode (reflect/journal/unburden) with the keystroke highlighted, instead of three bare verbs. tsc clean; 212 vitest pass; build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nfirmation) The save confirmation auto-vanished after 2.5s, so finishing a voice note felt like nothing was saved. It now holds until a keypress (CLI parity — the CLI's show_close awaits a key) with a 'press any key to continue' hint. The unburden release still fades on its own (a 'wait' flag on beginClosing distinguishes the two moments, keeping the dwell path for the release). tsc clean; 212 vitest pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ntries esc: space→done works (keys reach onData) but a bare Escape never did, and xterm's key-event handler didn't catch it either. Listen at the document in the CAPTURE phase instead — fires before xterm's textarea regardless of its focus quirks, handled once (stopPropagation), cleaned up on unload. esc now cancels a session (arms the discard prompt) and backs out of journal/help/closing. journal paragraphs: the CLI applies shape_entry(High) to journal entries on save (src/main.rs:383) but the web only space-joined finalClean — shapeEntry was referenced in a comment and never called. Apply it in buildPayload with the mode's level (journal → High paragraphizes, reflect → Light passes through), so kept + exported journal entries paragraphize exactly like the CLI. tsc clean; 212 vitest pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-named Adds the individual half of export alongside the existing whole-vault export: - journal view numbers each day + reflect thread; pressing the digit exports just that one. 'e' still exports everything. - Obsidian-friendly filenames for individual exports: a journal day is its bare date (2026-06-14.md — the daily-note pattern), a reflect thread/entry is its question slug (avoidance-slug.md). The whole-vault export keeps talk-journal.md. - new 'day' export scope folds one calendar day's journal entries into a single daily-note file (so multiple same-day entries land in one 2026-06-14.md, the way Obsidian expects), reusing the appendEntry markdown builder for byte-parity. Granularity is per-DAY (not per-entry) for journal so same-day notes coexist in one daily file and filenames never collide; per-THREAD for reflect. tsc clean; 214 vitest pass (incl. new day/thread filename + scope tests); build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…entries) The digit-export ([1]-[9]) capped at 9 and the journal view didn't scroll, so a growing journal overflowed the screen and later groups were unreachable. Replace digits with a selection cursor: ↑/↓ (or k/j) move the highlight, the viewport scrolls to keep it visible (↑/↓ more indicators), x/enter exports the selected day/thread. Works for any number of entries. e still exports everything. tsc clean; 214 vitest pass; build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…export) In the journal browser: d deletes the selected day/thread, D deletes everything — both arm a y/n confirm first (deletes are irreversible; localStorage only). esc cancels a pending confirm before it leaves the view. Export stays independent (no auto-delete) so a silently-failed download can't lose data. Store deletes write DIRECTLY against a fresh disk read rather than through save(): save() union-merges the on-disk blob to protect concurrent-tab adds, which would otherwise resurrect a just-deleted entry. Added deleteDay/deleteThread/clearAll; write failures surface like keep() does. tsc clean; 219 vitest pass (incl. deletion + 'not resurrected by a later keep' merge guard); build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… journaling) Now that the journal has a selection cursor, 'c' continues the SELECTED unit instead of always the most-recent thread: a reflect thread re-prompts its exact question; a journal day resumes journaling (a fresh freeform entry, landing under today). Replaces topThread/continueThreadView with continueSelectedUnit. tsc clean; 219 vitest pass; build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A journal day has no question to resume and journal entries always land under today, so c only continues a reflect thread now (no-op on a journal day). Updated the hint + help to say 'continue a thread'. tsc clean; 219 vitest pass; build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
momentmaker
marked this pull request as ready for review
June 14, 2026 23:40
The CSP was broadened to let transformers.js fetch the Whisper weights from HuggingFace (the .en files 302-redirect to *.huggingface.co / *.hf.co LFS+Xet hosts). Update the canary to that documented allowlist: it still asserts the EXACT set of origins and still proves a disallowed origin is refused at runtime, but now rejects only BLANKET wildcards (bare *, scheme-only, scheme://*) — the two HF entries are scoped wildcards in the allowlist. Production self-hosts on cdn.pilgrimapp.org and narrows back to 'self' + cdn (no HF, no wildcards). e2e: 2/2 pass locally (chromium). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — foundation only. Not for merge until the feature is whole (Phase C–E after the U1 spike).
The on-device, in-browser voice-reflection web port of
talk, modeled on the siblingmeditate-cli/web. Built through the full dream → spec → review → plan → review → work pipeline.docs/brainstorms/2026-06-13-talk-web-requirements.mddocs/plans/2026-06-13-001-feat-talk-web-plan.mdWhat's in this PR (Phase B foundation + the U1 spike harness)
talk.pilgrimapp.orgtalk-wasm— a wasm-bindgen façade over puretalk-core(cleanup, append, palette, Settle, compose, question selection). Cross-compiles to wasm32; no listen/sherpa/libc dep leaktalk-wasm palette(); paints a static composed reflect screen)web/spike/— the runnable sherpa-onnx WASM measurement tool (RTF, footprint, CSP-loadability, threading, egress)It builds and paints a static rust reflect screen through the real two-pass render path. It does not transcribe yet — that's U6, deliberately gated on U1.
Verified
cargo test -p talk-core -p talk-wasm→ 110 + 23 passcargo build --target wasm32-unknown-unknown -p talk-wasm→ okcd web && npm test(vitest) → 19 pass ·tsc --noEmitclean ·vite buildokReviewed
A 10-persona
ce-code-reviewran on this diff: no P0s; 17 P1/P2/P3 findings auto-resolved (façade contract hardening, theme no-silent-mis-paint + palette-drift fix,loop.tsno-flash test, and making the U1 spike honest about its main-thread-only egress measurement).The gate: U1 needs real devices
This build is spike-first by design. U1 must run on a desktop + a mid-range phone to select the build branch (base.en vs Moonshine on mobile · two-pass vs single-pass · single-threaded vs threaded host · Cache vs OPFS · the real CSP directive set). See
web/spike/README.md. Once the go/no-go memo lands in the plan's Open Questions, Phase C resumes (U5 models, U6 ASR + U3-B pairing lift, U7 controls) → D (modes, journal) → E (zero-egress canary, chrome, Pages deploy).🤖 Generated with Claude Code