Skip to content

feat(expose): macOS-style Exposé TUI (Ctrl-A g) - #122

Merged
TraderSamwise merged 5 commits into
masterfrom
feat-expose
Jun 14, 2026
Merged

feat(expose): macOS-style Exposé TUI (Ctrl-A g)#122
TraderSamwise merged 5 commits into
masterfrom
feat-expose

Conversation

@TraderSamwise

@TraderSamwise TraderSamwise commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a macOS-Exposé-style TUI popup to aimux, opened with the tmux leader combo Ctrl-A g. It tiles live capture-pane previews of agent windows across the screen with 19 quick-jump.

Built on the existing switcher/inbox-popup popup pattern (a display-popup running a Node TUI), keeping it in the tmux control layer per the navigation-layer rule in AGENTS.md — not a new in-process layer.

Behavior

  • On the dashboard → all active agents across all worktrees (each tile shows its worktree name).
  • Inside an agent → scoped to that agent's worktree (detected via the focused window's worktreePath).
  • 19 jump to an agent; arrows/hjkl/n/p move, Enter opens, q/Esc closes.
  • Real-time previews — each tile re-captures its pane ~every 1s; selecting one switch-clients to that window via the existing tmux-control path.
  • Config overrideexpose.forceGlobalScope (default false) in .aimux/config.json / ~/.aimux/config.json; when true, Exposé always shows all agents regardless of where it's invoked.

Ctrl-A e was already bound to team, so Exposé uses Ctrl-A g ("grid").

Changes

  • Phase 1 (934fbf6): expose.forceGlobalScope config flag + src/tmux/expose-model.ts (scope resolution + agent listing). listSwitchableAgentItems gains a scope option (default "worktree", so switcher/next/prev are unchanged).
  • Phase 2 (922cdec): src/tmux/expose.ts (runTmuxExpose) tiled TUI + aimux expose CLI command.
  • Phase 3 (707cd29): expose action in scripts/tmux-control.sh, Ctrl-A g binding in runtime-manager.ts, tests + README hotkey docs.

Testing

  • typecheck, lint, full suite (1124 tests — incl. new config / expose-model / control-script / runtime-manager coverage), and yarn build all green.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Exposé grid view—press Ctrl+A g to see active agents with live previews in an interactive tiled layout. Jump to agents using keys 1–9, navigate with arrow keys or vi-style keys (h/j/k/l), and open with Enter. Configure agent scope (global or worktree-only) via the new expose.forceGlobalScope setting.

test and others added 3 commits June 14, 2026 19:38
Add `expose.forceGlobalScope` (default false) to AimuxConfig and a
non-UI core that resolves Exposé scope and lists agent windows:

- `listSwitchableAgentItems` gains a `scope` option ("all" | "worktree",
  default "worktree" so switcher/next/prev are unchanged); "all" bypasses
  the worktree-path filter while still excluding team + dashboard windows.
- `src/tmux/expose-model.ts`: `resolveExposeScope` (forced-global or
  dashboard/unidentified window => all; inside an agent => worktree) and
  `listExposeAgentItems`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `src/tmux/expose.ts` (`runTmuxExpose`), a switcher-style popup that
tiles live `capture-pane` previews of the scoped agent windows in a grid:

- Scope + agent list from `listExposeAgentItems` (config-aware).
- Bordered tiles with index badge, label, worktree (in global scope), and
  last-N plain preview lines; selected tile highlighted.
- ~1s interval re-captures into a cache and re-renders; tiny-terminal
  widths are clamped so borders never under/overflow.
- Keys: 1-9 jump, arrows/hjkl/n/p move, Enter open, q/Esc close. Switch
  reuses tmux-control.sh `window`.

Register `.command("expose")` in main.ts mirroring `switcher`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the Exposé TUI to the tmux leader prefix:

- scripts/tmux-control.sh: new `expose` action that opens a near-fullscreen
  centered `display-popup` running `aimux expose ...` (mirrors the switcher
  popup), threaded through arg-parse, path map, fallback list, and
  fallback_local_control.
- runtime-manager.ts: unbind + bind `Ctrl-A g` in the prefix table to run
  the control script's `expose` action (mirrors `Ctrl-A s`).
- Tests for the binding and the popup invocation; README hotkey docs.

`Ctrl-A g` works on the dashboard (all worktrees) and inside an agent
(that worktree), honoring expose.forceGlobalScope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jun 14, 2026 12:38pm

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@TraderSamwise, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 1 minute and 51 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec9e1206-9c73-4936-8db5-c7263f998dc3

📥 Commits

