This repository was archived by the owner on Jul 16, 2026. It is now read-only.
Repository navigation
convo ui refactor to react - #6
Merged
Merged
Conversation
`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>
rickchen007
approved these changes
May 18, 2026
3 of 13 tasks
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
No description provided.