Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

## [Unreleased]

### 🐛 问题修复

- 修复命令面板 / 项目切换器在输入时读取不到浮动面板内的实时搜索文本;同时将 `⌘P` / `⌘⇧P` 统一走同一套带模式的展示逻辑,让两个快捷键在切换与关闭时行为保持一致,不再维护两条分叉代码路径 ([#84](https://github.com/vaayne/mori/issues/84))

## [0.4.1] - 2026-04-19

### ✨ 新功能
Expand Down
6 changes: 3 additions & 3 deletions Sources/Mori/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) },
Expand Down Expand Up @@ -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
Expand Down
91 changes: 64 additions & 27 deletions Sources/Mori/App/CommandPaletteController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}

Expand All @@ -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()
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Loading