Reviewing files that changed from the base of the PR and between 707cd29 and 03f5a1f.

📒 Files selected for processing (7)
  • src/config.test.ts
  • src/config.ts
  • src/fast-control.ts
  • src/main.ts
  • src/tmux/control-script.test.ts
  • src/tmux/expose-model.test.ts
  • src/tmux/expose.ts
📝 Walkthrough

Walkthrough

Adds a new "Exposé" feature to aimux: an interactive tiled-grid terminal UI (aimux expose) that displays live tmux captures of agent windows and supports keyboard navigation and 1–9 jumping. The feature introduces a forceGlobalScope config toggle, a prefix g tmux keybinding, shell script dispatch via tmux-control.sh, and full test coverage.

Changes

Exposé Grid Feature

Layer / File(s) Summary
ExposeConfig schema and defaults
src/config.ts, src/config.test.ts
Adds ExposeConfig interface with forceGlobalScope: boolean, extends RuntimeConfig to include expose: ExposeConfig, sets the default to false, and tests default and local-config-override behavior.
AgentListScope type and listSwitchableAgentItems filtering
src/fast-control.ts
Exports AgentListScope = "all" | "worktree", extends listSwitchableAgentItems with optional opts.scope, and updates window filtering to skip metadata.team entries, pass all when scope === "all", or match by worktreePath otherwise.
resolveExposeScope and listExposeAgentItems
src/tmux/expose-model.ts, src/tmux/expose-model.test.ts
Adds resolveExposeScope (returns "all" for dashboard/missing window/force-global, else "worktree"), the ExposeAgentList interface, and listExposeAgentItems, with full test coverage for all scope resolution branches and both listing modes.
Interactive Exposé UI and CLI entrypoint
src/tmux/expose.ts, src/main.ts
Implements TmuxExposeOptions, dynamic tiled grid layout, periodic per-window tmux capture refresh, keyboard navigation (arrows, vi-keys, 1–9 jump, Enter/q/Esc), no-items fallback, and runTmuxExpose. Wires aimux expose CLI command in main.ts.
Tmux prefix g binding and shell script expose action
src/tmux/runtime-manager.ts, src/tmux/runtime-manager.test.ts, scripts/tmux-control.sh, src/tmux/control-script.test.ts
Adds unbind-key/bind-key for prefix gexpose in configureSession with test assertions. Extends tmux-control.sh with show_local_expose, the expose fallback branch, action whitelist entry, and path="" routing. Adds integration test for display-popup dispatch.
README hotkeys documentation
README.md
Adds the Ctrl+A g Exposé entry to the hotkeys table and expands explanatory text for worktree vs. global scope via expose.forceGlobalScope.

Sequence Diagram

sequenceDiagram
  participant User
  participant tmux as tmux prefix g
  participant controlScript as tmux-control.sh
  participant CLI as aimux expose (main.ts)
  participant runTmuxExpose as runTmuxExpose (expose.ts)
  participant exposeModel as listExposeAgentItems
  participant tmuxCapture as tmux capture-pane

  User->>tmux: Ctrl+A g
  tmux->>controlScript: expose action + context
  controlScript->>controlScript: show_local_expose → display-popup
  controlScript->>CLI: aimux expose --project-root ... --current-window ...
  CLI->>runTmuxExpose: TmuxExposeOptions
  runTmuxExpose->>exposeModel: context + config
  exposeModel-->>runTmuxExpose: { scope: "worktree"|"all", items[] }
  runTmuxExpose->>runTmuxExpose: enter raw + alternate screen
  loop periodic refresh
    runTmuxExpose->>tmuxCapture: capture each agent window pane
    tmuxCapture-->>runTmuxExpose: preview lines
    runTmuxExpose->>User: render tiled grid with live previews
  end
  User->>runTmuxExpose: press 1–9 or Enter to select
  runTmuxExpose->>controlScript: switch action + target window id
  controlScript-->>runTmuxExpose: exit status
  runTmuxExpose->>runTmuxExpose: restore terminal state
  runTmuxExpose-->>CLI: exit code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hippity-hop, press G and behold,
A grid full of agents, tiled and scrolled!
Each pane comes alive with a glowing preview,
Jump one through nine — pick the agent for you.
Worktree or global, the scope is your call,
The rabbit hops fast and can visit them all! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(expose): macOS-style Exposé TUI (Ctrl-A g)' accurately describes the main feature being added—a macOS-Exposé-style terminal UI accessible via Ctrl-A g.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-expose

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/tmux/control-script.test.ts (1)

1270-1275: ⚡ Quick win

