diff --git a/Sources/CodeIsland/AppState.swift b/Sources/CodeIsland/AppState.swift index 2dbb0a4..4a396ce 100644 --- a/Sources/CodeIsland/AppState.swift +++ b/Sources/CodeIsland/AppState.swift @@ -38,6 +38,8 @@ final class AppState { private var completionQueue: [String] = [] /// Mouse must enter the panel before auto-collapse is allowed (prevents instant dismiss) var completionHasBeenEntered = false + /// Auto-collapse timer fired but mouse is inside panel — defer collapse until mouse leaves + var deferCollapseOnMouseLeave = false private var processMonitors: [String: (source: DispatchSourceProcess, process: ProcessIdentity)] = [:] private var exitingSessions: [String: ProcessIdentity] = [:] private var saveTimer: Timer? @@ -621,6 +623,7 @@ final class AppState { activeSessionId = sessionId surface = .completionCard(sessionId: sessionId) completionHasBeenEntered = false + deferCollapseOnMouseLeave = false autoCollapseTask?.cancel() autoCollapseTask = Task { @MainActor in @@ -633,9 +636,15 @@ final class AppState { func cancelCompletionQueue() { autoCollapseTask?.cancel() completionQueue.removeAll() + deferCollapseOnMouseLeave = false } private func showNextCompletionOrCollapse() { + // Once the mouse has entered the completion card, defer collapse until it leaves + if completionHasBeenEntered { + deferCollapseOnMouseLeave = true + return + } // showNextPending handles: interactive items first, then completionQueue, then collapse if showNextPending() { return } withAnimation(NotchAnimation.close) { diff --git a/Sources/CodeIsland/NotchPanelView.swift b/Sources/CodeIsland/NotchPanelView.swift index 91ab9ba..8020f1f 100644 --- a/Sources/CodeIsland/NotchPanelView.swift +++ b/Sources/CodeIsland/NotchPanelView.swift @@ -219,13 +219,14 @@ struct NotchPanelView: View { // Completion card: mark entered on hover-in, block collapse until entered if hovering { appState.completionHasBeenEntered = true - } else if appState.completionHasBeenEntered { - // Mouse entered then left — allow collapse + } else if appState.completionHasBeenEntered || appState.deferCollapseOnMouseLeave { + // Mouse entered then left — allow collapse (immediate or deferred) hoverTimer?.invalidate() hoverTimer = nil + appState.deferCollapseOnMouseLeave = false + appState.cancelCompletionQueue() withAnimation(NotchAnimation.close) { appState.surface = .collapsed - appState.cancelCompletionQueue() } } return