From dcce58dfc3548f8e48b06e7871464896e13cbb51 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Tue, 21 Apr 2026 11:18:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix:=20restore=20command=20p?= =?UTF-8?q?alette=20filtering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the live query from the search field's field editor while the palette is active instead of relying on NSTextField.stringValue, which can lag inside the nonactivating panel. This makes ⌘P / ⌘⇧P filtering stable again and avoids stealing arrow/return handling from IME marked text. Fixes #84 Assisted-by: pi:gpt-5.4 --- CHANGELOG.md | 4 ++++ CHANGELOG.zh-Hans.md | 4 ++++ .../Mori/App/CommandPaletteController.swift | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8b4eab..63ac1714 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, so `⌘P` and `⌘⇧P` no longer intermittently collapse to an empty result list ([#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 72553d71..d427ac97 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/CommandPaletteController.swift b/Sources/Mori/App/CommandPaletteController.swift index 66649dcb..9c926f2d 100644 --- a/Sources/Mori/App/CommandPaletteController.swift +++ b/Sources/Mori/App/CommandPaletteController.swift @@ -220,9 +220,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 +234,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 +298,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 From 8696587275271a7b566a5a9a62640d6a673672f9 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Tue, 21 Apr 2026 11:46:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20unify=20co?= =?UTF-8?q?mmand=20palette=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Model the command palette as a single mode-aware controller instead of separate full/project-only entry points. Cmd+P and Cmd+Shift+P now share the same toggle path, so pressing the active shortcut dismisses the panel and switching shortcuts swaps modes in place. Assisted-by: pi:gpt-5.4 --- CHANGELOG.md | 2 +- CHANGELOG.zh-Hans.md | 2 +- Sources/Mori/App/AppDelegate.swift | 6 +- .../Mori/App/CommandPaletteController.swift | 70 ++++++++++++------- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ac1714..d0a0b7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 🐛 Bug Fixes -- Fix command palette / project switcher filtering while typing by reading the live field-editor text inside the floating panel, so `⌘P` and `⌘⇧P` no longer intermittently collapse to an empty result list ([#84](https://github.com/vaayne/mori/issues/84)) +- 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 diff --git a/CHANGELOG.zh-Hans.md b/CHANGELOG.zh-Hans.md index d427ac97..c93d4fe3 100644 --- a/CHANGELOG.zh-Hans.md +++ b/CHANGELOG.zh-Hans.md @@ -9,7 +9,7 @@ ### 🐛 问题修复 -- 修复命令面板 / 项目切换器在输入时读取不到浮动面板内的实时搜索文本,解决 `⌘P` 和 `⌘⇧P` 输入后结果偶发性全部消失的问题 ([#84](https://github.com/vaayne/mori/issues/84)) +- 修复命令面板 / 项目切换器在输入时读取不到浮动面板内的实时搜索文本;同时将 `⌘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 872a71f8..9fa87b66 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 9c926f2d..68098695 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 @@ -222,7 +244,7 @@ final class CommandPaletteController: NSWindowController { private func updateResults(query: String? = nil) { let searchQuery = query ?? currentSearchQuery() - results = dataSource?.search(query: searchQuery) ?? [] + results = dataSource.search(query: searchQuery) selectedIndex = results.isEmpty ? -1 : 0 tableView.reloadData()