feat(mode-indicator): full 3-mode UI recovered from Apr 21 jsonl#519
feat(mode-indicator): full 3-mode UI recovered from Apr 21 jsonl#519
Conversation
Restores the 3-mode (active / meeting / presenter) UI that was lost
when `git reset --hard HEAD~2` on dev/apr-21-local-ships at 2026-04-23
06:22 PT discarded uncommitted working-tree edits alongside two
sync-memory commits.
Recovered by replaying 15 Edit calls from session 6eb8f10e jsonl
(2026-04-21 23:18-23:54 PT) on top of dev/apr-21-local-ships's
presenter-dot indicator base. Replay applied 15/15 cleanly.
Sutando.app (`src/Sutando/main.swift`):
- New properties: `voiceMode`, `presenterModeActive`, three menu-item
weak refs (`modeActiveMenuItem`, `modeMeetingMenuItem`,
`modePresenterMenuItem`).
- Three-mode radio in menu bar dropdown: "Mode: Active" /
"Mode: Meeting" / "Mode: Presenter". Exactly one has ● at a time.
Clicking any item switches: Active/Meeting write
`state/voice-mode.request` (voice-agent picks up on 1s poll);
Presenter POSTs to `:7877/presenter/on`.
- Avatar badge: `avatarImage(presenterActive:meetingActive:)` paints
a small purple dot when presenter is active, or amber dot for
meeting — matches the web UI mode-pill colors.
- New polls: `pollPresenterMode()` + `pollVoiceMode()` every 1s on
the same timer. Both silent-fail if their backend is down.
- `updateModeMenuItem()` recomposes the radio whenever either signal
flips. Priority: presenter > meeting > active.
Voice-agent (`src/voice-agent.ts`):
- `switchModeTool.execute()` writes `state/voice-mode.txt` on
switch_mode("active"|"meeting") so cross-process readers (web-client,
Sutando.app) can resolve the unified mode.
- `applyModeRequest()` polls `state/voice-mode.request` every 1s,
applies the mode flip, deletes the request file. Lets Sutando.app
send mode requests without an HTTP server in voice-agent.
Web client (`src/web-client.ts`):
- `mode` field added to /sse-status response — built from sync read of
`state/voice-mode.txt` + cached presenter-active boolean (refreshed
every 1s in background from :7877/presenter).
- Mode pill in top-bar UI: 3 CSS variants (mode-active dim /
mode-meeting amber / mode-presenter purple-glow). Polls /sse-status
every 1.5s.
Recovery details: notes/3-mode-recovered-edits-2026-04-21.md.
Earlier #518 (simpler text-only version) is closed in favor of this
fuller recovery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…m detect
Two related bugs in the recovered 3-mode work surfaced when Chi tested
the menu radio: clicking "Mode: Meeting" silently no-op'd because the
in-memory `meetingActive` flag and the on-disk `state/voice-mode.txt`
sentinel had diverged.
(1) `switchModeTool.execute` flipped `meetingActive` but never called
`writeVoiceModeSentinel()`. So a voice-triggered switch_mode kept
the sentinel stale, and a subsequent menu click that wrote
`voice-mode.request="meeting"` hit the `meetingActive === want`
early-return in `applyModeRequest` — request consumed without
writing the sentinel. Sutando.app's pollVoiceMode kept reading
"active" → menu radio stuck.
(2) The startup `writeVoiceModeSentinel()` call ran BEFORE the Zoom
auto-detect, so even when Zoom was detected as in-meeting and
`meetingActive` was set to true, the sentinel had already been
written as "active" with no second write.
Fix: call `writeVoiceModeSentinel()` inside `switchModeTool.execute`,
and move the startup call to after the Zoom auto-detect block.
Verified live: kickstart → sentinel "meeting" matches in-memory state
(Zoom-running case); write voice-mode.request="meeting" → sentinel
flips, log line "External request applied: mode=meeting" fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the switch_mode pattern. Without this, "presenter mode on" / "the talk starts" routes to the work tool, producing "working on it" instead of silently flipping the mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #505 (dup-name guard) inadvertently removed loadSkillManifestTools() along with the personalTools spread into inlineTools. Voice-agent had no way to register highlight_slide / presenter_mode tools from skills/personal-iclr-highlight/, so the autonav cue silently no-op'd during the ICLR talk rehearsal. Restored verbatim from 9b545c2 (the original local-ships commit). Smoke- tested: voice-agent boots with "[skill-loader] loaded 3 tool(s) from iclr-highlight" — highlightSlideTool, presenterModeTool, fullscreenTool (renamed to fullscreen_presenter to dodge the dup-name guard). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…llscreen Two pre-talk hardenings observed during ICLR rehearsal: 1. recording-tools.ts: screen_record description now explicitly rejects "fullscreen" / "full screen" / "play fullscreen" cues. Chi asked to play the cross-owner video fullscreen and Sutando self-fired screen_record (06:34:46) right after fullscreen_presenter + play_video — STT or model association from the word "screen" was matching screen_record. 2. voice-agent.ts: restored FILLERS ARE NOT REQUESTS rule (originally on the unmerged 9b545c2 local-ships branch). Short utterances "hmm" / "um" / "ok" / "[BLANK_AUDIO]" are not instructions — Sutando should stay silent or ack, not call work and say "queued up". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
left a comment
There was a problem hiding this comment.
Codex sandbox re-audit — all 4 P1 findings from Mini's earlier review (06:21 PT) now addressed in commits e4d3bd5 / f309b5e / d12aee8:
- ✅ web-client.ts mode-badge / presenter-badge mismatch — composite 3-mode radio in main.swift replaces the single badge;
presenterMenuItemkept as back-compat alias (main.swift line 38-52, 174-184). - ✅ presenter_mode tool elevation —
presenterModeActivestate +setPresenter(on:)action wired (commit f309b5e); voice-agent prompt addresses presenter_mode as direct tool trigger. - ✅ voice-mode sentinel write on switch_mode —
writeVoiceModeSentinel()added at voice-agent.ts:346, called insideswitchModeTool.executeso menu clicks no longer hit the early-return path (commit e4d3bd5). - ✅ setup-time menu init — three new menu items (Mode: Active / Meeting / Presenter) added inside the menu-build block at main.swift:188-204, after
applicationDidFinishLaunching's separator. Composite update viaupdateModeMenuItem().
Post-merge sanity: presenterModeActive-driven setPresenter(on:false) early-returns are non-destructive (lines 88, 93). Avatar image selection (line 236) honors the composite state. CI green on cd8c5b3.
LGTM from the codex-sandbox angle. Owner approval still required before merge.
…icator-recovered-v2
Pulls in the bodhi #14 silent-context-injection fix on Gemini reconnect. Combined with PR #515 (merged into main, now in this branch via merge), this covers both reconnect paths: - Gemini-internal reconnect: silent injection drops "Say I'm back" prompt - Client reconnect <60s: getSecondsSinceLastTurn suppresses "Welcome back" Voice-agent verified: skill-loader still fires 3 tools on boot (PID 23502). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
left a comment
There was a problem hiding this comment.
Re-review after main-merge + bodhi bump (HEAD f9a4be4):
✅ Verified additions on top of prior LGTM:
7370f8e9— Mergeorigin/maincleanly (pulls ingetSecondsSinceLastTurnfrom PR #515 + tests). No conflict.f9a4be43—package-lock.jsonbodhi-realtime-agent git-ref bump from4d1592eb→ee58489a(PR #14 merge commit). Version stays 0.1.5; only the resolved-from sha changes. Verified PR #14's silent-context-injection fix is indist/index.jsper MacBook's report.
CI: tsc + tests (clean install) → SUCCESS. MERGEABLE.
Status: all 4 P1 findings from Mini's earlier codex review remain addressed (badge ID composite radio, presenter_mode tool, sentinel write, setup-time menu init). Plus welcome-back patches now baked in via the bodhi pin. Talk-day stack is consistent.
LGTM from codex-sandbox angle. Owner approval needed for merge.
…ged)" This reverts commit f9a4be4.
…rompts Closes the gap that surfaced during talk rehearsal: voice-agent restart loses co-presenter mode anchor. Even though :7877/presenter still says active=true, the model gets the default Sutando system prompt and defaults to "Echo Act IV" generic greeting, routing slide-topic phrases to work instead of highlight_slide+narrate. Adds getPresenterStateMarker() that synchronously curls the highlight server and returns "[System: PRESENTER MODE IS CURRENTLY ACTIVE — apply the CO-PRESENTER protocol...]" when active. Appended to both greeting paths: - Fresh connect (line 484): generic greeting + presenter marker - Reconnect with history (line 453): reconnect prompt + presenter marker Failure-silent — if curl fails the marker is empty string, so non-talk sessions and missing iclr-highlight skill are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reconnect path was still emitting "Welcome back" when presenter is on, which breaks the co-presenter flow mid-talk. Reuses the existing quick-reconnect silent-reconnect hint when presenterActive is true, keyed off getPresenterStateMarker() returning non-empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…esenter marker Previous a284190 + 5eb09af put the presenter-state marker in the GREETING string. Gemini Live treats the greeting as a user-style turn — the model called get_core_status to verify the "claim" instead of trusting it, and answered "presenter mode is not currently active" despite :7877/presenter being active. System instructions are authoritative. This commit: 1. Converts mainAgent.instructions from a static joined string to a factory function `() => [...].join('\n')`. Each session.start() now re-evaluates the array, picking up live state. 2. Adds getPresenterStateMarker() as the FIRST slot in the array — so the system prompt opens with "[System: PRESENTER MODE IS CURRENTLY ACTIVE...]" when the iclr-highlight server says active=true. Failure-silent — if the curl fails, the marker is empty string and the slot drops out of the join cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with the system_instruction presenter marker (374cebf): with the marker now authoritative + sent on every session.start, the new bodhi's silent context injection should be safe — the per-cue gating in voice- context.txt is enforced from the system_instruction, not relying on the flat history replay to anchor model behavior. Trade-off in case of regression: revert this commit, voice-agent runs on old bodhi 4d1592eb (welcome-back fires, but no concatenation risk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Restores the 3-mode (active / meeting / presenter) Sutando.app + web-UI indicator that was lost when
git reset --hard HEAD~2ondev/apr-21-local-shipsat 2026-04-23 06:22 PT discarded uncommitted working-tree edits alongside two sync-memory commits.Recovered by replaying 15 Edit calls extracted from session
6eb8f10ejsonl (2026-04-21 23:18-23:54 PT), applied on top ofdev/apr-21-local-ships's presenter-dot indicator base. Clean 15/15 apply.Supersedes #518 (the simpler text-only version that was the fallback).
What ships
Sutando.app (
src/Sutando/main.swift):state/voice-mode.request; Presenter POSTs to:7877/presenter/onpollPresenterMode()+pollVoiceMode()on a single timerupdateModeMenuItem()recomposes radio on either signal change. Priority: presenter > meeting > activeVoice-agent (
src/voice-agent.ts):switch_modetool writesstate/voice-mode.txtfor cross-process exposureapplyModeRequest()pollsstate/voice-mode.requestevery 1s and applies mode flips written by Sutando.app's menu clicksWeb client (
src/web-client.ts):/sse-statusgainsmodefield (sync read ofvoice-mode.txt+ cached presenter-active)Recovery notes
Full extraction at
notes/3-mode-recovered-edits-2026-04-21.md(666 lines). Original work was uncommitted-and-discarded by reset --hard; this is the full restoration.Lessons saved to memory:
feedback_commit_uncommitted_work_within_hour.md(commit-and-push within the hour for any architectural Swift/TS work; reset --hard discards working tree).Test plan
swiftc -typecheck main.swiftclean (only pre-existingdropFileunused warning)npx tsc --noEmitclean🤖 Generated with Claude Code