Skip to content
This repository was archived by the owner on Jul 16, 2026. It is now read-only.

convo ui refactor to react - #6

Merged
jgluck96 merged 23 commits into
mainfrom
josh/cherry-pick-fixes-2026-05-15
May 18, 2026
Merged

jgluck96 merged 23 commits into
mainfrom
josh/cherry-pick-fixes-2026-05-15

Conversation

@jgluck96

Copy link
Copy Markdown

No description provided.

SirEntropy and others added 22 commits May 15, 2026 14:42
`health-check.py` had four sites reading `.env` as `REPO_DIR / ".env"`.
On packaged-`.app` installs `REPO_DIR` is `/Applications/Sutando.app/
Contents/Resources/repo`, which is sealed and never contains the user's
`.env` — so every health check reported `.env` missing while the real
file sat at `$SUTANDO_HOME/.env` and was being read by the running
processes via `load-env.ts`.

Route all four sites through `state_path(".env")` (already imported,
already uses `_state_root()` which honors `SUTANDO_HOME`). Critical-file
check, memory-sync probe, phone Twilio detect, and ngrok-domain reader
all now agree with what the running bot actually loads.

Sites changed:
- check_memory_sync (L95)
- critical files list (L554)
- phone conversation Twilio detect (L571)
- ngrok domain reader in --fix path (L833)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ux socket

src/voice-agent.ts:841 appended to data/voice-metrics.jsonl without ensuring
the parent dir existed. On a fresh install, $SUTANDO_HOME/data/ doesn't
exist, so every voice session logged ENOENT in voice-agent.log and dropped
its metrics. Switching to statePathEnsured() (already exported from
util_paths) creates the dir on first use.

src/tmux-status.ts:45 called tmux capture-pane without specifying the
socket, but the tmux server actually runs on /tmp/sutando-tmux.sock per
startup.sh, run-core-agent.sh (SOCKET= line 26), and Sutando/main.swift
(sutandoTmuxSocket constant). The capture call was hitting the default
socket and failing with "can't find pane: sutando-core" — effectiveAgentState's
tmux backstop has been silently broken. Added DEFAULT_SOCKET constant
mirroring the existing SUTANDO_TMUX_SESSION env-override pattern.
…rough state_dir/state_path

Routes TASKS_DIR, RESULTS_DIR, STATE_DIR, and .env lookups in discord-bridge.py,
telegram-bridge.py, github-webhook.py, event_log.py, and notify.sh through
util_paths state_dir()/state_path() instead of hardcoded REPO/ paths.

Mirrors the pattern already applied in archive-stale-results.py (main) and
health-check.py (cherry-picked). Bridges and webhook break silently when
SUTANDO_HOME is set to a non-repo dir (e.g. the packaged .app layout).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accidentally committed on josh/ui-update when Package.swift was added and
.build/ was not yet ignored. Adds the ignore entry so future `git add .`
from the repo root doesn't sweep up multi-MB .pcm precompiled module files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…1/2)

Step 1 of folding com.sutando.web-client into com.sutando.voice-agent.
Splits src/web-client.ts into two modules without touching imports yet:

- src/web-client-html.ts exports renderWebClientHtml() — the giant inline
  HTML template, unchanged. Stepping stone toward PR-B (Vite + React).
- src/web-server.ts exports startWebServer({ port, host, wsPort }) — the
  HTTP server, SSE plumbing, /sse-status / /mute-state / /toggle / /mute /
  /voice-mode / /note-viewing endpoints, lifted verbatim and wrapped in
  a function.

Inert until step 2 wires voice-agent.ts to call startWebServer() and
deletes src/web-client.ts + com.sutando.web-client.plist.template.
…p 2/2)

Completes the consolidation started in ff04cf1 (web-server.ts +
web-client-html.ts extraction). One Node process now owns both
the WebSocket on :9900 and the HTTP conversation server on :8080.

