Skip to content

feat(live-voice): dock the call bar as a global desktop widget - #1271

Merged
vastsa merged 5 commits into
mainfrom
feat/live-voice-floating-bar
Oct 1, 2026
Merged

vastsa merged 5 commits into
mainfrom
feat/live-voice-floating-bar

Conversation

@vastsa

@vastsa vastsa commented Oct 1, 2026

Copy link
Copy Markdown
Owner

What

The Live Voice compact call bar becomes a docked desktop widget: its own frameless, transparent, always-on-top window that floats above other applications and can be dragged anywhere on the desktop. The main window no longer draws the bar.

  • Widget window (main process): placement is remembered in live-voice-widget.json and clamped to the work area; the window is sized to the box the bar itself reports; the status row and its controls stay on one row (a folded status line would read as two rows of chrome, and a wrapped line would shrink the window into wrapping again); it joins every Space and never transforms the process type (ADR 0086).
  • The widget is a view, never an owner: every press is forwarded to the main window — which keeps the microphone, the media and the call-scoped work — and the resulting state returns through the same authoritative view the owner receives. A failure only the owner frame can observe (a refused mute, a playback retry that failed) is reported back so the bar names it in place next to its verbatim LIVE_* code.
  • Main window: draws no call bar and stays the frame that runs the actions; it keeps the details surface, which the widget's Details action opens after bringing that window forward.
  • Copy: the preparation panel drops its explanatory prose (only a blocking cause is stated) and the two catalog entries it no longer renders are removed from all nine locales.
  • Widget language: the widget surface does not boot the settings store, so it applies the persisted settings.language itself when it becomes visible.
  • IPC: voice/live/widget/visibility, voice/live/widget/action, voice/live/widget/issue, voice/live/event/widgetState, voice/live/event/widgetAction. The widget's channels are validated as that window and never enter owner derivation; every owner-validated channel still refuses another window with PERMISSION_DENIED.
  • Docs: ADR 0314 plus EN and zh-CN updates for the IPC protocol table, the component spec, the live-voice spec and the e2e plan.

Verification

  • Checks: apps/desktop typecheck, biome, style-token guard, check-architecture, check-agent-policy-sync, docs:check (83 EN/zh pairs, 542 pages).
  • Unit tests: apps/desktop 3323/3323 · packages/shared 1156/1156 · packages/i18n 28/28.
  • Live Voice mounted interaction fixture: 49/49, including the new guards — one-row status at a 240px window with the reported width ≥ the width it needs, widget presses travelling to the owner frame as actions, and the widget following the configured language.
  • pnpm test:e2e:live-voice: 5/5 journeys, run repeatedly against the real app, driving both CDP targets (main window and widget window).
  • Pr-base: the branch already contains the latest origin/main (7e775e6c9).

Notes for review

  • The widget window is created with the first call and kept warm while hidden, so the bar appears without a load delay mid-call.
  • The failure line inside the bar remains a wrapping sentence (it is a longer localized text plus a code); the status row is the part pinned to one row.

vastsa added 2 commits October 1, 2026 10:08
The compact call bar was AppShell chrome, so it disappeared whenever the app was
not the foreground window. It is now its own frameless, transparent,
always-on-top window that floats above other applications and can be dragged
anywhere on the desktop.

- main owns the widget window: placement is remembered and clamped to the work
  area, the window is sized to the box the bar reports, and the status row plus
  its controls stay on one row
- the widget is a view, never an owner: every press is forwarded to the main
  window, which keeps the microphone, media and call-scoped work, and a failure
  only that frame can observe is reported back so the bar names it in place
- the main window draws no call bar and keeps the details surface, opened by the
  widget's Details action
- the preparation panel carries no explanatory prose; only a blocking cause is
  stated
- ipc: widget/visibility, widget/action, widget/issue, event/widgetState,
  event/widgetAction; ADR 0314 plus EN/zh spec updates and e2e coverage
Copilot AI balanced review requested due to automatic review settings October 1, 2026 03:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Widget action forwarding loses playback user activation, and initial widget state delivery has a subscription race.

Review effort: Balanced
Findings: 2 Medium severity · 2 Low severity

Open (4)
What changed in this PR

Moves Live Voice call controls into a persistent, draggable desktop widget while retaining call ownership in the main renderer.

Changes:

  • Adds the widget window, IPC bridge, geometry persistence, and renderer UI.
  • Updates interaction/E2E coverage and localized preparation copy.
  • Documents the architecture and user experience in specs and ADR 0314.
