Skip to content

Track 3 submission: rework of notch floating bar into a chat-first dynamic island#10247

Open
katipally wants to merge 26 commits into
BasedHardware:mainfrom
katipally:notch-rework
Open

Track 3 submission: rework of notch floating bar into a chat-first dynamic island#10247
katipally wants to merge 26 commits into
BasedHardware:mainfrom
katipally:notch-rework

Conversation

@katipally

@katipally katipally commented Jul 21, 2026

Copy link
Copy Markdown

This is my submission for the Omi Summer challenge (Track 3 — Extra). I already submitted for Track 1 (Agents) #10007 #10088 . While I wait to hear back, I went looking for something in the Omi desktop app I could make meaningfully better, and the macOS notch stood out. The notch already ships; this PR improves its UI/UX so it is clearer to understand and smoother to use. Nothing about what the notch can do was thrown away. I reworked how it looks, animates, and flows.

Where this stands (for maintainers)

This is a first pass I'm putting up so you can actually try the direction. What's here is finished and works; it just isn't a complete, top-to-bottom redesign of the notch. I focused on the core interaction (hover-to-chat, the grow animation, and voice) and kept every existing feature working.

If you like where it's going, I'm happy to double down: finish the full notch design, get every animation buttery smooth and clean, and make sure every feature maps over perfectly. Tell me what you'd change or how far you want me to take it, and I'll finish it out.

The problem I found

The notch is Omi's always-on floating bar, the quickest way to reach Omi without opening the full app. But a couple of things about it get in the way. The animation glitches: three separate drivers run at once and desync, an AppKit window-frame animation (NSAnimationContext + animator().setFrame), a SwiftUI spring on the switcher morph (notchSwitcherProgress), and a scale plus opacity on the reveal (notchRevealProgress), so opening it reads as a fade or a jump instead of an actual grow. The flow is confusing too: the logo (an agent-pills row) opens the agent switcher, and the Ask-Omi button is wired to open the main app window (onAskAIopenMainAppWindow()) rather than letting you chat right in the notch. It never reads as one clean surface.

So I cleaned it up. Same feature set, clearer design: hover the notch and your Omi chat grows straight out of it, with one motion and a flow that does the obvious thing.

BEFORE                              AFTER
+------------------+                +------------------+
|   notch (jumpy)  |   hover        |   notch          |   hover (0.28s)
+------------------+   ---->        +------------------+   ---->
   opens agent                          grows into full
   switcher, Ask Omi                    chat, composer
   leaves to main                       rides below, all
   window, 3 anims                      one motion
   fighting

Demo (under 2 minutes)

t3_demo.mp4

What changed, and why it feels right now

  • One animation, grown from the notch. The panel is a fixed size per display and never animates its own frame. Only the inner content size and the notch shape's corner radii animate, anchored to the top, so every open literally grows out of the physical notch instead of fading in. The black island is a stable shape layer whose frame always interpolates; content crossfades inside it. I verified the motion by extracting frames from a screen recording and confirming the intermediate sizes are actually there.
  • Streaming has its own calm timeline. As a reply streams in, the height rides a separate smooth animation keyed on measured content height. Keeping it isolated from the open/close springs is the whole trick: merge them and the measure loop oscillates. Split, it just breathes.
  • Chat-first, no detour. Hovering opens the same shared timeline the main window renders (ChatProvider.mainInstance, not a copy), with a floating glass composer below it (attach, growing field, Return to send, Stop while responding). Ask Omi and the global hotkey now open the notch into chat instead of leaving to the main window. Height fits the conversation, caps at half the screen, and is remembered so the first open doesn't jump.
  • Voice you can see. Holding push-to-talk turns the composer into a live waveform, and your words now appear in it as you speak, with a stop button to end the turn. Your question lands in chat the moment speech-to-text settles, and the reply streams under it while Omi speaks. A closed notch auto-opens on the first token. If the free-tier limit blocks PTT, it now shows a visible hint instead of silently doing nothing (that one was a real dead end before).
  • A composer that gets out of the way. Sending empties the field instantly (no sent text lingering or reappearing after the reply), and it's put back only if the send never lands. Delegated-agent cards in the timeline are tappable now: tap the card to open the agent, not just a tiny corner icon.
  • Omi identity stays put. The omi mark (with the recording dot) and the settings gear are always visible on the closed notch, hugging the camera housing. The gear opens the main window's settings; the mark opens the agents tab.
  • Close behavior that respects you. Pointer-based auto-close with menu-aim detection (moving toward the panel never closes it), click-away, Esc, and a short post-open grace. Pointer monitors only exist while a panel is open, so idle CPU stays at zero.

