Skip to content

Commit c972ba6

Browse files
authored
Fix onboarding text splitting, button timing, and transition crash (#5366)
## Summary - Fix AI chat text being split into multiple bubbles (e.g. "encrypted!Should") by using `message.text` directly instead of reassembling from content blocks - Only show Continue button after `complete_onboarding` AND no pending questions/permissions remain - Defer `hasCompletedOnboarding` state change to prevent SwiftUI crash during view transition - Pre-load hero logo as static property to avoid NSImage crash during body evaluation ## Test plan - [ ] Onboarding chat shows AI text as single continuous bubble, no broken words - [ ] Continue button only appears after all questions and permissions are handled - [ ] Clicking "Take me to my tasks" transitions without crash - [ ] App loads main view without hero logo crash 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents bab0cd2 + e1d7051 commit c972ba6

File tree

3 files changed

+22
-18
lines changed

3 files changed

+22
-18
lines changed

desktop/Desktop/Sources/MainWindow/DesktopHomeView.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ struct DesktopHomeView: View {
2929
@State private var previousIndexBeforeSettings: Int = 0
3030
@State private var logoPulse = false
3131

32+
// Pre-loaded hero logo to avoid NSImage init crashes during SwiftUI body evaluation
33+
private static let heroLogoImage: NSImage? = {
34+
guard let url = Bundle.resourceBundle.url(forResource: "herologo", withExtension: "png"),
35+
let data = try? Data(contentsOf: url) else { return nil }
36+
return NSImage(data: data)
37+
}()
38+
3239

3340
/// Whether we're currently viewing the settings page
3441
private var isInSettings: Bool {
@@ -40,8 +47,7 @@ struct DesktopHomeView: View {
4047
if authState.isRestoringAuth {
4148
// State 0: Restoring auth session - show loading
4249
VStack(spacing: 16) {
43-
if let iconURL = Bundle.resourceBundle.url(forResource: "herologo", withExtension: "png"),
44-
let nsImage = NSImage(contentsOf: iconURL) {
50+
if let nsImage = Self.heroLogoImage {
4551
Image(nsImage: nsImage)
4652
.resizable()
4753
.scaledToFit()
@@ -226,8 +232,7 @@ struct DesktopHomeView: View {
226232

227233
if !viewModelContainer.isInitialLoadComplete {
228234
VStack(spacing: 24) {
229-
if let iconURL = Bundle.resourceBundle.url(forResource: "herologo", withExtension: "png"),
230-
let nsImage = NSImage(contentsOf: iconURL) {
235+
if let nsImage = Self.heroLogoImage {
231236
Image(nsImage: nsImage)
232237
.resizable()
233238
.scaledToFit()

desktop/Desktop/Sources/OnboardingChatView.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,9 @@ struct OnboardingChatView: View {
244244
}
245245
}
246246

247-
// "Continue" button — shown after AI calls complete_onboarding,
248-
// or while exploration is running so user can proceed.
249-
// Hidden when quick reply buttons are showing to avoid confusing the user.
250-
if (onboardingCompleted || explorationRunning) && !chatProvider.isSending && quickReplyOptions.isEmpty {
247+
// "Continue" button — shown only after AI calls complete_onboarding
248+
// and no pending questions or permissions remain.
249+
if onboardingCompleted && !chatProvider.isSending && quickReplyOptions.isEmpty && pendingPermissionType == nil {
251250
Button(action: {
252251
handleOnboardingComplete()
253252
}) {
@@ -914,14 +913,9 @@ struct OnboardingChatBubble: View {
914913
.cornerRadius(18)
915914
}
916915
} else {
917-
// Combine all text blocks into one bubble, render tool indicators separately
918-
let allText = message.contentBlocks.compactMap { block -> String? in
919-
if case .text(_, let text) = block {
920-
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
921-
return trimmed.isEmpty ? nil : trimmed
922-
}
923-
return nil
924-
}.joined(separator: "\n\n")
916+
// Use the full message text (which streams continuously) for a single bubble.
917+
// contentBlocks splits text around tool calls, but message.text is uninterrupted.
918+
let allText = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
925919

926920
if !allText.isEmpty {
927921
Markdown(allText)

desktop/Desktop/Sources/OnboardingView.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,6 @@ struct OnboardingView: View {
219219

220220
// Navigate to Tasks page after transition
221221
UserDefaults.standard.set(true, forKey: "onboardingJustCompleted")
222-
223-
appState.hasCompletedOnboarding = true
224222
UserDefaults.standard.set(true, forKey: "hasCompletedFileIndexing")
225223

226224
// Start essential services
@@ -253,6 +251,13 @@ struct OnboardingView: View {
253251
if let onComplete = onComplete {
254252
onComplete()
255253
}
254+
255+
// Defer the view hierarchy change so SwiftUI finishes rendering the
256+
// current button before the OnboardingView is removed from the tree.
257+
// Setting this synchronously crashes in Button.body.getter.
258+
DispatchQueue.main.async {
259+
appState.hasCompletedOnboarding = true
260+
}
256261
}
257262
}
258263

0 commit comments

Comments
 (0)