Wiring:
- voice-agent.ts imports startWebServer + calls it at the top of main().
  CLIENT_PORT (8080) + CLIENT_HOST (0.0.0.0) env vars preserve the remote-
  browser access path (iPhone over Tailscale, EC2 desktop).
- LaunchAgentInstaller.swift learns about retired labels: every install
  boots out + deletes stale com.sutando.web-client.plist so upgrades from
  pre-PR-A clean up automatically.
- Test (agent-state-endpoint) now imports startWebServer directly instead
  of spawning `tsx src/web-client.ts` — faster boot, no subprocess hangs.

Removed:
- src/web-client.ts (133 KB, 2945 lines) — content lives in
  web-client-html.ts (HTML template) + web-server.ts (HTTP server).
- app/LaunchAgents/com.sutando.web-client.plist.template — the service
  is gone; LaunchAgentInstaller cleans up the deployed plist on next install.

Touched for consistency:
- startup.sh: dropped web-client block, renumbered 3→2 / 4→3 / 5→4 / 5a→4a,
  VERIFY_PORTS lists 8080 as voice-agent-http.
- restart.sh: removed web-client.ts pkill + STOP_PATTERN.
- health-check.py: dropped com.sutando.web-client from plist_map, added an
  8080 http_check pinned to web-server.ts mtime + voice-agent.ts pgrep.
- verify-setup.sh: file-existence check now looks for web-server.ts +
  web-client-html.ts; port check renames 8080 to voice-agent-http.
- screen-capture-server.py + main.swift + skills/MANIFEST.md + CONTRIBUTING.md
  + README.md + app/README.md + app/LaunchAgents/README.md + DISTRIBUTION.md
  + scripts/test-pr354-retention-sweep.sh: doc + reference updates.
- .gitignore: dropped src/web-client.js (the file the .ts compiled to no
  longer exists; web-client-html.ts ships .ts only).

No UI changes. The legacy HTML template is byte-identical, just now imported
by web-server.ts from web-client-html.ts. PR-B will replace it with a Vite +
React bundle per CLAUDE.md § Frontend Conventions.
PR-A moved the HTTP server from a subprocess into the voice-agent.ts
process. Pre-PR-A the agent-state-endpoint test SIGKILL'd the subprocess
on teardown so the 30s SSE heartbeat interval didn't matter. Post-PR-A
the in-process timer keeps Node alive after server.close(), pinning the
test runner for 30s+ on every CI run.

setInterval.unref() lets Node exit when the heartbeat is the only thing
holding the event loop open. No behavior change while the server is
serving traffic — sseClients still get :\n\n pings every 30s — but a
lone heartbeat no longer blocks shutdown.

Verified: test suite now exits in 3.2s (was timing out at 60s+).
Stands up the React frontend per CLAUDE.md § Frontend Conventions.
Lives at client/ in a pnpm workspace, builds to client/dist/, served by
src/web-server.ts at /v2. The legacy HTML at GET / is untouched — PR-C+
incrementally migrate features into the React tree before flipping
/v2 ↔ / and deleting src/web-client-html.ts.

Stack:
- Vite 6 + React 19 + TypeScript 5.6 + Tailwind 4 (CSS-first config).
- @ alias resolves to client/src/ in both tsconfig.app.json and vite.config.ts.
- pnpm workspace ('.' + 'client') — `pnpm install` at the root installs both
  packages, `pnpm build:client` / `pnpm dev:client` proxy to the workspace.

Directory structure (matches the convention table in CLAUDE.md):
  client/src/
    pages/{conversation,core-cli,dashboard,settings}/  — thin orchestration
      <Page>.tsx + index.ts                            — one per UnifiedPane
    components/atoms/{NavTabs,PageHeader}/             — pure presentational
    components/molecules/AppShell/                     — composed of atoms
    components/organisms/                              — reserved (.gitkeep)
    contexts/                                          — reserved (.gitkeep)
    hooks/{useCurrentRoute,useAgentStatus}.ts          — business logic
    utils/                                             — reserved (.gitkeep)
    const-values/{app-routes,app-copy}.ts              — no hardcoded strings
    lib/{config,api,sse}.ts                            — infra (server-agnostic)