Assert that no HTTP fallback is attempted in the “locally” expose path.

This test validates popup dispatch, but it doesn’t verify that curl was never called. Add a curlLog assertion so regressions in local-vs-remote routing are caught.

As per coding guidelines, “For live-pane, latency-sensitive navigation, prefer tmux-local metadata and tmux bindings over Node-side session lists; inspect src/tmux/control-script.test.ts as source of truth.”

Suggested patch
     const log = readLog(envRoot);
+    const curlLog = readCurlLog(envRoot);
     expect(
       log.some((entry) => entry.includes("display-popup -c /dev/live -T aimux exposé -x C -y C -w 90% -h 90% -E exec")),
     ).toBe(true);
     expect(log.some((entry) => entry.includes("expose --project-root"))).toBe(true);
+    expect(curlLog).toEqual([]);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tmux/control-script.test.ts` around lines 1270 - 1275, The test currently
validates that the popup dispatch and expose command are logged correctly, but
it does not verify that curl was not invoked, which means regressions in
local-vs-remote routing could go undetected. Add a curlLog assertion after the
existing expect statements to ensure that the local expose path does not fall
back to HTTP by verifying that curl was never called. This assertion should
check that the curlLog either remains empty or does not contain entries related
to curl invocation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fast-control.ts`:
- Around line 154-159: The conditional check order in the fast-control.ts file
is causing force-global scope to be bypassed by teammate window restrictions.
Move the check for `scope === "all"` to execute before the `if
(teammateParentSessionId)` check so that force-global visibility takes
precedence and returns true immediately, preventing the teammate-parent branch
from short-circuiting the logic. Additionally, add a regression test case in
expose-model.test.ts that verifies the behavior when forceGlobalScope=true is
set and the current window metadata contains a team.parentSessionId, ensuring
the force-global scope correctly overrides teammate window restrictions.

In `@src/tmux/expose.ts`:
- Around line 163-169: The exit function properly restores terminal state, but
unexpected exceptions in the setInterval callback (interval refresh) and input
parsing sections can bypass this exit path, leaving the terminal in raw mode.
Wrap the main interactive loop containing the interval setup and input parsing
logic in a try/finally block where the finally clause calls the exit function
(or performs equivalent terminal restoration). This ensures terminal state is
restored even when unexpected errors occur in the callback handlers, preventing
the terminal from becoming unusable.

---

Nitpick comments:
In `@src/tmux/control-script.test.ts`:
- Around line 1270-1275: The test currently validates that the popup dispatch
and expose command are logged correctly, but it does not verify that curl was
not invoked, which means regressions in local-vs-remote routing could go
undetected. Add a curlLog assertion after the existing expect statements to
ensure that the local expose path does not fall back to HTTP by verifying that
curl was never called. This assertion should check that the curlLog either
remains empty or does not contain entries related to curl invocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66cd07bf-c00f-411e-a1d8-0d5c4f50b8a6

📥 Commits

Reviewing files that changed from the base of the PR and between eecd679 and 707cd29.

📒 Files selected for processing (12)
  • README.md
  • scripts/tmux-control.sh
  • src/config.test.ts
  • src/config.ts
  • src/fast-control.ts
  • src/main.ts
  • src/tmux/control-script.test.ts
  • src/tmux/expose-model.test.ts
  • src/tmux/expose-model.ts
  • src/tmux/expose.ts
  • src/tmux/runtime-manager.test.ts
  • src/tmux/runtime-manager.ts

Comment thread src/fast-control.ts Outdated
Comment thread src/tmux/expose.ts
… sanitize

- fast-control: force-global ("all") scope no longer narrowed by the
  teammate-window branch, so Exposé shows all agents as intended; add a
  regression test in expose-model.test.ts.
- expose.ts: route interval/keypress callback errors and SIGINT/SIGTERM
  through the shutdown path so the terminal is always restored; sanitize
  captured pane output (strip escapes + control bytes) to prevent escape
  injection / tile misalignment; clamp grid height to real terminal rows.
- control-script.test: assert no HTTP fallback (curlLog empty) in the
  local expose popup path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TraderSamwise

Copy link
Copy Markdown
Owner Author

Note (independent review finding, by design): the all-worktrees Exposé scope intentionally still excludes teammate sub-agent windows (if (metadata.team) return false). This matches the existing dashboard/switcher convention — teammate windows are hidden from the cross-worktree list and only shown within their team's context — and matches CodeRabbit's own suggested fix, which kept that guard. The scope !== "all" change only prevents over-narrowing to a single teammate group; it deliberately does not surface teammates in the global view.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant