From c2639d147203a02db3690ff70e82dbbf95348a54 Mon Sep 17 00:00:00 2001 From: Skander Karoui Date: Tue, 21 Jul 2026 13:23:28 -0400 Subject: [PATCH 1/3] fix(desktop): repair Home redesign UI regressions (hub catcher trap, hover-only actions, Continue-in-Omi landing, knows-list dedup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four bugs introduced by the Home redesign (#10184) and floating-bar typing removal (#10181): - Hub click/Esc trap: the collapse catchers mounted whenever homeMode != homeRestingMode, which with chat history present put them OVER the hub — a stray click or Esc then *opened* the chat. Catchers now mount only over a non-resting panel, never the hub (HomeStageMode.collapseCatcherActive). Also corrected the autoOpenChatForExistingHistoryIfNeeded comment, which described an 'Esc back to the hub' that cannot exist once history is present. - Chat message actions/timestamps were opacity-gated on pointer hover only, leaving Tab / Full Keyboard Access focused on invisible buttons. The metadata row now also reveals while any of its controls holds keyboard focus (shared FocusState + ChatBubbleMetadataReveal). - 'Continue in Omi' only called openMainAppWindow(), landing the user on whatever tab the main window last showed. Both floating-bar affordances now route through openMainAppChat(): a one-shot MainChatNavigationRequestStore request switches DesktopHomeView to the Home tab and DashboardPage opens the chat panel on mount or in place. - HomeKnowsListComposer derives question-row IDs from text; duplicate suggestions produced colliding ForEach IDs. Questions are deduped. Failure-Class: none Verification: - xcrun swift build -c debug --package-path Desktop — clean. - xcrun swift test --filter 'HomeKnowsComposerTests|HomeStageCollapseCatcherTests|MainChatNavigationRequestStoreTests|ChatBubbleMetadataRevealTests' — 15/15 pass (new regression tests for all four fixes). - Guard suites ChatTimelineContinuityTests, DashboardCaptureStateTests, HomeSuggestionsStoreTests, HomeStatusStoreTests — pass. - desktop-core-harness.sh --self-check and check-e2e-flow-coverage.py --strict — pass (MainChatNavigationRequest.swift covered by home-stage.yaml). - Live named-bundle exercise was attempted but is blocked on this machine: no signed-in session to seed, no redis/docker for the hermetic T2 stack, and the Auth-emulator local profile launch wedges in SecItemAdd under the console-user launch path. The behavior changes are covered by the unit regression tests above; the real user-facing paths were NOT exercised end-to-end. --- .../FloatingControlBarView.swift | 4 +- .../MainWindow/Components/ChatBubble.swift | 30 +++++++- .../Dashboard/HomeKnowsComposer.swift | 5 +- .../Sources/MainWindow/DesktopHomeView.swift | 5 ++ .../MainChatNavigationRequest.swift | 32 +++++++++ .../MainWindow/Pages/DashboardPage.swift | 55 +++++++++++--- desktop/macos/Desktop/Sources/OmiApp.swift | 8 +++ .../Tests/HomeKnowsComposerTests.swift | 12 ++++ .../Tests/HomeRedesignRegressionTests.swift | 71 +++++++++++++++++++ .../20260721-chat-actions-keyboard-focus.json | 3 + .../20260721-continue-in-omi-opens-chat.json | 3 + .../20260721-home-hub-stray-click-fix.json | 3 + desktop/macos/e2e/flows/home-stage.yaml | 1 + 13 files changed, 219 insertions(+), 13 deletions(-) create mode 100644 desktop/macos/Desktop/Sources/MainWindow/MainChatNavigationRequest.swift create mode 100644 desktop/macos/Desktop/Tests/HomeRedesignRegressionTests.swift create mode 100644 desktop/macos/changelog/unreleased/20260721-chat-actions-keyboard-focus.json create mode 100644 desktop/macos/changelog/unreleased/20260721-continue-in-omi-opens-chat.json create mode 100644 desktop/macos/changelog/unreleased/20260721-home-hub-stray-click-fix.json diff --git a/desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarView.swift b/desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarView.swift index 8fffc586e81..b53ca2355d5 100644 --- a/desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarView.swift +++ b/desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarView.swift @@ -1691,7 +1691,7 @@ struct FloatingControlBarView: View { onEscape: onEscape, onOpenMainApp: { (window as? FloatingControlBarWindow)?.closeAIConversation() - (NSApp.delegate as? AppDelegate)?.openMainAppWindow() + (NSApp.delegate as? AppDelegate)?.openMainAppChat() }, onRate: onRate, onShareLink: onShareLink, @@ -2295,7 +2295,7 @@ private struct AgentMainChatView: View { HStack(spacing: OmiSpacing.xs) { Button { onEscape() - (NSApp.delegate as? AppDelegate)?.openMainAppWindow() + (NSApp.delegate as? AppDelegate)?.openMainAppChat() } label: { HStack(spacing: OmiSpacing.xs) { Text("Continue in Omi") diff --git a/desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift b/desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift index 03dfb26ceb1..ea8f9428300 100644 --- a/desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift +++ b/desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift @@ -25,6 +25,10 @@ struct ChatBubble: View { @State private var showRatingFeedback = false @State private var showInfoPopover = false @State private var lastSubmittedRating: Int? + // Shared across every metadata control: true while any of them holds + // keyboard focus, so Tab / Full Keyboard Access never lands on an + // invisible button. + @FocusState private var isMetadataControlFocused: Bool init( message: ChatMessage, app: OmiApp?, onRate: @escaping (Int?) -> Void, @@ -329,10 +333,18 @@ struct ChatBubble: View { } } // Quiet timeline: actions and timestamps only surface while the reader - // is on the message (or mid-interaction with them). - .opacity(isRowHovering || showRatingFeedback || showCopied || showInfoPopover ? 1 : 0) + // is on the message — by pointer hover or keyboard focus — or + // mid-interaction with them. + .opacity( + ChatBubbleMetadataReveal.isVisible( + hovering: isRowHovering, + controlFocused: isMetadataControlFocused, + transientFeedback: showRatingFeedback || showCopied || showInfoPopover + ) ? 1 : 0 + ) .omiAnimation(.easeInOut(duration: 0.12), value: isTimestampHovering) .omiAnimation(.easeInOut(duration: 0.15), value: isRowHovering) + .omiAnimation(.easeInOut(duration: 0.15), value: isMetadataControlFocused) } @ViewBuilder @@ -351,6 +363,7 @@ struct ChatBubble: View { .foregroundColor(message.rating == 1 ? OmiColors.accent : OmiColors.textTertiary) } .buttonStyle(.plain) + .focused($isMetadataControlFocused) .help("Helpful response") // Thumbs down @@ -366,6 +379,7 @@ struct ChatBubble: View { .foregroundColor(message.rating == -1 ? .red : OmiColors.textTertiary) } .buttonStyle(.plain) + .focused($isMetadataControlFocused) .help("Not helpful") if showRatingFeedback { @@ -408,6 +422,7 @@ struct ChatBubble: View { .foregroundColor(showCopied ? .green : OmiColors.textTertiary) } .buttonStyle(.plain) + .focused($isMetadataControlFocused) .help("Copy message") } @@ -422,6 +437,7 @@ struct ChatBubble: View { .foregroundColor(showInfoPopover ? OmiColors.textPrimary : OmiColors.textTertiary) } .buttonStyle(.plain) + .focused($isMetadataControlFocused) .help("View response context") .popover(isPresented: $showInfoPopover, arrowEdge: .bottom) { if let metadata = message.metadata { @@ -431,6 +447,16 @@ struct ChatBubble: View { } } +/// Visibility rule for the quiet timeline's per-message metadata row +/// (rating / copy / info / timestamp). Keyboard parity is part of the +/// contract: focus on any metadata control must reveal the row, otherwise +/// Tab / Full Keyboard Access ends up on an invisible button. +enum ChatBubbleMetadataReveal { + static func isVisible(hovering: Bool, controlFocused: Bool, transientFeedback: Bool) -> Bool { + hovering || controlFocused || transientFeedback + } +} + struct BackgroundAgentSummary: Equatable { let agentID: UUID? let prompt: String diff --git a/desktop/macos/Desktop/Sources/MainWindow/Dashboard/HomeKnowsComposer.swift b/desktop/macos/Desktop/Sources/MainWindow/Dashboard/HomeKnowsComposer.swift index 64ad05fd7cb..1cb453ed690 100644 --- a/desktop/macos/Desktop/Sources/MainWindow/Dashboard/HomeKnowsComposer.swift +++ b/desktop/macos/Desktop/Sources/MainWindow/Dashboard/HomeKnowsComposer.swift @@ -54,10 +54,13 @@ enum HomeKnowsListComposer { rows.append(HomeKnowsRow(kind: .task(id: task.id), text: task.text)) } + // Question rows are identified by their text, so a repeated suggestion + // would collide as a ForEach ID — keep only the first occurrence. + var seenQuestions = Set() let cleanQuestions = questions .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } + .filter { !$0.isEmpty && seenQuestions.insert($0).inserted } // Insights fill the middle; one slot stays reserved for a question so the // list always ends with something the user can ask. diff --git a/desktop/macos/Desktop/Sources/MainWindow/DesktopHomeView.swift b/desktop/macos/Desktop/Sources/MainWindow/DesktopHomeView.swift index c1f9684c170..6ebc0cb28a8 100644 --- a/desktop/macos/Desktop/Sources/MainWindow/DesktopHomeView.swift +++ b/desktop/macos/Desktop/Sources/MainWindow/DesktopHomeView.swift @@ -486,6 +486,11 @@ struct DesktopHomeView: View { notification in handleAutomationNavigation(notification) } + // "Continue in Omi" from the floating bar: switch to the Home tab; the + // dashboard consumes the pending request and opens the chat panel. + .onReceive(NotificationCenter.default.publisher(for: .openMainChatRequested)) { _ in + selectedIndex = SidebarNavItem.dashboard.rawValue + } } private func enforceMainWindowMinimumSize() { diff --git a/desktop/macos/Desktop/Sources/MainWindow/MainChatNavigationRequest.swift b/desktop/macos/Desktop/Sources/MainWindow/MainChatNavigationRequest.swift new file mode 100644 index 00000000000..3b871fe134e --- /dev/null +++ b/desktop/macos/Desktop/Sources/MainWindow/MainChatNavigationRequest.swift @@ -0,0 +1,32 @@ +import Foundation + +/// One-shot "open the main chat" request raised by surfaces outside the main +/// window (the floating bar's "Continue in Omi" affordances). Revealing the +/// window alone is not enough: the main window may be resting on any tab, so +/// the conversation the user asked to continue would be nowhere in sight. +/// +/// Flow: the raiser calls `request()` (which also posts +/// `.openMainChatRequested`); `DesktopHomeView` switches to the Home tab on +/// the notification, and `DashboardPage` consumes the pending request when it +/// is (or becomes) visible and opens the chat panel. +@MainActor +final class MainChatNavigationRequestStore { + static let shared = MainChatNavigationRequestStore() + + private(set) var isPending = false + + func request() { + isPending = true + NotificationCenter.default.post(name: .openMainChatRequested, object: nil) + } + + /// Returns whether a request was pending, and clears it. + func consume() -> Bool { + defer { isPending = false } + return isPending + } +} + +extension Notification.Name { + static let openMainChatRequested = Notification.Name("openMainChatRequested") +} diff --git a/desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift b/desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift index 71163a5d98a..1b633694075 100644 --- a/desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift +++ b/desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift @@ -461,7 +461,13 @@ struct DashboardPage: View { } } + // Split in two (`applyHomeLifecycle` → `applyHomeStageObservers`) so each + // modifier chain stays within the type-checker's budget. private func applyHomeLifecycle(to content: Content) -> some View { + applyHomeStageObservers(to: applyHomeLifecycleCore(to: content)) + } + + private func applyHomeLifecycleCore(to content: Content) -> some View { content .onAppear { if PostOnboardingPromptSuggestions.shouldShowPopup && !postOnboardingSuggestions.isEmpty { @@ -469,6 +475,7 @@ struct DashboardPage: View { } syncCaptureState() autoOpenChatForExistingHistoryIfNeeded() + consumePendingMainChatOpenRequest() reportHomeAutomationMode() intelligenceStore.setRecommendationActionHandler { recommendation in await openRecommendation(recommendation) @@ -516,6 +523,15 @@ struct DashboardPage: View { .onReceive(NotificationCenter.default.publisher(for: .screenCaptureKitBroken)) { _ in syncCaptureState() } + } + + private func applyHomeStageObservers(to content: Content) -> some View { + content + // "Continue in Omi" while the dashboard is already mounted; the + // not-yet-mounted case is covered by the consume in onAppear. + .onReceive(NotificationCenter.default.publisher(for: .openMainChatRequested)) { _ in + consumePendingMainChatOpenRequest() + } // Chat history is the home surface: as soon as the (async) history // load shows prior messages, land on the chat panel, not the greeting. .onChange(of: chatProvider.messages.count) { _, _ in @@ -657,10 +673,12 @@ struct DashboardPage: View { HomeCanvasBackground() // Clicking anywhere outside the chat / connect panel collapses - // back to the hub (panels and the ask bar consume their own - // clicks above this catcher). When chat history exists, chat IS the - // resting Home surface, so no catcher is mounted over it. - if homeMode != homeRestingMode { + // back to the resting surface (panels and the ask bar consume their + // own clicks above this catcher). When chat history exists, chat IS + // the resting Home surface, so no catcher is mounted over it — and + // the hub is never an overlay, so no catcher is ever mounted over + // the hub either (a stray click must not throw the user into chat). + if HomeStageMode.collapseCatcherActive(mode: homeMode, resting: homeRestingMode) { Color.black.opacity(0.001) .ignoresSafeArea() .contentShape(Rectangle()) @@ -703,8 +721,10 @@ struct DashboardPage: View { // Esc collapses the connect tray (and, with no chat history, the // inline chat) back to the resting surface — but only while no modal // overlay owns the key. Chat with history is Home itself and cannot - // be escaped. - if homeMode != homeRestingMode && !isHomeModalPresented { + // be escaped; the hub is likewise never escaped *into* a panel. + if HomeStageMode.collapseCatcherActive(mode: homeMode, resting: homeRestingMode) + && !isHomeModalPresented + { OverlayModalEscapeCatcher { collapseHomeStagePanel() } @@ -1151,7 +1171,9 @@ struct DashboardPage: View { /// Chat with history is the default Home surface: whenever prior messages /// exist, the hub greeting yields to the chat panel. Runs once per page - /// visit so an explicit Esc back to the hub is respected afterwards. + /// visit so the automation bridge's `home_close_panel` hub jump is not + /// immediately overridden by the next `messages.count` change. (There is no + /// user-facing path back to the hub once history exists — chat is Home.) private func autoOpenChatForExistingHistoryIfNeeded() { guard !didAutoOpenChatForHistory else { return } guard !useLegacyHomeDesign, homeMode == .hub, !chatProvider.messages.isEmpty else { return } @@ -1160,6 +1182,14 @@ struct DashboardPage: View { reportHomeAutomationMode() } + /// Floating-bar "Continue in Omi": land directly on the chat panel instead + /// of whatever surface Home was resting on. + private func consumePendingMainChatOpenRequest() { + guard MainChatNavigationRequestStore.shared.consume() else { return } + guard !useLegacyHomeDesign else { return } + openHomeChat() + } + private func openHomeChat(focusInput: Bool = true) { guard homeMode != .chat else { return } OmiMotion.withGated(Self.homeStageAnimation) { @@ -2123,11 +2153,20 @@ private enum HomeDestinationProminence { case quiet } -private enum HomeStageMode: Equatable { +enum HomeStageMode: Equatable { case hub case chat case connect + /// Whether the user-facing collapse catchers (click-outside + Esc) mount. + /// Only a panel that can collapse to a *different* resting surface gets a + /// catcher. The hub is the base surface, never an overlay: mounting a + /// catcher over hub-with-history would invert the gesture and make a stray + /// click or Esc *open* the chat. + static func collapseCatcherActive(mode: HomeStageMode, resting: HomeStageMode) -> Bool { + mode != resting && mode != .hub + } + var automationLabel: String { switch self { case .hub: return "hub" diff --git a/desktop/macos/Desktop/Sources/OmiApp.swift b/desktop/macos/Desktop/Sources/OmiApp.swift index f9ba1116ebb..c70c1d63374 100644 --- a/desktop/macos/Desktop/Sources/OmiApp.swift +++ b/desktop/macos/Desktop/Sources/OmiApp.swift @@ -1037,6 +1037,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, @unchecked S openMainAppWindow() } + /// "Continue in Omi": bring the main window forward *and* land on the chat + /// timeline, wherever the window was last resting. The pending request + /// survives window creation, so a freshly created window also lands on chat. + @MainActor func openMainAppChat() { + MainChatNavigationRequestStore.shared.request() + openMainAppWindow() + } + /// Bring the main Omi window to the front, creating it if needed. Shared by /// the menu-bar "Open Omi" item, the global Open Omi (formerly Ask Omi) /// shortcut, and the floating bar's "Continue in Omi" affordance. diff --git a/desktop/macos/Desktop/Tests/HomeKnowsComposerTests.swift b/desktop/macos/Desktop/Tests/HomeKnowsComposerTests.swift index 519c815f838..f75e299d5a5 100644 --- a/desktop/macos/Desktop/Tests/HomeKnowsComposerTests.swift +++ b/desktop/macos/Desktop/Tests/HomeKnowsComposerTests.swift @@ -71,4 +71,16 @@ final class HomeKnowsComposerTests: XCTestCase { func testEverythingEmptyProducesNoRows() { XCTAssertTrue(HomeKnowsListComposer.compose(tasks: [], insights: [], questions: []).isEmpty) } + + func testDuplicateQuestionsCollapseToOneRowWithUniqueIDs() { + // Question rows derive their ForEach ID from the text; a repeated + // suggestion must not produce two rows with a colliding ID. + let rows = HomeKnowsListComposer.compose( + tasks: [], insights: [], + questions: ["What should I do today?", " What should I do today? ", "Second question?"]) + + XCTAssertEqual(rows.count, 2) + XCTAssertEqual(rows.map(\.text), ["What should I do today?", "Second question?"]) + XCTAssertEqual(Set(rows.map(\.id)).count, rows.count) + } } diff --git a/desktop/macos/Desktop/Tests/HomeRedesignRegressionTests.swift b/desktop/macos/Desktop/Tests/HomeRedesignRegressionTests.swift new file mode 100644 index 00000000000..809590478a5 --- /dev/null +++ b/desktop/macos/Desktop/Tests/HomeRedesignRegressionTests.swift @@ -0,0 +1,71 @@ +import XCTest + +@testable import Omi_Computer + +/// Regressions from the chat-as-home redesign (#10184) and the floating-bar +/// typing removal (#10181). +final class HomeStageCollapseCatcherTests: XCTestCase { + func testChatWithHistoryIsRestingSoNoCatcherMounts() { + // Chat with history is Home itself: no click-outside / Esc catcher. + XCTAssertFalse(HomeStageMode.collapseCatcherActive(mode: .chat, resting: .chat)) + } + + func testHubNeverGetsACatcherEvenWhenChatIsResting() { + // Regression: with history present the hub differs from the resting mode, + // which used to mount the catchers over the hub — a stray click or Esc + // then *opened* the chat instead of leaving the user on the hub. + XCTAssertFalse(HomeStageMode.collapseCatcherActive(mode: .hub, resting: .chat)) + XCTAssertFalse(HomeStageMode.collapseCatcherActive(mode: .hub, resting: .hub)) + } + + func testNonRestingPanelsStillCollapse() { + // Empty-history chat and the connect tray remain escapable overlays. + XCTAssertTrue(HomeStageMode.collapseCatcherActive(mode: .chat, resting: .hub)) + XCTAssertTrue(HomeStageMode.collapseCatcherActive(mode: .connect, resting: .hub)) + XCTAssertTrue(HomeStageMode.collapseCatcherActive(mode: .connect, resting: .chat)) + } +} + +@MainActor +final class MainChatNavigationRequestStoreTests: XCTestCase { + func testRequestIsConsumedExactlyOnce() { + let store = MainChatNavigationRequestStore.shared + _ = store.consume() // clear any pending state from other tests + + XCTAssertFalse(store.consume()) + + store.request() + XCTAssertTrue(store.isPending) + XCTAssertTrue(store.consume()) + XCTAssertFalse(store.consume()) + } + + func testRequestPostsOpenMainChatNotification() { + let store = MainChatNavigationRequestStore.shared + _ = store.consume() + + let expectation = expectation( + forNotification: .openMainChatRequested, object: nil, notificationCenter: .default) + store.request() + wait(for: [expectation], timeout: 1) + _ = store.consume() + } +} + +final class ChatBubbleMetadataRevealTests: XCTestCase { + func testKeyboardFocusAloneRevealsMetadataRow() { + // Regression: the quiet-timeline redesign gated the row on pointer hover + // only, leaving Tab / Full Keyboard Access focused on invisible buttons. + XCTAssertTrue( + ChatBubbleMetadataReveal.isVisible(hovering: false, controlFocused: true, transientFeedback: false)) + } + + func testHiddenOnlyWhenNeitherHoveredNorFocusedNorMidInteraction() { + XCTAssertFalse( + ChatBubbleMetadataReveal.isVisible(hovering: false, controlFocused: false, transientFeedback: false)) + XCTAssertTrue( + ChatBubbleMetadataReveal.isVisible(hovering: true, controlFocused: false, transientFeedback: false)) + XCTAssertTrue( + ChatBubbleMetadataReveal.isVisible(hovering: false, controlFocused: false, transientFeedback: true)) + } +} diff --git a/desktop/macos/changelog/unreleased/20260721-chat-actions-keyboard-focus.json b/desktop/macos/changelog/unreleased/20260721-chat-actions-keyboard-focus.json new file mode 100644 index 00000000000..4282e2f1d55 --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260721-chat-actions-keyboard-focus.json @@ -0,0 +1,3 @@ +{ + "change": "Chat message actions and timestamps now also reveal on keyboard focus, not only mouse hover" +} diff --git a/desktop/macos/changelog/unreleased/20260721-continue-in-omi-opens-chat.json b/desktop/macos/changelog/unreleased/20260721-continue-in-omi-opens-chat.json new file mode 100644 index 00000000000..a8ebf7200e2 --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260721-continue-in-omi-opens-chat.json @@ -0,0 +1,3 @@ +{ + "change": "The floating bar's Continue in Omi button now lands on the chat in the main window instead of whatever page was last open" +} diff --git a/desktop/macos/changelog/unreleased/20260721-home-hub-stray-click-fix.json b/desktop/macos/changelog/unreleased/20260721-home-hub-stray-click-fix.json new file mode 100644 index 00000000000..0c4d7deafa2 --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260721-home-hub-stray-click-fix.json @@ -0,0 +1,3 @@ +{ + "change": "Fixed a stray click or Esc on the Home greeting unexpectedly opening the chat" +} diff --git a/desktop/macos/e2e/flows/home-stage.yaml b/desktop/macos/e2e/flows/home-stage.yaml index 69f1db68539..6ebd8e34804 100644 --- a/desktop/macos/e2e/flows/home-stage.yaml +++ b/desktop/macos/e2e/flows/home-stage.yaml @@ -7,6 +7,7 @@ covers: - desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift - desktop/macos/Desktop/Sources/MainWindow/Dashboard/HomeSuggestionsStore.swift - desktop/macos/Desktop/Sources/MainWindow/Dashboard/HomeKnowsComposer.swift + - desktop/macos/Desktop/Sources/MainWindow/MainChatNavigationRequest.swift - desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift preconditions: - automation_bridge_ready From 28c76587e84721ceb1ac0fe31cac2a8f75a931d9 Mon Sep 17 00:00:00 2001 From: Skander Karoui Date: Tue, 21 Jul 2026 13:23:37 -0400 Subject: [PATCH 2/3] test(desktop): update floating-bar tripwires for the retired typed composer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three AgentPillLifecycleTests source tripwires still pinned the typed follow-up composer that #10181 deleted, and have been red on main since it merged: testProviderRequestsStayInTheModelLoop and testSubagentComposerOnlyContinuesItsCanonicalSession expected manager.continueAgent(from: pill, ...), and testTypedSendDelegatesResponseSizingToWindow expected the view to call state.archiveCurrentExchange (it moved into FloatingControlBarWindow's query paths). The tripwires now pin the replacement contract: the composer's only affordance is openMainAppChat(), no view-side archive, no second send path. Verification: xcrun swift test --filter AgentPillLifecycleTests — 75/75 pass (was 72/75 on clean main; reproduced the 3 failures on main at 476feab2d7 before this change). --- .../Tests/AgentPillLifecycleTests.swift | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/desktop/macos/Desktop/Tests/AgentPillLifecycleTests.swift b/desktop/macos/Desktop/Tests/AgentPillLifecycleTests.swift index a461fac1801..eb7b6686064 100644 --- a/desktop/macos/Desktop/Tests/AgentPillLifecycleTests.swift +++ b/desktop/macos/Desktop/Tests/AgentPillLifecycleTests.swift @@ -71,9 +71,9 @@ import XCTest } func testMainChatSpawnReceiptProjectsTheExistingFloatingPill() throws { + let pillSource = try agentPillSource() let providerSource = try chatProviderSource() let viewSource = try floatingControlBarViewSource() - let pillSource = try agentPillSource() XCTAssertTrue(providerSource.contains("AgentPillsManager.shared.upsertSpawnedPill(")) XCTAssertTrue(providerSource.contains("producingJournalSurface: mainChatSurfaceReference()")) @@ -373,9 +373,9 @@ import XCTest XCTAssertFalse(windowSource.contains("resolveDelegationAndDispatch")) XCTAssertTrue(windowSource.contains("await dispatchChatQuery(")) XCTAssertFalse(source.contains("AgentPillFollowUpRoutingPolicy")) - // The bar has no typed pill composer since typing moved to the app's chat: - // pill steering routes to the main app instead of parsing wording locally. - XCTAssertTrue(source.contains("(NSApp.delegate as? AppDelegate)?.openMainAppWindow()")) + // The bar's typed follow-up composer was retired (#10181): its "Continue + // in Omi" affordance routes to the main chat instead of spawning. + XCTAssertTrue(source.contains("openMainAppChat()")) } func testSubagentChatRendersMarkdownAndLargeBackHitTarget() throws { @@ -835,13 +835,14 @@ import XCTest } func testSubagentFollowUpsOnlyContinueTheCanonicalSession() throws { - // omi-test-quality: source-inspection -- static contract: the bar's typed composer is gone; follow-ups continue only through the manager bound to the pill's canonical session. + // omi-test-quality: source-inspection -- static contract: the bar's typed composer is gone; the pill's only affordance opens the main chat. let viewSource = try floatingControlBarViewSource() - let pillSource = try agentPillSource() XCTAssertFalse(viewSource.contains("AgentPillFollowUpRoutingPolicy")) - XCTAssertTrue(pillSource.contains("guard pill.canonicalSessionId == sessionId else { return }")) - XCTAssertTrue(pillSource.contains("DesktopCoordinatorService.shared.continueAgent(")) + // Typed steering from the pill was retired (#10181): the composer's only + // affordance opens the main chat, so no second send path can exist. + XCTAssertFalse(viewSource.contains("manager.continueAgent(from:")) + XCTAssertTrue(viewSource.contains("openMainAppChat()")) } func testSpawnAgentToolCallOpensSubagentChat() throws { @@ -926,10 +927,10 @@ import XCTest let inputSource = String(viewSource[inputRange.lowerBound.. Date: Tue, 21 Jul 2026 13:41:45 -0400 Subject: [PATCH 3/3] chore: raise line-count baselines for the Home redesign bug fixes ChatBubble 1926->1952 (keyboard-focus reveal), DashboardPage 4543->4582 (catcher predicate + chat-landing consume + lifecycle chain split), OmiApp 1645->1653 (openMainAppChat). Justifications updated in the baseline files. --- .../desktop-swift-mainwindow.json | 8 ++++---- .../desktop-swift-root.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-mainwindow.json b/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-mainwindow.json index d4ab37d85cc..aa1df5f9119 100644 --- a/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-mainwindow.json +++ b/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-mainwindow.json @@ -1,16 +1,16 @@ { "files": { - "desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift": 1926, + "desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift": 1952, "desktop/macos/Desktop/Sources/MainWindow/Pages/AppsPage.swift": 3643, - "desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift": 4543, + "desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift": 4582, "desktop/macos/Desktop/Sources/MainWindow/Pages/MemoriesPage.swift": 3298, "desktop/macos/Desktop/Sources/MainWindow/Pages/TasksPage.swift": 5518, "desktop/macos/Desktop/Sources/MainWindow/SidebarView.swift": 1570 }, "raise_justifications": { - "desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift": "Keeps all chat tool groups compact and collapsed while streamed steps increment, with an explicit expansion policy, fixed collapsed height, and regression coverage.", + "desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift": "Metadata-row reveal now honors keyboard focus (shared FocusState + testable ChatBubbleMetadataReveal) so Full Keyboard Access never lands on invisible actions.", "desktop/macos/Desktop/Sources/MainWindow/Pages/AppsPage.swift": "Rebase reconciliation applies the pinned swift-format to the main-branch app-detail safety fixes; runtime behavior is unchanged.", - "desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift": "Home redesign: chat-as-home surface plus the 2nd-brain knows-list hub replace the wordmark/ribbon/WMN-card layout; a component split is tracked follow-up.", + "desktop/macos/Desktop/Sources/MainWindow/Pages/DashboardPage.swift": "Home redesign bug fixes: hub-safe collapse-catcher predicate, Continue-in-Omi chat landing consume, and a lifecycle chain split for the type-checker; component split remains tracked follow-up.", "desktop/macos/Desktop/Sources/MainWindow/Pages/MemoriesPage.swift": "Bridge memory search now awaits the active initial or pagination projection before refreshing, preventing an immediate post-navigation marker search from using stale lifecycle capability state.", "desktop/macos/Desktop/Sources/MainWindow/Pages/TasksPage.swift": "The Load-more-tasks action reads displayTasks.last at execution time via loadMoreTapped() instead of force-unwrapping (an emptied list no longer crashes); merged with main binding first-load/loading/error/retry state to the selected To Do or Done view.", "desktop/macos/Desktop/Sources/MainWindow/SidebarView.swift": "Strict concurrency imports annotate legacy AppKit image usage without changing runtime behavior." diff --git a/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-root.json b/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-root.json index 8cb958b2ca3..ab6b8525f8f 100644 --- a/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-root.json +++ b/.github/scripts/product_file_line_count_ratchet_baseline/desktop-swift-root.json @@ -4,13 +4,13 @@ "desktop/macos/Desktop/Sources/CloudConnectorFormAutomation.swift": 1678, "desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift": 4238, "desktop/macos/Desktop/Sources/MemoryExportService.swift": 1578, - "desktop/macos/Desktop/Sources/OmiApp.swift": 1638 + "desktop/macos/Desktop/Sources/OmiApp.swift": 1646 }, "raise_justifications": { "desktop/macos/Desktop/Sources/AuthService.swift": "Apple first-auth name capture signals observers (authNameDidUpdate) and persists the name to Firebase so it survives reinstalls.", "desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift": "Reach-error card gains a debug bridge action so the actionable failure surface is verifiable in-process.", "desktop/macos/Desktop/Sources/MemoryExportService.swift": "Pinned swift-format normalizes line layout without changing this source's runtime behavior.", - "desktop/macos/Desktop/Sources/OmiApp.swift": "shared openMainAppWindow helper for menu bar + Open Omi shortcut" + "desktop/macos/Desktop/Sources/OmiApp.swift": "openMainAppChat() gives the floating bar Continue-in-Omi affordances a chat-landing entry next to openMainAppWindow()." }, "threshold": 1500 }