Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
c4fdaf1
Add notch island floating bar UI
Git-on-my-level Jun 25, 2026
906e04c
Address notch mode screen switch review
Git-on-my-level Jun 25, 2026
dbef128
Validate audio override device before reuse
Git-on-my-level Jun 25, 2026
18ba0fb
Remove stale notch branch after screen switch
Git-on-my-level Jun 25, 2026
08edc8e
Clear response glow when realtime audio idles
Git-on-my-level Jun 25, 2026
ed974fe
Honor hidden bar preference on notch toggle
Git-on-my-level Jun 25, 2026
0a1be5f
Exercise production notch grow frame in test
Git-on-my-level Jun 25, 2026
c7d5f86
Relax pi tool schema composition test
Git-on-my-level Jun 25, 2026
07f7fb1
Create logo report parent before rendering
Git-on-my-level Jun 25, 2026
56d755b
Clarify notch logo render save order
Git-on-my-level Jun 25, 2026
103f02b
Fix notch geometry returns after rebase
Git-on-my-level Jun 25, 2026
5316fbf
Keep notch ask control clickable before hover settles
Git-on-my-level Jun 25, 2026
5e494eb
Preserve notch agent width after PTT
Git-on-my-level Jun 25, 2026
4a5d1ae
Clear realtime voice glow on exit
Git-on-my-level Jun 25, 2026
ea88678
Refresh notch settings hover after voice ends
Git-on-my-level Jun 25, 2026
b6f4a22
Preserve realtime glow until playback idle
Git-on-my-level Jun 25, 2026
b45ff7d
Use compact agent pills inside notch island
Git-on-my-level Jun 25, 2026
22a960d
Reanchor notch island on screen changes
Git-on-my-level Jun 25, 2026
f495d06
Address review feedback: fix notch mode screen targeting, dedupe stat…
Git-on-my-level Jun 25, 2026
c0e3a82
fix: keep agent pill controls available in notch mode
Git-on-my-level Jun 25, 2026
9ffe42b
Share agent pill indicator color
Git-on-my-level Jun 25, 2026
4c2e70c
Use shared agent pill indicator color
Git-on-my-level Jun 25, 2026
30994d4
Keep notch agent pill actions reachable
Git-on-my-level Jun 25, 2026
fdcc536
Tighten notch recenter and glow resize
Git-on-my-level Jun 25, 2026
5a2be52
Use one agent pill status tint
Git-on-my-level Jun 25, 2026
76aa35f
Let notch island own agent pill controls
Git-on-my-level Jun 25, 2026
2fcc998
Only subtract notch glow after it exists
Git-on-my-level Jun 25, 2026
ab6f165
fix: preserve notch response height when glow starts
Git-on-my-level Jun 25, 2026
2fefd76
fix: keep response glow padding when closing ask omi
Git-on-my-level Jun 25, 2026
417073c
fix: compare response growth against surface height
Git-on-my-level Jun 25, 2026
54f3508
fix: compare notch response growth to surface height
Git-on-my-level Jun 25, 2026
9cf6a55
fix: keep notch agent pills reachable
Git-on-my-level Jun 25, 2026
6b7280f
Add notch mode changelog entry
Git-on-my-level Jun 25, 2026
4e57816
fix: track resolved audio override listener state
Git-on-my-level Jun 25, 2026
9856797
Sync audio listeners after override fallback
Git-on-my-level Jun 25, 2026
7439fdd
Account for notch input panel padding
Git-on-my-level Jun 25, 2026
a0e2f3e
Stabilize notch voice response playback glow
Git-on-my-level Jun 25, 2026
c96d37c
Merge origin/main into notch island UI
Git-on-my-level Jun 25, 2026
d53cf91
Prevent nested floating agent spawns
Git-on-my-level Jun 25, 2026
2b87584
fix(desktop): harden notch agent pill contracts
Git-on-my-level Jun 25, 2026
cec4648
fix(desktop): keep all notch status groups reachable
Git-on-my-level Jun 25, 2026
d1f1b8c
fix(desktop): collapse overflow notch status groups
Git-on-my-level Jun 25, 2026
d344fd6
Polish notch chat and agent popovers
Git-on-my-level Jun 25, 2026
caadfdd
fix(desktop): layer notch response glow above fill
Git-on-my-level Jun 25, 2026
6f221ee
Experiment with subagent chat takeover
Git-on-my-level Jun 25, 2026
08d1dfc
Stack notch subagent switcher
Git-on-my-level Jun 26, 2026
e3a606a
Use Omi dots for notch subagents
Git-on-my-level Jun 26, 2026
41520f5
Refine notch subagent fanout layout
Git-on-my-level Jun 26, 2026
e59a560
Fix notch fanout and chat voice resizing
Git-on-my-level Jun 26, 2026
e2168df
Fix notch resize and subagent slot cleanup
Git-on-my-level Jun 26, 2026
77bcfdb
Refine notch fanout animation and glow surface
Git-on-my-level Jun 26, 2026
64fec7b
Polish notch fanout and preserve response sizing
Git-on-my-level Jun 26, 2026
0ab238c
Fix notch output refresh and glow stability
Git-on-my-level Jun 26, 2026
d26d2dc
Stabilize notch chat resize and glow layout
Git-on-my-level Jun 26, 2026
5372951
Merge remote-tracking branch 'origin/main' into codex/notch-island-ui
Git-on-my-level Jun 26, 2026
369ea5c
Checkpoint notch subagent animation polish
Git-on-my-level Jun 26, 2026
a5edb7a
Refine notch island subagent UI
Git-on-my-level Jun 26, 2026
8e2d70b
Merge remote-tracking branch 'origin/codex/notch-island-ui' into code…
Git-on-my-level Jun 26, 2026
8a0f43d
Merge remote-tracking branch 'origin/main' into omi-monitor/pr-8347
Git-on-my-level Jun 26, 2026
f79af1c
fix: correct inverted glow-order assertion in AgentPillLifecycleTests
Git-on-my-level Jun 26, 2026
e88541a
fix: reformat backend Python files to match upstream main
Git-on-my-level Jun 26, 2026
5687c87
fix: remove purple accents and reset response sizing on agent chat exit
Git-on-my-level Jun 26, 2026
0968ed2
Reduce Ask Omi chat idle power
Git-on-my-level Jun 26, 2026
e456e34
fix: address review feedback — fail power.sample on process death and…
Git-on-my-level Jun 26, 2026
b261ca1
fix: remove purple pill tint and guard stale stdout chunks
Git-on-my-level Jun 26, 2026
dc7bc05
fix: stop filtering named harness app and replace purple settings toggle
Git-on-my-level Jun 26, 2026
e57ae47
fix: address review feedback — reinstall response sizing on agent tog…
Git-on-my-level Jun 26, 2026
fe2ef39
refactor: centralize agent-exit observer reset into exitAgentSurface …
Git-on-my-level Jun 26, 2026
be093a6
fix: address review feedback — stdout generation, agent surface escapes
Git-on-my-level Jun 26, 2026
575e85e
fix: address review feedback — agent surface and notch switcher edge …
Git-on-my-level Jun 26, 2026
1b108d7
fix: address review feedback — compact glow resize and viewed-pill ex…
Git-on-my-level Jun 26, 2026
97e1b37
fix: address round 2 review feedback — terminal statusText and handof…
Git-on-my-level Jun 26, 2026
9a4912d
fix: address round 3 review feedback — escape input sizing and PTT gl…
Git-on-my-level Jun 27, 2026
862d497
fix: address review feedback — behavioral question-starter exclusion …
Git-on-my-level Jun 27, 2026
c678df0
fix: address round 4 review feedback — ancestor PID filter and local …
Git-on-my-level Jun 27, 2026
7ed24c5
fix: address review feedback — speech glow race, active pill trim, mo…
Git-on-my-level Jun 27, 2026
58d5507
fix: stop queued local speech on barge-in before synthesizer starts
Git-on-my-level Jun 27, 2026
972add6
Add compact subagent row summaries
Git-on-my-level Jun 26, 2026
43c6488
Try compact notch list chat switching
Git-on-my-level Jun 26, 2026
b4c0b55
Refine notch subagent list hit target
Git-on-my-level Jun 26, 2026
4e38c7f
Refine notch chat chrome
Git-on-my-level Jun 27, 2026
e98dc76
fix: address 5 cubic/codex P2 review comments on notch island UI
Git-on-my-level Jun 27, 2026
815fab5
fix: address 6 cubic/codex review comments on notch island UI
Git-on-my-level Jun 27, 2026
5b4b6eb
fix: address 5 cubic/codex review comments on notch island UI
Git-on-my-level Jun 27, 2026
334d412
fix: scope negation guard and add notch button accessibility
Git-on-my-level Jun 27, 2026
a9d0b43
fix: fully scope negation guard, interrupt pill on follow-up, gate st…
Git-on-my-level Jun 27, 2026
71aeed2
fix: add cancellation guard to polling loop and consolidate agent-nou…
Git-on-my-level Jun 27, 2026
1178036
Fix notch chat sizing and PTT audio fallback
Git-on-my-level Jun 27, 2026
bbddf3b
Merge remote-tracking branch 'origin/main' into codex/notch-island-ui
Git-on-my-level Jun 27, 2026
6e53113
Merge remote-tracking branch 'origin/codex/notch-island-ui' into code…
Git-on-my-level Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion desktop/macos/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
{
"unreleased": [],
"unreleased": [
"Added Notch Mode for a MacBook notch-integrated floating bar with compact voice, chat, and agent controls",
"Fixed Notch Mode voice response audio and glow stability during streamed playback",
"Improved Notch Mode Ask Omi responsiveness by reducing main-thread animation work",
"Refined Notch Mode subagent indicators into direct and grouped glowing status orbs",
"Improved Notch Mode notch-safe layout measurement and subagent status refresh",
"Improved desktop agent session continuity across bridge restarts and control-tool ownership changes.",
"Improves update release safety checks",
"Smoothed the notch logo-to-dots animation so the Omi logo unfurls into the agent dots when chat opens and furls back when it closes",
"Made Notch Mode subagent chat independent from main chat content and reduced idle animation work",
"Added an Ask Omi chat power benchmark harness for repeatable CPU and memory sampling",
"Added compact status summaries to Notch Mode subagent rows",
"Made Notch Mode switch between subagent list and chat instead of stacking both",
"Made the Notch Mode Omi logo morph into settings on hover while revealing subagents, and added bottom spacing to the subagent list"
],
"releases": [
{
"version": "0.11.542",
Expand Down
201 changes: 99 additions & 102 deletions desktop/macos/Desktop/Sources/AudioCaptureService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class AudioCaptureService: @unchecked Sendable {
private var defaultDeviceListenerBlock: AudioObjectPropertyListenerBlock?
private var deviceFormatListenerBlock: AudioObjectPropertyListenerBlock?
private var isCapturing = false
private var isTrackingOverrideDevice = false

/// Optional explicit device to open instead of the system default input.
/// Used by the silent-mic fallback path to bind directly to the built-in mic.
Expand Down Expand Up @@ -174,33 +175,9 @@ class AudioCaptureService: @unchecked Sendable {

/// Performs all blocking CoreAudio HAL setup. Must be called on audioQueue, not the main thread.
private func startCaptureOnQueue() throws {
// 1. Resolve input device: explicit override (fallback path) wins over system default.
var inputDeviceID: AudioDeviceID = kAudioObjectUnknown

if let override = overrideDeviceID {
inputDeviceID = override
log("AudioCapture: Using override device ID \(override)")
} else {
var size = UInt32(MemoryLayout<AudioDeviceID>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)

let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&address,
0,
nil,
&size,
&inputDeviceID
)

guard status == noErr, inputDeviceID != kAudioObjectUnknown else {
throw AudioCaptureError.noInputAvailable
}
}
// 1. Resolve input device: explicit override wins while available, otherwise
// fall back to the system default instead of pinning capture to a stale device.
let inputDeviceID = try resolveInputDeviceID()
self.deviceID = inputDeviceID

// 2. Get device stream format
Expand Down Expand Up @@ -293,6 +270,7 @@ class AudioCaptureService: @unchecked Sendable {
targetFormat = nil
detectedSampleRate = 0.0
smoothedLevel = 0.0
isTrackingOverrideDevice = false

// AudioDeviceStop can block waiting for the IO thread — run off main thread
if let procID = procID, devID != kAudioObjectUnknown {
Expand Down Expand Up @@ -356,6 +334,49 @@ class AudioCaptureService: @unchecked Sendable {

// MARK: - Private Methods

private func resolveInputDeviceID() throws -> AudioDeviceID {
if let override = overrideDeviceID {
if Self.isAvailableInputDevice(override) {
log("AudioCapture: Using override device ID \(override)")
isTrackingOverrideDevice = true
return override
}
log("AudioCapture: Override device ID \(override) is unavailable; falling back to default input")
Comment thread
Git-on-my-level marked this conversation as resolved.
}

guard let defaultDeviceID = Self.currentDefaultInputDeviceID() else {
throw AudioCaptureError.noInputAvailable
}
isTrackingOverrideDevice = false
return defaultDeviceID
}

private static func currentDefaultInputDeviceID() -> AudioDeviceID? {
var deviceID: AudioDeviceID = kAudioObjectUnknown
var size = UInt32(MemoryLayout<AudioDeviceID>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)

let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&address,
0,
nil,
&size,
&deviceID
)

guard status == noErr, isAvailableInputDevice(deviceID) else { return nil }
return deviceID
}

private static func isAvailableInputDevice(_ deviceID: AudioDeviceID) -> Bool {
deviceID != kAudioObjectUnknown && deviceID != kAudioDeviceUnknown && deviceHasInputChannels(deviceID)
}

/// Get stream format for a device on input scope
private func getStreamFormat(for deviceID: AudioObjectID) -> AudioStreamBasicDescription? {
var address = AudioObjectPropertyAddress(
Expand Down Expand Up @@ -530,30 +551,45 @@ class AudioCaptureService: @unchecked Sendable {
// MARK: - Property Listeners

private func installPropertyListeners() {
// Listen for default input device changes — only when we're tracking the
// system default. If we're using an explicit override (silent-mic fallback path)
// we deliberately ignore default-device changes so we stay pinned to our target.
if overrideDeviceID == nil {
var defaultDeviceAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
updateDefaultDeviceListener()
installDeviceFormatListener()
}

let deviceBlock: AudioObjectPropertyListenerBlock = { [weak self] numberAddresses, addresses in
self?.audioQueue.async {
self?.handleConfigurationChange()
}
}
self.defaultDeviceListenerBlock = deviceBlock
private func updateDefaultDeviceListener() {
if isTrackingOverrideDevice {
removeDefaultDeviceListener()
return
}

AudioObjectAddPropertyListenerBlock(
AudioObjectID(kAudioObjectSystemObject),
&defaultDeviceAddress,
listenerQueue,
deviceBlock
)
guard defaultDeviceListenerBlock == nil else { return }

// Listen for default input device changes when the resolved capture device
// is the system default. If an explicit override was requested but is
// unavailable, capture falls back to the default and must still observe
// default-device changes.
var defaultDeviceAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)

let deviceBlock: AudioObjectPropertyListenerBlock = { [weak self] numberAddresses, addresses in
self?.audioQueue.async {
self?.handleConfigurationChange()
}
}
self.defaultDeviceListenerBlock = deviceBlock

AudioObjectAddPropertyListenerBlock(
AudioObjectID(kAudioObjectSystemObject),
&defaultDeviceAddress,
listenerQueue,
deviceBlock
)
}

private func installDeviceFormatListener() {
guard deviceFormatListenerBlock == nil else { return }

// Listen for format changes on current device
var formatAddress = AudioObjectPropertyAddress(
Expand All @@ -578,6 +614,11 @@ class AudioCaptureService: @unchecked Sendable {
}

private func removePropertyListeners() {
removeDefaultDeviceListener()
removeDeviceFormatListener()
}

private func removeDefaultDeviceListener() {
if let block = defaultDeviceListenerBlock {
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
Expand All @@ -592,7 +633,9 @@ class AudioCaptureService: @unchecked Sendable {
)
defaultDeviceListenerBlock = nil
}
}

private func removeDeviceFormatListener() {
if let block = deviceFormatListenerBlock, deviceID != kAudioObjectUnknown {
var address = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreamFormat,
Expand Down Expand Up @@ -626,21 +669,8 @@ class AudioCaptureService: @unchecked Sendable {
ioProcID = nil
}

// Remove old format listener (device may have changed)
if let block = deviceFormatListenerBlock, deviceID != kAudioObjectUnknown {
var address = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreamFormat,
mScope: kAudioDevicePropertyScopeInput,
mElement: kAudioObjectPropertyElementMain
)
AudioObjectRemovePropertyListenerBlock(
deviceID,
&address,
listenerQueue,
block
)
deviceFormatListenerBlock = nil
}
// Remove old format listener (device may have changed).
removeDeviceFormatListener()

// Delay to let the audio hardware settle after device change
audioQueue.asyncAfter(deadline: .now() + 0.3) { [weak self] in
Expand All @@ -651,25 +681,10 @@ class AudioCaptureService: @unchecked Sendable {
private static let maxRetries = 3

private func reconfigureAfterChange(retryCount: Int) {
// Get new default input device
var newDeviceID: AudioDeviceID = kAudioObjectUnknown
var size = UInt32(MemoryLayout<AudioDeviceID>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)

let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&address,
0,
nil,
&size,
&newDeviceID
)

guard status == noErr, newDeviceID != kAudioObjectUnknown else {
let newDeviceID: AudioDeviceID
do {
newDeviceID = try resolveInputDeviceID()
Comment thread
Git-on-my-level marked this conversation as resolved.
} catch {
log("AudioCapture: No valid input device after config change (attempt \(retryCount + 1))")
retryOrGiveUp(retryCount: retryCount)
return
Expand Down Expand Up @@ -738,26 +753,8 @@ class AudioCaptureService: @unchecked Sendable {
return
}

// Install format listener on new device
var formatAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreamFormat,
mScope: kAudioDevicePropertyScopeInput,
mElement: kAudioObjectPropertyElementMain
)

let formatBlock: AudioObjectPropertyListenerBlock = { [weak self] numberAddresses, addresses in
self?.audioQueue.async {
self?.handleConfigurationChange()
}
}
self.deviceFormatListenerBlock = formatBlock

AudioObjectAddPropertyListenerBlock(
deviceID,
&formatAddress,
listenerQueue,
formatBlock
)
updateDefaultDeviceListener()
installDeviceFormatListener()

log("AudioCapture: Restarted with new configuration")
isReconfiguring = false
Expand Down
Loading
Loading