Routing: no react-router yet. `?page=<id>` query param drives the active
page via useCurrentRoute(). Matches UnifiedPane ids so the desktop
WKWebView can load any pane via the same bundle URL.

Server-agnostic config (client/src/lib/config.ts):
  ?ws=  / ?api=  /  ?page=                   (highest priority)
  → window.__SUTANDO_CONFIG__                (host shell injection)
  → window.location.host                     (default — works for desktop
                                              WKWebView, remote browser over
                                              Tailscale/EC2, future mobile
                                              thin-client — no rebuild needed)

src/web-server.ts:
- /v2[/*] → reads client/dist/, serves the bundle with sensible cache
  headers (no-cache for index.html, immutable for hashed assets).
- Falls through to a friendly "bundle not built" 404 when client/dist/
  is missing (fresh checkout without `pnpm build:client`), so the desktop
  app keeps working during incremental adoption.

Verified:
- pnpm typecheck: clean.
- pnpm test: 9/9 endpoint tests pass, full suite green.
- pnpm build:client: 49 modules → 201 KB JS / 9 KB CSS in 602 ms.
- Smoke test: GET /v2 returns React index.html, GET /v2/assets/index-*.js
  serves the 201 KB bundle, GET / still serves legacy HTML, GET /sse-status
  still returns 200.

.gitignore: client/node_modules + client/dist + client/.vite.
First user-visible piece of the React migration: connecting from /v2 now
streams mic audio to voice-agent.ts and plays back the assistant audio,
just like the legacy / page.

Architecture (framework-free → adapter → UI):
- lib/audio.ts: downsample, float32↔int16, mic availability check
  (ported verbatim from web-client-html.ts so audio behavior is
  byte-identical — don't drift without a parity test)
- lib/voice-session.ts: VoiceSession class owns WebSocket + AudioContext
  + mic capture + gapless playback scheduling; emits VoiceSessionState
  via callbacks so the same engine can drive React or a Swift WKWebView
  later
- hooks/useVoiceSession.ts: thin useSyncExternalStore adapter; creates
  the session once per mount, tears it down cleanly on unmount/
  StrictMode double-invoke

UI (atoms → molecules → page):
- atoms/StatusBadge, atoms/ConnectButton, atoms/MuteButton
- molecules/VoiceSessionCard composes them + bytes-sent/received stats
- ConversationPage now renders the voice card plus a server-view card
  showing what voice-agent.ts reports about itself

Out of scope for PR-C and migrating in follow-ups: transcript display,
task list, dynamic region (decision options, image/video previews),
Chrome SpeechRecognition fallback, avatar speaking detection.

Verified: pnpm build:client clean, pnpm typecheck clean, pnpm test
green, /v2 smoke test confirms new bundle ships and contains the
voice-session entrypoints.

Co-authored-by: Cursor <cursoragent@cursor.com>
…p 2)

Migrates the live transcript from web-client-html.ts into the React tree
and brings PR-B/step-1 components into compliance with the new
frontend-conventions skill (kebab-case component directories).

Transcript migration:
- types/conversation.ts — TranscriptEntry/TranscriptRole/ConversationSnapshot
- lib/conversation-store.ts — framework-free module-level store. Mirrors
  the legacy handleTranscript + turn.end/turn.interrupted semantics:
    * user partial → create or update an interim entry
    * user final   → finalize the entry, drop pointer
    * assistant    → always tracked, finalized on partial=false
    * turn.end / turn.interrupted → drop orphan-interim user entry,
      clear both pointers (audio cleanup stays in VoiceSession)
- hooks/useConversation.ts — useSyncExternalStore adapter
- VoiceSession now emits onTranscript/onTurnEnd/onTurnInterrupted/
  onSystemMessage; useVoiceSession wires those into the store

UI (kebab-case dirs per frontend-conventions skill):
- atoms/copy-button — Copy → Copied transient state with cleanup on
  unmount (uses navigator.clipboard, fails silently in restricted iframes)
- molecules/transcript-entry-row — role-toned row (user/assistant/system)
  with copy button for finalized non-system entries
- organisms/transcript — scrollable list with sticky-bottom auto-scroll
  (only sticks when the user is already near the bottom — doesn't fight
  manual scroll-up)
- ConversationPage now renders <Transcript /> alongside VoiceSessionCard
  and the server-view card

Refactor-on-touch (frontend-conventions rule 5): renamed all component
directories from PascalCase to kebab-case across PR-B + PR-C step 1.
File names keep PascalCase (e.g. connect-button/ConnectButton.tsx).
All imports updated; residual PascalCase dir refs scan is clean.

Verified: pnpm build:client clean, pnpm typecheck clean, pnpm test
green, /v2 smoke confirms handleTranscript / "transcribing" /
appendSystem markers are bundled.

Co-authored-by: Cursor <cursoragent@cursor.com>
Brings the live /tasks/active feed from the Python agent API into the
React tree. /v2 now matches / on the three biggest conversation features:
voice session, transcript, and task list.

Architecture (framework-free → adapter → UI):
- types/task.ts — Task, TaskStatus, ApiTask, ApiTasksResponse,
  SystemHealth, TaskListSnapshot
- const-values/task-config.ts — POLL_INTERVAL_MS=3000, MAX_DISPLAYED=30,
  legacy persistence keys (sutando-taskmap-v1 / expanded / show-done) so
  existing users carry state across the / → /v2 migration
- lib/task-summary.ts — pure summarizeTaskText + tagVoiceFallback, ported
  from web-client-html.ts and rewritten declaratively (reduce, find,
  composed pipeline — no let, no ++)
- lib/tasks-api.ts — fetchActiveTasks(agentApiOrigin, signal)
- lib/task-store.ts — module-level store with localStorage persistence,
  preserves the legacy auto-expand rules (new working tasks expand,
  working→done transitions expand one-shot, both gated by userCollapsed)
  and the stale-working pruning behaviour
- lib/config.ts — adds agentApiOrigin (default http://<host>:7843,
  overridable via ?agent-api=)
- hooks/useTasks.ts — useSyncExternalStore subscription + taskActions
- hooks/useTaskPolling.ts — abort-aware setInterval driver, swallows
  network blips (legacy parity), cancels in-flight requests on unmount

UI (kebab-case dirs per frontend-conventions):
- atoms/task-status-icon — pending/working/done/error glyph + tone
- atoms/relative-time — "Ns ago" via pure utils/format-relative-ago.ts
  (now-injectable for tests)
- molecules/task-row — click-to-expand row, keyboard accessible, shows
  full text + result pane when expanded; selection-aware (no toggle
  when the user is text-selecting the title)
- organisms/task-list — header (show/hide done, collapse/expand all,
  system-health badges), top-30 sort, empty/hidden-all states
- ConversationPage now mounts <TaskList /> below the transcript

Deferred to PR-C step 4 (dynamic region): parseDecisionOptions buttons,
reply-to-task input, toast notifications, /questions wiring.

Verified: pnpm build:client clean (84 modules, 225.6 KB bundle), pnpm
typecheck clean, pnpm test green, /v2 smoke confirms /tasks/active,
"expand all"/"show done", and sutando-taskmap-v1 markers are bundled.

Co-authored-by: Cursor <cursoragent@cursor.com>
Expanded tasks now render a reply form, with quick-pick buttons when the
result looks like a decision prompt. Mirrors the legacy task-action-btn /
task-action-input UX from web-client-html.ts.

- lib/parse-decision-options.ts — pure port of parseDecisionOptions.
  Two patterns: `Say "alpha", "beta", or "gamma".` and
  `DECISION: alpha / beta / gamma`. Returns null when text isn't a
  decision prompt; caller falls back to free-form input only.
- lib/tasks-api.ts — adds postTaskReply(origin, taskId, answer) that
  POSTs to /task with from: `web-reply:<taskId>`, matching the legacy
  contract so the bridge can correlate the new task with the result.
- hooks/useTaskReply.ts — per-row idle → sending → sent/error state.
  Hook scope evaporates with the row when collapsed (same lifetime as
  the legacy DOM-rebuild-on-render path).
- atoms/decision-option-button — quick-pick option chip
- molecules/task-reply-form — flat layout (option chips + input + send),
  collapses to "Replied: <answer>" on success, surfaces the API error
  on failure.
- TaskRow now mounts <TaskReplyForm /> alongside the result pre when a
  task is expanded.

Verified: pnpm build:client clean (90 modules, 228.9 KB bundle).
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes the three remaining gaps that ship in the legacy / page but were
missing from /v2: inline image/video from gui.update payloads, the
pending-questions panel, and toast notifications for new + completed
tasks.

gui.update media in transcript:
- types/conversation.ts — adds TranscriptMedia (image|video, base64,
  mimeType, description)
- conversation-store.ts — new appendMedia() helper, system entry that
  carries the payload
- voice-session.ts — handleGuiUpdate() routes payload.data by type:
  image|video → onGuiMedia, subprocess_log → console log; also handles
  legacy top-level `image` / `video` / `gui.notification` WS frames
- atoms/transcript-media — img/video tag with download link, builds the
  data URL once per mount (base64 strings get big)
- TranscriptEntryRow now renders <TranscriptMediaView /> when entry.media
  is set

Pending questions panel:
- types/task.ts — PendingQuestion {id, text, detail?, options?}; adds
  questions: readonly PendingQuestion[] to TaskListSnapshot
- task-store.handleApiTasks() now surfaces api.questions; adds
  removeQuestion() for optimistic post-answer dismissal
- lib/questions-api.ts — postQuestionAnswer(origin, id, answer) POSTs
  to /answer with the legacy {id, answer} body
- hooks/useQuestionAnswer.ts — idle → sending → sent/error, 1.5s
  confirmation linger before optimistic store removal (matches the
  legacy setTimeout behaviour)
- molecules/question-card — Yes/No (or custom options) + free-form
  input, Enter-to-send, confirmation pill on success
- organisms/questions-panel — renders nothing when 0 questions

Toasts:
- types/toast.ts + lib/toast-store.ts — framework-free queue, 4s auto-
  expire matching the legacy showToast linger
- hooks/useToasts + useTaskToastDriver — diff the task snapshot against
  ref state to fire "Context received" on first sighting and "Done" on
  working→done transition (legacy parity)
- atoms/toast-item + organisms/toast-overlay — bottom-right floating
  stack, mounted once in AppShell so toasts persist across page nav

ConversationPage now mounts <QuestionsPanel /> above <TaskList /> and
calls useTaskToastDriver() to wire diff-based toasts.

Verified: pnpm build:client clean (105 modules, 236.5 KB bundle), pnpm
typecheck clean, pnpm test green, /v2 smoke confirms /answer, "Pending
questions", "Context received", "Download ", emerald option button
class, and "web-reply:" prefix markers are all bundled.

Co-authored-by: Cursor <cursoragent@cursor.com>
GET / now serves the Vite + React bundle from client/dist/ — the React
tree has reached feature parity with the legacy inline HTML on every
conversation-page surface (voice session, transcript, task list with
inline replies, gui.update media, /questions panel, toasts). Existing
bookmarks pointing at /v2 keep working as an alias for /.

The original inline HTML survives at GET /legacy as a one-release escape
hatch in case a regression in the React build needs to be worked around
in production. PR-C step 6 will retire web-client-html.ts and drop the
/legacy route together.

Route behaviour:
- /        → client/dist (React). Falls back to inline HTML when dist
             isn't built — fresh-checkout safety net so the desktop app
             still boots before `pnpm build:client` runs.
- /v2[/*]  → identical alias of /. Kept so the menu bar's "open in
             browser" and any user-saved bookmark don't 404.
- /legacy[/*] → renderWebClientHtml inline HTML. Will be removed in
             PR-C step 6.
- Unknown paths fall through to the SPA index.html for forward-
  compatible deep-linking (current SPA uses ?page=, so this is a no-op
  today).

web-server.ts also gains a serveDistFile() helper to dedupe the
filesystem read + cache-control headers across the two SPA-serving
branches.

Doc + comment updates so future-me reads coherent state:
- client/README.md, client/package.json description, vite.config.ts
  doc — flip is past tense.
- CONTRIBUTING.md "Web client" section explains the routes + retirement
  plan.
- client/src/lib/config.ts WKWebView reference updated to localhost:8080
  (the Swift app already loads / — no Swift change needed).
- client/src/const-values/{app-copy,task-config}.ts and
  client/src/lib/task-store.ts comments updated.
- ConversationPage scaffold notice now points to /legacy.

Verified: pnpm typecheck clean, pnpm build:client clean, pnpm test green.
Smoke confirms: / serves React with #root, /v2 returns identical body,
/legacy still contains #transcript inline element, /sse-status keys
unchanged, and / falls back to inline HTML when client/dist/ is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
PR-C step 5 made GET / serve client/dist/ from src/web-server.ts, but
app/build-app.sh only staged src/ + skills/ + node_modules into the .app
bundle and never touched client/. Result: every rebuild produced a
bundle without the React build, so / silently fell back to the inline
HTML safety net (the exact symptom Josh hit five minutes after the
flip landed).

Adds a build + stage step after the existing repo-source copies:
- Runs `pnpm --filter @sutando/client build` when client/dist is
  missing or stale relative to client/package.json (so an iterative
  Swift-only rebuild doesn't pay the React build cost twice).
- Copies client/dist into the bundle's repo/client/dist, plus
  package.json + index.html for in-bundle diagnostics.

Runtime impact: zero — client/dist is plain static files served by
the existing serveDistFile() helper in web-server.ts. Build-time
impact: ~500ms when dist needs rebuilding, else just the rsync.

Co-authored-by: Cursor <cursoragent@cursor.com>
Atomic ditto stage-then-rename replaces cp -R to preserve xattrs.
Detects EPERM from App Management → retries with sudo chflags + rm.
Post-install verification of 5 required files fails loudly on partial installs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After PR-C step 5 flipped / to the React build, the server's catch-all
SPA route was serving index.html for every miss — including hashed asset
requests like /assets/index-XXXX.js. The browser would parse that HTML
as JS, fail with "Unexpected token '<'", and render a blank page.

Add an `spaFallback` flag to serveDistFile so the asset-shaped requests
(anything with an extension) return 404 cleanly, while extension-less
deep-links still fall back to index.html for SPA routing. Bundle-missing
on / still falls back to the legacy HTML so fresh checkouts boot.

Co-authored-by: Cursor <cursoragent@cursor.com>
The previous /v2 → / flip used a generic AppShell + Tailwind cards for
the conversation page, which looked nothing like the legacy web-client-
html.ts. This restyles the React app to match the legacy 1:1: light-mode-
first palette, avatar header, hero connect screen, keyboard shortcuts
bar, Starter/Tasks/Notes/Questions/Activity sub-tabs, starter chips, and
fixed bottom-panel transcript + input.

How it works:
- styles/legacy.css holds the ported palette + component CSS, scoped to
  a .legacy-shell wrapper so it doesn't bleed into other React routes.
- App.tsx bypasses AppShell on the conversation route — the page owns
  its own chrome (the legacy was single-page, AppShell would double up).
- ConversationPage assembles the legacy DOM and binds existing voice /
  conversation / task / question / toast stores. No store changes.
- Existing TaskList / QuestionsPanel / TaskRow / Toast components keep
  their data wiring but swap Tailwind class names for the legacy
  .task-item / .dr-questions / .q-btn / .toast classes so legacy.css
  handles the visuals.

New components (all kebab-cased per CLAUDE.md frontend conventions):
  atoms: avatar-svg-default, keyboard-shortcuts-bar, voice-status-pill
  molecules: legacy-header, legacy-hero, legacy-input-bar, legacy-
    transcript, starter-chips
  organisms: dynamic-region

Pixel-diff vs the legacy is clean across header, hero, kbd bar, tab
row, chips, transcript, toasts, and Tasks panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
Closes the parity gap between the new React client and src/web-client-html.ts
so the React build can stand on its own (next step: retire the legacy file).

  - Text input: wire LegacyInputBar to the voice WS as a `text_input`
    frame, with HTTP task-bridge fallback (POST /task → poll /result/<id>)
    when voice isn't connected. New hooks/useTextSubmit.ts.
  - SSE bridge: subscribe to /sse for `toggle-voice`, `toggle-mute`, and
    `agent-state` pushes. Reconnects on visibility change + 5s back-off
    on error — matches the legacy `initRemoteToggle()`. Drives the
    avatar ring color via .s-{state} classes on header + hero.
  - Identity fetch: read /stand-identity from the dashboard server
    (port 7844). When present, swap the inline SVG for the generated
    PNG and overlay "Sutando — <name>" + custom tagline.
  - Mute-state sync: POST /mute-state on voice/mute transitions plus a
    1Hz heartbeat while live so the menu-bar recording dot stays honest
    after a server restart.
  - Auto-reconnect: set sessionStorage `sutando-voice` flag on live →
    re-arm WS after a 500ms delay on page load. Cmd+R keeps voice up.

ConversationPage drops the `/sse-status` poll (still exported for now)
in favor of the push stream — same data, no 3s lag.

Co-authored-by: Cursor <cursoragent@cursor.com>
Closes PR-C step 6 of the React refactor. The Vite + React client now owns
every feature the legacy inline HTML provided, so the source-of-truth file
and the `/legacy` escape hatch can go.

Last functional gaps closed:
  - Transcript copy buttons on every finalized user/assistant entry,
    matching the legacy `.copy-btn` hover affordance and 1.5s "Copied"
    feedback.
  - Notes tab: fetch /notes from the dashboard (port 7844), search,
    click to view rendered markdown, delete. Opening a note POSTs to
    /note-viewing so the voice agent can pull the raw text into Gemini
    context. New lib/markdown.ts ports the legacy regex chain (headings,
    fenced + inline code, links, images, bold/italic, blockquote, hr,
    lists) dep-free.
  - Activity tab: fetch /activity from the Python agent API (port 7843)
    and render the commit + task feed.

Retire web-client-html.ts:
  - Drop the /legacy route from web-server.ts. Missing client/dist/
    now returns a 503 with a "run pnpm build:client" hint instead of
    a 2600-line stale HTML payload.
  - Delete src/web-client-html.ts (117 KB) and remove it from
    verify-setup.sh's key-files list.
  - Tidy lingering useAgentStatus / fetchAgentStatus exports that only
    existed to feed the (now SSE-pushed) avatar ring state.
  - Update CONTRIBUTING.md + main.swift comments to point at client/.

Co-authored-by: Cursor <cursoragent@cursor.com>
CGRequestScreenCaptureAccess on startup catches the cdhash-mismatch
case (rebuild invalidates TCC grant) before supervisor binds, surfacing
the system prompt instead of silent /capture failure. Evict any orphaned
screen-capture-server.py holding :7845 from a previous install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jgluck96
jgluck96 requested a review from rickchen007 May 15, 2026 23:46
@jgluck96 jgluck96 changed the title convo ui and refactor to react convo ui refactor to react May 15, 2026
@jgluck96
jgluck96 merged commit 21d530e into main May 18, 2026
1 check passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants