diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8b4ea..d0a0b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### 🐛 Bug Fixes + +- Fix command palette / project switcher filtering while typing by reading the live field-editor text inside the floating panel, and route `⌘P` / `⌘⇧P` through the same mode-aware presentation path so both shortcuts now switch or dismiss consistently instead of maintaining separate code paths ([#84](https://github.com/vaayne/mori/issues/84)) + ## [0.4.1] - 2026-04-19 ### ✨ Features diff --git a/CHANGELOG.zh-Hans.md b/CHANGELOG.zh-Hans.md index 72553d7..c93d4fe 100644 --- a/CHANGELOG.zh-Hans.md +++ b/CHANGELOG.zh-Hans.md @@ -7,6 +7,10 @@ ## [Unreleased] +### 🐛 问题修复 + +- 修复命令面板 / 项目切换器在输入时读取不到浮动面板内的实时搜索文本;同时将 `⌘P` / `⌘⇧P` 统一走同一套带模式的展示逻辑,让两个快捷键在切换与关闭时行为保持一致,不再维护两条分叉代码路径 ([#84](https://github.com/vaayne/mori/issues/84)) + ## [0.4.1] - 2026-04-19 ### ✨ 新功能 diff --git a/Sources/Mori/App/AppDelegate.swift b/Sources/Mori/App/AppDelegate.swift index 872a71f..9fa87b6 100644 --- a/Sources/Mori/App/AppDelegate.swift +++ b/Sources/Mori/App/AppDelegate.swift @@ -177,7 +177,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent self?.showSettingsWindow() }, onOpenCommandPalette: { [weak self] in - self?.commandPaletteController?.toggle() + self?.commandPaletteController?.toggle(mode: .allItems) }, onRequestPaneOutput: { [weak self, weak manager] paneId, completion in guard let self, let manager else { @@ -1508,7 +1508,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } keyMonitorActionMap = [ - "commandPalette.toggle": { [weak palette] in palette?.toggle() }, + "commandPalette.toggle": { [weak palette] in palette?.toggle(mode: .allItems) }, "worktrees.create": { [weak self] in self?.showCreateWorktreePanel() }, "worktrees.cycleNext": { [weak self] in self?.workspaceManager?.cycleWorktree(forward: true) }, "worktrees.cyclePrevious": { [weak self] in self?.workspaceManager?.cycleWorktree(forward: false) }, @@ -1547,7 +1547,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent "settings.reload": { [weak self] in self?.terminalAreaController?.reloadConfig() }, "other.openProject": { [weak self] in self?.showAddProjectPanel() }, "other.agentDashboard": { [weak self] in self?.toggleAgentDashboardAction() }, - "other.projectSwitcher": { [weak self] in self?.commandPaletteController?.showProjectsOnly() }, + "other.projectSwitcher": { [weak self] in self?.commandPaletteController?.toggle(mode: .projectsOnly) }, ] // Register key monitor that dispatches via the key binding store diff --git a/Sources/Mori/App/CommandPaletteController.swift b/Sources/Mori/App/CommandPaletteController.swift index 66649dc..6809869 100644 --- a/Sources/Mori/App/CommandPaletteController.swift +++ b/Sources/Mori/App/CommandPaletteController.swift @@ -13,9 +13,10 @@ final class CommandPaletteController: NSWindowController { // MARK: - State - private var dataSource: CommandPaletteDataSource? + private let dataSource: CommandPaletteDataSource private var results: [CommandPaletteItem] = [] private var selectedIndex: Int = 0 + private var mode: Mode = .allItems // MARK: - Views @@ -24,6 +25,34 @@ final class CommandPaletteController: NSWindowController { private let scrollView = NSScrollView() private let containerView = NSView() + // MARK: - Presentation Mode + + enum Mode: Equatable { + case allItems + case projectsOnly + + var placeholder: String { + switch self { + case .allItems: + return .localized("Search projects, worktrees, windows, actions...") + case .projectsOnly: + return .localized("Switch project...") + } + } + + var itemFilter: (@Sendable (CommandPaletteItem) -> Bool)? { + switch self { + case .allItems: + return nil + case .projectsOnly: + return { item in + if case .project = item { return true } + return false + } + } + } + } + // MARK: - Layout Constants private enum Layout { @@ -63,9 +92,9 @@ final class CommandPaletteController: NSWindowController { panel.hidesOnDeactivate = true panel.becomesKeyOnlyIfNeeded = true - super.init(window: panel) - self.dataSource = CommandPaletteDataSource(appState: appState) + + super.init(window: panel) setupUI() } @@ -76,30 +105,23 @@ final class CommandPaletteController: NSWindowController { // MARK: - Public - /// Toggle palette visibility. - /// If the project-only filter is active, switch to full palette instead of dismissing. - func toggle() { - if let panel = window, panel.isVisible, dataSource?.itemFilter == nil { + /// Toggle palette visibility for the requested mode. + /// Re-pressing the same shortcut dismisses the panel; switching shortcuts swaps modes in place. + func toggle(mode requestedMode: Mode = .allItems) { + guard let panel = window else { return } + + if panel.isVisible, mode == requestedMode { dismiss() - } else { - show() + return } - } - /// Show palette filtered to projects only (Cmd+P). - func showProjectsOnly() { - dataSource?.itemFilter = { item in - if case .project = item { return true } - return false - } - searchField.placeholderString = .localized("Switch project...") - presentPalette() + show(mode: requestedMode) } - func show() { - // Clear filter for full palette - dataSource?.itemFilter = nil - searchField.placeholderString = .localized("Search projects, worktrees, windows, actions...") + func show(mode requestedMode: Mode = .allItems) { + mode = requestedMode + dataSource.itemFilter = requestedMode.itemFilter + searchField.placeholderString = requestedMode.placeholder presentPalette() } @@ -136,7 +158,7 @@ final class CommandPaletteController: NSWindowController { private func setupSearchField() { searchField.translatesAutoresizingMaskIntoConstraints = false - searchField.placeholderString = .localized("Search projects, worktrees, windows, actions...") + searchField.placeholderString = Mode.allItems.placeholder searchField.font = .systemFont(ofSize: Layout.searchFontSize) searchField.isBordered = false searchField.focusRingType = .none @@ -220,9 +242,9 @@ final class CommandPaletteController: NSWindowController { // MARK: - Results - private func updateResults() { - let query = searchField.stringValue - results = dataSource?.search(query: query) ?? [] + private func updateResults(query: String? = nil) { + let searchQuery = query ?? currentSearchQuery() + results = dataSource.search(query: searchQuery) selectedIndex = results.isEmpty ? -1 : 0 tableView.reloadData() @@ -234,6 +256,18 @@ final class CommandPaletteController: NSWindowController { resizePanel() } + private func currentSearchQuery(from notification: Notification? = nil) -> String { + // While the search field is actively being edited, AppKit keeps the live text in the + // shared field editor. Reading it directly avoids stale stringValue reads in the palette panel. + if let fieldEditor = notification?.userInfo?["NSFieldEditor"] as? NSTextView { + return fieldEditor.string + } + if let fieldEditor = searchField.currentEditor() { + return fieldEditor.string + } + return searchField.stringValue + } + private func resizePanel() { guard let panel = window else { return } let panelHeight = computePanelHeight() @@ -286,10 +320,13 @@ final class CommandPaletteController: NSWindowController { extension CommandPaletteController: NSTextFieldDelegate { func controlTextDidChange(_ notification: Notification) { - updateResults() + updateResults(query: currentSearchQuery(from: notification)) } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if textView.hasMarkedText() { + return false + } if commandSelector == #selector(NSResponder.moveUp(_:)) { moveSelectionUp() return true