From 4867a71be2e4fd68c190a81bfb66f60002765f3d Mon Sep 17 00:00:00 2001 From: arzafran Date: Wed, 4 Nov 2026 17:18:03 -0300 Subject: [PATCH] docs: changelog, user docs and socket API for everything shipped since Oct 28 (INC-492) --- CHANGELOG.md | 16 +++ CLAUDE.md | 4 +- Resources/settings.schema.json | 2 +- docs/README.md | 2 +- docs/plans/rust-core-concepts.md | 13 ++- docs/release.md | 2 +- docs/settings-json.md | 2 +- docs/socket-api.md | 161 +++++++++++++++++++++++-------- docs/workspace-interface.md | 99 ++++++++++++++++++- scripts/reload.sh | 3 +- 10 files changed, 242 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e6b9e77..f20c96282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ Programa is a fork of [cmux](https://github.com/manaflow-ai/cmux); for history p ## [Unreleased] ### Added +- A Storage button in the sidebar footer opens a popover with your free disk space, the worktrees Programa can remove, Xcode, Android and package manager caches, and how much disk each open workspace uses with its biggest build folders. Each Clean button shows what it would remove and removes nothing until you confirm. See `docs/workspace-interface.md`. +- `programa storage scan` lists every git worktree and developer cache with its size and the reason it would be kept, and `programa storage clean` removes merged worktrees and the caches their tools rebuild on their own. `--xcode`, `--android` and `--packages` limit a run to one kind of cache and `--dry-run` shows what would go. The `storage.scan` and `storage.clean` socket methods do the same. +- Every 6 hours Programa removes worktrees under `~/.programa/worktrees` whose branch is merged and that went unused for 24 hours, along with Xcode DerivedData, Gradle and package manager caches unused for 14 days and unavailable simulators. It keeps any worktree with uncommitted changes, an ignored `.env` file, an open workspace or a process running inside it. Turn it off with `worktrees.autoRemoveMerged` and `storage.autoCleanCaches`. +- When free space on your home volume drops below 20 GB, Programa sends one notification that opens the Storage popover. Change the threshold with `storage.lowDiskWarningGB`, or set it to 0 to turn the warning off. +- On macOS 26 each pane card has a header with split right, split down, zoom and close buttons that act on that pane, plus a new tab button when the pane has one tab. In a narrow pane the buttons drop out one at a time, and close always stays. +- Drag a tab out of the window and release it to open it in a new window, with its shell and scrollback intact. Press Escape during the drag to cancel. - The "N helpers running" line on a sidebar row opens a popover with that workspace's helpers: Claude Code subagents, OSC 7501 child records and helpers reported over the socket. Each row shows the helper's state, task, run time and final report; copy the report or open it in a markdown pane beside the terminal. Finished helpers stay listed until you press Clear. See `docs/workspace-interface.md`. - Programs that report OSC 7501 child records get one helper per child in that popover, with a progress bar, and a notification when a child needs your input on a pane you are not looking at. - `programa resume` restarts the last Claude Code, Codex or OpenCode session that ended in a pane by typing its resume command into the pane's shell. After a restart, Resume Agent Session in the command palette does the same. @@ -31,6 +37,10 @@ Programa is a fork of [cmux](https://github.com/manaflow-ai/cmux); for history p - `programa aside install-mcp` also registers Programa in Aside's own MCP settings, so Aside can start agents in Programa panes and read their results. `uninstall-mcp` removes it and `programa aside status` shows it. See `docs/aside-browser.md`. ### Changed +- Sidebar rows start with the running agent's icon, or a terminal symbol, and show one status line under the title, with the agent's state as a colored pill and how long ago the status changed on the right. Hover a row for a ••• button that opens its right-click menu. The directory line is hidden when it is `~` or repeats the title, and the selected row is a raised white pill while the window is active. +- The sidebar toggle, notifications bell and new workspace button sit right after the window's traffic lights. +- The sidebar slides in and out when you toggle it, and fades instead when Reduce Motion is on. +- The diagnostics log keeps weeks of history instead of about a day and a half, because it no longer records routine socket connections that open and close within a second. - A workspace with a single tab hides its tab bar, along with the new-tab and split buttons, and leaves an empty strip in its place so the window can still be dragged from the top. A glass + button in that strip's right corner opens a new tab. The bar comes back when the pane gets a second tab or the workspace is split, so every split pane keeps its close and new-tab buttons. - A focused terminal that has nothing new to show uses about half as much CPU. It stops waking up on every screen refresh after a quarter second of quiet and starts again on the next change. - Only one copy of Programa runs at a time: launching a second copy brings the running app forward and exits. @@ -61,6 +71,12 @@ Programa is a fork of [cmux](https://github.com/manaflow-ai/cmux); for history p - The `programa omo`, `omx` and `omc` wrappers for oh-my-openagent, Oh My Codex and Oh My Claude Code. The `programa claude`, `codex` and `opencode` integrations are unchanged. See `docs/removed/omo-omx-omc-wrappers.md` to restore them. ### Fixed +- When Claude Code is killed, its tab stays open with a banner offering Resume or Close, and processes the session left running are stopped. +- `programa resume` skips a Claude session that ended before its first prompt, since there is nothing to reopen. +- After a program is killed, the next prompt resets mouse reporting and the alternate screen, so the terminal stops acting as if the program were still running. +- Closing a split no longer flashes the empty panel view for a frame. +- The usage popover in the sidebar footer shows Claude's usage when Claude Code has written only one of its two rate limit windows. The Claude section was hidden in that case, so only OpenAI showed. +- Force quitting an app you launched from a pane no longer quits Programa and every pane with it. Panes no longer share Programa's privacy permissions, so macOS asks the first time a pane reads Desktop, Documents or Downloads. - Pressing Esc to interrupt Claude Code no longer leaves its pane showing Working for good. Claude Code 2.1.295 and later reports its own status, so Programa's hooks stop writing it on top. - A shell that survives a restart keeps updating its pane: its prompt state, folder, branch and ports reach the restored pane, so `programa resume` works there without `--force`. Before, the shell kept reporting to the pane id from before the restart. - `programa handoff` pastes into a pane whose terminal has not started yet instead of failing. diff --git a/CLAUDE.md b/CLAUDE.md index 3eeed7e65..8835cf24e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ By default, `reload.sh` builds but does **not** launch the app. The script print ./scripts/reload.sh --tag fix-zsh-autosuggestions --launch ``` -By default, every `--tag` shares one DerivedData directory (`programa-shared`), so switching tags or re-running the same tag is a warm incremental build instead of a cold one. Tags still get their own bundle id, app name, socket, and log — only the build directory is shared. Pass `--isolated` to give a tag its own DerivedData directory again (today's old default), e.g. when you need two tags to build fully independently: +By default, every `--tag` shares one DerivedData directory (`programa-shared`), so switching tags or re-running the same tag is a warm incremental build instead of a cold one. Tags still get their own bundle id, app name, socket, and log; only the build directory is shared. Pass `--isolated` to give a tag its own DerivedData directory, e.g. when you need two tags to build fully independently: ```bash ./scripts/reload.sh --tag fix-zsh-autosuggestions --isolated @@ -178,7 +178,7 @@ The app has a **Debug** menu in the macOS menu bar (only in DEBUG builds). Use i These run in CI; run the relevant one before pushing: -- `python3 scripts/check-structural-budgets.py`: line budgets for lifecycle owner files. `CLI/programa.swift`, `CLI/CLI+Hooks.swift`, `Sources/TerminalController.swift` and `Sources/CommandPaletteController.swift` sit at their cap, so free lines (or move code into a new file) before adding any. +- `python3 scripts/check-structural-budgets.py`: line budgets for lifecycle owner files such as `CLI/programa.swift` and `Sources/TerminalController.swift`. Run it before adding lines to a file it lists; it prints each file's current count and cap. When a file is near its cap, free lines or move code into a new file first. - `scripts/check-v2-contract.sh`: v2 socket methods are contract-first. Edit `contracts/v2/methods.json`, then `python3 scripts/gen-v2-contract.py`; never hand-edit the generated Swift. - `python3 scripts/check-settings-docs.py`: every settings key and shortcut action is documented. - New Swift files need four `project.pbxproj` entries (PBXBuildFile, PBXFileReference, group child, Sources build phase) or the build fails with "cannot find type in scope". diff --git a/Resources/settings.schema.json b/Resources/settings.schema.json index 5113ae9d7..302026cb5 100644 --- a/Resources/settings.schema.json +++ b/Resources/settings.schema.json @@ -144,7 +144,7 @@ "showClaudeQuota": { "type": "boolean", "default": true, - "description": "Show Claude Code rate-limit headroom (5h/7d windows) in the sidebar footer, read from ~/.claude/tmp/rate-limits.json when present." + "description": "Show the Provider Usage button in the sidebar footer, with Claude and OpenAI usage when available." } } }, diff --git a/docs/README.md b/docs/README.md index 2385da432..765de45fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ - [keyboard-shortcuts.md](keyboard-shortcuts.md): default shortcuts, action ids and chord syntax for custom bindings. - [settings-json.md](settings-json.md): every key in `~/.config/programa/settings.json`. - [programa-json.md](programa-json.md): `programa.json` commands and recipes, the layout schema, and saved layouts. -- [workspace-interface.md](workspace-interface.md): the pane tab bar, agent logos on tabs, edge fades, and the ports panel. +- [workspace-interface.md](workspace-interface.md): the pane tab bar and header, moving a tab to a new window, sidebar rows, the ports, storage and provider usage popovers, helpers and turn checkpoints, and resuming an agent session. - [terminal-themes.md](terminal-themes.md): terminal themes, opacity, blur and font. - [notifications.md](notifications.md): notifications, the sidebar agent indicator, and agent hook examples. - [program-status.md](program-status.md): the OSC 7501 program status protocol for reporting what a program is doing. diff --git a/docs/plans/rust-core-concepts.md b/docs/plans/rust-core-concepts.md index 46c475903..d28cd320b 100644 --- a/docs/plans/rust-core-concepts.md +++ b/docs/plans/rust-core-concepts.md @@ -156,13 +156,12 @@ notify --title [--subtitle][--body][--tab][--panel]`, `list-notifications `CLI-MCP/MCPServer+Capabilities.swift`, `CLI-MCP/MCPSocketBridge.swift`, `CLI-MCP/ToolCatalog.swift`, `CLI-MCP/ResourceCatalog.swift`, `CLI-MCP/MCPErrorMapping.swift`). Separate binary embedded at `Contents/Resources/bin/programa-mcp` inside the app bundle; talks to -the running app over the same v2 socket. 187 tools, one per exposed socket method with `.` -> -`_` (`surface.read_text` -> `surface_read_text`, `review.comment.add` -> `review_comment_add`). -Thirteen tools carry an explicit `focus_` prefix because they are allowed to move macOS focus: -`focus_window`, `focus_workspace_select`, `focus_workspace_next`, `focus_workspace_previous`, -`focus_workspace_last`, `focus_surface`, `focus_pane`, `focus_pane_last`, `focus_review_open`, -`focus_worktree_open`, `focus_browser_webview`, `focus_browser_element`, -`focus_browser_tab_switch` (`worktree_create` is a deliberate exception: it drops the underlying +the running app over the same v2 socket. One tool per exposed socket method with `.` -> `_` +(`surface.read_text` -> `surface_read_text`, `review.comment.add` -> `review_comment_add`); the +tool definitions live in `CLI-MCP/Tools/`, and `tools/list` on a running server returns them. +Tools that are allowed to move macOS focus carry an explicit `focus_` prefix instead, such as +`focus_window`, `focus_surface` and `focus_pane`; `CLI-MCP/Tools/FocusTools.swift` defines them +(`worktree_create` is a deliberate exception: it drops the underlying method's `focus` param entirely). Not exposed: `debug.*` (DEBUG-only, UI-test hooks) and app-chrome methods (`auth.login`, `settings.open`, `feedback.*`, `markdown.open`, `app.*`). Resources: `programa://tree` (full window/workspace/pane/surface tree) and diff --git a/docs/release.md b/docs/release.md index f1c58aca1..1704f1676 100644 --- a/docs/release.md +++ b/docs/release.md @@ -71,7 +71,7 @@ repository secret with the same name is ignored by the release job. | `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password for notarization | | `APPLE_TEAM_ID` | Apple team id | | `SPARKLE_PRIVATE_KEY` | Signs the appcast and enclosures; the workflow derives the public key embedded in the app from it. The release fails without it | -| `APPLE_PROVISION_PROFILE_BASE64` | Provisioning profile embedded in the app. Kept as a safety net: no entitlement in `programa.entitlements` is Apple-restricted now, so the app launches without a profile and an embedded one is harmless. If a restricted `com.apple.developer.*` entitlement is added back, the profile is required, because macOS kills the app at launch without it even though signing and notarization succeed. The workflow step still fails the build when the secret is missing. Candidate for removal once a release is confirmed to launch without it | +| `APPLE_PROVISION_PROFILE_BASE64` | Provisioning profile embedded in the app. Kept as a safety net: no entitlement in `programa.entitlements` is Apple-restricted, so the app launches without a profile and an embedded one is harmless. If a restricted `com.apple.developer.*` entitlement is added, the profile is required, because macOS kills the app at launch without it even though signing and notarization succeed. The workflow step still fails the build when the secret is missing. Candidate for removal once a release is confirmed to launch without it | ## Milestone version bumps diff --git a/docs/settings-json.md b/docs/settings-json.md index d599856c1..c1f3fa0a8 100644 --- a/docs/settings-json.md +++ b/docs/settings-json.md @@ -40,7 +40,7 @@ Sidebar settings from Settings > Sidebar. | Key | Type | Default | What it does | |---|---|---|---| | `matchTerminalBackground` | boolean | `false` | Use the terminal background instead of the sidebar tint. | -| `showClaudeQuota` | boolean | `true` | Show Claude Code rate-limit headroom (5h/7d windows) in the sidebar footer, read from ~/.claude/tmp/rate-limits.json when present. | +| `showClaudeQuota` | boolean | `true` | Show the Provider Usage button in the sidebar footer, which opens Claude and OpenAI rate limit usage. See [workspace-interface.md](workspace-interface.md#provider-usage). | ## `automation` diff --git a/docs/socket-api.md b/docs/socket-api.md index f2e59fbdb..bef8c9a81 100644 --- a/docs/socket-api.md +++ b/docs/socket-api.md @@ -2,9 +2,9 @@ Programa exposes a local control socket. Its protocol is v2 JSON-RPC: one JSON object per line over a Unix domain socket. The CLI, the MCP server, shell integration and your own scripts all -use it. A line that does not start with `{` gets a terse `v1_removed` error. The method table -below lists the former line-protocol command names next to their v2 methods, for reading old -scripts and bug reports. +use it. A line that does not start with `{` gets a terse `v1_removed` error. The +[command name index](#command-name-index) maps the line-protocol command names that older +scripts and bug reports use to their v2 methods. The machine-readable contract is `contracts/v2/methods.json` (methods) and `contracts/v2/protocol.json` (framing and auth handshake). See [Contract](#contract). @@ -225,14 +225,20 @@ Notes: - `id` is echoed back when present (string or number). - v2 methods accept **IDs**; v2 responses may include ephemeral `index` fields for ordering/debugging, but IDs are the stable handles. - Short refs such as `workspace:3` or `surface:2` are numbered per app session: a ref keeps pointing at the same object while Programa runs, and the numbering starts over after a restart. Restored workspaces and surfaces also get new UUIDs, so a script that keeps ids across a restart has to look them up again, for example with `system.tree`. -- Auth (`auth.login`) is handled as a connection-level preamble before protocol dispatch, not a regular method — it worked the same way for v1 and v2 clients and was unaffected by the v1 removal. +- Auth (`auth.login`) is handled as a connection-level preamble before protocol dispatch, not as a regular method. -## Method Parity Reference (former v1 name -> v2 method) +## Command name index -The complete list of v2 methods, with their params and results, is `contracts/v2/methods.json`. +Each entry is a v2 method, preceded by its line-protocol command name when it has one +(`list_windows -> window.list`). The complete list of methods, with their params and results, +is `contracts/v2/methods.json`. System: - ping -> `system.ping` (liveness; the CLI's `ping` subcommand calls this and prints "PONG" on success) +- `system.identify` (params: `caller`, `surface_id`, `tab_id`, `window_id`, `workspace_id`) +- `settings.open` (params: `activate`, `target`) +- `feedback.open` (params: `activate`, `window_id`, `workspace_id`) +- `feedback.submit` (no params in the contract) Windows: - list_windows -> `window.list` @@ -248,29 +254,36 @@ Workspaces: - select_workspace -> `workspace.select` - current_workspace -> `workspace.current` - close_workspace -> `workspace.close` +- `workspace.next`, `workspace.previous`, `workspace.last` (params: `surface_id`, `tab_id`, `window_id`, `workspace_id`) +- `workspace.reorder` (params: `workspace_id` (required), `index`, `before_workspace_id`, `after_workspace_id`, `surface_id`, `tab_id`, `window_id`) +- `workspace.equalize_splits` (params: `orientation`, `surface_id`, `tab_id`, `window_id`, `workspace_id`) +- `workspace.action` (params: `action`, plus `title`, `description` or `color` for the actions that take them, and `surface_id`, `tab_id`, `window_id`, `workspace_id`). `action` is required, though the contract omits it: `pin`, `unpin`, `rename` (`title`), `clear_name`, `set_description` (`description`), `clear_description`, `move_up`, `move_down`, `move_top`, `close_others`, `close_above`, `close_below`, `mark_read`, `mark_unread`, `set_color` (`color`), `clear_color`. -Worktrees (new in v2, no v1 predecessor -- see "worktree.* / layout.*" below): -- (no v1 equivalent) -> `worktree.create` -- (no v1 equivalent) -> `worktree.open` -- (no v1 equivalent) -> `worktree.remove` -- (no v1 equivalent) -> `worktree.list` +Worktrees (see "worktree.* / layout.*" below): +- `worktree.create` +- `worktree.open` +- `worktree.remove` +- `worktree.list` -Storage (new in v2, no v1 predecessor -- see "storage.*" below): -- (no v1 equivalent) -> `storage.scan` -- (no v1 equivalent) -> `storage.clean` +Storage (see "storage.*" below): +- `storage.scan` +- `storage.clean` -Layouts (new in v2, no v1 predecessor -- see "worktree.* / layout.*" below): -- (no v1 equivalent) -> `layout.save` -- (no v1 equivalent) -> `layout.apply` -- (no v1 equivalent) -> `layout.list` +Layouts (see "worktree.* / layout.*" below): +- `layout.save` +- `layout.apply` +- `layout.list` -Session snapshot history (new in v2, no v1 predecessor -- see `docs/plans/snapshot-restore.md`): -- (no v1 equivalent) -> `snapshot.list` -- (no v1 equivalent) -> `snapshot.restore` +Session snapshot history (see `docs/plans/snapshot-restore.md`): +- `snapshot.list` +- `snapshot.restore` -Agent detection (new in v2, no v1 predecessor -- see "agent.detection.*" below): -- (no v1 equivalent) -> `agent.detection.list` -- (no v1 equivalent) -> `agent.detection.classify` +Agent helpers (see "agent.task.*" below): +- `agent.task.start`, `agent.task.update`, `agent.task.finish`, `agent.task.finish_session`, `agent.task.list` + +Agent detection (see "agent.detection.*" below): +- `agent.detection.list` +- `agent.detection.classify` Surfaces / Splits: - list_surfaces -> `surface.list` @@ -281,11 +294,16 @@ Surfaces / Splits: - drag_surface_to_split -> `surface.drag_to_split` - refresh_surfaces -> `surface.refresh` - surface_health -> `surface.health` -- trigger_flash -> `surface.trigger_flash` (new in v2) -- (no v1 equivalent) -> `surface.wait` (new in v2, see "surface.wait" below) -- (no v1 equivalent) -> `agent.prompt` (new in v2, see "agent.prompt" below) -- (no v1 equivalent) -> `subscribe`/`unsubscribe` (new in v2, see "Socket Event Subscriptions" below) -- (no v1 equivalent) -> `review.open` / `review.refresh` / `review.comment.add` / `review.comment.remove` / `review.comment.list` / `review.send_comments` (new in v2, see "Review Panel" below) +- trigger_flash -> `surface.trigger_flash` +- `surface.current` (params: `surface_id`, `tab_id`, `window_id`, `workspace_id`) +- `surface.move` (params: `surface_id` (required), `pane_id`, `index`, `before_surface_id`, `after_surface_id`, `focus`, `window_id`, `workspace_id`) +- `surface.reorder` (params: `surface_id` (required), `index`, `before_surface_id`, `after_surface_id`) +- `surface.clear_history` (params: `surface_id`, `tab_id`, `window_id`, `workspace_id`) +- `tab.action`, and its alias `surface.action` (params: `action`, `title` for `rename`, and `surface_id`, `tab_id`, `window_id`, `workspace_id`). `action` is required, though the contract omits it: `rename` (`title`), `clear_name`, `close_left`, `close_right`, `close_others`, `new_terminal_right`, `pin`, `unpin`, `mark_read`, `mark_unread`. +- `surface.wait` (see "surface.wait" below) +- `agent.prompt` (see "agent.prompt" below) +- `subscribe`/`unsubscribe` (see "Socket Event Subscriptions" below) +- `review.open` / `review.refresh` / `review.comment.add` / `review.comment.remove` / `review.comment.list` / `review.send_comments` (see "Review Panel" below) Surface Telemetry (report_*/ports/git/pr — off-main parse, main.async mutate; see "Socket command threading policy" in the root `CLAUDE.md`): @@ -305,6 +323,10 @@ Panes: - focus_pane -> `pane.focus` - list_pane_surfaces -> `pane.surfaces` - new_pane -> `pane.create` +- `pane.last` (params: `surface_id`, `tab_id`, `window_id`, `workspace_id`) +- `pane.break` (params: `pane_id`, `focus`, `surface_id`, `tab_id`, `window_id`, `workspace_id`) +- `pane.join` (params: `target_pane_id` (required), `pane_id`, `surface_id`, `focus`) +- `pane.swap` (params: `pane_id` (required), `target_pane_id` (required), `focus`) Input: - send / send_surface -> `surface.send_text` @@ -336,12 +358,12 @@ Notifications: - notify_surface -> `notification.create_for_surface` - notify_target -> `notification.create_for_target` - list_notifications -> `notification.list` -- clear_notifications -> `notification.clear` (accepts an optional `workspace_id` to scope the clear to one workspace, matching v1's `--tab=X`; omitting it clears all notifications, matching v1's bare `clear_notifications`) +- clear_notifications -> `notification.clear` (accepts an optional `workspace_id` to scope the clear to one workspace; omitting it clears all notifications) - With `--json`, the CLI's `list-notifications` prints `{"notifications": [...]}` and `clear-notifications` prints `{"ok": true}`, plus `"workspace_id"` and `"workspace_ref"` when the clear was scoped to one workspace. See [notifications.md](notifications.md#cli-commands). - set_app_focus -> `app.focus_override.set` - simulate_app_active -> `app.simulate_active` - reload_config -> `app.reload_config` -- (no v1 equivalent) -> `app.browsers` (new in v2: lists known browsers with install/running status and the system default; see [`app.browsers`](#appbrowsers) below) +- `app.browsers` (lists known browsers with install/running status and the system default; see [`app.browsers`](#appbrowsers) below) Debug / Test-only: - set_shortcut -> `debug.shortcut.set` @@ -352,12 +374,19 @@ Debug / Test-only: - read_terminal_text -> `debug.terminal.read_text` - render_stats -> `debug.terminal.render_stats` - layout_debug -> `debug.layout` -- bonsplit_underflow_count/reset -> `debug.bonsplit_underflow.*` -- empty_panel_count/reset -> `debug.empty_panel.*` +- bonsplit_underflow_count / bonsplit_underflow_reset -> `debug.bonsplit_underflow.count` / `debug.bonsplit_underflow.reset` +- empty_panel_count / empty_panel_reset -> `debug.empty_panel.count` / `debug.empty_panel.reset` - focus_notification -> `debug.notification.focus` -- flash_count/reset -> `debug.flash.*` -- panel_snapshot/panel_snapshot_reset -> `debug.panel_snapshot.*` +- flash_count / flash_reset -> `debug.flash.count` (`surface_id` required) / `debug.flash.reset` +- panel_snapshot / panel_snapshot_reset -> `debug.panel_snapshot` / `debug.panel_snapshot.reset` - screenshot -> `debug.window.screenshot` +- `debug.command_palette.toggle`, `debug.command_palette.visible`, `debug.command_palette.selection`, `debug.command_palette.results`, `debug.command_palette.rename_tab.open`, `debug.command_palette.rename_input.interact`, `debug.command_palette.rename_input.selection`, `debug.command_palette.rename_input.delete_backward` (params: `window_id`; `debug.command_palette.results` also takes `limit`) +- `debug.glass.set` (params: `enabled` (required), `surface` (required)) +- `debug.portal.stats` (no params) +- `debug.samples.stats`, `debug.samples.reset` (params: `bucket`) +- `debug.sidebar.visible` (params: `window_id` (required)) +- `debug.tab.tear_off` (params: `surface_id`, `x`, `y`, all required): moves a surface into a new window placed at a screen point, as a tab drag dropped outside every window does. +- `debug.terminals`, `debug.notification.banner_count`, `debug.notification.banner_reset` (see "Debug-only methods" below) ## `surface.wait` (#166) @@ -755,7 +784,7 @@ server-side cap on concurrent subscriptions. ## `worktree.*` / `layout.*` (docs/plans/worktree-and-layouts.md) -Native git worktree workflow + named layout configs. Both new in v2 (no v1 predecessor). All +Native git worktree workflow + named layout configs. All seven methods parse/validate params off-main; the actual git/filesystem I/O runs inside `GitWorktreeManager`/`ProgramaLayoutStore` (blocking by construction, same pattern as `GitMetadataProber`); `v2MainSync` is used only around the final `tabManager.addWorkspace` / @@ -957,6 +986,55 @@ and `agent`; `pane_id` and `pane_ref` identify the pane hosting the spawned term callers can target it without a separate lookup. The new workspace is created without taking focus. +## `agent.task.*` + +The helper registry behind the sidebar helpers popover (see +[workspace-interface.md](workspace-interface.md#helpers-popover)) and the MCP `agent_task_*` +tools. `contracts/v2/methods.json` does not list these params yet; the tables below come from +the handlers in `Sources/TerminalController+AgentSupervision.swift`. `state` is one of `idle`, +`working`, `blocked`, `completed`, `failed` or `cancelled`. `agent.task.start`, +`agent.task.update` and `agent.task.finish` return `{"agent": {...}}`; `agent.task.finish_session` +and `agent.task.list` return `{"agents": [...], "count": N}`. + +### `agent.task.start` + +Reports a helper that is already running. + +| Param | Required | Notes | +|---|---|---| +| `workspace_id` | yes | Workspace that owns the helper. | +| `host` | yes | Host name, such as `claude` or `codex`. | +| `agent_id` | no | Stable helper id. Programa creates one when omitted. | +| `parent_id` | no | Parent helper id, which must belong to the same workspace. | +| `surface_id` | no | Terminal surface in that workspace. | +| `session` | no | Host session id. | +| `task` | no | Short task description. | +| `role` | no | Helper role. | +| `summary` | no | Final report text. | +| `state` | no | Defaults to `working`. | +| `placement` | no | `runs_with_parent` (default), `nested_workspace` or `separate_worktree`. | + +### `agent.task.update` + +Updates a running helper. Params: `agent_id` (required), and any of `workspace_id`, +`surface_id`, `session`, `task`, `role` and `state`. + +### `agent.task.finish` + +Marks a helper as done. Params: `agent_id` (required), `state` (`completed`, the default, +`failed` or `cancelled`), `task`, and `summary`, the final report shown in the helpers popover. + +### `agent.task.finish_session` + +Finishes every helper still active for a host session when that session ends. Params: `host` +and `session` (both required), and `state` (`completed`, `failed` or `cancelled`; defaults to +`cancelled`). + +### `agent.task.list` + +Lists helpers. Params, all optional: `workspace_id`, `parent_id`, and `include_finished` +(defaults to `true`). + ## `agent.event` (docs/plans/agent-events.md) A normalized alternative to `surface.report_agent_state` for providers whose hooks carry more @@ -985,16 +1063,15 @@ every other telemetry report echoes. Errors: `invalid_params` (missing/invalid CLI: `programa agent-event --event [--provider

] [--session-id ] [--turn-id ] [--item-id ] [--label ] [--resolution ] [--workspace ] [--surface ]`. `surface.report_agent_state` and `agent.event` both also accept optional `provider`, `session_id`, -and `pid` params now. They key the report to a hook session and register the pid for the stale-PID -watchdog and port scanning; older CLI builds that omit them still work, they just leave the report -unkeyed. +and `pid` params. They key the report to a hook session and register the pid for the stale-PID +watchdog and port scanning; a report without them still works and is left unkeyed. ## `agent.needs_input` (docs/plans/agent-state-unification.md) Atomically reports a surface as blocked on user input and posts the matching notification, in one -socket round trip instead of the three separate calls (`notification.create_for_target`, -`workspace.set_status`, `surface.report_agent_state`) a blocking-prompt hook used to make. Any one -of those three could fail independently; `agent.needs_input` can't leave the sidebar badge, the +socket round trip instead of three separate calls (`notification.create_for_target`, +`workspace.set_status`, `surface.report_agent_state`). Any one of those three can fail +independently; `agent.needs_input` can't leave the sidebar badge, the notification panel, and agent supervision disagreeing with each other, because it's a single write. | Field | Type | Required | Notes | diff --git a/docs/workspace-interface.md b/docs/workspace-interface.md index 82314a3ba..1b164b6d2 100644 --- a/docs/workspace-interface.md +++ b/docs/workspace-interface.md @@ -1,9 +1,11 @@ -# Tabs, agent logos and the ports panel +# Workspace interface -Four parts of the window show what is running without opening anything: the pane tab bar, -the agent logo on a tab, the edge fades on an overflowing tab bar, and the ports panel in the -sidebar footer. None of them has a settings key. The shortcuts that act on tabs are listed in -[keyboard-shortcuts.md](keyboard-shortcuts.md). +This page covers the parts of the window you work in: the pane tab bar and pane header, moving +a tab to a new window, agent logos and edge fades on tabs, the sidebar rows, the ports, storage +and provider usage popovers in the sidebar footer, the helpers popover and turn checkpoints, +and resuming an agent session. The shortcuts that act on tabs and panes are listed in +[keyboard-shortcuts.md](keyboard-shortcuts.md), and the settings named here are in +[settings-json.md](settings-json.md). ## Pane tab bar @@ -19,6 +21,40 @@ Every pane has its own tab bar along its top edge. Each tab is a terminal surfac On macOS 26 the tab bar is native glass. Earlier macOS versions draw it without glass. +A workspace with one pane and one tab hides the tab bar and leaves an empty strip at the top, +so you can still drag the window from there. The bar comes back when the pane gets a second +tab or the workspace is split. In minimal mode with the sidebar hidden, the bar stays, because +it shares the titlebar row with the traffic lights. + +## Pane header + +On macOS 26 the trailing end of each pane's tab bar holds a row of quiet controls. They act on +that pane, not on the focused one, so you can split or close a pane without clicking into it +first: + +- **New Tab**, shown only when the pane has one tab. A pane with more tabs keeps the "+" after + its last tab instead. +- **Split Right** and **Split Down**. They dim, with the tooltip "Split limit reached", when the + workspace cannot take another split. +- **Zoom Pane**, which reads **Exit Zoom** while the pane is zoomed. It dims when the workspace + has only one pane. +- **Close Tab**, which closes the pane's selected tab. It dims when that tab is pinned. + +A pane with a single tab shows that tab as a plain header: a small icon and the title, drawn +without the glass tab shape and with no close button on the tab itself. When a pane is too +narrow for every control, they drop out in this order: Split Down, Split Right, New Tab, then Zoom Pane. Close Tab always +stays. The same actions have shortcuts (⌘ D, ⌘ ⇧ D and ⌘ ⇧ ↩ for split and zoom); see +[keyboard-shortcuts.md](keyboard-shortcuts.md#split-panes). Earlier macOS versions do not have +the pane header. + +## Move a tab to a new window + +Drag a tab out past the edge of every Programa window and release it. The tab opens in a new +window placed under the release point, and its shell keeps running with its scrollback intact, +because the terminal moves instead of starting over. Press Escape during the drag to cancel and +keep the tab where it was. The last tab of the last workspace in the last window stays put, so +no window is left empty. + ## Agent logos on tabs A terminal tab shows the logo of the agent running in it, in place of the terminal icon. The @@ -40,6 +76,36 @@ about 24 points. A fade on the left edge means tabs are hidden to the left, and right means tabs are hidden to the right. A row that fits shows no fade. Edge fades belong to the native glass tab bar on macOS 26. +## Sidebar rows + +Each workspace has one row in the sidebar. From left to right, its first line holds: + +- the logo of the agent running in the workspace, or a terminal symbol when no agent reports + (a worktree folder row shows a folder and its collapse arrow instead); +- the workspace title, which fades out at the right edge instead of being cut off; +- a branch badge for a git worktree workspace, a pin for a pinned workspace, and the unread + notification count; +- how long ago the status line below last changed ("now", "3m", "2h", "4d"), updated once a + minute. + +Under the title sits one status line. It shows the first of these that applies: + +1. The agent's state while it is working or waiting for you, as a colored pill. +2. The latest notification text. +3. The agent's resting state (Idle, Done or Error), as a pill. +4. The latest log line from `programa log`. + +The agent states and the "(stale)" label are described in +[notifications.md](notifications.md#sidebar-agent-indicator). Below the status line come the +workspace description, the helpers line (see [Helpers popover](#helpers-popover)), status and +metadata entries, the latest log line when the status line shows something else, a progress +bar, the branch and directory lines, and pull requests. A directory line is left out when it is +`~` or repeats the title. + +Hovering a row replaces the elapsed time with a ••• button that opens the same menu as a +right-click. The selected row is a raised white pill with a soft shadow while the window is +active, and flat gray when it is not. A custom sidebar selection color keeps a flat fill. + ## Ports panel The Ports button sits in the sidebar footer and shows a count of listening TCP ports. It @@ -90,6 +156,29 @@ Warnings re-arm after space rises above the threshold plus 5 GB. Set `storage.lowDiskWarningGB` in [settings.json](settings-json.md#storage) to change the threshold or use `0` to disable warnings. +## Provider usage + +The gauge button in the sidebar footer, labeled Provider Usage, opens a popover with how much of +your Claude and OpenAI rate limits is left. It reads the numbers each time you open it and +shows "Loading usage…" until the first answer arrives. + +- **Claude** appears when the `claude` command is installed and signed in, and Claude Code has + written `~/.claude/tmp/rate-limits.json` within the last hour. Some Claude Code setups never + write that file, and then the Claude section is left out. The rows are **Current session** + (the 5 hour window) and **Weekly**, whichever the file has. +- **OpenAI** appears when the `codex` command (or the Codex app) is installed and signed in. + Its header shows your plan and credits, and its rows are the windows Codex reports, such as + **Current window** and **Weekly**. + +Each row shows the percentage left, a bar of what remains, and when the window resets. The bar +turns orange once 60% is used and red at 85%. A provider that could not be read shows an error +line instead of rows. With neither provider available, the popover says "No usage available". + +Turn the button off with **Show Provider Usage** in Settings > Sidebar, or +`sidebarAppearance.showClaudeQuota` in [settings.json](settings-json.md#sidebarappearance). +Token counts per session are a different feature: see `programa usage` in +[agent-usage.md](agent-usage.md). + ## Helpers popover and checkpoints ### Helpers popover diff --git a/scripts/reload.sh b/scripts/reload.sh index 76edaaf05..054b0bcad 100755 --- a/scripts/reload.sh +++ b/scripts/reload.sh @@ -139,8 +139,7 @@ Options: Sets app name, bundle id, and derived data path unless overridden. --launch Launch the app after building. Without this flag, the script builds and prints the app path but does not open it. - --isolated Give this tag its own DerivedData directory (today's - default behavior before shared builds). Without this, + --isolated Give this tag its own DerivedData directory. Without this, all tags share one DerivedData dir so a tag switch is a warm incremental build instead of a cold one. Tags still get their own bundle id, app name, socket, and