File Description
scripts/​e2e/​live-voice-desktop.mjs Supports separate main/widget CDP targets.
scripts/​e2e-live-voice.mjs Drives call controls through the widget.
packages/​shared/​src/​types/​live-voice.ts Defines widget actions.
packages/​shared/​src/​protocol.ts Adds widget IPC channels.
packages/​i18n/​test/​catalogs.test.mjs Updates catalog expectations.
packages/​i18n/​src/​locales/​zh-TW/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​zh-CN/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​tr/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​pt-BR/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​ko/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​fr/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​es/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​en/​index.ts Removes obsolete preparation copy.
packages/​i18n/​src/​locales/​de/​index.ts Removes obsolete preparation copy.
docs/​zh-CN/​spec/​06-delivery/​04-e2e-test-plan.md Documents widget E2E coverage.
docs/​zh-CN/​spec/​04-ux/​08-component-spec.md Specifies widget UX.
docs/​zh-CN/​spec/​03-runtime/​live-voice.md Documents widget ownership.
docs/​zh-CN/​spec/​03-runtime/​01-ipc-protocol.md Documents widget IPC.
docs/​spec/​06-delivery/​04-e2e-test-plan.md Documents widget E2E coverage.
docs/​spec/​04-ux/​08-component-spec.md Specifies widget UX.
docs/​spec/​03-runtime/​live-voice.md Documents widget ownership.
docs/​spec/​03-runtime/​01-ipc-protocol.md Documents widget IPC.
docs/​adr/​README.md Registers ADR 0314.
docs/​adr/​0314-live-voice-docked-widget.md Records the widget architecture.
apps/​desktop/​test/​live-voice-widget.test.mjs Tests geometry and payload parsing.
apps/​desktop/​test/​live-voice-controls.test.mjs Updates call-bar presentation tests.
apps/​desktop/​test/​fixtures/​live-voice-interaction.jsx Simulates widget IPC interactions.
apps/​desktop/​test/​fixtures/​live-voice-interaction-runner.cjs Exercises widget behavior.
apps/​desktop/​src/​styles/​voice.css Repositions the details anchor.
apps/​desktop/​src/​styles/​live-voice-widget.css Styles the desktop widget.
apps/​desktop/​src/​main.tsx Dispatches the widget renderer surface.
apps/​desktop/​src/​features/​voice/​live/​LiveVoiceWidget.tsx Implements widget rendering and actions.
apps/​desktop/​src/​features/​voice/​live/​LiveVoiceStatusHost.tsx Executes forwarded actions in the owner.
apps/​desktop/​src/​features/​voice/​live/​LiveVoicePreparation.tsx Simplifies preparation copy.
apps/​desktop/​src/​features/​voice/​live/​LiveVoiceDetails.tsx Generalizes the details anchor type.
apps/​desktop/​src/​features/​voice/​live/​LiveVoiceCallBar.tsx Makes failures inline widget chrome.
apps/​desktop/​src/​features/​voice/​live/​live-voice-presentation.ts Derives widget snapshots and visibility.
apps/​desktop/​src/​features/​voice/​live/​live-voice-api.ts Exposes widget IPC helpers.
apps/​desktop/​electron/​main/​live-voice/​widget-window.ts Manages the native widget window.
apps/​desktop/​electron/​main/​live-voice/​widget-geometry.ts Clamps widget size and placement.
apps/​desktop/​electron/​main/​live-voice/​runtime.ts Publishes call views to the widget.
apps/​desktop/​electron/​main/​ipc/​register.ts Wires widget IPC dependencies.
apps/​desktop/​electron/​main/​ipc/​live-voice-ipc.ts Validates widget IPC requests.
apps/​desktop/​electron/​main/​index.ts Creates and disposes the widget.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +186 to +188
created.webContents.on("did-finish-load", () => {
loaded = true;
sendView();
if (!main.isVisible()) main.show();
main.focus();
}
main.webContents.send(IPC.event.liveVoiceWidgetAction, { action });
Comment on lines +271 to +272
await check("failed playback retry is visible without opening details",
`s.snapshot.errorCode === 'LIVE_PLAYBACK_FAILED' && document.body.textContent.includes('LIVE_PLAYBACK_FAILED') && ui.all('[role=status]').length > 0 && ${button("Resume sound")} && ${noDialog} && s.counts.play >= 2`);
"an unmeasurable box falls back to the default");
});

test("only the widget's own actions and box are accepted on its channels", async (t) => {
vastsa added 3 commits October 1, 2026 12:51
The compact call bar was AppShell chrome, so it disappeared whenever the app was
not the foreground window. It is now its own frameless, transparent,
always-on-top window that floats above other applications and can be dragged
anywhere on the desktop.

- main owns the widget window: placement is remembered and clamped to the work
  area, the window is sized to the box the bar reports, and the status row plus
  its controls stay on one row
- the widget is a view, never an owner: every press is forwarded to the main
  window, which keeps the microphone, media and call-scoped work, and a failure
  only that frame can observe is reported back so the bar names it in place
- the main window draws no call bar and keeps the details surface, opened by the
  widget's Details action
- the preparation panel carries no explanatory prose; only a blocking cause is
  stated
- ipc: widget/visibility, widget/action, widget/issue, event/widgetState,
  event/widgetAction; ADR 0314 plus EN/zh spec updates and e2e coverage
Replay the widget snapshot after renderer subscription and hand playback recovery to an owner-frame gesture so the widget cannot lose initial state or user activation. Add sender authorization coverage and exercise the owner-frame retry path without duplicating assertions.
@vastsa
vastsa merged commit 27fda48 into main Oct 1, 2026
2 of 4 checks passed
@vastsa
vastsa deleted the feat/live-voice-floating-bar branch October 6, 2026 16:56
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.

2 participants