Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Key rules:
- Dev bundle ID: `com.omi.desktop-dev`. Prod: `com.omi.computer-macos`.
- App flows & exploration skill: See `desktop/e2e/SKILL.md` for navigation architecture, interaction patterns, and reference flows.
- Full command reference: `agent-swift --help` or `agent-swift schema`.
- When asked to build or rebuild the desktop app for testing, don't stop at a successful compile: launch the dev app, interact with it programmatically to confirm it actually runs, and report any environment blocker if full interaction is impossible.

## Formatting

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ agent-swift screenshot /tmp/after-change.png # capture app window
- Works with any macOS app (SwiftUI, AppKit, Electron) — no Marionette or app-side setup.
- Bundle ID for dev: `com.omi.desktop-dev`. For prod: `com.omi.computer-macos`.
- **App flows & exploration skill**: See `desktop/e2e/SKILL.md` for navigation architecture, screen map, interaction patterns (click vs press), and known flows. Read this when developing features or exploring the app.
- When asked to build or rebuild the desktop app for testing, don't stop at a successful compile: launch the dev app, interact with it programmatically to confirm it actually runs, and report any environment blocker if full interaction is impossible.

## Formatting
<!-- Maintainers: @Thinh (Jan 19) -->
Expand Down
14 changes: 11 additions & 3 deletions desktop/Desktop/Sources/Chat/ACPBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -827,9 +827,17 @@ actor ACPBridge {

// 3. Check relative to current working directory
let cwdPath = FileManager.default.currentDirectoryPath
let cwdScript = (cwdPath as NSString).appendingPathComponent("acp-bridge/dist/index.js")
if FileManager.default.fileExists(atPath: cwdScript) {
return cwdScript
let cwdCandidates = [
"acp-bridge/dist/index.js",
"desktop/acp-bridge/dist/index.js",
"../desktop/acp-bridge/dist/index.js",
]
for relativePath in cwdCandidates {
let candidate = (cwdPath as NSString).appendingPathComponent(relativePath)
let resolved = (candidate as NSString).standardizingPath
if FileManager.default.fileExists(atPath: resolved) {
return resolved
}
}

return nil
Expand Down
104 changes: 46 additions & 58 deletions desktop/Desktop/Sources/OnboardingFloatingBarDemoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ struct OnboardingFloatingBarDemoView: View {

@State private var barActivated = false
@State private var showContinue = false
@State private var pulseAnimation = false
@State private var keyMonitor: Any?

var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Text("Ask omi anything")
Text("Ask omi which Mac fits you")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(OmiColors.textPrimary)

Expand All @@ -40,38 +38,21 @@ struct OnboardingFloatingBarDemoView: View {
Spacer()

// Content
VStack(spacing: 28) {
// Icon with glow
ZStack {
Circle()
.fill(OmiColors.purplePrimary.opacity(0.12))
.frame(width: 96, height: 96)
.blur(radius: 18)
.scaleEffect(pulseAnimation ? 1.15 : 1.0)
.animation(.easeInOut(duration: 2).repeatForever(autoreverses: true), value: pulseAnimation)

Image(systemName: "rectangle.and.text.magnifyingglass")
.font(.system(size: 40))
.foregroundStyle(
LinearGradient(
colors: [OmiColors.purplePrimary, OmiColors.purpleSecondary],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
.onAppear { pulseAnimation = true }
VStack(spacing: 24) {
MacLineupPreview()
.frame(maxWidth: 980)

VStack(spacing: 10) {
VStack(spacing: 12) {
Text("The Floating Bar")
.font(.system(size: 24, weight: .bold))
.foregroundColor(OmiColors.textPrimary)

Text("Ask anything and it responds using\neverything it knows about you.")
Text("Try asking: Which computer suits me best?")
.font(.system(size: 14))
.foregroundColor(OmiColors.textSecondary)
.multilineTextAlignment(.center)
.lineSpacing(4)
.frame(maxWidth: 560)
}

if !barActivated {
Expand Down Expand Up @@ -100,6 +81,7 @@ struct OnboardingFloatingBarDemoView: View {
.transition(.opacity)
}
}
.padding(.top, 88)
.padding(.horizontal, 40)

Spacer()
Expand All @@ -125,24 +107,28 @@ struct OnboardingFloatingBarDemoView: View {
.onAppear {
// Set up the real floating bar (creates the window if needed)
FloatingControlBarManager.shared.setup(appState: appState, chatProvider: chatProvider)
// Unregister global shortcuts so we handle Cmd+Enter ourselves
GlobalShortcutManager.shared.unregisterShortcuts()
installKeyMonitor()
// Use the same global shortcut flow as the normal app so onboarding
// behaves like production when the user presses Cmd+Enter.
GlobalShortcutManager.shared.registerShortcuts()
}
.onDisappear {
removeKeyMonitor()
// Close the AI conversation panel on the floating bar so the next step starts clean
if FloatingControlBarManager.shared.barState?.showingAIConversation == true {
FloatingControlBarManager.shared.toggleAIInput()
}
// Re-register global shortcuts for subsequent steps and normal use
GlobalShortcutManager.shared.registerShortcuts()
}
.onChange(of: barActivated) { _, activated in
if activated {
Task { await waitForResponse() }
}
}
.onReceive(Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()) { _ in
guard !barActivated,
FloatingControlBarManager.shared.barState?.showingAIConversation == true else { return }
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
barActivated = true
}
}
Comment on lines +125 to +131
Copy link
Contributor

Choose a reason for hiding this comment

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

Timer fires indefinitely after barActivated becomes true

Once barActivated is set to true, the guard exits early on every subsequent tick, but the timer is never cancelled — it keeps firing at 4 Hz for the entire lifetime of the view (which includes the 60-second waitForResponse loop). Storing the timer's Cancellable and cancelling it when activation is detected would avoid this waste:

// @State private var timerCancellable: AnyCancellable?   (declared at top of struct)

.onReceive(Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()) { _ in
    guard !barActivated,
          FloatingControlBarManager.shared.barState?.showingAIConversation == true else { return }
    withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
        barActivated = true
    }
    timerCancellable?.cancel()
}

Alternatively, using a dedicated Cancellable stored in a @State var gives explicit control over when polling stops.

Comment on lines +125 to +131
Copy link
Contributor

Choose a reason for hiding this comment

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

toggleAIInput can close the bar mid-interaction, causing a 60 s stall

The global shortcut now calls toggleAIInput() (via GlobalShortcutManager), which closes the AI panel if it is already visible. In the old code the key monitor always called openAIInput() (open-only). This creates a regression:

  1. User presses ⌘+Enter → showingAIConversation = true → timer sets barActivated = truewaitForResponse() starts.
  2. User types a question and the AI begins streaming.
  3. User accidentally presses ⌘+Enter again → toggleAIInput() calls closeAIConversation(), showingAIConversation drops to false.
  4. waitForResponse() polls barState.showingAIResponse — which is now false — for the full 60-second timeout before showing Continue.

The onboarding intent is "open the bar," not "toggle the bar." Consider using FloatingControlBarManager.shared.openAIInput() (which is idempotent when already open) instead of routing through the global shortcut machinery, or subscribe to a notification that the shortcut fired and always call openAIInput().

}

// MARK: - Response Observer
Expand All @@ -169,32 +155,6 @@ struct OnboardingFloatingBarDemoView: View {
}
}

// MARK: - Key Monitor

private func installKeyMonitor() {
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
if mods == .command && event.keyCode == 36 { // 36 = Return
if !barActivated {
// Activate the real floating bar's AI input
FloatingControlBarManager.shared.openAIInput()
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
barActivated = true
}
return nil
}
}
return event
}
}

private func removeKeyMonitor() {
if let monitor = keyMonitor {
NSEvent.removeMonitor(monitor)
keyMonitor = nil
}
}

// MARK: - Key Cap

private func keyCap(_ key: String) -> some View {
Expand All @@ -214,3 +174,31 @@ struct OnboardingFloatingBarDemoView: View {
)
}
}

private struct MacLineupPreview: View {
private static let lineupImage: NSImage? = {
guard let url = Bundle.resourceBundle.url(forResource: "onboarding_mac_lineup", withExtension: "png") else { return nil }
return NSImage(contentsOf: url)
}()
Comment on lines +179 to +182
Copy link
Contributor

Choose a reason for hiding this comment

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

Synchronous image loading blocks the main thread

NSImage(contentsOf:) performs synchronous file I/O. Because this is stored in a static let, the initializer runs lazily — but it runs on the main thread the very first time MacLineupPreview appears (during a SwiftUI body evaluation). A Mac lineup screenshot can easily be several MB, so the first render of this onboarding step can freeze the UI noticeably.

Consider loading the image off-thread and publishing it via a @State or @StateObject:

private struct MacLineupPreview: View {
    @State private var nsImage: NSImage?

    var body: some View {
        Group {
            if let nsImage {
                Image(nsImage: nsImage)
                    .resizable()
                    .interpolation(.high)
                    .scaledToFit()
                    .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
            } else {
                RoundedRectangle(cornerRadius: 24)
                    .fill(Color.white.opacity(0.06))
                    .frame(height: 280)
                    .overlay(
                        Text("Mac lineup image unavailable")
                            .font(.system(size: 14, weight: .medium))
                            .foregroundColor(OmiColors.textTertiary)
                    )
            }
        }
        .task {
            guard let url = Bundle.resourceBundle.url(
                forResource: "onboarding_mac_lineup", withExtension: "png") else { return }
            nsImage = await Task.detached(priority: .userInitiated) {
                NSImage(contentsOf: url)
            }.value
        }
    }
}


var body: some View {
Group {
if let nsImage = Self.lineupImage {
Image(nsImage: nsImage)
.resizable()
.interpolation(.high)
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
} else {
RoundedRectangle(cornerRadius: 24)
.fill(Color.white.opacity(0.06))
.frame(height: 280)
.overlay(
Text("Mac lineup image unavailable")
.font(.system(size: 14, weight: .medium))
.foregroundColor(OmiColors.textTertiary)
)
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.