Nothing was dropped

Every floating-bar feature still works after the redesign: PTT voice turns (hub and batch), voice playback settings, proactive notifications (queue, click-through, Execute, dismiss), the Ask-Omi hotkey, agent timeline opens, the recording indicator, show/hide/snooze preferences, the automation bridge (omi-ctl state now tells the truth about the notch), and usage limiting.

Two things I deliberately retired because the new design makes them meaningless: the manual bottom-corner resize (height is fully dynamic now) and the draggable pill preference (every display just gets a notch). The old window and view code stays dormant in-tree so removing it can be its own reviewable follow-up rather than a 7k-line delete buried in this PR.

Product invariants

  • INV-CHAT-1: the notch stays an I/O surface over the one kernel transcript. The chat tab renders ChatProvider.mainInstance.messages directly (no copy); the live voice strip is display-only mirror state, cleared once the journaled exchange lands; sends go through sendMainDraft and the kernel-routed query pipeline; snapshot aliases unchanged.
  • INV-DATA-1: no routing changes; check-mobile-production-routing.py passes (exit 0) on this branch.
  • INV-VOICE-1: PTT lifecycle unchanged. PTTBarPresenter still projects reducer state; the query pipeline keeps owner-bound dispatch, a single terminal outcome, and journal-acceptance semantics. Only the presentation surface moved from the legacy window to the notch state. The live listening pill now mirrors the provider's partial transcript for display only; it never enters the committed turn, which is still governed by the settled final.
  • INV-AUTH-1: no session-ownership change. sendMainDraft only clears and restores the local composer draft around the existing send path; it does not touch runtimeOwnerId, session mint, or AuthSessionCoordinator. The send_main_draft bridge action is non-production only and drives the same owner-bound send.

Verification

  • xcrun swift build -c debug --package-path Desktop green. ./scripts/agent-logic-harness.sh --swift-only green (INV-6 filter suites included). Full isolated Swift suite run (scripts/swift-test-suites.sh, 351 suites) green.
  • Live continuity gauntlet (agent-continuity-gauntlet.sh --suite continuity: typed, PTT, and blind recall against the real backend on a named bundle) passed on this branch's continuity code (passed: true). The chat and PTT files under test are unchanged since that run (verified byte-identical); later rebases only advanced the upstream base over release and changelog commits.
  • New hermetic suites: NotchViewModelTests (hover dwell via injected clock, grace, height clamp and persistence, sizing authority), NotchGeometryTests, NotchShapeTests, NotchAutoCloseZoneTests (zone math and menu-aim cone), NotchWindowLevelTests, NotchPresentationLadderTests. No wall-clock sleeps.
  • Exercised live on a named bundle (omi-notch-v2): hover-open into chat; typed send streamed back with the same message visible in the main window (one timeline); expansion checked frame by frame; Esc and click-away close; a real hub PTT turn with the listening waveform, a mid-stream capture showing the question above the streaming reply, and the journaled pair landing without duplication; the blocked-PTT hint; open_ask_omi automation opening the notch focused with a truthful omi-ctl state.
  • Composer clear verified through the real send path: a send_main_draft bridge action drives sendMainDraft, and after a full turn settles the composer draft reads empty (the sent text neither lingers nor resurfaces).
  • make preflight and scripts/pr-preflight pass with this body; changelog fragment, e2e flow covers, and the line-count and test-quality ratchets are all satisfied.

Failure-Class: none

Review in cubic

katipally added 24 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.
@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge needs-tests PR introduces logic that should be covered by tests labels Jul 22, 2026

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the thoughtful Track 3 submission — the chat-first notch direction is coherent and the new tests around geometry/presentation are useful. I can't approve or land this as-is yet because I found a couple of blocking integration regressions in the notch rewrite:

  1. Disabled/snoozed floating-bar state is bypassed at startup. NotchScreenManager.makePanel calls window.orderFrontRegardless() as soon as each panel is created, and FloatingControlBarManager.setup() starts the manager before checking isEnabled/snooze state. That means the notch panels can appear on launch even when the user had hidden/disabled or deferred the floating bar; the old window path created the window without necessarily ordering it front. Please make initial panel creation respect the persisted enabled/snoozed/deferred state, and only order panels front through the existing show/temporary-notification paths.

  2. Some legacy window-based paths are still wired even though the new setup leaves FloatingControlBarManager.window nil. For example closeAskOmiForAutomation now always returns floating_bar_window_unavailable, and deliverAgentArtifactCompletionToFloatingSurface drops completed agent artifacts behind guard let window else { return }. Those are part of the desktop automation/agent-surface contract and need to be ported to notchScreenManager/notchState rather than silently no-oping.

Given the size and UX direction here, this should also get human maintainer/product review after those blockers are fixed. I did not see an attack or supply-chain concern, and I appreciate the regression tests that were added; the remaining ask is to close these integration gaps and include validation evidence for the disabled/snoozed startup path plus the automation/agent-completion paths.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

katipally and others added 2 commits July 21, 2026 22:35
…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>
@undivisible

Copy link
Copy Markdown
Collaborator

charge your keyboard

@katipally

Copy link
Copy Markdown
Author

@Git-on-my-level Thanks for the careful review. Both blockers were real and are now fixed, with validation.

  1. Disabled/snoozed startup. Root cause: NotchScreenManager.makePanel ordered every panel front the moment it was created, so the launch policy's "hidden" decision was ignored. Panels are now created hidden and visibility runs through a single applyPanelVisibility authority: a panel is on screen when the notch is passively visible OR that panel is explicitly open. That also keeps the old "show temporarily, hide on close" behavior, the Ask Omi hotkey still surfaces the panel while the bar is disabled/snoozed, and it re-hides when the conversation closes.

Evidence (named bundle + Quartz on-screen window list): enabled launch = 2 notch windows, disabled launch = 0, open_ask_omi while disabled surfaces one, close_ask_omi hides it again. Hermetic NotchScreenManagerVisibilityTests assert real NSWindow.isVisible for created-hidden, showAll, hideAll, and summon-while-hidden.

  1. Legacy window paths. FloatingControlBarManager.window is never assigned in the notch build, so those paths silently no-oped. closeAskOmiForAutomation now drives notchScreenManager (closeAll + wait) exactly like open_ask_omi, so live close_ask_omi returns a clean result instead of floating_bar_window_unavailable. I removed the dead window field and everything that only reached it (the orphaned notification-reopen helpers, a write-only key, the vestigial PTT resize wrapper). On the agent artifact path: the completion isn't actually dropped, it's already projected into the shared notch timeline via projectJournalTurn (INV-6), so deliverAgentArtifactCompletionToFloatingSurface was dead legacy and I deleted it rather than resurrect the old auto-pop. The ~2000-line legacy FloatingControlBarWindow class is now unreferenced, but I left it for a separate follow-up so this diff stays reviewable.

Green across the board: full desktop Swift suite (352 suites), INV-6 continuity harness, swift-format, SwiftLint, line-count ratchet, e2e coverage. Failure-Class: FC-split-mutation-authority. No merge conflicts with main.

@katipally

Copy link
Copy Markdown
Author

@undivisible lol priorities, keyboard gets charged the moment this PR is done

@katipally

Copy link
Copy Markdown
Author

@undivisible
@Git-on-my-level need human response

Curious what you both think of this design direction. Worth me doubling down, more features, cleaner UI/UX and animations, pushing it toward a real Apple-native feel? Or keep it minimal for now? Want to build toward whatever fits your vision

@undivisible

Copy link
Copy Markdown
Collaborator

@undivisible @Git-on-my-level need human response

Curious what you both think of this design direction. Worth me doubling down, more features, cleaner UI/UX and animations, pushing it toward a real Apple-native feel? Or keep it minimal for now? Want to build toward whatever fits your vision

i personally love your design but we've been talking internally about removing chat from the notch to take a more verbal first direction when not using the app and keep text inputs within the app.
thanks for the pr though! won't close this yet in case we decide to keep the chat.

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

Labels

needs-maintainer-review Needs a human maintainer to sign off before merge needs-tests PR introduces logic that should be covered by tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants