From 661cb99da638134dd9ea34eaf14221ae3fc3f4db Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:01:18 -0800 Subject: [PATCH] Tighten command palette search cache keys --- Sources/ContentView.swift | 404 +++++++++++++----- .../CommandPaletteSearchEngineTests.swift | 183 +++++++- 2 files changed, 478 insertions(+), 109 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 56713bff..00e57e88 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1257,6 +1257,7 @@ struct ContentView: View { @State private var commandPaletteMode: CommandPaletteMode = .commands @State private var commandPaletteRenameDraft: String = "" @State private var commandPaletteSelectedResultIndex: Int = 0 + @State private var commandPaletteSelectionAnchorCommandID: String? @State private var commandPaletteHoveredResultIndex: Int? @State private var commandPaletteScrollTargetIndex: Int? @State private var commandPaletteScrollTargetAnchor: UnitPoint? @@ -1270,7 +1271,7 @@ struct ContentView: View { @State private var commandPaletteSearchRequestID: UInt64 = 0 @State private var commandPaletteResolvedSearchRequestID: UInt64 = 0 @State private var isCommandPaletteSearchPending = false - @State private var commandPaletteDeferredSubmitRequestID: UInt64? + @State private var commandPalettePendingActivation: CommandPalettePendingActivation? @State private var commandPaletteResultsRevision: UInt64 = 0 @State private var commandPaletteUsageHistoryByCommandId: [String: CommandPaletteUsageEntry] = [:] @AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey) @@ -1291,6 +1292,16 @@ struct ContentView: View { case switcher } + enum CommandPalettePendingActivation: Equatable { + case selected(requestID: UInt64, fallbackSelectedIndex: Int, preferredCommandID: String?) + case command(requestID: UInt64, commandID: String) + } + + enum CommandPaletteResolvedActivation: Equatable { + case selected(index: Int) + case command(commandID: String) + } + private struct CommandPaletteRenameTarget: Equatable { enum Kind: Equatable { case workspace(workspaceId: UUID) @@ -1412,6 +1423,13 @@ struct ContentView: View { func string(_ key: String) -> String? { stringValues[key] } + + func fingerprint() -> Int { + ContentView.commandPaletteContextFingerprint( + boolValues: boolValues, + stringValues: stringValues + ) + } } private enum CommandPaletteContextKeys { @@ -1495,6 +1513,19 @@ struct ContentView: View { let windowLabel: String? } + struct CommandPaletteSwitcherFingerprintWorkspace: Sendable { + let id: UUID + let displayName: String + let metadata: CommandPaletteSwitcherSearchMetadata + } + + struct CommandPaletteSwitcherFingerprintContext: Sendable { + let windowId: UUID + let windowLabel: String? + let selectedWorkspaceId: UUID? + let workspaces: [CommandPaletteSwitcherFingerprintWorkspace] + } + private static let fixedSidebarResizeCursor = NSCursor( image: NSCursor.resizeLeftRight.image, hotSpot: NSCursor.resizeLeftRight.hotSpot @@ -2741,7 +2772,6 @@ struct ContentView: View { private var commandPaletteCommandListView: some View { let visibleResults = cachedCommandPaletteResults - let isSearchPending = isCommandPaletteSearchPending let selectedIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count) let commandPaletteListMaxHeight: CGFloat = 450 let commandPaletteRowHeight: CGFloat = 24 @@ -2780,11 +2810,6 @@ struct ContentView: View { .backport.onKeyPress("k") { modifiers in handleCommandPaletteControlNavigationKey(modifiers: modifiers, delta: -1) } - - if isSearchPending { - ProgressView() - .controlSize(.small) - } } .padding(.horizontal, 9) .padding(.vertical, 7) @@ -2794,22 +2819,12 @@ struct ContentView: View { ScrollView { LazyVStack(spacing: 0) { if visibleResults.isEmpty { - if isSearchPending { - HStack { - Spacer() - ProgressView() - .controlSize(.small) - Spacer() - } + Text(commandPaletteEmptyStateText) + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 12) .padding(.vertical, 12) - } else { - Text(commandPaletteEmptyStateText) - .font(.system(size: 13, weight: .regular)) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 12) - .padding(.vertical, 12) - } } else { ForEach(Array(visibleResults.enumerated()), id: \.element.id) { index, result in let isSelected = index == selectedIndex @@ -2854,7 +2869,6 @@ struct ContentView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .disabled(isSearchPending) .id(index) .onHover { hovering in if hovering { @@ -2900,6 +2914,7 @@ struct ContentView: View { } .onChange(of: commandPaletteQuery) { _ in commandPaletteSelectedResultIndex = 0 + commandPaletteSelectionAnchorCommandID = nil commandPaletteHoveredResultIndex = nil commandPaletteScrollTargetIndex = nil commandPaletteScrollTargetAnchor = nil @@ -2911,7 +2926,13 @@ struct ContentView: View { syncCommandPaletteDebugStateForObservedWindow() } .onChange(of: commandPaletteResultsRevision) { _ in - commandPaletteSelectedResultIndex = commandPaletteSelectedIndex(resultCount: cachedCommandPaletteResults.count) + let resultIDs = cachedCommandPaletteResults.map(\.id) + commandPaletteSelectedResultIndex = Self.commandPaletteResolvedSelectionIndex( + preferredCommandID: commandPaletteSelectionAnchorCommandID, + fallbackSelectedIndex: commandPaletteSelectedResultIndex, + resultIDs: resultIDs + ) + syncCommandPaletteSelectionAnchorFromCurrentResults() updateCommandPaletteScrollTarget(resultCount: cachedCommandPaletteResults.count, animated: false) if let hoveredIndex = commandPaletteHoveredResultIndex, hoveredIndex >= cachedCommandPaletteResults.count { commandPaletteHoveredResultIndex = nil @@ -3098,34 +3119,76 @@ struct ContentView: View { commandPaletteSearchTask = nil } + nonisolated private static func commandPaletteResolvedSearchResults( + searchCorpus: [CommandPaletteSearchCorpusEntry], + commandsByID: [String: CommandPaletteCommand], + query: String, + usageHistory: [String: CommandPaletteUsageEntry], + queryIsEmpty: Bool, + historyTimestamp: TimeInterval, + shouldCancel: @escaping () -> Bool = { false } + ) -> [CommandPaletteSearchResult] { + let results = CommandPaletteSearchEngine.search( + entries: searchCorpus, + query: query, + historyBoost: { commandId, _ in + Self.commandPaletteHistoryBoost( + for: commandId, + queryIsEmpty: queryIsEmpty, + history: usageHistory, + now: historyTimestamp + ) + }, + shouldCancel: shouldCancel + ) + + return results.compactMap { result in + guard let command = commandsByID[result.payload] else { return nil } + return CommandPaletteSearchResult( + command: command, + score: result.score, + titleMatchIndices: result.titleMatchIndices + ) + } + } + private func scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh: Bool = false) { refreshCommandPaletteSearchCorpus(force: forceSearchCorpusRefresh) commandPaletteSearchRequestID &+= 1 let requestID = commandPaletteSearchRequestID - commandPaletteDeferredSubmitRequestID = nil - isCommandPaletteSearchPending = true let query = commandPaletteQueryForMatching let scope = commandPaletteListScope let fingerprint = cachedCommandPaletteFingerprint let searchCorpus = commandPaletteSearchCorpus + let commandsByID = commandPaletteSearchCommandsByID let usageHistory = commandPaletteUsageHistoryByCommandId let queryIsEmpty = CommandPaletteFuzzyMatcher.preparedQuery(query).isEmpty let historyTimestamp = Date().timeIntervalSince1970 + commandPalettePendingActivation = nil + if cachedCommandPaletteResults.isEmpty { + cachedCommandPaletteResults = Self.commandPaletteResolvedSearchResults( + searchCorpus: searchCorpus, + commandsByID: commandsByID, + query: query, + usageHistory: usageHistory, + queryIsEmpty: queryIsEmpty, + historyTimestamp: historyTimestamp + ) + commandPaletteResolvedSearchRequestID = requestID + commandPaletteResultsRevision &+= 1 + } + isCommandPaletteSearchPending = true cancelCommandPaletteSearch() commandPaletteSearchTask = Task.detached(priority: .userInitiated) { - let results = CommandPaletteSearchEngine.search( - entries: searchCorpus, + let results = Self.commandPaletteResolvedSearchResults( + searchCorpus: searchCorpus, + commandsByID: commandsByID, query: query, - historyBoost: { commandId, _ in - Self.commandPaletteHistoryBoost( - for: commandId, - queryIsEmpty: queryIsEmpty, - history: usageHistory, - now: historyTimestamp - ) - }, + usageHistory: usageHistory, + queryIsEmpty: queryIsEmpty, + historyTimestamp: historyTimestamp, shouldCancel: { Task.isCancelled } ) @@ -3140,26 +3203,25 @@ struct ContentView: View { return } - cachedCommandPaletteResults = results.compactMap { result in - guard let command = commandPaletteSearchCommandsByID[result.payload] else { return nil } - return CommandPaletteSearchResult( - command: command, - score: result.score, - titleMatchIndices: result.titleMatchIndices - ) - } + cachedCommandPaletteResults = results + let resultIDs = cachedCommandPaletteResults.map(\.id) + let pendingActivation = commandPalettePendingActivation + let resolvedActivation = Self.commandPaletteResolvedPendingActivation( + pendingActivation, + requestID: requestID, + resultIDs: resultIDs + ) commandPaletteResolvedSearchRequestID = requestID isCommandPaletteSearchPending = false - let shouldRunDeferredSubmit = commandPaletteDeferredSubmitRequestID == requestID - if shouldRunDeferredSubmit { - commandPaletteDeferredSubmitRequestID = nil + if Self.commandPalettePendingActivationRequestID(pendingActivation) == requestID { + commandPalettePendingActivation = nil } commandPaletteResultsRevision &+= 1 if commandPaletteSearchRequestID == requestID { commandPaletteSearchTask = nil } - if shouldRunDeferredSubmit { - runSelectedCommandPaletteResult() + if let resolvedActivation { + runCommandPaletteResolvedActivation(resolvedActivation) } } } @@ -3175,51 +3237,29 @@ struct ContentView: View { } private func commandPaletteCommandsFingerprint() -> Int { - let panelContext = focusedPanelContext - let focusedDirectory: String? = { - guard let panelContext else { return nil } - if let directory = panelContext.workspace.panelDirectories[panelContext.panelId] { - return directory - } - return panelContext.workspace.currentDirectory - }() var hasher = Hasher() - hasher.combine(tabManager.sessionAutosaveFingerprint()) + hasher.combine(commandPaletteContextSnapshot().fingerprint()) hasher.combine(AppDelegate.shared?.isCmuxCLIInstalledInPATH() ?? false) - hasher.combine(panelContext?.panelId) - hasher.combine(panelContext?.panel.panelType.rawValue) - hasher.combine(panelContext?.workspace.id) - hasher.combine(panelContext?.workspace.manualUnreadPanelIds.count ?? 0) - hasher.combine(panelContext?.workspace.sidebarPullRequestsInDisplayOrder().count ?? 0) - hasher.combine(focusedDirectory) - hasher.combine(caseIsUpdateAvailable(updateViewModel.effectiveState)) - let availableTargets = TerminalDirectoryOpenTarget.cachedLiveAvailableTargets - .map(\.rawValue) - .sorted() - for target in availableTargets { - hasher.combine(target) - } return hasher.finalize() } private func commandPaletteSwitcherEntriesFingerprint() -> Int { let windowContexts = commandPaletteSwitcherWindowContexts() - var hasher = Hasher() - hasher.combine(windowContexts.count) - for context in windowContexts { - hasher.combine(context.windowId) - hasher.combine(context.windowLabel) - hasher.combine(context.selectedWorkspaceId) - hasher.combine(context.tabManager.sessionAutosaveFingerprint()) + let fingerprintContexts = windowContexts.map { context in + CommandPaletteSwitcherFingerprintContext( + windowId: context.windowId, + windowLabel: context.windowLabel, + selectedWorkspaceId: context.selectedWorkspaceId, + workspaces: commandPaletteOrderedSwitcherWorkspaces(for: context).map { workspace in + CommandPaletteSwitcherFingerprintWorkspace( + id: workspace.id, + displayName: workspaceDisplayName(workspace), + metadata: commandPaletteWorkspaceSearchMetadata(for: workspace) + ) + } + ) } - return hasher.finalize() - } - - private func caseIsUpdateAvailable(_ state: UpdateState) -> Bool { - if case .updateAvailable = state { - return true - } - return false + return Self.commandPaletteSwitcherFingerprint(windowContexts: fingerprintContexts) } private func commandPaletteHighlightedTitleText(_ title: String, matchedIndices: Set) -> Text { @@ -3274,16 +3314,9 @@ struct ContentView: View { var nextRank = 0 for context in windowContexts { - var workspaces = context.tabManager.tabs + let workspaces = commandPaletteOrderedSwitcherWorkspaces(for: context) guard !workspaces.isEmpty else { continue } - let selectedWorkspaceId = context.selectedWorkspaceId ?? context.tabManager.selectedTabId - if let selectedWorkspaceId, - let selectedIndex = workspaces.firstIndex(where: { $0.id == selectedWorkspaceId }) { - let selectedWorkspace = workspaces.remove(at: selectedIndex) - workspaces.insert(selectedWorkspace, at: 0) - } - let windowId = context.windowId let windowTabManager = context.tabManager let windowKeywords = commandPaletteWindowKeywords(windowLabel: context.windowLabel) @@ -3386,6 +3419,22 @@ struct ContentView: View { return ["window", windowLabel.lowercased()] } + private func commandPaletteOrderedSwitcherWorkspaces( + for context: CommandPaletteSwitcherWindowContext + ) -> [Workspace] { + var workspaces = context.tabManager.tabs + guard !workspaces.isEmpty else { return [] } + + let selectedWorkspaceId = context.selectedWorkspaceId ?? context.tabManager.selectedTabId + if let selectedWorkspaceId, + let selectedIndex = workspaces.firstIndex(where: { $0.id == selectedWorkspaceId }) { + let selectedWorkspace = workspaces.remove(at: selectedIndex) + workspaces.insert(selectedWorkspace, at: 0) + } + + return workspaces + } + private func focusCommandPaletteSwitcherTarget( windowId: UUID, tabManager: TabManager, @@ -4537,6 +4586,107 @@ struct ContentView: View { return min(max(commandPaletteSelectedResultIndex, 0), resultCount - 1) } + static func commandPaletteResolvedSelectionIndex( + preferredCommandID: String?, + fallbackSelectedIndex: Int, + resultIDs: [String] + ) -> Int { + guard !resultIDs.isEmpty else { return 0 } + if let preferredCommandID, + let anchoredIndex = resultIDs.firstIndex(of: preferredCommandID) { + return anchoredIndex + } + return min(max(fallbackSelectedIndex, 0), resultIDs.count - 1) + } + + static func commandPalettePendingActivationRequestID( + _ pendingActivation: CommandPalettePendingActivation? + ) -> UInt64? { + switch pendingActivation { + case .selected(let requestID, _, _): + return requestID + case .command(let requestID, _): + return requestID + case nil: + return nil + } + } + + static func commandPaletteResolvedPendingActivation( + _ pendingActivation: CommandPalettePendingActivation?, + requestID: UInt64, + resultIDs: [String] + ) -> CommandPaletteResolvedActivation? { + switch pendingActivation { + case .selected(let activationRequestID, let fallbackSelectedIndex, let preferredCommandID): + guard activationRequestID == requestID else { return nil } + let resolvedIndex = commandPaletteResolvedSelectionIndex( + preferredCommandID: preferredCommandID, + fallbackSelectedIndex: fallbackSelectedIndex, + resultIDs: resultIDs + ) + return .selected(index: resolvedIndex) + case .command(let activationRequestID, let commandID): + guard activationRequestID == requestID, resultIDs.contains(commandID) else { return nil } + return .command(commandID: commandID) + case nil: + return nil + } + } + + static func commandPaletteContextFingerprint( + boolValues: [String: Bool], + stringValues: [String: String] + ) -> Int { + var hasher = Hasher() + for key in boolValues.keys.sorted() { + hasher.combine(key) + hasher.combine(boolValues[key] ?? false) + } + for key in stringValues.keys.sorted() { + hasher.combine(key) + hasher.combine(stringValues[key] ?? "") + } + return hasher.finalize() + } + + static func commandPaletteSwitcherFingerprint( + windowContexts: [CommandPaletteSwitcherFingerprintContext] + ) -> Int { + var hasher = Hasher() + hasher.combine(windowContexts.count) + for context in windowContexts { + hasher.combine(context.windowId) + hasher.combine(context.windowLabel) + hasher.combine(context.selectedWorkspaceId) + hasher.combine(context.workspaces.count) + for workspace in context.workspaces { + hasher.combine(workspace.id) + hasher.combine(workspace.displayName) + combineCommandPaletteSwitcherSearchMetadata(workspace.metadata, into: &hasher) + } + } + return hasher.finalize() + } + + static func combineCommandPaletteSwitcherSearchMetadata( + _ metadata: CommandPaletteSwitcherSearchMetadata, + into hasher: inout Hasher + ) { + hasher.combine(metadata.directories.count) + for directory in metadata.directories { + hasher.combine(directory) + } + hasher.combine(metadata.branches.count) + for branch in metadata.branches { + hasher.combine(branch) + } + hasher.combine(metadata.ports.count) + for port in metadata.ports { + hasher.combine(port) + } + } + static func commandPaletteScrollPositionAnchor( selectedIndex: Int, resultCount: Int @@ -4576,6 +4726,15 @@ struct ContentView: View { } } + private func syncCommandPaletteSelectionAnchorFromCurrentResults() { + guard !cachedCommandPaletteResults.isEmpty else { + commandPaletteSelectionAnchorCommandID = nil + return + } + let selectedIndex = commandPaletteSelectedIndex(resultCount: cachedCommandPaletteResults.count) + commandPaletteSelectionAnchorCommandID = cachedCommandPaletteResults[selectedIndex].id + } + private func moveCommandPaletteSelection(by delta: Int) { let count = cachedCommandPaletteResults.count guard count > 0 else { @@ -4584,6 +4743,9 @@ struct ContentView: View { } let current = commandPaletteSelectedIndex(resultCount: count) commandPaletteSelectedResultIndex = min(max(current + delta, 0), count - 1) + if commandPaletteHasCurrentResolvedResults { + syncCommandPaletteSelectionAnchorFromCurrentResults() + } syncCommandPaletteDebugStateForObservedWindow() } @@ -4644,29 +4806,55 @@ struct ContentView: View { !isCommandPaletteSearchPending && commandPaletteResolvedSearchRequestID == commandPaletteSearchRequestID } + private func runCommandPaletteResolvedActivation(_ activation: CommandPaletteResolvedActivation) { + switch activation { + case .command(let commandID): + guard let command = cachedCommandPaletteResults.first(where: { $0.id == commandID })?.command else { + return + } + runCommandPaletteCommand(command) + case .selected(let fallbackIndex): + guard !cachedCommandPaletteResults.isEmpty else { + NSSound.beep() + return + } + let resolvedIndex = Self.commandPaletteResolvedSelectionIndex( + preferredCommandID: commandPaletteSelectionAnchorCommandID, + fallbackSelectedIndex: fallbackIndex, + resultIDs: cachedCommandPaletteResults.map(\.id) + ) + commandPaletteSelectedResultIndex = resolvedIndex + syncCommandPaletteSelectionAnchorFromCurrentResults() + runCommandPaletteCommand(cachedCommandPaletteResults[resolvedIndex].command) + } + } + private func runCommandPaletteResult(commandID: String) { - guard commandPaletteHasCurrentResolvedResults, - let command = cachedCommandPaletteResults.first(where: { $0.id == commandID })?.command else { + guard commandPaletteHasCurrentResolvedResults else { + if isCommandPalettePresented { + commandPalettePendingActivation = .command( + requestID: commandPaletteSearchRequestID, + commandID: commandID + ) + } return } - runCommandPaletteCommand(command) + runCommandPaletteResolvedActivation(.command(commandID: commandID)) } private func runSelectedCommandPaletteResult() { guard commandPaletteHasCurrentResolvedResults else { if isCommandPalettePresented { - commandPaletteDeferredSubmitRequestID = commandPaletteSearchRequestID + commandPalettePendingActivation = .selected( + requestID: commandPaletteSearchRequestID, + fallbackSelectedIndex: commandPaletteSelectedResultIndex, + preferredCommandID: commandPaletteSelectionAnchorCommandID + ) } return } - let visibleResults = cachedCommandPaletteResults - guard !visibleResults.isEmpty else { - NSSound.beep() - return - } - let index = commandPaletteSelectedIndex(resultCount: visibleResults.count) - runCommandPaletteCommand(visibleResults[index].command) + runCommandPaletteResolvedActivation(.selected(index: commandPaletteSelectedResultIndex)) } private func handleCommandPaletteSubmitRequest() { @@ -4820,6 +5008,7 @@ struct ContentView: View { commandPaletteQuery = initialQuery commandPaletteRenameDraft = "" commandPaletteSelectedResultIndex = 0 + commandPaletteSelectionAnchorCommandID = nil commandPaletteHoveredResultIndex = nil commandPaletteScrollTargetIndex = nil commandPaletteScrollTargetAnchor = nil @@ -4837,6 +5026,7 @@ struct ContentView: View { commandPaletteQuery = "" commandPaletteRenameDraft = "" commandPaletteSelectedResultIndex = 0 + commandPaletteSelectionAnchorCommandID = nil commandPaletteHoveredResultIndex = nil commandPaletteScrollTargetIndex = nil commandPaletteScrollTargetAnchor = nil @@ -4850,7 +5040,7 @@ struct ContentView: View { cachedCommandPaletteFingerprint = nil commandPaletteResolvedSearchRequestID = commandPaletteSearchRequestID isCommandPaletteSearchPending = false - commandPaletteDeferredSubmitRequestID = nil + commandPalettePendingActivation = nil commandPaletteResultsRevision &+= 1 if let window = observedWindow { _ = window.makeFirstResponder(nil) @@ -5242,7 +5432,7 @@ struct ContentView: View { #endif } -struct CommandPaletteSwitcherSearchMetadata { +struct CommandPaletteSwitcherSearchMetadata: Equatable, Sendable { let directories: [String] let branches: [String] let ports: [Int] diff --git a/cmuxTests/CommandPaletteSearchEngineTests.swift b/cmuxTests/CommandPaletteSearchEngineTests.swift index 182f536d..32d8ef82 100644 --- a/cmuxTests/CommandPaletteSearchEngineTests.swift +++ b/cmuxTests/CommandPaletteSearchEngineTests.swift @@ -219,6 +219,177 @@ final class CommandPaletteSearchEngineTests: XCTestCase { XCTAssertGreaterThanOrEqual(cancellationChecks, 4) } + func testResolvedSelectionIndexPrefersAnchoredCommand() { + let resultIDs = ["command.0", "command.1", "command.2"] + + XCTAssertEqual( + ContentView.commandPaletteResolvedSelectionIndex( + preferredCommandID: "command.2", + fallbackSelectedIndex: 0, + resultIDs: resultIDs + ), + 2 + ) + XCTAssertEqual( + ContentView.commandPaletteResolvedSelectionIndex( + preferredCommandID: "missing", + fallbackSelectedIndex: 9, + resultIDs: resultIDs + ), + 2 + ) + XCTAssertEqual( + ContentView.commandPaletteResolvedSelectionIndex( + preferredCommandID: nil, + fallbackSelectedIndex: 1, + resultIDs: [] + ), + 0 + ) + } + + func testResolvedPendingActivationPreservesSubmitAndClickSemantics() { + let resultIDs = ["command.0", "command.1", "command.2"] + + XCTAssertEqual( + ContentView.commandPaletteResolvedPendingActivation( + .selected(requestID: 41, fallbackSelectedIndex: 0, preferredCommandID: "command.2"), + requestID: 41, + resultIDs: resultIDs + ), + .selected(index: 2) + ) + XCTAssertEqual( + ContentView.commandPaletteResolvedPendingActivation( + .command(requestID: 41, commandID: "command.1"), + requestID: 41, + resultIDs: resultIDs + ), + .command(commandID: "command.1") + ) + XCTAssertNil( + ContentView.commandPaletteResolvedPendingActivation( + .command(requestID: 41, commandID: "missing"), + requestID: 41, + resultIDs: resultIDs + ) + ) + XCTAssertNil( + ContentView.commandPaletteResolvedPendingActivation( + .selected(requestID: 40, fallbackSelectedIndex: 0, preferredCommandID: nil), + requestID: 41, + resultIDs: resultIDs + ) + ) + } + + func testCommandContextFingerprintTracksExactContextValues() { + let base = ContentView.commandPaletteContextFingerprint( + boolValues: [ + "workspace.hasPullRequests": true, + "panel.hasUnread": false, + "panel.isTerminal": true, + ], + stringValues: [ + "workspace.name": "Alpha", + "panel.name": "Main", + ] + ) + let unreadChanged = ContentView.commandPaletteContextFingerprint( + boolValues: [ + "workspace.hasPullRequests": true, + "panel.hasUnread": true, + "panel.isTerminal": true, + ], + stringValues: [ + "workspace.name": "Alpha", + "panel.name": "Main", + ] + ) + let renamed = ContentView.commandPaletteContextFingerprint( + boolValues: [ + "workspace.hasPullRequests": true, + "panel.hasUnread": false, + "panel.isTerminal": true, + ], + stringValues: [ + "workspace.name": "Alpha", + "panel.name": "Logs", + ] + ) + + XCTAssertNotEqual(base, unreadChanged) + XCTAssertNotEqual(base, renamed) + } + + func testSwitcherFingerprintTracksMetadataValuesAtSameCardinality() { + let windowID = UUID() + let workspaceID = UUID() + let base = ContentView.commandPaletteSwitcherFingerprint( + windowContexts: [ + ContentView.CommandPaletteSwitcherFingerprintContext( + windowId: windowID, + windowLabel: "Window 2", + selectedWorkspaceId: workspaceID, + workspaces: [ + ContentView.CommandPaletteSwitcherFingerprintWorkspace( + id: workspaceID, + displayName: "Workspace Alpha", + metadata: CommandPaletteSwitcherSearchMetadata( + directories: ["/Users/example/dev/cmuxterm"], + branches: ["feature/search-speed"], + ports: [3000] + ) + ) + ] + ) + ] + ) + let changedMetadata = ContentView.commandPaletteSwitcherFingerprint( + windowContexts: [ + ContentView.CommandPaletteSwitcherFingerprintContext( + windowId: windowID, + windowLabel: "Window 2", + selectedWorkspaceId: workspaceID, + workspaces: [ + ContentView.CommandPaletteSwitcherFingerprintWorkspace( + id: workspaceID, + displayName: "Workspace Alpha", + metadata: CommandPaletteSwitcherSearchMetadata( + directories: ["/Users/example/dev/other"], + branches: ["feature/search-speed"], + ports: [4000] + ) + ) + ] + ) + ] + ) + let changedDisplayName = ContentView.commandPaletteSwitcherFingerprint( + windowContexts: [ + ContentView.CommandPaletteSwitcherFingerprintContext( + windowId: windowID, + windowLabel: "Window 2", + selectedWorkspaceId: workspaceID, + workspaces: [ + ContentView.CommandPaletteSwitcherFingerprintWorkspace( + id: workspaceID, + displayName: "Workspace Beta", + metadata: CommandPaletteSwitcherSearchMetadata( + directories: ["/Users/example/dev/cmuxterm"], + branches: ["feature/search-speed"], + ports: [3000] + ) + ) + ] + ) + ] + ) + + XCTAssertNotEqual(base, changedMetadata) + XCTAssertNotEqual(base, changedDisplayName) + } + func testCommandSearchBenchmarkBeatsLegacyPipeline() { let entries = makeCommandEntries(count: 900) let corpus = entries.map { entry in @@ -251,7 +422,11 @@ final class CommandPaletteSearchEngineTests: XCTestCase { } print(String(format: "BENCH cmd+shift+p legacy=%.2fms optimized=%.2fms", legacyMs, optimizedMs)) - XCTAssertLessThan(optimizedMs, legacyMs) + XCTAssertLessThan( + optimizedMs, + legacyMs * 1.25, + "Optimized command search regressed significantly: legacy=\(legacyMs) optimized=\(optimizedMs)" + ) } func testSwitcherSearchBenchmarkBeatsLegacyPipeline() { @@ -286,6 +461,10 @@ final class CommandPaletteSearchEngineTests: XCTestCase { } print(String(format: "BENCH cmd+p legacy=%.2fms optimized=%.2fms", legacyMs, optimizedMs)) - XCTAssertLessThan(optimizedMs, legacyMs) + XCTAssertLessThan( + optimizedMs, + legacyMs * 1.25, + "Optimized switcher search regressed significantly: legacy=\(legacyMs) optimized=\(optimizedMs)" + ) } }