Skip to content

Track 3 follow-up: rework the notch into a voice-first dynamic island#10332

Open
katipally wants to merge 48 commits into
BasedHardware:mainfrom
katipally:notch-voice-first
Open

Track 3 follow-up: rework the notch into a voice-first dynamic island#10332
katipally wants to merge 48 commits into
BasedHardware:mainfrom
katipally:notch-voice-first

Conversation

@katipally

@katipally katipally commented Jul 22, 2026

Copy link
Copy Markdown

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

  • One mark, three states, no cross-fade. The logo, the mic waveform, and the thinking spinner are the same eight dots morphing between a ring and a bar layout, rendered once as a persistent layer so a phase change animates the geometry instead of swapping views. The ring uses the exact NotchOmiMark ratios, so at rest it is the logo.
  • Waveforms that actually react. Listening reads the live mic level. Speaking reads a new playbackLevel computed from the RMS of each streamed TTS chunk in StreamingPCMPlayer, so the bars move with Omi's voice, not a canned loop.
  • The island grows, it doesn't pop. Width is fixed; only the height grows as the reply wraps, capped at 40% of the screen. Longer replies scroll and follow the newest line only once they overflow, so adding a line never jumps.
  • The reply reads at a speaking pace. Rather than dumping tokens as fast as the model streams them, the reply reveals a word at a time at roughly speaking speed and finishes in place, so the words track the voice and the stream-to-final handoff is seamless.
  • It waits for you. When Omi finishes, the reply lingers a few seconds so you can read it, resting as the Omi logo. Hover holds it, Esc dismisses it, and tapping it (or the "Open in Omi" hint that fades in) opens the full conversation in the main window, where text lives now.
  • Barge-in just works. Push-to-talk while Omi is speaking interrupts and starts a new turn; the notch flips straight back to listening. This was already in the realtime hub, the notch just reflects it now.
  • Quiet by default. A soft cue on turn start (already there) and a matching one when a turn completes. On the closed notch, hover does nothing, the mark opens the app, the gear opens settings.

What I dropped, on purpose

  • Chat in the notch. That's the point, text input moves back to the app. The chat views are left dormant in-tree so removing them can be its own small PR rather than noise here.
  • A live in-hold transcript. I tried showing your words as you speak, but the STT providers in use don't reliably emit partial transcripts during the hold, so it appeared inconsistently. Rather than ship something that works for some setups and blanks for others, listening is a clean "Listening…" and the words appear in the reply phase. The plumbing is still there to turn back on per-provider.

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

  • INV-CHAT-1: the notch stays an I/O surface over the one kernel transcript. The reply is the display-only liveVoiceAssistantText mirror, never a second store; the paced reveal and the linger are presentation over that mirror; sends and journaling are unchanged.
  • INV-VOICE-1: the PTT reducer and journal lifecycle are untouched. This is a presentation change plus display-only signals (playbackLevel, and a non-prod usage-limit test bypass). VoiceTurnCoordinator gains exactly one side effect, a soft end cue on terminal(.success).
  • INV-UI-1: no purple anywhere; white and neutral on black.
  • INV-AUTH-1 / INV-DATA-1: no session-ownership or routing changes.
  • INV-INT-1: no change to the integrations surface. The connector / memory-export files in the diff are carried unchanged from Track 3 submission: rework of notch floating bar into a chat-first dynamic island #10247; the voice layer adds nothing to them.

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

  • Full desktop Swift suite green (353 isolated suites). agent-logic-harness --swift-only green (PTT lifecycle/state).
  • New hermetic tests: 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-format lint and SwiftLint clean. Line-count ratchet updated (bridge raise justified, PushToTalkManager ratcheted down after the cue refactor).
  • Exercised on a named bundle (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.
  • All 19 preflight checks pass.

Review in cubic

katipally and others added 30 commits July 21, 2026 16:59
…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>
katipally and others added 18 commits July 22, 2026 03:44
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>
@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge feature-fit-review Needs review of product/feature direction with Omi mission/vision workflow-review Needs maintainer review for workflow, automation, hooks, or CI behavior labels Jul 22, 2026
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

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:

  • whether the voice-first notch direction is the intended product direction versus the chat-first version in the stacked/prior work;
  • multi-display, fullscreen/Spaces, permission-dialog, and third-party overlay behavior with the new NotchWindow level and per-display NotchScreenManager;
  • the debug bridge additions (reset_usage_limit, send_main_draft, notch capture fallback) remain strictly non-production and match the repo’s automation expectations;
  • a real macOS build/UI pass, since the only reported check on this head is the neutral Cubic reviewer and I could not exercise the AppKit/SwiftUI runtime from this environment.

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 need human response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-fit-review Needs review of product/feature direction with Omi mission/vision needs-maintainer-review Needs a human maintainer to sign off before merge workflow-review Needs maintainer review for workflow, automation, hooks, or CI behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants