Update floating bar onboarding Mac lineup#5610
Conversation
Greptile SummaryThis PR refreshes the floating bar onboarding step by replacing the animated icon illustration with a Key changes:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[View onAppear] --> B[FloatingControlBarManager.setup]
B --> C[GlobalShortcutManager.registerShortcuts]
C --> D[0.25s Timer starts polling showingAIConversation]
D -->|showingAIConversation == true| E[barActivated = true]
D -->|showingAIConversation == false| D
E --> F[waitForResponse task starts]
F -->|polls every 0.5s up to 60s| G{showingAIResponse && !isStreaming?}
G -->|yes| H[showContinue = true → Continue button shown]
G -->|no, timeout| H
subgraph GlobalShortcut [Global Shortcut: ⌘+Enter]
I[toggleAIInput] -->|showingAIConversation == false| J[openAIInput → showingAIConversation = true]
I -->|showingAIConversation == true| K[closeAIConversation ⚠️ regression]
end
UserPress[User presses ⌘+Enter] --> I
J --> D
K -->|barActivated already true| F
Last reviewed commit: a774dbc |
| private static let lineupImage: NSImage? = { | ||
| guard let url = Bundle.resourceBundle.url(forResource: "onboarding_mac_lineup", withExtension: "png") else { return nil } | ||
| return NSImage(contentsOf: url) | ||
| }() |
There was a problem hiding this comment.
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
}
}
}| .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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| .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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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:
- User presses ⌘+Enter →
showingAIConversation = true→ timer setsbarActivated = true→waitForResponse()starts. - User types a question and the AI begins streaming.
- User accidentally presses ⌘+Enter again →
toggleAIInput()callscloseAIConversation(),showingAIConversationdrops tofalse. waitForResponse()pollsbarState.showingAIResponse— which is nowfalse— 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().
Summary
Verification