fix(chat): harden voice toggle against first-paint race + SSE-reconnect drift - #37
Conversation
…ct drift
The operator reported the dashboard's chat-header voice toggle was not
working. The /api/voice/config API path is verified working end-to-end
via curl — the backend writes voice.json and master broadcasts the SSE
voice_config event correctly. The bug surface is in the client wiring.
Two latent races that could each present as "the toggle button does
nothing":
1. Click handler read `current` from window.__subctlVoiceEnabled, which
is populated by an async GET /api/voice/status fired at mount(). If
the user clicks before that fetch resolves (or if it failed), the
global is undefined — coerced to false — so the handler thinks voice
is off and sends `{enabled: true}`. When voice was actually already
on, this is a no-op from the server's POV and the operator sees no
visible change. Switch to reading the button's own DOM class
(`chat-toolbar-btn--active`) as the source of truth — the operator
can see it, and it can't get out of sync with itself.
2. refreshVoiceEnabled() (invoked on every SSE connect/reconnect) used
to update window.__subctlVoiceEnabled but did NOT re-render the
button. If voice.json changed while the dashboard was reconnecting
the SSE stream, the global would refresh but the visible button
label would stay stale. Now it also calls renderVoiceBtnState() so
the chat-header label stays in sync with master.
Defensive secondary: if the POST response shape ever drifts (no
`config.enabled` key), the handler now falls back to the value it
requested so the button still flips visually instead of silently
ignoring an apparently-successful toggle.
No behavior change for the happy path — same URLs, same payloads,
same response handling. Purely tightens the state-source ordering.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR fixes voice toggle state synchronization in the chat interface. The click handler now derives button state from its CSS class instead of relying on a potentially-stale global, the POST response handler trusts server-echoed config, and the refresh function now re-renders the button UI to maintain consistency across all state-update paths. ChangesVoice toggle state synchronization
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
…g + Hermes docs (#40) Cuts v3.3.4 bundling the three commits since v3.3.3: - #38 fix(claude-teams): PATH-shim collision + master→evy import leftovers - #37 fix(chat): voice toggle first-paint race + SSE reconnect drift - #39 chore: Hermes research docs + ORCHESTRATION log + .codegraph gitignore Per the version-cap doctrine, stays on the v3.3.x patch line — no v3.4 / v4 bump until operator decides. Supersedes the stale `fix/policy-snapshot-evy-rename` branch (commit 557b19f), which documented only the `_write_snapshot.ts` half of the broken-imports breakage. The shipped v3.3.4 fix covers both `.ts` import sites plus the six `bin/claude-*` shims. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
The operator reported the dashboard's chat-header voice toggle was not working. The
/api/voice/configAPI path is verified working end-to-end via curl — backend writes voice.json and master broadcasts the SSEvoice_configevent correctly. The bug is in the client wiring.Two latent races
1. First-paint race. The click handler read
currentfromwindow.__subctlVoiceEnabled, populated by an asyncGET /api/voice/statusfired atmount(). If the user clicked before that fetch resolved, the global was undefined → coerced tofalse. The handler thought voice was off and sent{enabled: true}— but when voice was actually already on, this is a no-op from the server's POV. Operator sees no visible change. Fix: read state from the button's own DOM class (chat-toolbar-btn--active). Operator-visible, can't get out of sync with itself.2. SSE-reconnect drift.
refreshVoiceEnabled()updatedwindow.__subctlVoiceEnabledbut did NOT re-render the button. If voice.json changed during a reconnect, the global refreshed but the label stayed stale. Fix: also callrenderVoiceBtnState().Defensive secondary: if the POST response shape ever drifts (no
config.enabledkey), handler falls back to the requested value — button still flips visually instead of silently ignoring an apparently-successful toggle.Diff
Single file: `dashboard/public/tabs/chat.js` (+25/-4). No behavior change for the happy path — same URLs, same payloads, same response handling. Purely tightens state-source ordering.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes