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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
160 changes: 72 additions & 88 deletions CLI/programa.swift

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- [x] **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 — already implemented; covered by tests_v2/test_workspace_relative.py)
- [x] **P0** Remove `close-workspace` with no args — require explicit workspace short ID or UUID, with clear error message if missing (#134)

Expand Down
24 changes: 3 additions & 21 deletions tests_v2/cmux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions tests_v2/test_close_surface_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions tests_v2/test_close_workspace_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion tests_v2/test_ctrl_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading