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
25 changes: 15 additions & 10 deletions CLI/CLI+TmuxCompat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -709,22 +709,27 @@ extension ProgramaCLI {
context["pane_width"] = String(columns)
context["pane_height"] = String(rows)

let cellW = pane["cell_width_px"] as? Int ?? 0
let cellH = pane["cell_height_px"] as? Int ?? 0
// Frames are in layout points, so divide by the point cell size (`cell_width_px` is
// device pixels and would halve every column on a Retina display).
let cellW = (pane["cell_width"] as? NSNumber)?.doubleValue ?? 0
let cellH = (pane["cell_height"] as? NSNumber)?.doubleValue ?? 0
guard cellW > 0, cellH > 0 else { return }

// pixel_frame is in window coordinates; tmux positions are relative to the window area.
let originX = (containerFrame?["x"] as? NSNumber)?.doubleValue ?? 0
let originY = (containerFrame?["y"] as? NSNumber)?.doubleValue ?? 0
if let frame = pane["pixel_frame"] as? [String: Any] {
let px = frame["x"] as? Double ?? 0
let py = frame["y"] as? Double ?? 0
context["pane_left"] = String(Int(px) / cellW)
context["pane_top"] = String(Int(py) / cellH)
let px = (frame["x"] as? NSNumber)?.doubleValue ?? 0
let py = (frame["y"] as? NSNumber)?.doubleValue ?? 0
context["pane_left"] = String(max(Int(((px - originX) / cellW).rounded()), 0))
context["pane_top"] = String(max(Int(((py - originY) / cellH).rounded()), 0))
}

if let cf = containerFrame {
let cw = cf["width"] as? Double ?? 0
let ch = cf["height"] as? Double ?? 0
context["window_width"] = String(max(Int(cw) / cellW, 1))
context["window_height"] = String(max(Int(ch) / cellH, 1))
let cw = (cf["width"] as? NSNumber)?.doubleValue ?? 0
let ch = (cf["height"] as? NSNumber)?.doubleValue ?? 0
context["window_width"] = String(max(Int(cw / cellW), 1))
context["window_height"] = String(max(Int(ch / cellH), 1))
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4436,7 +4436,7 @@ struct ContentView: View {
mode = "workspace_description_input"
}

let rows = Array(commandPaletteController.commandPaletteVisibleResults.prefix(20)).map { result in
let rows = Array(commandPaletteController.commandPaletteVisibleResults.prefix(100)).map { result in
CommandPaletteDebugResultRow(
commandId: result.command.id,
title: result.command.title,
Expand Down
8 changes: 7 additions & 1 deletion Sources/TerminalController+Debug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -552,12 +552,18 @@ extension TerminalController {
action = .selectWorkspaceByNumber
case "surface_digits", "surface_number", "select_surface_by_number":
action = .selectSurfaceByNumber
case "new_window", "newwindow":
action = .newWindow
case "close_window", "closewindow":
action = .closeWindow
case "rename_tab", "renametab":
action = .renameTab
default:
action = nil
}

guard let action else {
return "ERROR: Unknown shortcut name. Supported: focus_left, focus_right, focus_up, focus_down, workspace_digits, surface_digits"
return "ERROR: Unknown shortcut name. Supported: focus_left, focus_right, focus_up, focus_down, workspace_digits, surface_digits, new_window, close_window, rename_tab"
}

if combo.lowercased() == "clear" || combo.lowercased() == "default" || combo.lowercased() == "reset" {
Expand Down
2 changes: 2 additions & 0 deletions Sources/TerminalController+Pane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ extension TerminalController {
"window_ref": v2Ref(kind: .window, uuid: windowId)
]
payload["container_frame"] = [
"x": snapshot.containerFrame.x,
"y": snapshot.containerFrame.y,
"width": snapshot.containerFrame.width,
"height": snapshot.containerFrame.height
]
Expand Down
51 changes: 41 additions & 10 deletions programaUITests/SidebarHelpMenuUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,34 @@ final class SidebarHelpMenuUITests: XCTestCase {

final class CommandPaletteAllSurfacesUITests: XCTestCase {
private var socketPath = ""
private var diagnosticsPath = ""
private var launchTag = ""
private let hiddenSurfaceToken = "programa-command-palette-hidden-surface"
private let visibleSurfaceToken = "programa-command-palette-visible-surface"
private let noMatchWorkspaceQuery = "programa-command-palette-no-match"

override func setUp() {
super.setUp()
continueAfterFailure = false
socketPath = "/tmp/programa-ui-test-command-palette-\(UUID().uuidString).sock"
// The UI test runner is sandboxed: connect() to a socket under /tmp fails with EPERM,
// but its own temporary directory is reachable (the app is not sandboxed and can bind there).
socketPath = (NSTemporaryDirectory() as NSString)
.appendingPathComponent("pc-\(UUID().uuidString.prefix(8).lowercased()).sock")
diagnosticsPath = "/tmp/programa-ui-test-command-palette-\(UUID().uuidString).json"
launchTag = "ui-tests-command-palette-\(UUID().uuidString.prefix(8).lowercased())"
try? FileManager.default.removeItem(atPath: socketPath)
try? FileManager.default.removeItem(atPath: diagnosticsPath)
// A test that turns on all-surfaces search persists it in the app's defaults; start clean.
let reset = Process()
reset.executableURL = URL(fileURLWithPath: "/usr/bin/defaults")
reset.arguments = ["delete", "com.darkroom.programa.debug", "commandPalette.switcherSearchAllSurfaces"]
try? reset.run()
reset.waitUntilExit()
}

override func tearDown() {
try? FileManager.default.removeItem(atPath: socketPath)
try? FileManager.default.removeItem(atPath: diagnosticsPath)
super.tearDown()
}

Expand All @@ -206,6 +221,11 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
app.launchEnvironment["PROGRAMA_SOCKET_PATH"] = socketPath
app.launchEnvironment["PROGRAMA_SOCKET_ENABLE"] = "1"
app.launchEnvironment["PROGRAMA_SOCKET_MODE"] = "allowAll"
// Debug launches need a tag, and a tagged launch honors PROGRAMA_SOCKET_PATH only with the override.
app.launchEnvironment["PROGRAMA_TAG"] = launchTag
app.launchEnvironment["PROGRAMA_ALLOW_SOCKET_OVERRIDE"] = "1"
app.launchEnvironment["PROGRAMA_UI_TEST_SOCKET_SANITY"] = "1"
app.launchEnvironment["PROGRAMA_UI_TEST_DIAGNOSTICS_PATH"] = diagnosticsPath
if showSettingsWindow {
app.launchEnvironment["PROGRAMA_UI_TEST_SHOW_SETTINGS"] = "1"
}
Expand All @@ -222,7 +242,7 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
},
"Expected the main window to be visible"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath). client=\(ControlSocketClient.lastFailure) diagnostics=\(loadDiagnostics(at: diagnosticsPath) ?? [:])")

let mainWindowId = try XCTUnwrap(currentWindowId(), "Expected window.current to return a window id")

Expand Down Expand Up @@ -274,10 +294,15 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
firstWorkspaceRow["title"] as? String,
"Expected the restored workspace row to include a title. snapshot=\(switcherSnapshot)"
)
let workspaceLabel = app.staticTexts[workspaceTitle].firstMatch
// The sidebar row can share the workspace's title (and sits behind the palette), so look
// the palette row up by its own identifier and compare the command it represents.
let workspaceCommandId = try XCTUnwrap(firstWorkspaceRow["command_id"] as? String)
let workspaceRow = app.descendants(matching: .any)
.matching(identifier: "CommandPaletteResultRow.0")
.firstMatch
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 2.0) {
workspaceLabel.exists && workspaceLabel.isHittable
workspaceRow.exists && workspaceRow.isHittable && (workspaceRow.value as? String) == workspaceCommandId
},
"Expected the restored workspace row to be visibly rendered. title=\(workspaceTitle) snapshot=\(switcherSnapshot)"
)
Expand Down Expand Up @@ -335,7 +360,7 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
},
"Expected the main window and Settings window to be visible"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath). client=\(ControlSocketClient.lastFailure) diagnostics=\(loadDiagnostics(at: diagnosticsPath) ?? [:])")

let mainWindowId = try XCTUnwrap(currentWindowId(), "Expected window.current to return a window id")
let secondaryWorkspaceId = try XCTUnwrap(createWorkspaceId(), "Expected workspace.create to return a workspace ID")
Expand Down Expand Up @@ -400,7 +425,8 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
let trailingLabel = row["trailing_label"] as? String ?? ""
return commandId.hasPrefix("switcher.surface.") && trailingLabel == "Terminal"
}
}
},
"Expected a hidden terminal row. snapshot=\(String(describing: commandPaletteSnapshot(windowId: mainWindowId)))"
)

