Skip to content

feat(mode-indicator): full 3-mode UI recovered from Apr 21 jsonl#519

Closed
sonichi wants to merge 12 commits intomainfrom
feat/sutando-mode-indicator-recovered-v2
Closed

feat(mode-indicator): full 3-mode UI recovered from Apr 21 jsonl#519
sonichi wants to merge 12 commits intomainfrom
feat/sutando-mode-indicator-recovered-v2

Conversation

@sonichi
Copy link
Copy Markdown
Owner

@sonichi sonichi commented Apr 25, 2026

Summary

Restores the 3-mode (active / meeting / presenter) Sutando.app + web-UI indicator 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 extracted from session 6eb8f10e jsonl (2026-04-21 23:18-23:54 PT), applied on top of dev/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):

  • Three-mode radio in menu dropdown: "Mode: Active" / "Mode: Meeting" / "Mode: Presenter" with ● indicating the current composite
  • Click any item to switch: Active/Meeting write state/voice-mode.request; Presenter POSTs to :7877/presenter/on
  • Avatar badge: purple dot for presenter, amber dot for meeting (composes with existing avatar)
  • 1s polls: pollPresenterMode() + pollVoiceMode() on a single timer
  • updateModeMenuItem() recomposes radio on either signal change. Priority: presenter > meeting > active

Voice-agent (src/voice-agent.ts):

  • switch_mode tool writes state/voice-mode.txt for cross-process exposure
  • applyModeRequest() polls state/voice-mode.request every 1s and applies mode flips written by Sutando.app's menu clicks

Web client (src/web-client.ts):

  • /sse-status gains mode field (sync read of voice-mode.txt + cached presenter-active)
  • Top-bar mode pill: 3 CSS variants (dim / amber / purple-glow), polls /sse-status every 1.5s

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.swift clean (only pre-existing dropFile unused warning)
  • npx tsc --noEmit clean
  • Manual: switch_mode("meeting") via voice → menu radio shifts ● to "Mode: Meeting", web pill turns amber within 1.5s
  • Manual: click "Mode: Active" in menu → voice-agent picks up state/voice-mode.request within 1s, switches mode
  • Manual: presenter_mode("on") → menu radio shifts ● to "Mode: Presenter", avatar gains purple dot

🤖 Generated with Claude Code

sonichi and others added 5 commits April 25, 2026 00:34
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>
Copy link
Copy Markdown
Owner Author

@sonichi sonichi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex sandbox re-audit — all 4 P1 findings from Mini's earlier review (06:21 PT) now addressed in commits e4d3bd5 / f309b5e / d12aee8:

  1. web-client.ts mode-badge / presenter-badge mismatch — composite 3-mode radio in main.swift replaces the single badge; presenterMenuItem kept as back-compat alias (main.swift line 38-52, 174-184).
  2. presenter_mode tool elevationpresenterModeActive state + setPresenter(on:) action wired (commit f309b5e); voice-agent prompt addresses presenter_mode as direct tool trigger.
  3. voice-mode sentinel write on switch_modewriteVoiceModeSentinel() added at voice-agent.ts:346, called inside switchModeTool.execute so menu clicks no longer hit the early-return path (commit e4d3bd5).
  4. 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 via updateModeMenuItem().

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.

sonichi and others added 2 commits April 25, 2026 09:19
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>
Copy link
Copy Markdown
Owner Author

@sonichi sonichi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review after main-merge + bodhi bump (HEAD f9a4be4):

Verified additions on top of prior LGTM:

  1. 7370f8e9 — Merge origin/main cleanly (pulls in getSecondsSinceLastTurn from PR #515 + tests). No conflict.
  2. f9a4be43package-lock.json bodhi-realtime-agent git-ref bump from 4d1592ebee58489a (PR #14 merge commit). Version stays 0.1.5; only the resolved-from sha changes. Verified PR #14's silent-context-injection fix is in dist/index.js per 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.

sonichi and others added 5 commits April 25, 2026 10:08
…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>
@sonichi
Copy link
Copy Markdown
Owner Author

sonichi commented Apr 26, 2026

Subsumed by #520 (which branched from this stack and squash-merged the full 13-commit set into main as commit 31ff373). All commits landed: mode-indicator UI, presenter-mode marker injection, bodhi re-bump, Welcome-back suppression, QuickTime fullscreen fix.

@sonichi sonichi closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant