Merge pull request #362 from manaflow-ai/task-cmd-p-show-all-windows

Cmd+P switcher includes workspaces from every window
This commit is contained in:
Lawrence Chen 2026-02-23 04:33:36 -08:00 committed by GitHub
commit 47d0b60cd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 295 additions and 61 deletions

View file

@ -1328,6 +1328,13 @@ struct ContentView: View {
var id: String { command.id }
}
private struct CommandPaletteSwitcherWindowContext {
let windowId: UUID
let tabManager: TabManager
let selectedWorkspaceId: UUID?
let windowLabel: String?
}
private static let fixedSidebarResizeCursor = NSCursor(
image: NSCursor.resizeLeftRight.image,
hotSpot: NSCursor.resizeLeftRight.hotSpot
@ -2792,94 +2799,193 @@ struct ContentView: View {
}
private func commandPaletteSwitcherEntries() -> [CommandPaletteCommand] {
var workspaces = tabManager.tabs
guard !workspaces.isEmpty else { return [] }
if let selectedWorkspaceId = tabManager.selectedTabId,
let selectedIndex = workspaces.firstIndex(where: { $0.id == selectedWorkspaceId }) {
let selectedWorkspace = workspaces.remove(at: selectedIndex)
workspaces.insert(selectedWorkspace, at: 0)
}
let windowContexts = commandPaletteSwitcherWindowContexts()
guard !windowContexts.isEmpty else { return [] }
var entries: [CommandPaletteCommand] = []
entries.reserveCapacity(workspaces.count * 4)
let estimatedCount = windowContexts.reduce(0) { partial, context in
partial + max(1, context.tabManager.tabs.count) * 4
}
entries.reserveCapacity(estimatedCount)
var nextRank = 0
for workspace in workspaces {
let workspaceName = workspaceDisplayName(workspace)
let workspaceCommandId = "switcher.workspace.\(workspace.id.uuidString.lowercased())"
let workspaceKeywords = CommandPaletteSwitcherSearchIndexer.keywords(
baseKeywords: [
"workspace",
"switch",
"go",
"open",
workspaceName
],
metadata: commandPaletteWorkspaceSearchMetadata(for: workspace),
detail: .workspace
)
entries.append(
CommandPaletteCommand(
id: workspaceCommandId,
rank: nextRank,
title: workspaceName,
subtitle: "Workspace",
shortcutHint: nil,
keywords: workspaceKeywords,
dismissOnRun: true,
action: {
tabManager.focusTab(workspace.id, suppressFlash: true)
}
)
)
nextRank += 1
for context in windowContexts {
var workspaces = context.tabManager.tabs
guard !workspaces.isEmpty else { continue }
var orderedPanelIds = workspace.sidebarOrderedPanelIds()
if let focusedPanelId = workspace.focusedPanelId,
let focusedIndex = orderedPanelIds.firstIndex(of: focusedPanelId) {
orderedPanelIds.remove(at: focusedIndex)
orderedPanelIds.insert(focusedPanelId, at: 0)
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)
}
for panelId in orderedPanelIds {
guard let panel = workspace.panels[panelId] else { continue }
let panelTitle = panelDisplayName(workspace: workspace, panelId: panelId, fallback: panel.displayTitle)
let typeLabel: String = (panel.panelType == .browser) ? "Browser" : "Terminal"
let panelKeywords = CommandPaletteSwitcherSearchIndexer.keywords(
let windowId = context.windowId
let windowTabManager = context.tabManager
let windowKeywords = commandPaletteWindowKeywords(windowLabel: context.windowLabel)
for workspace in workspaces {
let workspaceName = workspaceDisplayName(workspace)
let workspaceCommandId = "switcher.workspace.\(workspace.id.uuidString.lowercased())"
let workspaceKeywords = CommandPaletteSwitcherSearchIndexer.keywords(
baseKeywords: [
"tab",
"surface",
"panel",
"workspace",
"switch",
"go",
workspaceName,
panelTitle,
typeLabel.lowercased()
],
metadata: commandPalettePanelSearchMetadata(in: workspace, panelId: panelId)
"open",
workspaceName
] + windowKeywords,
metadata: commandPaletteWorkspaceSearchMetadata(for: workspace),
detail: .workspace
)
let workspaceId = workspace.id
entries.append(
CommandPaletteCommand(
id: "switcher.surface.\(workspace.id.uuidString.lowercased()).\(panelId.uuidString.lowercased())",
id: workspaceCommandId,
rank: nextRank,
title: panelTitle,
subtitle: "\(typeLabel)\(workspaceName)",
title: workspaceName,
subtitle: commandPaletteSwitcherSubtitle(base: "Workspace", windowLabel: context.windowLabel),
shortcutHint: nil,
keywords: panelKeywords,
keywords: workspaceKeywords,
dismissOnRun: true,
action: {
tabManager.focusTab(workspace.id, surfaceId: panelId, suppressFlash: true)
focusCommandPaletteSwitcherTarget(
windowId: windowId,
tabManager: windowTabManager,
workspaceId: workspaceId,
panelId: nil
)
}
)
)
nextRank += 1
var orderedPanelIds = workspace.sidebarOrderedPanelIds()
if let focusedPanelId = workspace.focusedPanelId,
let focusedIndex = orderedPanelIds.firstIndex(of: focusedPanelId) {
orderedPanelIds.remove(at: focusedIndex)
orderedPanelIds.insert(focusedPanelId, at: 0)
}
for panelId in orderedPanelIds {
guard let panel = workspace.panels[panelId] else { continue }
let panelTitle = panelDisplayName(workspace: workspace, panelId: panelId, fallback: panel.displayTitle)
let typeLabel: String = (panel.panelType == .browser) ? "Browser" : "Terminal"
let panelKeywords = CommandPaletteSwitcherSearchIndexer.keywords(
baseKeywords: [
"tab",
"surface",
"panel",
"switch",
"go",
workspaceName,
panelTitle,
typeLabel.lowercased()
] + windowKeywords,
metadata: commandPalettePanelSearchMetadata(in: workspace, panelId: panelId)
)
entries.append(
CommandPaletteCommand(
id: "switcher.surface.\(workspace.id.uuidString.lowercased()).\(panelId.uuidString.lowercased())",
rank: nextRank,
title: panelTitle,
subtitle: commandPaletteSwitcherSubtitle(
base: "\(typeLabel)\(workspaceName)",
windowLabel: context.windowLabel
),
shortcutHint: nil,
keywords: panelKeywords,
dismissOnRun: true,
action: {
focusCommandPaletteSwitcherTarget(
windowId: windowId,
tabManager: windowTabManager,
workspaceId: workspaceId,
panelId: panelId
)
}
)
)
nextRank += 1
}
}
}
return entries
}
private func commandPaletteSwitcherWindowContexts() -> [CommandPaletteSwitcherWindowContext] {
let fallback = CommandPaletteSwitcherWindowContext(
windowId: windowId,
tabManager: tabManager,
selectedWorkspaceId: tabManager.selectedTabId,
windowLabel: nil
)
guard let appDelegate = AppDelegate.shared else { return [fallback] }
let summaries = appDelegate.listMainWindowSummaries()
guard !summaries.isEmpty else { return [fallback] }
let orderedSummaries = summaries.sorted { lhs, rhs in
let lhsIsCurrent = lhs.windowId == windowId
let rhsIsCurrent = rhs.windowId == windowId
if lhsIsCurrent != rhsIsCurrent { return lhsIsCurrent }
if lhs.isKeyWindow != rhs.isKeyWindow { return lhs.isKeyWindow }
if lhs.isVisible != rhs.isVisible { return lhs.isVisible }
return lhs.windowId.uuidString < rhs.windowId.uuidString
}
var windowLabelById: [UUID: String] = [:]
if orderedSummaries.count > 1 {
for (index, summary) in orderedSummaries.enumerated() where summary.windowId != windowId {
windowLabelById[summary.windowId] = "Window \(index + 1)"
}
}
var contexts: [CommandPaletteSwitcherWindowContext] = []
var seenWindowIds: Set<UUID> = []
for summary in orderedSummaries {
guard let manager = appDelegate.tabManagerFor(windowId: summary.windowId) else { continue }
guard seenWindowIds.insert(summary.windowId).inserted else { continue }
contexts.append(
CommandPaletteSwitcherWindowContext(
windowId: summary.windowId,
tabManager: manager,
selectedWorkspaceId: summary.selectedWorkspaceId,
windowLabel: windowLabelById[summary.windowId]
)
)
}
if contexts.isEmpty {
return [fallback]
}
return contexts
}
private func commandPaletteSwitcherSubtitle(base: String, windowLabel: String?) -> String {
guard let windowLabel else { return base }
return "\(base)\(windowLabel)"
}
private func commandPaletteWindowKeywords(windowLabel: String?) -> [String] {
guard let windowLabel else { return [] }
return ["window", windowLabel.lowercased()]
}
private func focusCommandPaletteSwitcherTarget(
windowId: UUID,
tabManager: TabManager,
workspaceId: UUID,
panelId: UUID?
) {
_ = AppDelegate.shared?.focusMainWindow(windowId: windowId)
if let panelId {
tabManager.focusTab(workspaceId, surfaceId: panelId, suppressFlash: true)
} else {
tabManager.focusTab(workspaceId, suppressFlash: true)
}
}
private func commandPaletteWorkspaceSearchMetadata(for workspace: Workspace) -> CommandPaletteSwitcherSearchMetadata {
// Keep workspace rows coarse so surface rows win for directory/branch-specific queries.
let directories = [workspace.currentDirectory]

View file

@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
Regression test: cmd+p switcher should include workspaces from every window.
Why: switcher rows were sourced from the current window's TabManager only, so
Cmd+P could not jump to workspaces/tabs owned by other windows.
"""
import os
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from cmux import cmux, cmuxError
SOCKET_PATH = os.environ.get("CMUX_SOCKET", "/tmp/cmux-debug.sock")
def _wait_until(predicate, timeout_s: float = 6.0, interval_s: float = 0.05, message: str = "timeout") -> None:
start = time.time()
while time.time() - start < timeout_s:
if predicate():
return
time.sleep(interval_s)
raise cmuxError(message)
def _palette_visible(client: cmux, window_id: str) -> bool:
payload = client._call("debug.command_palette.visible", {"window_id": window_id}) or {}
return bool(payload.get("visible"))
def _palette_results(client: cmux, window_id: str, limit: int = 20) -> dict:
return client.command_palette_results(window_id=window_id, limit=limit)
def _set_palette_visible(client: cmux, window_id: str, visible: bool) -> None:
if _palette_visible(client, window_id) == visible:
return
client._call("debug.command_palette.toggle", {"window_id": window_id})
_wait_until(
lambda: _palette_visible(client, window_id) == visible,
message=f"palette visibility in {window_id} did not become {visible}",
)
def main() -> int:
with cmux(SOCKET_PATH) as client:
client.activate_app()
time.sleep(0.2)
window_a = client.current_window()
for row in client.list_windows():
other_id = str(row.get("id") or "")
if other_id and other_id != window_a:
client.close_window(other_id)
time.sleep(0.2)
client.focus_window(window_a)
client.activate_app()
time.sleep(0.2)
window_b = client.new_window()
time.sleep(0.25)
token_suffix = f"{int(time.time() * 1000)}"
token_a = f"cmdp-window-a-{token_suffix}"
token_b = f"cmdp-window-b-{token_suffix}"
workspace_a = client.new_workspace(window_id=window_a)
client.rename_workspace(token_a, workspace=workspace_a)
workspace_b = client.new_workspace(window_id=window_b)
client.rename_workspace(token_b, workspace=workspace_b)
time.sleep(0.25)
client.focus_window(window_a)
client.activate_app()
time.sleep(0.2)
_set_palette_visible(client, window_a, False)
_set_palette_visible(client, window_b, False)
client.simulate_shortcut("cmd+p")
_wait_until(
lambda: _palette_visible(client, window_a),
message="cmd+p did not open palette in window A",
)
_wait_until(
lambda: str(_palette_results(client, window_a).get("mode") or "") == "switcher",
message="cmd+p did not open switcher mode in window A",
)
client.simulate_type(token_b)
_wait_until(
lambda: token_b in str(_palette_results(client, window_a).get("query") or "").strip().lower(),
message="switcher query did not update with window B token",
)
result_rows = (_palette_results(client, window_a, limit=64).get("results") or [])
target_workspace_command = f"switcher.workspace.{workspace_b.lower()}"
if not any(str((row or {}).get("command_id") or "") == target_workspace_command for row in result_rows):
raise cmuxError(
f"cmd+p switcher in window A did not include workspace from window B "
f"(expected {target_workspace_command}); rows={result_rows[:8]}"
)
client.simulate_shortcut("enter")
_wait_until(
lambda: not _palette_visible(client, window_a),
message="palette did not close after selecting cross-window switcher row",
)
_wait_until(
lambda: client.current_workspace().lower() == workspace_b.lower(),
message="Enter on cross-window switcher row did not move to window B workspace",
)
_wait_until(
lambda: client.current_window().lower() == window_b.lower(),
message="Enter on cross-window switcher row did not focus window B",
)
print("PASS: cmd+p switcher includes and navigates to workspaces from other windows")
return 0
if __name__ == "__main__":
raise SystemExit(main())