From ae81c226bf9a0582a9562f794e325fcdacb2a2c2 Mon Sep 17 00:00:00 2001 From: arzafran Date: Mon, 20 Jul 2026 14:36:48 -0300 Subject: [PATCH] refactor: reject bare indexes for window/workspace/pane/surface targets (#132) Indexes shift when panes and workspaces open or close, so any agent holding one can silently hit the wrong target. Targets must now be a UUID or a short ref (workspace:2, surface:4). - CLI: normalizeHandle, resolveWorkspaceId, and resolveSurfaceId no longer resolve an all-digits argument by position; they fail with an error naming the accepted formats and the list command that shows refs. - Help text: becomes everywhere (56 strings), the usage banner drops the indexes clause, and the stale focus-window example no longer shows --window 0. - tests_v2/cmux.py resolvers raise the same error for int arguments; the five test files that passed positional ints now resolve real ids from the list calls first. Unchanged on purpose: placement options (reorder --index, move-surface --index), browser tab ordinals and find-nth (browser-DOM conventions), tmux-compat numeric targets (protocol-mandated), and the index field in list output (tmux compat consumes it). Verified against a tagged debug build: index args now fail with the new error; test_tab_dragging (15/15), test_ctrl_socket (5/5), test_close_workspace_selection and test_close_surface_selection all pass. --- CHANGELOG.md | 1 + CLI/programa.swift | 160 ++++++++++----------- TODO.md | 2 +- tests_v2/cmux.py | 24 +--- tests_v2/test_close_surface_selection.py | 5 +- tests_v2/test_close_workspace_selection.py | 13 +- tests_v2/test_ctrl_socket.py | 4 +- tests_v2/test_tab_dragging.py | 159 +++++++++++--------- tests_v2/test_visual_screenshots.py | 59 +++++--- 9 files changed, 222 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e699bfe..76424fbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Programa is a fork of [cmux](https://github.com/manaflow-ai/cmux); for history p ## [Unreleased] ### Changed +- CLI target arguments no longer accept bare indexes: anywhere a command takes a window, workspace, pane, or surface, pass a UUID or short ref (workspace:2, surface:4). Indexes shift when things open or close, so agents holding one could hit the wrong target; the error now names the accepted formats. Positional options like reorder --index and browser tab ordinals are unchanged. - New installs now start with the minimal workspace layout (theme already follows the system). Anyone who previously toggled the mode keeps their stored setting. - CI and release policy checks now exercise executable helpers and built artifacts instead of asserting source-file text; release binaries are also checked for the expected architecture before signing. - Debug and settings UI copy is fully localized in English and Japanese, and obsolete cmux branding and unassigned dark app-icon variants have been removed. diff --git a/CLI/programa.swift b/CLI/programa.swift index a70a2f36..7d49635d 100644 --- a/CLI/programa.swift +++ b/CLI/programa.swift @@ -1635,15 +1635,15 @@ struct ProgramaCLI { CommandDescriptor( names: ["identify"], - helpLines: ["identify [--workspace ] [--surface ] [--no-caller]"], + helpLines: ["identify [--workspace ] [--surface ] [--no-caller]"], detailedUsage: """ - Usage: programa identify [--workspace ] [--surface ] [--no-caller] + Usage: programa identify [--workspace ] [--surface ] [--no-caller] Print server identity and caller context details. Flags: - --workspace Caller workspace context (default: $PROGRAMA_WORKSPACE_ID) - --surface Caller surface context (default: $PROGRAMA_SURFACE_ID) + --workspace Caller workspace context (default: $PROGRAMA_WORKSPACE_ID) + --surface Caller surface context (default: $PROGRAMA_SURFACE_ID) --no-caller Omit caller context from the request """, execute: { ctx in @@ -1762,15 +1762,15 @@ struct ProgramaCLI { names: ["focus-window"], helpLines: ["focus-window --window "], detailedUsage: """ - Usage: programa focus-window --window + Usage: programa focus-window --window Focus (bring to front) the specified window. Flags: - --window Window to focus (required) + --window Window to focus (required) Example: - programa focus-window --window 0 + programa focus-window --window programa focus-window --window window:1 """, execute: { ctx in @@ -1795,12 +1795,12 @@ struct ProgramaCLI { names: ["close-window"], helpLines: ["close-window --window "], detailedUsage: """ - Usage: programa close-window --window + Usage: programa close-window --window Close the specified window. Flags: - --window Window to close (required) + --window Window to close (required) Example: programa close-window --window 0 @@ -1826,13 +1826,13 @@ struct ProgramaCLI { names: ["move-workspace-to-window"], helpLines: ["move-workspace-to-window --workspace --window "], detailedUsage: """ - Usage: programa move-workspace-to-window --workspace --window + Usage: programa move-workspace-to-window --workspace --window Move a workspace to a different window. Flags: - --workspace Workspace to move (required) - --window Target window (required) + --workspace Workspace to move (required) + --window Target window (required) Example: programa move-workspace-to-window --workspace workspace:2 --window window:1 @@ -1856,22 +1856,22 @@ struct ProgramaCLI { CommandDescriptor( names: ["reorder-workspace"], - helpLines: ["reorder-workspace --workspace (--index | --before | --after ) [--window ]"], + helpLines: ["reorder-workspace --workspace (--index | --before | --after ) [--window ]"], detailedUsage: """ - Usage: programa reorder-workspace [--workspace | ] [flags] + Usage: programa reorder-workspace [--workspace | ] [flags] Reorder a workspace within its window. Flags: - --workspace Workspace to reorder (required unless passed positionally) + --workspace Workspace to reorder (required unless passed positionally) --index Place at this index - --before Place before this workspace - --before-workspace + --before Place before this workspace + --before-workspace Alias for --before - --after Place after this workspace - --after-workspace + --after Place after this workspace + --after-workspace Alias for --after - --window Window context + --window Window context Example: programa reorder-workspace --workspace workspace:2 --index 0 @@ -1884,7 +1884,7 @@ struct ProgramaCLI { CommandDescriptor( names: ["workspace-action"], - helpLines: ["workspace-action --action [--workspace ] [--title ] [--color ] [--description ]"], + helpLines: ["workspace-action --action [--workspace ] [--title ] [--color ] [--description ]"], detailedUsage: """ Usage: programa workspace-action --action [flags] @@ -1901,7 +1901,7 @@ struct ProgramaCLI { Flags: --action Action name (required if not positional) - --workspace Target workspace (default: current/$PROGRAMA_WORKSPACE_ID) + --workspace Target workspace (default: current/$PROGRAMA_WORKSPACE_ID) --title Title for rename --color Color for set-color (name or #RRGGBB hex) --description Description for set-description @@ -2290,22 +2290,22 @@ struct ProgramaCLI { CommandDescriptor( names: ["move-surface"], - helpLines: ["move-surface --surface [--pane ] [--workspace ] [--window ] [--before ] [--after ] [--index ] [--focus ]"], + helpLines: ["move-surface --surface [--pane ] [--workspace ] [--window ] [--before ] [--after ] [--index ] [--focus ]"], detailedUsage: """ - Usage: programa move-surface [--surface | ] [flags] + Usage: programa move-surface [--surface | ] [flags] Move a surface to a different pane, workspace, or window. Flags: - --surface Surface to move (required unless passed positionally) - --pane Target pane - --workspace Target workspace - --window Target window - --before Place before this surface - --before-surface + --surface Surface to move (required unless passed positionally) + --pane Target pane + --workspace Target workspace + --window Target window + --before Place before this surface + --before-surface Alias for --before - --after Place after this surface - --after-surface + --after Place after this surface + --after-surface Alias for --after --index Place at this index --focus Focus the surface after moving @@ -2321,20 +2321,20 @@ struct ProgramaCLI { CommandDescriptor( names: ["reorder-surface"], - helpLines: ["reorder-surface --surface (--index | --before | --after )"], + helpLines: ["reorder-surface --surface (--index | --before | --after )"], detailedUsage: """ - Usage: programa reorder-surface [--surface | ] [flags] + Usage: programa reorder-surface [--surface | ] [flags] Reorder a surface within its pane. Flags: - --surface Surface to reorder (required unless passed positionally) - --workspace Workspace context - --before Place before this surface - --before-surface + --surface Surface to reorder (required unless passed positionally) + --workspace Workspace context + --before Place before this surface + --before-surface Alias for --before - --after Place after this surface - --after-surface + --after Place after this surface + --after-surface Alias for --after --index Place at this index @@ -2349,7 +2349,7 @@ struct ProgramaCLI { CommandDescriptor( names: ["tab-action"], - helpLines: ["tab-action --action [--tab ] [--surface ] [--workspace ] [--title ] [--url ]"], + helpLines: ["tab-action --action [--tab ] [--surface ] [--workspace ] [--title ] [--url ]"], detailedUsage: """ Usage: programa tab-action --action [flags] @@ -2365,9 +2365,9 @@ struct ProgramaCLI { Flags: --action Action name (required if not positional) - --tab Target tab (accepts tab: or surface:; default: $PROGRAMA_TAB_ID, then $PROGRAMA_SURFACE_ID, then focused tab) - --surface Alias for --tab (backward compatibility) - --workspace Workspace context (default: current/$PROGRAMA_WORKSPACE_ID) + --tab Target tab (accepts tab: or surface:; default: $PROGRAMA_TAB_ID, then $PROGRAMA_SURFACE_ID, then focused tab) + --surface Alias for --tab (backward compatibility) + --workspace Workspace context (default: current/$PROGRAMA_WORKSPACE_ID) --title Title for rename (or pass trailing title text) --url Optional URL for new-browser-right @@ -2432,7 +2432,7 @@ struct ProgramaCLI { let (panelArg, rem1) = self.parseOption(rem0, name: "--panel") let surface = surfaceArg ?? panelArg guard let surface else { - throw CLIError(message: "drag-surface-to-split requires --surface ") + throw CLIError(message: "drag-surface-to-split requires --surface ") } guard let direction = rem1.first else { throw CLIError(message: "drag-surface-to-split requires a direction") @@ -2685,12 +2685,12 @@ struct ProgramaCLI { names: ["close-workspace"], helpLines: ["close-workspace --workspace "], detailedUsage: """ - Usage: programa close-workspace --workspace + Usage: programa close-workspace --workspace Close the specified workspace. Flags: - --workspace Workspace to close (required) + --workspace Workspace to close (required) Example: programa close-workspace --workspace workspace:2 @@ -2714,12 +2714,12 @@ struct ProgramaCLI { names: ["select-workspace"], helpLines: ["select-workspace --workspace "], detailedUsage: """ - Usage: programa select-workspace --workspace + Usage: programa select-workspace --workspace Select (switch to) the specified workspace. Flags: - --workspace Workspace to select (required) + --workspace Workspace to select (required) Example: programa select-workspace --workspace workspace:2 @@ -2744,13 +2744,13 @@ struct ProgramaCLI { "rename-window [--workspace ] ", ], detailedUsage: """ - Usage: programa rename-workspace [--workspace <id|ref|index>] [--] <title> + Usage: programa rename-workspace [--workspace <id|ref>] [--] <title> Rename a workspace. Defaults to the current workspace. tmux-compatible alias: rename-window Flags: - --workspace <id|ref|index> Workspace to rename (default: current/$PROGRAMA_WORKSPACE_ID) + --workspace <id|ref> Workspace to rename (default: current/$PROGRAMA_WORKSPACE_ID) Example: programa rename-workspace "backend logs" @@ -3494,7 +3494,7 @@ struct ProgramaCLI { CommandDescriptor( names: ["browser"], helpLines: [ - "browser [--surface <id|ref|index> | <surface>] <subcommand> ...", + "browser [--surface <id|ref> | <surface>] <subcommand> ...", "browser open [url] (create browser split in caller's workspace; if surface supplied, behaves like navigate)", "browser open-split [url]", "browser goto|navigate <url> [--snapshot-after]", @@ -3526,7 +3526,7 @@ struct ProgramaCLI { "browser addinitscript <script>", "browser addscript <script>", "browser addstyle <css>", - "browser identify [--surface <id|ref|index>]", + "browser identify [--surface <id|ref>]", ], execute: { ctx in try self.runBrowserCommand(commandArgs: ctx.commandArgs, client: ctx.client, jsonOutput: ctx.jsonOutput, idFormat: ctx.idFormat) @@ -3980,9 +3980,10 @@ struct ProgramaCLI { } /// Generic handle normalizer shared by window/workspace/pane/surface lookups. - /// Resolves a raw CLI argument (UUID, handle ref, or list index) to a canonical + /// Resolves a raw CLI argument (UUID or handle ref) to a canonical /// handle ref/id, optionally scoped to a parent handle and falling back to a - /// caller-supplied "current"/"focused" resolver when `raw` is nil. + /// caller-supplied "current"/"focused" resolver when `raw` is nil. Bare indexes + /// are rejected with a clear error. private func normalizeHandle( _ raw: String?, client: SocketClient, @@ -3999,20 +4000,11 @@ struct ProgramaCLI { if isUUID(trimmed) || isHandleRef(trimmed) { return trimmed } - guard let wantedIndex = Int(trimmed) else { - throw CLIError(message: "Invalid \(kind) handle: \(trimmed) (expected UUID, ref like \(kind):1, or index)") - } - - var params: [String: Any] = [:] - if let filterParam { - params[filterParam.key] = filterParam.value + if Int(trimmed) != nil { + let listCommand = kind == "surface" ? "list-pane-surfaces" : "list-\(kind)s" + throw CLIError(message: "\(kind): bare indexes are no longer accepted; use a UUID or short ref like \(kind):2 (see \(listCommand))") } - let listed = try client.sendV2(method: "\(kind).list", params: params) - let items = listed["\(kind)s"] as? [[String: Any]] ?? [] - for item in items where intFromAny(item["index"]) == wantedIndex { - return (item["ref"] as? String) ?? (item["id"] as? String) - } - throw CLIError(message: "\(kind.capitalized) index not found") + throw CLIError(message: "Invalid \(kind) handle: \(trimmed) (expected UUID or ref like \(kind):1)") } func normalizeWindowHandle(_ raw: String?, client: SocketClient, allowCurrent: Bool = false) throws -> String? { @@ -4351,7 +4343,7 @@ struct ProgramaCLI { ) throws { let surfaceRaw = optionValue(commandArgs, name: "--surface") ?? commandArgs.first guard let surfaceRaw else { - throw CLIError(message: "move-surface requires --surface <id|ref|index>") + throw CLIError(message: "move-surface requires --surface <id|ref>") } let workspaceRaw = optionValue(commandArgs, name: "--workspace") @@ -4401,7 +4393,7 @@ struct ProgramaCLI { ) throws { let surfaceRaw = optionValue(commandArgs, name: "--surface") ?? commandArgs.first guard let surfaceRaw else { - throw CLIError(message: "reorder-surface requires --surface <id|ref|index>") + throw CLIError(message: "reorder-surface requires --surface <id|ref>") } let workspaceRaw = optionValue(commandArgs, name: "--workspace") @@ -4437,7 +4429,7 @@ struct ProgramaCLI { ) throws { let workspaceRaw = optionValue(commandArgs, name: "--workspace") ?? commandArgs.first guard let workspaceRaw else { - throw CLIError(message: "reorder-workspace requires --workspace <id|ref|index>") + throw CLIError(message: "reorder-workspace requires --workspace <id|ref>") } let windowRaw = optionValue(commandArgs, name: "--window") @@ -4743,13 +4735,8 @@ struct ProgramaCLI { throw CLIError(message: "Workspace ref not found: \(raw)") } - if let raw, let index = Int(raw) { - let listed = try client.sendV2(method: "workspace.list") - let items = listed["workspaces"] as? [[String: Any]] ?? [] - for item in items where intFromAny(item["index"]) == index { - if let id = item["id"] as? String { return id } - } - throw CLIError(message: "Workspace index not found") + if let raw, Int(raw) != nil { + throw CLIError(message: "workspace: bare indexes are no longer accepted; use a UUID or short ref like workspace:2 (see list-workspaces)") } let current = try client.sendV2(method: "workspace.current") @@ -4770,16 +4757,13 @@ struct ProgramaCLI { throw CLIError(message: "Surface ref not found: \(raw)") } + if let raw, Int(raw) != nil { + throw CLIError(message: "surface: bare indexes are no longer accepted; use a UUID or short ref like surface:2 (see list-pane-surfaces)") + } + let listed = try client.sendV2(method: "surface.list", params: ["workspace_id": workspaceId]) let items = listed["surfaces"] as? [[String: Any]] ?? [] - if let raw, let index = Int(raw) { - for item in items where intFromAny(item["index"]) == index { - if let id = item["id"] as? String { return id } - } - throw CLIError(message: "Surface index not found") - } - if let focused = items.first(where: { ($0["focused"] as? Bool) == true }) { if let id = focused["id"] as? String { return id } } @@ -5097,7 +5081,7 @@ struct ProgramaCLI { case "move-surface": let parsed = try parse(values: ["surface", "pane", "workspace", "window", "before", "before-surface", "after", "after-surface", "index", "focus"], maxPositionals: 1) guard parsed.options["surface"] != nil || parsed.positional.count == 1 else { - throw CLIError(message: "move-surface requires --surface <id|ref|index>") + throw CLIError(message: "move-surface requires --surface <id|ref>") } guard !(parsed.options["surface"] != nil && !parsed.positional.isEmpty) else { throw CLIError(message: "move-surface: provide the surface once") @@ -5116,7 +5100,7 @@ struct ProgramaCLI { case "reorder-surface": let parsed = try parse(values: ["surface", "workspace", "index", "before", "before-surface", "after", "after-surface"], maxPositionals: 1) guard parsed.options["surface"] != nil || parsed.positional.count == 1 else { - throw CLIError(message: "reorder-surface requires --surface <id|ref|index>") + throw CLIError(message: "reorder-surface requires --surface <id|ref>") } guard !(parsed.options["surface"] != nil && !parsed.positional.isEmpty) else { throw CLIError(message: "reorder-surface: provide the surface once") @@ -6039,7 +6023,7 @@ struct ProgramaCLI { programa [global-options] <command> [options] Handle Inputs: - Use UUIDs, short refs (window:1/workspace:2/pane:3/surface:4), or indexes where commands accept window, workspace, pane, or surface inputs. + Use UUIDs or short refs (window:1/workspace:2/pane:3/surface:4) where commands accept window, workspace, pane, or surface inputs. `tab-action` also accepts `tab:<n>` in addition to `surface:<n>`. Output defaults to refs; pass --id-format uuids or --id-format both to include UUIDs. diff --git a/TODO.md b/TODO.md index 05102380..895844f9 100644 --- a/TODO.md +++ b/TODO.md @@ -67,7 +67,7 @@ Roadmap tracking: #141 (Phase 1 P0s #131-#134, Phase 2 upstream port round #135, - [ ] Browser cmd+shift+H ring flashes only once (should flash twice like other shortcuts) ## Refactoring -- [ ] **P0** Remove all index-based APIs in favor of short ID refs (surface:N, pane:N, workspace:N, window:N) (#132) +- [x] **P0** Remove all index-based APIs in favor of short ID refs (surface:N, pane:N, workspace:N, window:N) (#132) - [ ] **P0** CLI commands should be workspace-relative using PROGRAMA_WORKSPACE_ID env var (not focused workspace) so agents in background workspaces don't affect the user's active workspace. Affected: send, send-key, send-panel, send-key-panel, new-split, new-pane, new-surface, close-surface, list-panes, list-pane-surfaces, list-panels, focus-pane, focus-panel, surface-health (#133) - [ ] **P0** Remove `close-workspace` with no args — require explicit workspace short ID or UUID, with clear error message if missing (#134) diff --git a/tests_v2/cmux.py b/tests_v2/cmux.py index 840d7c2f..6a411617 100755 --- a/tests_v2/cmux.py +++ b/tests_v2/cmux.py @@ -284,11 +284,7 @@ def _resolve_workspace_id(self, workspace: Union[str, int, None]) -> Optional[st return str(wsid) if isinstance(workspace, int): - items = (self._call("workspace.list") or {}).get("workspaces") or [] - for row in items: - if int(row.get("index", -1)) == workspace: - return str(row.get("id")) - raise cmuxError(f"Workspace index not found: {workspace}") + raise cmuxError(f"bare index {workspace!r} is no longer accepted; pass a UUID or ref like workspace:2") s = str(workspace).strip() if not s: @@ -310,14 +306,7 @@ def _resolve_surface_id(self, surface: Union[str, int, None], workspace_id: Opti return None if sid in (None, "", {}) else str(sid) if isinstance(surface, int): - params: Dict[str, Any] = {} - if workspace_id: - params["workspace_id"] = workspace_id - items = (self._call("surface.list", params) or {}).get("surfaces") or [] - for row in items: - if int(row.get("index", -1)) == surface: - return str(row.get("id")) - raise cmuxError(f"Surface index not found: {surface}") + raise cmuxError(f"bare index {surface!r} is no longer accepted; pass a UUID or ref like surface:2") s = str(surface).strip() if not s: @@ -338,14 +327,7 @@ def _resolve_pane_id(self, pane: Union[str, int, None], workspace_id: Optional[s return None if pid in (None, "", {}) else str(pid) if isinstance(pane, int): - params: Dict[str, Any] = {} - if workspace_id: - params["workspace_id"] = workspace_id - items = (self._call("pane.list", params) or {}).get("panes") or [] - for row in items: - if int(row.get("index", -1)) == pane: - return str(row.get("id")) - raise cmuxError(f"Pane index not found: {pane}") + raise cmuxError(f"bare index {pane!r} is no longer accepted; pass a UUID or ref like pane:2") s = str(pane).strip() if not s: diff --git a/tests_v2/test_close_surface_selection.py b/tests_v2/test_close_surface_selection.py index 7a24ec6a..caf9bb0f 100644 --- a/tests_v2/test_close_surface_selection.py +++ b/tests_v2/test_close_surface_selection.py @@ -76,7 +76,8 @@ def test_close_middle_keeps_index(client: cmux) -> TestResult: _ensure_surfaces(client, 3) # Focus index 1. - client.focus_surface(1) + initial = client.list_surfaces() + client.focus_surface(initial[1][1]) if not _wait_focused_index(client, 1, timeout=4.0): result.failure("Failed to focus surface index 1") return result @@ -123,7 +124,7 @@ def test_close_last_selects_previous(client: cmux) -> TestResult: last_index = len(before) - 1 expected_prev_id = before[last_index - 1][1] - client.focus_surface(last_index) + client.focus_surface(before[last_index][1]) if not _wait_focused_index(client, last_index, timeout=4.0): result.failure(f"Failed to focus surface index {last_index}") return result diff --git a/tests_v2/test_close_workspace_selection.py b/tests_v2/test_close_workspace_selection.py index cfa9ba22..5edc2026 100644 --- a/tests_v2/test_close_workspace_selection.py +++ b/tests_v2/test_close_workspace_selection.py @@ -66,7 +66,12 @@ def test_close_middle_selects_next(client: cmux) -> TestResult: try: _ensure_workspaces(client, 3) - client.select_workspace(1) + initial = client.list_workspaces() + target = _by_index(initial, 1) + if target is None: + result.failure("Expected a workspace at index 1") + return result + client.select_workspace(target[1]) time.sleep(0.15) before = client.list_workspaces() @@ -114,7 +119,11 @@ def test_close_last_selects_previous(client: cmux) -> TestResult: return result last_index = len(before) - 1 - client.select_workspace(last_index) + target = _by_index(before, last_index) + if target is None: + result.failure(f"Expected a workspace at index {last_index}") + return result + client.select_workspace(target[1]) time.sleep(0.15) before = client.list_workspaces() diff --git a/tests_v2/test_ctrl_socket.py b/tests_v2/test_ctrl_socket.py index 772e096c..175ef153 100755 --- a/tests_v2/test_ctrl_socket.py +++ b/tests_v2/test_ctrl_socket.py @@ -302,7 +302,9 @@ def run_tests(): try: client.new_workspace() time.sleep(0.6) - client.focus_surface(0) + surfaces = client.list_surfaces() + if surfaces: + client.focus_surface(surfaces[0][1]) time.sleep(0.2) except Exception as e: # Continue; individual tests will report a clearer failure. diff --git a/tests_v2/test_tab_dragging.py b/tests_v2/test_tab_dragging.py index 6b9a133d..b61430b2 100644 --- a/tests_v2/test_tab_dragging.py +++ b/tests_v2/test_tab_dragging.py @@ -71,7 +71,7 @@ def ensure_focused_terminal(client: cmux) -> None: health = client.surface_health() term = next((h for h in health if h.get("type") == "terminal"), None) if term is not None: - client.focus_surface(term["index"]) + client.focus_surface(term["id"]) time.sleep(0.2) wait_for_terminal_in_window(client, term["index"], timeout=5.0) except Exception: @@ -170,17 +170,20 @@ def test_initial_terminal_responsive(client: cmux) -> TestResult: # Prefer targeting a specific terminal surface by index so this test # doesn't depend on "focused terminal" state. term_idx = None + term_id = None try: health = client.surface_health() term = next((h for h in health if h.get("type") == "terminal"), None) if term is not None: term_idx = term.get("index") - client.focus_surface(term_idx) + term_id = term.get("id") + client.focus_surface(term_id) wait_for_terminal_in_window(client, term_idx, timeout=5.0) except Exception: term_idx = None + term_id = None - if verify_terminal_responsive(client, marker, surface_idx=term_idx): + if verify_terminal_responsive(client, marker, surface_idx=term_id): result.success("Initial terminal is responsive") clear_marker(marker) else: @@ -213,18 +216,18 @@ def test_split_right_responsive(client: cmux) -> TestResult: return result # Test first surface - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("First terminal not responsive after split") clear_marker(marker0) clear_marker(marker1) return result # Test second surface - client.focus_surface(1) + client.focus_surface(surfaces[1][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker1, surface_idx=1): + if not verify_terminal_responsive(client, marker1, surface_idx=surfaces[1][1]): result.failure("Second terminal not responsive after split") clear_marker(marker0) clear_marker(marker1) @@ -266,18 +269,18 @@ def test_split_down_responsive(client: cmux) -> TestResult: return result # Test first surface - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("First terminal not responsive after vertical split") clear_marker(marker0) clear_marker(marker1) return result # Test second surface - client.focus_surface(1) + client.focus_surface(surfaces[1][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker1, surface_idx=1): + if not verify_terminal_responsive(client, marker1, surface_idx=surfaces[1][1]): result.failure("Second terminal not responsive after vertical split") clear_marker(marker0) clear_marker(marker1) @@ -313,7 +316,8 @@ def test_multiple_splits_responsive(client: cmux) -> TestResult: time.sleep(0.8) # Focus first pane and split down - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.3) client.new_split("down") time.sleep(0.8) @@ -322,7 +326,7 @@ def test_multiple_splits_responsive(client: cmux) -> TestResult: surfaces = client.list_surfaces() # Find the right pane (should be index 2 after the first split down) if len(surfaces) >= 3: - client.focus_surface(2) + client.focus_surface(surfaces[2][1]) time.sleep(0.3) client.new_split("down") time.sleep(0.8) @@ -339,9 +343,9 @@ def test_multiple_splits_responsive(client: cmux) -> TestResult: # Test each surface for i in range(min(len(surfaces), len(markers))): - client.focus_surface(i) + client.focus_surface(surfaces[i][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, markers[i], surface_idx=i): + if not verify_terminal_responsive(client, markers[i], surface_idx=surfaces[i][1]): result.failure(f"Terminal {i} not responsive after multiple splits") for m in markers: clear_marker(m) @@ -375,15 +379,17 @@ def test_focus_switching(client: cmux) -> TestResult: # Create two splits client.new_split("right") time.sleep(0.8) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.3) client.new_split("down") time.sleep(0.8) + surfaces = client.list_surfaces() # Rapidly switch focus between panes and verify each is responsive for cycle in range(2): for i in range(3): - client.focus_surface(i) + client.focus_surface(surfaces[i][1]) time.sleep(0.15) # Allow terminals to stabilize after rapid switching @@ -391,12 +397,12 @@ def test_focus_switching(client: cmux) -> TestResult: # After rapid switching, verify all are still responsive for i in range(3): - client.focus_surface(i) + client.focus_surface(surfaces[i][1]) time.sleep(0.5) # Give more time for focus to settle - if not verify_terminal_responsive(client, markers[i], surface_idx=i): + if not verify_terminal_responsive(client, markers[i], surface_idx=surfaces[i][1]): # Retry once if it fails (timing-related issues) time.sleep(0.5) - if not verify_terminal_responsive(client, markers[i], surface_idx=i): + if not verify_terminal_responsive(client, markers[i], surface_idx=surfaces[i][1]): result.failure(f"Terminal {i} not responsive after focus switching") for m in markers: clear_marker(m) @@ -428,6 +434,7 @@ def test_split_ratio_50_50(client: cmux) -> TestResult: # Create a horizontal split client.new_split("right") time.sleep(2.0) # Wait for animation and layout to complete + surfaces = client.list_surfaces() # Retry logic for getting column counts for attempt in range(3): @@ -436,7 +443,7 @@ def test_split_ratio_50_50(client: cmux) -> TestResult: clear_marker(cols_file_1) # Get columns from first terminal - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.5) client.send_key("ctrl-c") time.sleep(0.3) @@ -445,7 +452,7 @@ def test_split_ratio_50_50(client: cmux) -> TestResult: time.sleep(1.5) # Get columns from second terminal - client.focus_surface(1) + client.focus_surface(surfaces[1][1]) time.sleep(0.5) client.send_key("ctrl-c") time.sleep(0.3) @@ -581,8 +588,10 @@ def test_pane_commands(client: cmux) -> TestResult: try: client.focus_pane(pane_id) except Exception: - # Fallback to index-based focus if the pane UUID changed unexpectedly. - client.focus_pane(0) + # Fallback: refresh the pane list and retry with the first pane's id. + fresh_panes = client.list_panes() + if fresh_panes: + client.focus_pane(fresh_panes[0][1]) time.sleep(0.3) if not verify_terminal_responsive(client, marker): @@ -612,11 +621,12 @@ def test_close_horizontal_split(client: cmux) -> TestResult: time.sleep(0.5) # Wait for the initial surface view to attach so send/send_key are reliable. wait_for_terminal_in_window(client, 0, timeout=5.0) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.2) # Verify initial terminal works - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("Initial terminal not responsive") clear_marker(marker0) clear_marker(marker1) @@ -635,24 +645,24 @@ def test_close_horizontal_split(client: cmux) -> TestResult: return result # Verify both terminals work before close - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("First terminal not responsive before close") clear_marker(marker0) clear_marker(marker1) return result - client.focus_surface(1) + client.focus_surface(surfaces[1][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker1, surface_idx=1): + if not verify_terminal_responsive(client, marker1, surface_idx=surfaces[1][1]): result.failure("Second terminal not responsive before close") clear_marker(marker0) clear_marker(marker1) return result # Close the second (right) surface - client.close_surface(1) + client.close_surface(surfaces[1][1]) time.sleep(1.5) # Verify we now have 1 surface (with retry for timing) @@ -670,9 +680,9 @@ def test_close_horizontal_split(client: cmux) -> TestResult: # Verify remaining terminal is responsive clear_marker(marker0) - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.2) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("Remaining terminal not responsive after close") clear_marker(marker0) clear_marker(marker1) @@ -701,11 +711,12 @@ def test_close_vertical_split(client: cmux) -> TestResult: client.new_workspace() time.sleep(0.5) wait_for_terminal_in_window(client, 0, timeout=5.0) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.2) # Verify initial terminal works - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("Initial terminal not responsive") clear_marker(marker0) clear_marker(marker1) @@ -724,24 +735,24 @@ def test_close_vertical_split(client: cmux) -> TestResult: return result # Verify both terminals work before close - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("First terminal not responsive before close") clear_marker(marker0) clear_marker(marker1) return result - client.focus_surface(1) + client.focus_surface(surfaces[1][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker1, surface_idx=1): + if not verify_terminal_responsive(client, marker1, surface_idx=surfaces[1][1]): result.failure("Second terminal not responsive before close") clear_marker(marker0) clear_marker(marker1) return result # Close the second (bottom) surface - client.close_surface(1) + client.close_surface(surfaces[1][1]) time.sleep(1.5) # Verify we now have 1 surface (with retry for timing) @@ -759,9 +770,9 @@ def test_close_vertical_split(client: cmux) -> TestResult: # Verify remaining terminal is responsive clear_marker(marker0) - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.2) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("Remaining terminal not responsive after close") clear_marker(marker0) clear_marker(marker1) @@ -794,11 +805,12 @@ def test_close_first_pane_vertical_split(client: cmux) -> TestResult: client.new_workspace() time.sleep(0.5) wait_for_terminal_in_window(client, 0, timeout=5.0) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.2) # Verify initial terminal works - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("Initial terminal not responsive") clear_marker(marker0) clear_marker(marker1) @@ -817,24 +829,24 @@ def test_close_first_pane_vertical_split(client: cmux) -> TestResult: return result # Verify both terminals work before close - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("First (top) terminal not responsive before close") clear_marker(marker0) clear_marker(marker1) return result - client.focus_surface(1) + client.focus_surface(surfaces[1][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, marker1, surface_idx=1): + if not verify_terminal_responsive(client, marker1, surface_idx=surfaces[1][1]): result.failure("Second (bottom) terminal not responsive before close") clear_marker(marker0) clear_marker(marker1) return result # Close the FIRST (top) surface - this is the bug case - client.close_surface(0) + client.close_surface(surfaces[0][1]) time.sleep(1.5) # Verify we now have 1 surface (with retry for timing) @@ -853,9 +865,9 @@ def test_close_first_pane_vertical_split(client: cmux) -> TestResult: # Verify remaining terminal is responsive (this is the critical check) clear_marker(marker0) clear_marker(marker1) - client.focus_surface(0) + client.focus_surface(surfaces[0][1]) time.sleep(0.2) - if not verify_terminal_responsive(client, marker0, surface_idx=0): + if not verify_terminal_responsive(client, marker0, surface_idx=surfaces[0][1]): result.failure("Remaining terminal not responsive after closing first pane!") clear_marker(marker0) return result @@ -888,11 +900,14 @@ def test_close_nested_splits(client: cmux) -> TestResult: # Create 2x2 grid client.new_split("right") time.sleep(0.8) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.3) client.new_split("down") time.sleep(0.8) - client.focus_surface(2) + surfaces = client.list_surfaces() + if len(surfaces) >= 3: + client.focus_surface(surfaces[2][1]) time.sleep(0.3) client.new_split("down") time.sleep(0.8) @@ -907,7 +922,7 @@ def test_close_nested_splits(client: cmux) -> TestResult: # Close one at a time and verify remaining terminals # Close surface 3 (bottom-right) - client.close_surface(3) + client.close_surface(surfaces[3][1]) time.sleep(1.0) surfaces = client.list_surfaces() @@ -919,16 +934,16 @@ def test_close_nested_splits(client: cmux) -> TestResult: # Verify remaining 3 terminals work for i in range(3): - client.focus_surface(i) + client.focus_surface(surfaces[i][1]) time.sleep(0.3) - if not verify_terminal_responsive(client, markers[i], surface_idx=i): + if not verify_terminal_responsive(client, markers[i], surface_idx=surfaces[i][1]): result.failure(f"Terminal {i} not responsive after first close") for m in markers: clear_marker(m) return result # Close another - client.close_surface(0) + client.close_surface(surfaces[0][1]) time.sleep(1.0) surfaces = client.list_surfaces() @@ -940,10 +955,10 @@ def test_close_nested_splits(client: cmux) -> TestResult: # Verify remaining 2 terminals work for i in range(2): - client.focus_surface(i) + client.focus_surface(surfaces[i][1]) time.sleep(0.3) clear_marker(markers[i]) - if not verify_terminal_responsive(client, markers[i], surface_idx=i): + if not verify_terminal_responsive(client, markers[i], surface_idx=surfaces[i][1]): result.failure(f"Terminal {i} not responsive after second close") for m in markers: clear_marker(m) @@ -975,11 +990,12 @@ def test_rapid_split_close_vertical(client: cmux) -> TestResult: client.new_workspace() time.sleep(0.5) wait_for_terminal_in_window(client, 0, timeout=5.0) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.2) # Verify initial terminal works - if not verify_terminal_responsive(client, marker, surface_idx=0): + if not verify_terminal_responsive(client, marker, surface_idx=surfaces[0][1]): result.failure("Initial terminal not responsive") clear_marker(marker) return result @@ -993,13 +1009,15 @@ def test_rapid_split_close_vertical(client: cmux) -> TestResult: time.sleep(0.4) # Brief delay for split # Immediately close the bottom (new) pane - client.close_surface(1) + surfaces = client.list_surfaces() + client.close_surface(surfaces[1][1]) time.sleep(0.4) # Brief delay for close # Check if remaining terminal is responsive - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.2) - if not verify_terminal_responsive(client, marker, surface_idx=0, retries=2): + if not verify_terminal_responsive(client, marker, surface_idx=surfaces[0][1], retries=2): result.failure(f"Terminal blank after cycle {cycle + 1}") clear_marker(marker) return result @@ -1028,11 +1046,12 @@ def test_rapid_split_close_first_pane(client: cmux) -> TestResult: client.new_workspace() time.sleep(0.5) wait_for_terminal_in_window(client, 0, timeout=5.0) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.2) # Verify initial terminal works - if not verify_terminal_responsive(client, marker, surface_idx=0): + if not verify_terminal_responsive(client, marker, surface_idx=surfaces[0][1]): result.failure("Initial terminal not responsive") clear_marker(marker) return result @@ -1046,13 +1065,15 @@ def test_rapid_split_close_first_pane(client: cmux) -> TestResult: time.sleep(0.4) # Brief delay for split # Close the FIRST (top/original) pane - this is the bug case - client.close_surface(0) + surfaces = client.list_surfaces() + client.close_surface(surfaces[0][1]) time.sleep(0.4) # Brief delay for close # Check if remaining terminal is responsive - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(0.2) - if not verify_terminal_responsive(client, marker, surface_idx=0, retries=2): + if not verify_terminal_responsive(client, marker, surface_idx=surfaces[0][1], retries=2): result.failure(f"Terminal blank after closing first pane, cycle {cycle + 1}") clear_marker(marker) return result diff --git a/tests_v2/test_visual_screenshots.py b/tests_v2/test_visual_screenshots.py index 5eedd1d5..2c1514a8 100644 --- a/tests_v2/test_visual_screenshots.py +++ b/tests_v2/test_visual_screenshots.py @@ -388,7 +388,8 @@ def _close_and_verify(client: cmux, change: StateChange, close_idx: int, """Shared logic: close a surface, verify count, verify responsiveness, capture after.""" change.before, change.before_state = capture(client, before_label) try: - client.close_surface(close_idx) + surfaces = client.list_surfaces() + client.close_surface(surfaces[close_idx][1]) time.sleep(CLOSE_WAIT) if not wait_surface_count(client, expected): change.error = f"Expected {expected} surface(s), got {surface_count(client)}" @@ -463,7 +464,8 @@ def test_c8_3way_close_middle(client: cmux) -> StateChange: ) client.new_split("right") time.sleep(SPLIT_WAIT) - client.focus_surface(1) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[1][1]) time.sleep(SHORT_WAIT) client.new_split("right") time.sleep(SPLIT_WAIT) @@ -479,13 +481,14 @@ def test_c9_grid_close_topleft(client: cmux) -> StateChange: ) client.new_split("right") time.sleep(SPLIT_WAIT) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(SHORT_WAIT) client.new_split("down") time.sleep(SPLIT_WAIT) surfaces = client.list_surfaces() if len(surfaces) >= 3: - client.focus_surface(2) + client.focus_surface(surfaces[2][1]) time.sleep(SHORT_WAIT) client.new_split("down") time.sleep(SPLIT_WAIT) @@ -501,13 +504,14 @@ def test_c10_grid_close_bottomright(client: cmux) -> StateChange: ) client.new_split("right") time.sleep(SPLIT_WAIT) - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(SHORT_WAIT) client.new_split("down") time.sleep(SPLIT_WAIT) surfaces = client.list_surfaces() if len(surfaces) >= 3: - client.focus_surface(2) + client.focus_surface(surfaces[2][1]) time.sleep(SHORT_WAIT) client.new_split("down") time.sleep(SPLIT_WAIT) @@ -524,7 +528,8 @@ def test_d11_nested_close_bottomright(client: cmux) -> StateChange: ) client.new_split("right") time.sleep(SPLIT_WAIT) - client.focus_surface(1) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[1][1]) time.sleep(SHORT_WAIT) client.new_split("down") time.sleep(SPLIT_WAIT) @@ -540,7 +545,8 @@ def test_d12_nested_close_top(client: cmux) -> StateChange: ) client.new_split("down") time.sleep(SPLIT_WAIT) - client.focus_surface(1) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[1][1]) time.sleep(SHORT_WAIT) client.new_split("right") time.sleep(SPLIT_WAIT) @@ -556,11 +562,13 @@ def test_d13_4pane_close_second(client: cmux) -> StateChange: ) client.new_split("right") time.sleep(SPLIT_WAIT) - client.focus_surface(1) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[1][1]) time.sleep(SHORT_WAIT) client.new_split("right") time.sleep(SPLIT_WAIT) - client.focus_surface(2) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[2][1]) time.sleep(SHORT_WAIT) client.new_split("right") time.sleep(SPLIT_WAIT) @@ -590,7 +598,8 @@ def test_e14_browser_close_terminal(client: cmux) -> StateChange: change.before, change.before_state = capture(client, "e14_before") try: # Find and close the first terminal (index 0) - client.close_surface(0) + surfaces = client.list_surfaces() + client.close_surface(surfaces[0][1]) time.sleep(CLOSE_WAIT) change.passed = wait_surface_count(client, before_count - 1) if not change.passed: @@ -629,7 +638,8 @@ def test_e15_browser_close_browser(client: cmux) -> StateChange: return change change.before, change.before_state = capture(client, "e15_before") try: - client.close_surface(before_count - 1) + surfaces = client.list_surfaces() + client.close_surface(surfaces[before_count - 1][1]) # Browser close leaves behind an auto-created terminal that may need # extra time for its shell to initialize, so wait longer. time.sleep(2.0) @@ -680,7 +690,8 @@ def test_g17_rapid_down_close_top(client: cmux) -> StateChange: client.new_split("down") time.sleep(SPLIT_WAIT) # Close the first (top) surface — wait for it to complete - client.close_surface(0) + surfaces = client.list_surfaces() + client.close_surface(surfaces[0][1]) if not wait_surface_count(client, 1, timeout=5.0): change.error = f"Cycle {i+1}: expected 1 surface, got {surface_count(client)}" change.passed = False @@ -710,7 +721,8 @@ def test_g18_rapid_right_close_left(client: cmux) -> StateChange: for i in range(5): client.new_split("right") time.sleep(0.8) - client.close_surface(0) + surfaces = client.list_surfaces() + client.close_surface(surfaces[0][1]) time.sleep(1.0) if not wait_surface_count(client, 1, timeout=5.0): change.error = f"Cycle {i+1}: expected 1 surface, got {surface_count(client)}" @@ -742,11 +754,12 @@ def test_g19_alternating_close_reverse(client: cmux) -> StateChange: change.before, change.before_state = capture(client, "g19_before") try: for i in range(4, 0, -1): - n = surface_count(client) + surfaces = client.list_surfaces() + n = len(surfaces) if n <= 1: break expected = n - 1 - client.close_surface(n - 1) + client.close_surface(surfaces[n - 1][1]) if not wait_surface_count(client, expected, timeout=5.0): change.error = f"Close {i}: expected {expected} surfaces, got {surface_count(client)}" change.passed = False @@ -797,7 +810,9 @@ def test_h20_workspace_switch_back(client: cmux) -> StateChange: if original_ws: client.select_workspace(original_ws) else: - client.select_workspace(0) + fallback_workspaces = client.list_workspaces() + if fallback_workspaces: + client.select_workspace(fallback_workspaces[0][1]) time.sleep(SHORT_WAIT) after_count = surface_count(client) @@ -934,7 +949,8 @@ def test_i24_browser_drag_split_right_focus_bounce(client: cmux) -> StateChange: time.sleep(0.4) # Focus bounce - client.focus_surface(0) + surfaces = client.list_surfaces() + client.focus_surface(surfaces[0][1]) time.sleep(SHORT_WAIT) client.focus_surface(browser_id) time.sleep(SHORT_WAIT) @@ -981,10 +997,11 @@ def test_i25_browser_drag_split_right_then_switch_panes(client: cmux) -> StateCh change.passed = False change.error = f"Expected 2 panes after drag split, got {pane_count(client)}" else: - # Switch panes by index (stable order from list_panes). - client.focus_pane(0) + # Switch panes by ref (stable order from list_panes). + panes = client.list_panes() + client.focus_pane(panes[0][1]) time.sleep(SHORT_WAIT) - client.focus_pane(1) + client.focus_pane(panes[1][1]) time.sleep(SHORT_WAIT) blank_err = verify_all_responsive(client, "i25_after_drag")