XCTAssertTrue(
Expand Down Expand Up @@ -492,7 +518,7 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
},
"Expected the main window and Settings window to be visible"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath). client=\(ControlSocketClient.lastFailure) diagnostics=\(loadDiagnostics(at: diagnosticsPath) ?? [:])")

let mainWindowId = try XCTUnwrap(currentWindowId(), "Expected window.current to return a window id")

Expand Down Expand Up @@ -580,7 +606,7 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
},
"Expected the main window to be visible"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath). client=\(ControlSocketClient.lastFailure) diagnostics=\(loadDiagnostics(at: diagnosticsPath) ?? [:])")

let mainWindowId = try XCTUnwrap(currentWindowId(), "Expected window.current to return a window id")
try seedWorkspaceSwitcherCorpus(workspaceCount: 96)
Expand Down Expand Up @@ -619,7 +645,8 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
return (snapshot["query"] as? String) == String(repeating: "z", count: 8)
&& self.commandPaletteResultRows(from: snapshot).isEmpty
},
"Expected the switcher to reach a visible no-results state before refining the query"
"Expected the switcher to reach a visible no-results state before refining the query. " +
"labelExists=\(emptyLabel.exists) snapshot=\(String(describing: commandPaletteSnapshot(windowId: mainWindowId)))"
)

try debugTypeText("z")
Expand Down Expand Up @@ -980,6 +1007,7 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
}

private final class ControlSocketClient {
nonisolated(unsafe) static var lastFailure = ""
private let path: String
private let responseTimeout: TimeInterval

Expand Down Expand Up @@ -1044,7 +1072,10 @@ final class CommandPaletteAllSurfacesUITests: XCTestCase {
connect(fd, sa, addrLen)
}
}
guard connected == 0 else { return nil }
guard connected == 0 else {
Self.lastFailure = "connect errno=\(errno) \(String(cString: strerror(errno)))"
return nil
}

let payload = line + "\n"
let wrote: Bool = payload.withCString { cString in
Expand Down
1 change: 0 additions & 1 deletion scripts/nightly-ui-classes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ TerminalCmdClickUITests
!BonsplitTabDragUITests/testStandardModeKeepsWorkspaceControlsOutOfSidebar
!SidebarHelpMenuUITests/testVisibleSidebarHeaderToggleHidesItsOwningSidebar
!CommandPaletteAllSurfacesUITests/testCmdPSearchCanIncludeSurfacesFromOtherWorkspacesWhenEnabled
!CommandPaletteAllSurfacesUITests/testCmdShiftPBackspaceReturnsToWorkspaceResults
!CommandPaletteAllSurfacesUITests/testCommandPaletteCanEnableAndDisableMinimalMode
!CommandPaletteAllSurfacesUITests/testSwitcherEmptyStateDoesNotBlinkWhileRefiningNoMatchQuery
!WorkspaceDescriptionUITests/testSidebarRendersSavedDescriptionWithLineBreaks
7 changes: 7 additions & 0 deletions tests_v2/nightly_subset.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# tests_v2 files run nightly by .github/workflows/nightly-extended-tests.yml (not per PR).
# One basename per line. Add a file here only after it passes in a manual dispatch of that workflow.
test_agent_screen_manifest_detection.py
test_claude_install_integration.py
test_cli_global_flags_and_v1_error_contract.py
test_cli_new_workspace_external_git_branch_refresh.py
test_cli_non_focus_commands_preserve_workspace.py
test_cli_sidebar_metadata_commands.py
test_close_surface_selection.py
test_close_workspace_selection.py
test_codex_install_integration.py
test_command_palette_backspace_go_back.py
test_command_palette_focus.py
test_command_palette_focus_lock_workspace_spawn.py
Expand All @@ -15,6 +17,9 @@ test_command_palette_modes.py
test_command_palette_navigation_keys.py
test_command_palette_rename_enter.py
test_command_palette_rename_select_all.py
test_command_palette_search_action_sync.py
test_command_palette_search_typing_stability.py
test_command_palette_shortcut_hint_sync.py
test_command_palette_switcher_all_windows.py
test_command_palette_window_scope.py
test_ctrl_socket.py
Expand All @@ -34,6 +39,7 @@ test_pane_resize_preserves_visible_content.py
test_ports_list_stop.py
test_rename_tab_cli_parity.py
test_rename_window_workspace_parity.py
test_review_diff_panel.py
test_shortcut_window_scope.py
test_snapshot_list_and_restore_param_validation.py
test_socket_limits_and_focus_sweep.py
Expand All @@ -45,6 +51,7 @@ test_surface_move_reorder_api.py
test_suspended_subscriber_does_not_block_server.py
test_tab_workspace_action_naming.py
test_terminal_focus_routing.py
test_tmux_compat_geometry.py
test_visual_typing_char_by_char.py
test_workspace_create_background_starts_terminal.py
test_workspace_split_title_updates.py
3 changes: 3 additions & 0 deletions tests_v2/test_agent_screen_manifest_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
sleep 8
printf '\\nDo you want to proceed?\\n❯ 1. Yes\\n'
sleep 8
# Clear the screen like a real approval box closing: blocked outranks idle while its text is
# still within the last 12 lines.
printf '\\033[2J\\033[H'
printf '\\n❯ '
sleep 30
"""
Expand Down
12 changes: 8 additions & 4 deletions tests_v2/test_codex_install_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"PermissionRequest",
"SessionEnd",
]
# The unguarded form older installs wrote. Ownership is an exact match on a known command, so
# only this form (not an arbitrary `programa codex-hook <anything>` line) is replaced on reinstall.
STALE_SESSION_START_COMMAND = f"{CODEX_HOOK_MARKER} session-start"
STALE_STOP_COMMAND = f"{CODEX_HOOK_MARKER} stop"


def _run(
Expand Down Expand Up @@ -141,12 +145,12 @@ def test_preserves_user_hooks_and_replaces_stale_entry(cli: str) -> None:
"hooks": [{"type": "command", "command": "echo my-custom-start-hook"}],
},
{
"hooks": [{"type": "command", "command": f"{CODEX_HOOK_MARKER} old-form"}],
"hooks": [{"type": "command", "command": STALE_SESSION_START_COMMAND}],
},
],
"Stop": [
{
"hooks": [{"type": "command", "command": f"{CODEX_HOOK_MARKER} stop-old-format"}],
"hooks": [{"type": "command", "command": STALE_STOP_COMMAND}],
}
],
}
Expand Down Expand Up @@ -184,15 +188,15 @@ def test_preserves_user_hooks_and_replaces_stale_entry(cli: str) -> None:
)
_must(len(programa_groups) == 1, f"expected exactly 1 programa SessionStart group, got: {programa_groups}")
_must(
"old-form" not in programa_groups[0]["hooks"][0]["command"],
programa_groups[0]["hooks"][0]["command"] != STALE_SESSION_START_COMMAND,
f"stale programa SessionStart entry should be replaced, got: {programa_groups[0]}",
)

# Stop: exactly one programa entry, stale one replaced.
stop_commands = _hooks_commands(hooks, "Stop")
_must(len(stop_commands) == 1, f"expected exactly 1 Stop hook command, got: {stop_commands}")
_must(CODEX_HOOK_MARKER in stop_commands[0], f"Stop hook should be a programa hook, got: {stop_commands}")
_must("stop-old-format" not in stop_commands[0], f"stale Stop command should be replaced, got: {stop_commands}")
_must(stop_commands[0] != STALE_STOP_COMMAND, f"stale Stop command should be replaced, got: {stop_commands}")

print(" PASS: install preserves user hooks and replaces stale programa entries")

Expand Down
5 changes: 4 additions & 1 deletion tests_v2/test_command_palette_search_action_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ def main():
before_rows = before.get("results") or []
if not before_rows:
raise ProgramaClientError(f"no results for 'open': {before}")
if str(before_rows[0].get("command_id") or "") != "palette.terminalOpenDirectory":
# "Open Folder…" and the per-app "Open Current Directory in <app>" commands
# (palette.terminalOpenDirectory.<app>, one per installed app) all match "open".
top_before = str(before_rows[0].get("command_id") or "")
if top_before != "palette.openFolder" and not top_before.startswith("palette.terminalOpenDirectory."):
raise ProgramaClientError(f"unexpected top command for 'open': {before_rows[0]}")

client.simulate_shortcut("cmd+a")
Expand Down
13 changes: 9 additions & 4 deletions tests_v2/test_command_palette_search_typing_stability.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,23 @@ def _close_palette_if_open(client, window_id):


def _open_palette(client, window_id):
"""Opens the palette and returns the prefilled query length (the toggle opens in command
mode with the ">" prefix, caret after it)."""
_close_palette_if_open(client, window_id)
client._call("debug.command_palette.toggle", {"window_id": window_id})
_wait_until(
lambda: _palette_visible(client, window_id),
message="command palette failed to open",
)
base_length = len(">")
_wait_for_input_state(
client,
window_id,
expected_text_length=0,
message="search input did not focus with empty query",
expected_text_length=base_length,
message="search input did not focus with the command prefix only",
timeout_s=4.0,
)
return base_length


def main():
Expand All @@ -94,13 +99,13 @@ def main():
probe = "typingstability"
cycles = 4
for cycle in range(cycles):
_open_palette(client, window_id)
base_length = _open_palette(client, window_id)
for idx, ch in enumerate(probe, start=1):
client.simulate_type(ch)
_wait_for_input_state(
client,
window_id,
expected_text_length=idx,
expected_text_length=base_length + idx,
timeout_s=0.7,
message=(
f"search typing did not accumulate at cycle {cycle + 1}/{cycles}, "
Expand Down
Loading
Loading