Track 3 follow-up: rework the notch into a voice-first dynamic island#10332
Track 3 follow-up: rework the notch into a voice-first dynamic island#10332katipally wants to merge 48 commits into
Conversation
…orner radii Top corners curve inward into the bezel, bottom corners flare out; both radii animate via AnimatablePair so the shape morphs between closed (6/14) and open (20/26).
…math Hardware measurement (auxiliary top areas + safe-area inset) extracted as injectable pure functions; open/close/tab springs live in one place.
…ontent open > listening > thinking > hint > notification > idle, as a pure derive function with the priority order pinned by tests.
Fixed window sized to the largest any presentation needs; measured chat height (4pt jitter filter upstream) clamps to half the screen and persists across opens. Hover dwell and grace use injected clocks so the tests never sleep.
Non-activating panel at the assistive-tech level (clears notch-companion and overlay apps, covers the menu bar), menu-tracking level drop so the notch cannot occlude its own menus, yields to system permission dialogs, and takes the keyboard only while expanded.
Settings gear and notification click-through activate the main window and post navigation only once its receivers are mounted (no fixed delays).
The notch chat is ChatMessagesView over ChatProvider.mainInstance (one transcript, INV-CHAT-1); the shared view gains an optional content height callback that drives the panel growth, and a live voice strip renders the in-flight turn until the journaled pair lands.
…aveform One glass slot below the body: the composer (paperclip picker + drop, 1-3 line field, Return sends / Shift+Return newlines), a pulsing Stop pill while responding, and a live waveform pill with the in-flight transcript while push-to-talk records. Liquid Glass on macOS 26 with a HUD-blur fallback at the 14.0 floor.
Lists running/recent agent sessions; a row opens that agent's conversation rendered by the same shared ChatMessagesView.
Same click-through/Execute/dismiss affordances; queueing and journal admission stay in the manager.
…imelines The black island is its own NotchShape layer whose frame always interpolates (identity can never break), content crossfades inside it. Discrete morphs ride the open/close/tab springs keyed on the presentation; streaming growth rides an isolated smooth timeline so the measure loop cannot oscillate. Closed chrome keeps the omi mark (with recording dot) and gear hugging the camera; open header tabs flank the camera void; voice answers auto-open the panel under the mouse.
One panel per display keyed by CGDirectDisplayID with debounced rebuilds; pointer monitors install only while a panel is open. The close zone is body-plus-tray generously inset, held while the pointer aims at it (velocity cone), with click-away, Esc, post-open grace, and a long outside dwell; guards for responding/voice/drag are honored.
setup() starts NotchScreenManager (the legacy window is no longer created; its code stays dormant in-tree for a follow-up removal). The query pipeline (openAIInputWithQuery, routeQuery, sendAIQuery, sendVoiceOnlyQuery, follow-ups, busy/journal handling), notification queue/present/dismiss, show/hide/snooze, toggles, agent timeline opens, recording indicator feed, and the automation surface all run on notchState and the screen manager. The Ask-Omi hotkey opens notch chat. Facade signatures unchanged for all external callers.
Hub voice turns journal only at turn end, so the bar state mirrors the finalized transcript and the streaming reply purely for display; the notch chat renders question-above-reply mid-turn and the journaled pair replaces the strip without duplication. A new hold clears the previous mirror. PTT blocked by the free-tier limit flashes a visible notch hint instead of silently no-oping.
Re-pins moved contracts (dot-ring mark extraction, drill-in clearing, canonical-session seam on AgentPillsManager) and drops asserts for strings absent on main.
…urface sendMainDraft cleared draftText only at journal-acceptance time. When a send creates a session mid-turn the active draft key drifts, so the sent text stayed persisted under the original key and reloaded into the composer once the context returned to it. Clear the draft up front (writing the empty value under the key it was typed against) and restore it only if the send never enters the timeline (usage limit, offline, busy).
…ntrol Clear the composer field in the same run loop as the tap so it repaints immediately (TextField(axis:.vertical) does not reliably reflect an async external clear during focus). Add a stop button to the listening pill that cancels the in-flight push-to-talk turn.
The partial transcript was suppressed on the bar by the noise policy, so the pill only ever read "Listening…". Surface the growing partial into the pill's ephemeral live text so the user sees their words as they speak; the settled final still governs the committed chat transcript.
The spawn card exposed only a 12pt corner link-out icon and no body tap, so it read as non-interactive. Make the entire card open the agent (the icon stays as an affordance); a spawn card has no competing tap action, so this is safe on every surface that renders it.
…d path ask_main_chat drives sendMessage directly and never exercised the composer's sendMainDraft path, so the draft-clear/restore behavior had no automation coverage. Add a non-prod send_main_draft action that drives the exact composer path so the notch/main text send can be verified end to end.
…nches NotchScreenManager.makePanel ordered every panel front the moment it was created, so the notch appeared on launch even when the user had disabled or snoozed the floating bar (the old window path created its window without ordering it front). Panels are now created hidden; visibility flows through applyPanelVisibility, which orders a panel on screen when the notch is passively visible OR that panel is explicitly open. An Ask Omi summon still surfaces the panel while the bar is disabled/snoozed and hides it again on close, matching the pre-notch "show temporarily, hide on close" behavior. Verification: - Hermetic: NotchScreenManagerVisibilityTests asserts real NSWindow.isVisible — panels start hidden, showAll reveals one per display, hideAll hides, and an explicit open surfaces while passively hidden then re-hides on close. - Live (named bundle, Quartz on-screen window list): enabled launch shows 2 notch windows; disabled launch shows 0; open_ask_omi while disabled surfaces one, close_ask_omi hides it again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p stale paths
FloatingControlBarManager.window is never assigned in the notch architecture,
so every window-guarded path silently no-oped. closeAskOmiForAutomation
(bridge action close_ask_omi) always returned floating_bar_window_unavailable;
it now drives notchScreenManager (closeAll + wait), mirroring open_ask_omi.
Removed the dead window field and the paths that only reached it:
- deliverAgentArtifactCompletionToFloatingSurface (completions already surface
via provider.projectJournalTurn into the shared notch timeline, INV-6)
- the orphaned openRecentNotificationConversation / openNotificationConversation
/ archiveVisibleConversationIfNeeded island (live path is openNotificationAsChat)
- the vestigial resizeForPTT wrapper (notch PTT sizing is owned by the
presentation ladder) and its now-dead caller
- write-only mostRecentNotificationKey and the unused recentNotificationReuseInterval
The legacy FloatingControlBarWindow class itself is left for a separate follow-up
to keep this diff reviewable.
Verification:
- Live (named bundle): close_ask_omi returns {"askOmiOpen":"false"} instead of an
error, in both enabled and disabled states.
- AgentPillLifecycleTests updated to pin the real completion delivery
(projectJournalTurn, no window path) and archiving on notchState.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
capture_floating_bar_png only found the legacy FloatingControlBarWindow, which is never created in the notch build, so it always returned no_floating_bar_window. Fall back to the visible NotchWindow so the in-process capture works. Also add an optional text param to debug_bar_state so the live transcript / reply can be exercised without a mic during visual verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworks the notch push-to-talk turn into a Dynamic-Island-style voice surface. The same 8 Omi marks render as one persistent element across the whole turn and morph in place: an audio waveform while listening (mic level) and while Omi speaks (system output level), reforming into the rotating dot-ring while thinking. The island grows in height as the live transcript / streaming reply wraps to new lines; the width stays fixed, so it grows out of the notch instead of popping wide. Content is centered. - New .responding presentation state (drives the streamed reply); listening and responding are expanded surfaces, thinking is the compact pill between. - Voice height rides the isolated smooth timeline (voiceBodyHeight), reset between turns; capped at half the screen. - Tapping the reply opens the main app window (text chat lives in the app). - Hover no longer opens anything; the closed notch keeps the inert Omi mark and the settings gear only. The Ask Omi hotkey opens the app window. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add .responding to the ladder tests (thinking beats responding while awaiting the answer; responding beats hint/notification; listening and responding are expanded surfaces). Cover the voice dynamic-height clamp, its transience, and the .responding size mapping. Replace the retired hover-dwell tests with one asserting hover never opens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give listening/responding a bit more width and narrow the thinking pill so the island visibly contracts into "thinking" and expands back (the width rides the same morph spring). Align the orb ring to NotchOmiMark's exact ratios so the rotating ring reads as the Omi logo dots, the same dots that stretch into the waveform. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Named test bundles hit the free-tier usage limit, which blocks PTT before a turn can start. Add a non-production debugBypassLimit override and a reset_usage_limit bridge action so a real spoken PTT turn can be exercised on a named bundle. Guarded to non-production; production stays enforced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The orb's speaking wave read AudioLevelMonitor.systemLevel, which tracks system-audio capture, not the TTS output, so it barely moved while Omi spoke. Compute the RMS of each streamed PCM chunk in StreamingPCMPlayer and publish it as a dedicated playbackLevel; the orb's speaking mode now reacts to the actual spoken response. Listening still reacts to the mic. Also ease the ring<->wave morph a touch so the logo visibly stretches into the waveform and back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thinking no longer shrinks narrower than the camera module (it now matches the closed-notch width), so it reads as a deliberate compact pill rather than an undersized box. The responding state drops the "Thinking…" placeholder — the reply streams as Omi speaks, and the speaking orb carries the brief gap before the first text token instead of relabeling as thinking. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The finished reply now lingers on screen for a few seconds after the turn ends instead of vanishing the instant Omi stops speaking. Hovering the notch pauses the dismissal so you can read it; leaving reschedules it. The orb rests as the Omi logo (equal dots, no spin) while it lingers. - Every voice-state label (transcript and reply) gets a slow light-sweep shimmer, off under reduced motion. - The rotating "thinking" ring reads as the Omi logo: equal dots at rest, only the spinner carries a leading trail. Bigger ring sized off height. - Waveform toned down (calmer amplitude) for both mic and TTS. - Narrower voice states, floored at the camera-module width. - The closed-notch Omi mark opens the main window again (was inert); the gear still opens settings. Expanded voice surfaces cast the depth shadow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests for beginResponseLinger dismissing after the hold, hover pausing and resuming the dismiss, and an empty reply never lingering. Uses the injected manual sleeper (no wall-clock waits). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Centralize the existing start earcon (previously inlined in both PTT start paths) into PTTCue and add a soft end cue that plays when a turn completes with a delivered answer (reason == .success), not on cancel, barge-in, or error. Reuses the same pttSoundsEnabled setting the start cue always used. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- A reply longer than the half-screen cap now scrolls within the capped panel instead of clipping; short replies still size to fit. - Esc dismisses a lingering reply. The notch is non-activating, so a panel-key handler can't see Esc without stealing focus from the active app; instead an Esc monitor lives only while a reply lingers (zero idle cost, removed the moment it clears). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Cap the voice panel at 30% of the screen (was 50%) — it's verbal and shouldn't dominate; longer replies scroll within the cap. - While the transcript or reply is actively growing, auto-scroll so the newest line stays visible (you always see the words as Omi speaks them); once the reply settles it scrolls back to the top. - A subtle "Open in Omi" hint fades in once the reply lingers (kept clean while streaming); tapping the reply or the hint opens the full conversation in the main window. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Omi's reply now reveals a word at a time at a natural speaking cadence (~3 words/sec) instead of dumping tokens as fast as the model streams them, so the words track the spoken voice. Snaps to the full text once the turn stops streaming. - Voice text is justified when it wraps to multiple lines (clean, flush edges) and centered when it fits one line (a single justified line would pin left). SwiftUI Text can't justify, so this wraps an AppKit label that reports its wrapped height to the existing measure loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reply lingered via state set in an onChange that ran after a render, so for one frame nothing was lingering and the notch collapsed to idle before re-expanding — a visible flicker when a response finished. Drive the linger from heldReply, captured while the response streams, so isLingeringReply is already true the instant the response goes inactive: the notch stays expanded and the reply settles smoothly, no collapse-and-readjust. Also give the voice content room to breathe: more horizontal padding and a larger gap between the orb and the text so it isn't pressed against the border. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reveal snapped to the full text the instant streaming stopped, and the reply's text source switched from the live mirror to the held copy at the same moment — together that read as a reload/re-adjust when a response finished. Now the reply reveal always paces toward one source (the held reply, captured live) and simply *finishes* in place instead of snapping, and the scroll follows the reveal by height (no jump to top). The streaming-to-final transition is seamless. Listening now shows a "Listening…" caption with the live transcript beneath it, faded and shimmer-free, so you can check we heard you right without mistaking it for Omi's reply. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The live transcript was centered, so it re-centered (jumped) on every new word. Left-align it so it flows naturally as you speak. - The reply reveal lagged the audio: bump it to ~4 words/sec and show the opening word immediately (no first-word startup delay), so the words keep pace with Omi's voice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Center-align the listening transcript (and its caption) rather than left-aligning it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cript - The panel flickered when a new line was added because the auto-scroll fired while the panel was still growing to fit. Only auto-follow once the content actually overflows the cap; below it the panel just grows, so a new line reveals smoothly with no jump. - Raise the voice cap from 30% to 40% of the screen (30% felt cramped). - The STT provider doesn't reliably emit partial transcripts during the hold, so the live transcript showed inconsistently. Drop it: listening shows a clean "Listening…", and the words are revealed in the response phase. (Kept the plumbing; can re-enable per-provider later.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Raise DesktopAutomationBridge (non-prod voice verification actions + notch capture fallback) with justification; ratchet PushToTalkManager down after the PTTCue refactor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the new voice sources (orb, text, view, PTTCue, AudioLevelMonitor) to the floating-bar-functional e2e flow's covers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the detailed follow-up and demo. I did a static maintainer pass on this head. This is a cohesive direction around the macOS notch/voice surface, and the implementation is thoughtfully structured: the new notch presentation/view-model split is much easier to reason about than embedding all of the states in the legacy floating-bar window, and the added pure tests around geometry, presentation ladder, visibility, and reply reveal are good coverage for the pieces that can be tested without a full UI run. I’m not going to formally approve this from automation, though. This changes a core desktop interaction model: the notch becomes voice-first, text entry moves back into the main app, the panel now owns per-display high-level always-on windows, and non-production automation/debug bridge behavior is extended for this flow. Those are product/UX and desktop-behavior decisions that need a human maintainer to try on-device and sign off before merge. Maintainer review should especially validate:
No security/supply-chain issue stood out in the static diff I reviewed. This looks like a serious and useful prototype/follow-up, but it needs human product and desktop-runtime validation before it should be merged. by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with |
This is a follow-up to my Track 3 notch redesign (#10247), taking it voice-first: hold push-to-talk and the notch becomes a spoken conversation instead of a chat, and text input moves back into the app.
Where this stands (for maintainers)
This is the verbal-first take on the notch, built on my #10247 redesign, so it carries that redesign plus the voice layer on top. #10247 is the chat-first version; this is the voice-first one, so you can try the two directions side by side. Every push-to-talk feature still works; what changed is that the notch now shows the voice turn instead of opening a chat. The new work here is the voice layer, the
Notch/NotchVoice*files, the morphing orb, the paced reveal and linger, and the audio-reactive waveforms.The idea
A voice turn has a shape of its own, you talk, it thinks, it talks back, and the notch can be that shape. Chat used the island as a scroll view; voice uses it as the turn itself.
So the Omi mark stops being a static logo and becomes the whole interaction. The same eight dots are a live mic waveform while you talk, reform into the rotating logo ring while Omi thinks, then a waveform that moves with Omi's own voice while it speaks. It never swaps one icon for another, it morphs in place, and the black island grows out of the camera housing to hold whatever the turn needs.
Demo (under 2 minutes)
t3_voice.mp4
What changed, and why it feels right
NotchOmiMarkratios, so at rest it is the logo.playbackLevelcomputed from the RMS of each streamed TTS chunk inStreamingPCMPlayer, so the bars move with Omi's voice, not a canned loop.What I dropped, on purpose
Everything else from the PTT lifecycle is untouched: hub and batch turns, voice playback, the Ask-Omi hotkey (now opens the app), the recording indicator, usage limiting, and the automation bridge.
Product invariants
liveVoiceAssistantTextmirror, never a second store; the paced reveal and the linger are presentation over that mirror; sends and journaling are unchanged.playbackLevel, and a non-prod usage-limit test bypass).VoiceTurnCoordinatorgains exactly one side effect, a soft end cue onterminal(.success).Failure-Class: none
The
fix:commits are UI-polish corrections inside this new feature (flicker, alignment, spacing, reveal pacing), not repairs of a shipped failure-class boundary.Testing
agent-logic-harness --swift-onlygreen (PTT lifecycle/state).NotchPresentationLadderTests(the responding state and ladder ordering),NotchViewModelTests(voice sizing, the 40% clamp, the linger lifecycle with an injected clock, hover pause/resume, Esc dismiss),ReplyRevealModelTests(whole-word paced reveal, completion, restart),FloatingBarUsageLimiterTests(the test bypass).swift-formatlint and SwiftLint clean. Line-count ratchet updated (bridge raise justified,PushToTalkManagerratcheted down after the cue refactor).omi-notch-voice) and captured frame by frame: closed chrome, the listening waveform, the rotating-logo thinking ring, the paced justified reply, the 40% cap and scroll, the lingering reply with the open-in-app hint, and the blocked-PTT hint. The demo above is a real hold-and-speak turn end to end.