Skip to content

mux: implement the 8 proposed protocol-6 commands (wait-for, run, send-key, copy, ids, notify, list-agents, report-agent) - #7584

Merged
lawrencecchen merged 1 commit into
mainfrom
feat-mux-proposed-verbs
Jul 8, 2026
Merged

lawrencecchen merged 1 commit into
mainfrom
feat-mux-proposed-verbs

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Implements the eight commands that were marked proposed in mux/spec/commands.md, server-side in mux-core with CLI verbs and full tests, and flips their spec status to implemented. Protocol stays v6 (additive).

  • wait-for --surface --pattern --timeout-ms: blocks on the surface's output stream (attach-then-check ordering, so a one-shot match that lands before the wait is never missed), regex match with a real deadline, no busy-spin.
  • run -- <argv> / --command: spawns a surface running an explicit command (vs the login shell), with --pane/--new-workspace/--cwd/--name placement.
  • send-key <key>...: encodes named keys/chords (enter, ctrl+c, arrows, alt+…) through the ghostty key encoder synced from the terminal's modes.
  • copy --mode screen|selection|scrollback: viewport / current selection / full scrollback text.
  • ids --kind: tree ids (with short ids).
  • notify --title --body [--level] [--surface]: per-surface unread state + a notification event. The mux TUI renders the user-requested UX — an attention border on the selected tab/pane in the level color, an unread dot in the tab bar, and a workspace sidebar dot — cleared when the tab is focused, never stealing focus, and skipped for the already-active surface. A Ratatui TestBackend test asserts all three indicators appear and clear.
  • report-agent / list-agents: a per-surface agent-state store with hook > socket authority (a newer hook wins; a socket report never clobbers a hook). This is the explicit-report path; auto-detection is separate (mux: per-surface agent state machine with hook authority and title detection #7412).

Tests: per-command mux-core behavior tests, a CLI integration subset, the Ratatui render test, and a new conformance fixture so the commands run (not SKIP). Reviewed via a 2-round plan/code/judge loop (APPROVE). Verified locally: cargo fmt/clippy/test (130), conformance 6/6, smoke-tui, and a live CLI transcript exercising every verb (incl. agent-authority ordering and wait-for match/timeout).

Note: this and #7412 (agent state machine) both touch per-surface agent state; whichever lands second reconciles the store.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Adds blocking wait-for on the control socket and new PTY spawn paths via run, plus agent/notification state that must stay consistent with related work (#7412); scope is large but covered by conformance and integration tests.

Overview
Implements the eight proposed protocol-6 control socket commands (wait-for, run, send-key, copy, ids, notify, list-agents, report-agent) end-to-end: mux-core handlers, mux-tui CLI verbs, specs marked implemented, plus a conformance fixture that exercises the full flow.

Automation: wait-for regex-matches viewport text on the surface attach stream; run spawns PTY tabs with explicit argv or shell command and placement options; send-key uses new key_input_from_chord in ghostty-vt; copy reads screen, scrollback, or mux-stored selection text.

Telemetry & UI: notify drives per-surface unread state, notification events on subscribe/attach, and unread metadata in list-workspaces. The TUI shows level-colored pane borders, tab-bar dots, and sidebar dots (theme keys added), cleared when the user views the tab without stealing focus. report-agent / list-agents store agent state with hook over socket authority.

Adds regex dependency, mux unit/integration tests, and selection persistence when copying from the TUI.

Reviewed by Cursor Bugbot for commit 6bdfc73. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Implements the eight proposed protocol‑6 commands across mux-core and the CLI, adds a notification event with TUI indicators, and keeps the protocol at v6. This enables automation sync, explicit command spawns, key injection, text copy, id listing with short ids, notifications, and agent state APIs.

  • New Features

    • Implemented v6 commands: wait-for, run, send-key, copy, ids, notify, list-agents, report-agent.
      • wait-for: regex match on viewport text with attach‑then‑check ordering and real deadlines.
      • run: spawn a PTY with -- <argv> or --command, place in --pane or --new-workspace, set --cwd/--name; returns surface/pane/screen/workspace.
      • send-key: named chords via ghostty-vt encoder synced to terminal modes.
      • copy: screen | selection | scrollback text.
      • ids: list workspace/screen/pane/surface with stable six-char short ids.
      • notify: posts a notification event; per-surface unread state that doesn’t steal focus and is skipped for the active surface.
      • list-agents/report-agent: per-surface agent store with hook authority over socket; filterable listing.
    • CLI verbs added for all commands; subscribe/attach streams include notification events.
    • TUI: attention border on the selected tab/pane in level color, unread dot in tab bar, and workspace sidebar dot; cleared on focus.
    • Spec updated to mark these commands “implemented” in protocol 6.
  • Dependencies

    • Added regex to mux and mux-core.

Written for commit 6bdfc73. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added new control-socket and CLI commands for waiting on screen text, sending keys, copying content, listing IDs, posting notifications, and reporting/listing agent status.
    • Added unread notification indicators in the tab bar, sidebar, and pane borders, with configurable notification colors.
    • Added keyboard chord parsing for common shortcuts and function keys.
  • Bug Fixes

    • Improved handling of selection state and notification clearing when switching views.
    • wait-for now supports timeout-based matching more reliably.

Implements wait-for, run, send-key, copy, ids, notify, list-agents,
report-agent server-side (mux-core) with CLI verbs (mux-tui) and
flips their status to implemented in the spec. Protocol stays 6
(additive). Highlights:

- wait-for: registers the attach tap before the first screen check
  (attach-then-check, race-free) so a one-shot match is never missed.
- notify: per-surface unread state + a notification event; the mux
  TUI renders an attention border on the selected tab/pane, an unread
  dot in the tab bar, and a workspace sidebar dot, cleared on focus;
  never steals focus; skips the unread flag for the already-active
  surface. Ratatui TestBackend render test covers all three indicators.
- report-agent/list-agents: per-surface agent-state store with
  hook > socket authority (a newer hook wins; socket never clobbers).
- send-key via the ghostty key encoder; copy screen/selection/scrollback;
  run spawns an explicit command; ids lists tree ids.

Conformance gains an implemented-verbs fixture; new mux-core + CLI +
ratatui tests. Reviewed via the plan/code/judge loop (2 rounds).
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements eight new mux control-socket verbs (wait-for, run, send-key, copy, ids, notify, list-agents, report-agent), adds a notification and agent-tracking subsystem to mux-core, per-surface text selection state, a key-chord parser, corresponding mux-tui CLI/rendering support, conformance fixtures, and spec/docs status updates.

Changes

Implemented verbs and notification/agent system

Layer / File(s) Summary
Key chord parsing
mux/crates/ghostty-vt/src/key.rs, mux/crates/ghostty-vt/src/lib.rs
Adds key_input_from_chord to parse chord strings into KeyInput, re-exports it, and adds tests.
Mux-core notification/agent state
mux/crates/mux-core/src/mux.rs, mux/crates/mux-core/src/lib.rs
Adds notification/agent types, Mux tracking fields, post_notification/report_agent/list_agents APIs, a new MuxEvent::Notification variant, and tests.
Surface selection and spawn refactor
mux/crates/mux-core/src/surface.rs, mux/crates/mux-core/src/browser.rs, mux/crates/mux-core/src/mux.rs
Adds per-surface selection text storage, refactors surface spawning to support commands, reworks run_command_surface, and clears viewed notifications on focus/tab/screen/workspace selection.
Control-socket command handling
mux/crates/mux-core/src/server.rs, mux/Cargo.toml, mux/crates/mux-core/Cargo.toml, mux/crates/mux-core/tests/pty.rs
Adds regex dependency, updates command parsing, threads notifications into JSON output, implements handlers for all new verbs, streams notification events, and adds an integration test.
mux-tui CLI verbs
mux/crates/mux-tui/src/cli.rs, mux/crates/mux-tui/src/main.rs
Registers new verb specs, argument/positional parsing, request builders, response printers, and usage text.
mux-tui notification rendering
mux/crates/mux-tui/src/config.rs, mux/crates/mux-tui/src/session/*, mux/crates/mux-tui/src/ui/*, mux/crates/mux-tui/src/app.rs
Adds notification theme colors, threads notification state through session tree building, renders unread indicators, and stores selection text on copy.
Conformance fixtures and tests
mux/bindings/conformance/fixtures.json, mux/bindings/conformance/runner.py, mux/crates/mux-tui/tests/cli.rs
Adds a new fixture and dispatch entries for the new verbs and extends the CLI integration test.
Spec and docs updates
mux/spec/cli.md, mux/spec/commands.md, mux/spec/events.md, mux/docs/configuration.md
Marks new commands as implemented, documents behavior nuances, adds the notification event spec, and documents new theme keys.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 21

❌ Failed checks (1 warning, 20 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and testing, but it misses the required template sections for demo video, review trigger, and checklist. Add the missing Summary/Testing formatting, a Demo Video entry for UI changes, the review trigger block, and the checklist section.
Cmux Swift Actor Isolation ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Blocking Runtime ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Browser Automation Off-Main ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Expensive Synchronous Load ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Cache Substitution Correctness ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux No Hacky Sleeps ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Algorithmic Complexity ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Concurrency ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift @Concurrent ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift File And Package Boundaries ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swiftpm Lockfiles ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Logging ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux User-Facing Error Privacy ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Full Internationalization ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swiftui State Layout ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Architecture Rethink ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Swift Auxiliary Window Close Shortcuts ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux Source Artifacts ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux No Test Or Debug Seam In Production Source ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Cmux No Ambient Global State ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing the eight protocol-6 commands.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-mux-proposed-verbs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements the remaining proposed mux protocol-6 commands. The main changes are:

  • New mux-core handlers for wait, run, key input, copy, ids, notifications, and agent reports.
  • CLI verbs and conformance fixtures for the new commands.
  • Notification and agent state surfaced through the TUI tree and rendering paths.
  • Updated protocol, CLI, event, and configuration docs.

Confidence Score: 4/5

The new command paths are mostly mergeable, but browser key input and selection copy need focused fixes.

  • Browser surfaces are rejected by the new send-key handler despite the documented command behavior.
  • Selection copy can return cached text after the actual terminal selection changes.
  • Remote notification clearing can leave stale unread indicators until another tree update happens.

mux/crates/mux-core/src/server.rs; mux/crates/mux-core/src/mux.rs

Important Files Changed

Filename Overview
mux/crates/mux-core/src/server.rs Adds protocol handlers for the new commands; browser send-key is rejected and selection copy can return cached text.
mux/crates/mux-core/src/mux.rs Adds notification and agent stores; focus-based notification clearing can fail to notify subscribers.
mux/crates/ghostty-vt/src/key.rs Adds chord parsing for PTY key encoding.
mux/crates/mux-tui/src/cli.rs Adds CLI parsing, request building, and output formatting for the new commands.
mux/crates/mux-tui/src/session/tree.rs Extends tree snapshots with notification metadata.
mux/crates/mux-tui/src/ui/pane.rs Renders notification state in pane borders and tab labels.
mux/crates/mux-tui/src/ui/sidebar.rs Renders workspace unread dots from tab notification state.
mux/spec/commands.md Marks the protocol-6 commands implemented and documents their behavior.

Comments Outside Diff (2)

  1. mux/crates/mux-core/src/server.rs, line 1290-1291 (link)

    P1 Browser Keys Are Rejected

    The command spec says send-key supports browser surfaces when the browser runtime is local, but this handler rejects every non-PTY surface before any browser translation can run. A CLI or socket caller targeting a browser surface always gets surface does not support key input, so the new protocol command is broken for that documented surface type.

  2. mux/crates/mux-core/src/server.rs, line 1317-1318 (link)

    P2 Selection Text Goes Stale

    copy --mode selection returns the cached SurfaceMeta.selection value, and the inspected write path only updates that cache when the TUI copies non-empty selected text. If the terminal later changes or the selection is cleared without another copy, socket callers can receive an old selection instead of the current mux selection.

Reviews (1): Last reviewed commit: "mux: implement the 8 proposed protocol-6..." | Re-trigger Greptile

Comment on lines +558 to +561
surface: surface.id,
pane: pane_id,
screen: screen_id,
workspace: ws_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Cleared Notifications Stay Rendered

This helper removes unread state without emitting the same tree-change signal used by explicit notification clearing. When a remote client focuses the notified tab, the server state is cleared but subscribers may keep rendering the old unread dot until another unrelated tree event arrives.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6bdfc73. Configure here.

.filter(|record| surface.is_none_or(|surface| record.surface == surface))
.filter(|record| state.is_none_or(|state| record.state == state))
.collect()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agent records persist after close

Medium Severity

The new per-surface agent_records store is populated by report-agent but never cleared when a surface is reaped via close_surface or surface_exited. Unfiltered list-agents still returns those entries, so automation can treat closed tabs as active agents long after they leave the tree.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6bdfc73. Configure here.

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mux/crates/mux-tui/src/app.rs (1)

2259-2274: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Remote selections need to update the server-side surface state

copy_selection only persists the selected text when surface is SurfaceHandle::Local. For remote-attached sessions, Command::Copy { mode: "selection" } still reads surface.selection_text() from the authoritative server-side Surface, so the selected text never becomes available there.

If remote sessions are meant to support copy --mode selection, this path needs to write the selection back to the remote Surface too.

🤖 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 `@mux/crates/mux-tui/src/app.rs` around lines 2259 - 2274, The selection copy
path only stores text on SurfaceHandle::Local, so remote-attached sessions never
persist the selection where Command::Copy with mode selection reads it. Update
copy_selection to write the selected text back to the authoritative Surface for
remote sessions as well, using the existing surface/selection state handling
around selection_text_absolute and set_selection_text, while keeping the
clipboard and toast behavior unchanged.
♻️ Duplicate comments (1)
mux/crates/mux-core/src/server.rs (1)

733-879: 🎯 Functional Correctness | 🟠 Major

Command handlers look correct overall (WaitFor's attach-before-check ordering avoids the missed one-shot-match race, Run's argv/command and pane/new_workspace mutual-exclusion checks are sound, SendKey/Copy/Ids/Notify/ListAgents/ReportAgent all validate existence and enum values before mutating state).

One dependent concern: Notify/ReportAgent validate surface existence via get_surface before calling into mux.post_notification/mux.report_agent, but as noted on mux.rs, those entries are never cleaned up when the surface later closes — see that comment for the root cause.

🤖 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 `@mux/crates/mux-core/src/server.rs` around lines 733 - 879, The Notify and
ReportAgent handlers are fine, but their surface lookup relies on entries that
can remain stale after a surface closes. Fix the cleanup path in mux state
management so surfaces are removed when they are closed, and ensure the logic
around get_surface, mux.post_notification, and mux.report_agent cannot see dead
surfaces. Update the relevant surface teardown/close code in mux.rs to keep
these handlers working against only live surfaces.
🤖 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 `@mux/bindings/conformance/fixtures.json`:
- Around line 139-141: Add a requires field to the implemented-verbs-json
fixture so it skips gracefully on older servers. Update the fixture definition
in fixtures.json to reference the new protocol v6 command set using the same
pattern as move-tab-same-position, ensuring the fixture is gated by the relevant
commands for run, wait-for, send-key, copy, ids, notify, list-agents, and
report-agent.

In `@mux/crates/ghostty-vt/src/key.rs`:
- Around line 138-151: The single-character chord handling in the key parsing
path is emitting unshifted UTF-8 for shifted letters, so `shift+a` is treated
like plain `a` while `SHIFT` is consumed. Update the single-character branch in
`physical_key_for_char` handling so letters preserve shifted output in
`input.utf8` when `Mods::SHIFT` is present, while keeping
`input.unshifted_codepoint` lowercase and only consuming shift where
appropriate. Apply the same fix in the matching parser in `keys.rs`, and add
tests covering `shift+a` plus one shifted symbol case to confirm the behavior.

In `@mux/crates/mux-core/src/mux.rs`:
- Around line 139-140: Purge per-surface state when a surface is closed:
`agent_records` and `surface_notifications` in `Mux` currently retain entries
forever because `post_notification` and `report_agent` insert by `SurfaceId` but
`close_surface`/`close_surfaces` never remove them. Update the surface teardown
path in `Mux::close_surface` and the batch cleanup in `Mux::close_surfaces` to
delete both maps’ entries for every closed `SurfaceId`, using the existing
notification cleanup pattern as the reference so `list_agents` cannot return
stale data.

In `@mux/crates/mux-core/src/server.rs`:
- Around line 670-701: spawn_attach_notification_stream currently creates a
mux-wide subscriber thread that lives until writer.send() fails, so it never
ends with the attached surface and is also started before attach setup succeeds.
Update spawn_attach_notification_stream to tie its recv loop to the target
surface_id lifecycle (for example, exit when mux.surface(surface_id) is gone or
share the same shutdown path as the main attach stream), and only spawn it after
the corresponding attach_stream()/attach_frames() setup has succeeded. Also
apply the same lifetime fix in RemoteSession’s notification handling path.

In `@mux/crates/mux-tui/src/cli.rs`:
- Around line 665-682: build_run currently checks --command against positional
argv locally, but it does not mirror the server-side mutual exclusion between
--pane and --new-workspace. Update build_run in cli.rs to validate that these
two options cannot be used together, returning a UsageError before constructing
the JSON payload, alongside the existing command/argv checks.

In `@mux/crates/mux-tui/src/ui/sidebar.rs`:
- Around line 16-23: The sidebar unread indicator in
workspace_has_unread/sidebar rendering currently treats every unread
notification the same and ignores severity. Update the sidebar dot logic to
derive its color from the unread tab’s notification level, using the same
notification_color-based severity mapping already used in ui/pane.rs
(draw_box/draw_tab_bar), so warning/error states are preserved instead of always
showing notification_info. Reference the workspace_has_unread helper and the
sidebar dot rendering path when applying the fix.

In `@mux/crates/mux-tui/tests/cli.rs`:
- Around line 123-124: The send-key test only checks that the command succeeds,
but it does not confirm the key actually reached the terminal. Update the
`send_key` scenario in the CLI test to follow the `send-key` call with a
readback assertion, using the existing test helpers and the same `surface`/`cli`
flow, so it verifies terminal output (for example via `copy` or `read-screen`)
instead of only exit status. Use the surrounding test case in `cli.rs` as the
place to add the follow-up verification and mirror the conformance fixture’s
pattern of sending input then asserting the resulting screen text.

In `@mux/spec/cli.md`:
- Around line 83-90: The verb table in cli.md is being parsed incorrectly
because the pipe-separated option values are treated as extra columns. Update
the affected rows for copy, ids, notify, and report-agent in the table so the
literal pipe characters inside cells are escaped, keeping the column structure
intact while preserving the existing unique command symbols.

---

Outside diff comments:
In `@mux/crates/mux-tui/src/app.rs`:
- Around line 2259-2274: The selection copy path only stores text on
SurfaceHandle::Local, so remote-attached sessions never persist the selection
where Command::Copy with mode selection reads it. Update copy_selection to write
the selected text back to the authoritative Surface for remote sessions as well,
using the existing surface/selection state handling around
selection_text_absolute and set_selection_text, while keeping the clipboard and
toast behavior unchanged.

---

Duplicate comments:
In `@mux/crates/mux-core/src/server.rs`:
- Around line 733-879: The Notify and ReportAgent handlers are fine, but their
surface lookup relies on entries that can remain stale after a surface closes.
Fix the cleanup path in mux state management so surfaces are removed when they
are closed, and ensure the logic around get_surface, mux.post_notification, and
mux.report_agent cannot see dead surfaces. Update the relevant surface
teardown/close code in mux.rs to keep these handlers working against only live
surfaces.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 62bb93cd-75f8-4a88-8ab1-aa36ff6ba05f

📥 Commits

Reviewing files that changed from the base of the PR and between da3707b and 6bdfc73.

⛔ Files ignored due to path filters (1)
  • mux/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • mux/Cargo.toml
  • mux/bindings/conformance/fixtures.json
  • mux/bindings/conformance/runner.py
  • mux/crates/ghostty-vt/src/key.rs
  • mux/crates/ghostty-vt/src/lib.rs
  • mux/crates/mux-core/Cargo.toml
  • mux/crates/mux-core/src/browser.rs
  • mux/crates/mux-core/src/lib.rs
  • mux/crates/mux-core/src/mux.rs
  • mux/crates/mux-core/src/server.rs
  • mux/crates/mux-core/src/surface.rs
  • mux/crates/mux-core/tests/pty.rs
  • mux/crates/mux-tui/src/app.rs
  • mux/crates/mux-tui/src/cli.rs
  • mux/crates/mux-tui/src/config.rs
  • mux/crates/mux-tui/src/main.rs
  • mux/crates/mux-tui/src/session/mod.rs
  • mux/crates/mux-tui/src/session/remote.rs
  • mux/crates/mux-tui/src/session/tree.rs
  • mux/crates/mux-tui/src/ui/pane.rs
  • mux/crates/mux-tui/src/ui/sidebar.rs
  • mux/crates/mux-tui/tests/cli.rs
  • mux/docs/configuration.md
  • mux/spec/cli.md
  • mux/spec/commands.md
  • mux/spec/events.md

Comment on lines +139 to +141
{
"name": "implemented-verbs-json",
"steps": [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a requires field for backward compatibility.

All eight commands in this fixture (run, wait-for, send-key, copy, ids, notify, list-agents, report-agent) are new in protocol v6. Without a requires field, the fixture will fail hard against older servers instead of skipping gracefully. The existing move-tab-same-position fixture sets "requires": { "commands": ["move-tab"] } for the same reason.

♻️ Suggested addition
     {
       "name": "implemented-verbs-json",
+      "requires": { "commands": ["run", "wait-for", "send-key", "copy", "ids", "notify", "list-agents", "report-agent"] },
       "steps": [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"name": "implemented-verbs-json",
"steps": [
{
"name": "implemented-verbs-json",
"requires": { "commands": ["run", "wait-for", "send-key", "copy", "ids", "notify", "list-agents", "report-agent"] },
"steps": [
🤖 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 `@mux/bindings/conformance/fixtures.json` around lines 139 - 141, Add a
requires field to the implemented-verbs-json fixture so it skips gracefully on
older servers. Update the fixture definition in fixtures.json to reference the
new protocol v6 command set using the same pattern as move-tab-same-position,
ensuring the fixture is gated by the relevant commands for run, wait-for,
send-key, copy, ids, notify, list-agents, and report-agent.

Comment on lines +138 to +151
name if name.len() == 1 => {
let c = name.chars().next()?;
input.key = physical_key_for_char(c);
if input.key == sys::GHOSTTY_KEY_UNIDENTIFIED {
return None;
}
input.unshifted_codepoint = c.to_ascii_lowercase() as u32;
if !mods.contains(Mods::CTRL) {
input.utf8 = c.to_string();
if mods.contains(Mods::SHIFT) {
input.consumed_mods = Mods::SHIFT;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n== key.rs relevant section ==\n'
sed -n '100,220p' mux/crates/ghostty-vt/src/key.rs

printf '\n== tests in key.rs / nearby ==\n'
rg -n "send-key|ctrl\\+c|enter|up|invalid chord|shift\\+" mux/crates/ghostty-vt/src -n

printf '\n== all references to consumed_mods / unshifted_codepoint / utf8 ==\n'
rg -n "consumed_mods|unshifted_codepoint|utf8 =" mux/crates/ghostty-vt/src

printf '\n== physical_key_for_char definition ==\n'
rg -n "fn physical_key_for_char|physical_key_for_char\\(" mux/crates/ghostty-vt/src -n

Repository: manaflow-ai/cmux

Length of output: 7853


🏁 Script executed:

set -euo pipefail

printf '\n== physical_key_for_char ==\n'
sed -n '1,90p' mux/crates/ghostty-vt/src/key.rs

printf '\n== tests section ==\n'
sed -n '260,360p' mux/crates/ghostty-vt/src/key.rs

printf '\n== encoder-side semantics in Rust wrapper ==\n'
sed -n '200,250p' mux/crates/ghostty-vt/src/key.rs

printf '\n== search for keyboard protocol docs/comments in repo ==\n'
rg -n "unshifted_codepoint|consumed_mods|shift.*utf8|SHIFT.*utf8|GhosttyKeyEvent|kitty keyboard protocol|keyboard protocol" mux ghostty -g '!**/target/**'

Repository: manaflow-ai/cmux

Length of output: 25215


🏁 Script executed:

set -euo pipefail

printf '\n== mux-tui key parser ==\n'
sed -n '70,120p' mux/crates/mux-tui/src/keys.rs

printf '\n== key_encode.zig: shift / utf8 handling ==\n'
sed -n '730,770p' ghostty/src/input/key_encode.zig
sed -n '1490,1565p' ghostty/src/input/key_encode.zig
sed -n '2010,2055p' ghostty/src/input/key_encode.zig
sed -n '2160,2285p' ghostty/src/input/key_encode.zig

printf '\n== key.zig event semantics ==\n'
sed -n '1,120p' ghostty/src/input/key.zig

Repository: manaflow-ai/cmux

Length of output: 16589


🏁 Script executed:

set -euo pipefail

printf '\n== key_input_from_chord definition and docs ==\n'
rg -n -A4 -B4 "fn key_input_from_chord|send-key|chord" mux/crates/ghostty-vt/src/key.rs mux/crates/mux-tui/src/keys.rs mux/spec/commands.md ghostty/include/ghostty/vt/key.h ghostty/include/ghostty/vt/README.md

printf '\n== any docs/examples mentioning shift+a or shifted symbols ==\n'
rg -n "shift\\+a|shift\\+1|shift\\+,|shift\\+<|shift\\+letter|shifted symbol|shifted" mux ghostty -g '!**/target/**'

Repository: manaflow-ai/cmux

Length of output: 5642


Shifted single-character chords need shifted UTF-8.
shift+a hits this branch with c = 'a', so it currently emits "a" while also marking SHIFT as consumed. That drops the shift for letters; utf8 should be "A" there, with unshifted_codepoint staying a. The same parser in mux/crates/mux-tui/src/keys.rs has the same behavior. Shifted symbols like shift+1 and shift+, still need explicit mapping if send-key is expected to support them. Add coverage for shift+a and one shifted symbol.

🤖 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 `@mux/crates/ghostty-vt/src/key.rs` around lines 138 - 151, The
single-character chord handling in the key parsing path is emitting unshifted
UTF-8 for shifted letters, so `shift+a` is treated like plain `a` while `SHIFT`
is consumed. Update the single-character branch in `physical_key_for_char`
handling so letters preserve shifted output in `input.utf8` when `Mods::SHIFT`
is present, while keeping `input.unshifted_codepoint` lowercase and only
consuming shift where appropriate. Apply the same fix in the matching parser in
`keys.rs`, and add tests covering `shift+a` plus one shifted symbol case to
confirm the behavior.

Comment on lines +139 to +140
agent_records: Mutex<HashMap<SurfaceId, AgentRecord>>,
surface_notifications: Mutex<HashMap<SurfaceId, SurfaceNotification>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

agent_records / surface_notifications are never purged when a surface closes — unbounded leak + stale data.

post_notification and report_agent insert into surface_notifications / agent_records keyed by SurfaceId, but close_surface/close_surfaces (unchanged in this diff) only touch state.surfaces/state.panes/workspaces — they never call anything analogous to clear_surface_notification or remove from agent_records. Since Mux lives for the whole session/daemon lifetime and SurfaceId is a monotonically increasing counter that's never reused, every surface that is ever created and closed leaves a permanent orphaned entry in these two maps.

This isn't just a memory-growth concern: list_agents(None, ...) / report-agent+list-agents will keep returning agent state for surfaces that no longer exist in the tree (since list_agents reads agent_records directly, independent of state), which is a real functional/data-integrity problem for any automation consuming this API over a long-running session with surface churn (exactly the intended use case for the new agent-reporting feature).

🐛 Proposed fix — purge on surface removal
     pub fn close_surface(&self, target: SurfaceId) {
         let (removed, empty) = {
             let mut state = self.state.lock().unwrap();
             (remove_surface(&mut state, target), state.workspaces.is_empty())
         };
         if let Some(surface) = removed {
             surface.kill();
+            self.agent_records.lock().unwrap().remove(&target);
+            self.surface_notifications.lock().unwrap().remove(&target);
             self.emit(MuxEvent::TreeChanged);
         }
         if empty {
             self.emit(MuxEvent::Empty);
         }
     }

Apply the same cleanup in close_surfaces for the batch pane/screen/workspace-close paths.

Also applies to: 370-417

🤖 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 `@mux/crates/mux-core/src/mux.rs` around lines 139 - 140, Purge per-surface
state when a surface is closed: `agent_records` and `surface_notifications` in
`Mux` currently retain entries forever because `post_notification` and
`report_agent` insert by `SurfaceId` but `close_surface`/`close_surfaces` never
remove them. Update the surface teardown path in `Mux::close_surface` and the
batch cleanup in `Mux::close_surfaces` to delete both maps’ entries for every
closed `SurfaceId`, using the existing notification cleanup pattern as the
reference so `list_agents` cannot return stale data.

Comment on lines +670 to +701
fn spawn_attach_notification_stream(
mux: Arc<Mux>,
surface_id: SurfaceId,
writer: LineWriter,
) -> std::io::Result<()> {
let events = mux.subscribe();
std::thread::Builder::new()
.name("mux-attach-notifications".into())
.spawn(move || {
while let Ok(event) = events.recv() {
let MuxEvent::Notification(notification) = event else {
continue;
};
if notification.surface != Some(surface_id) {
continue;
}
let value = json!({
"event": "notification",
"notification": notification.notification,
"title": notification.title,
"body": notification.body,
"level": notification.level.as_str(),
"surface": notification.surface,
});
if writer.send(&value).is_err() {
break;
}
}
})
.map(|_| ())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

spawn_attach_notification_stream leaks a full-mux subscriber thread per attached surface for the life of the connection.

This thread subscribes to the entire mux event stream and only exits when writer.send() fails — i.e. only when the whole connection closes. Unlike the sibling attach-out thread (which naturally terminates via attach.stream.recv()/frames.notify.recv() returning Err when the specific surface exits), this thread has no tie to surface_id's lifecycle at all. A client that attaches to many distinct surfaces over one long-lived connection (e.g. a TUI switching through many transient tabs) accumulates one permanently-blocked thread + one permanent Mux::subscribers entry per surface ever attached, not per currently-attached surface.

Separately, it's spawned unconditionally before the actual attach succeeds: if surface.attach_stream()? fails for a PTY surface, the command returns an error to the client, but the already-spawned notification thread keeps running regardless.

Consider tying this thread's lifetime to the surface (e.g. break the loop once mux.surface(surface_id) returns None, or drive it from the same select/recv_timeout loop as the primary attach thread and share a shutdown signal), and only spawn it after attach_stream()/attach_frames() succeeds.

RemoteSession's message handler processes incoming JSON events and forwards notification-related updates into the app's event stream.

Also applies to: 1167-1169

🤖 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 `@mux/crates/mux-core/src/server.rs` around lines 670 - 701,
spawn_attach_notification_stream currently creates a mux-wide subscriber thread
that lives until writer.send() fails, so it never ends with the attached surface
and is also started before attach setup succeeds. Update
spawn_attach_notification_stream to tie its recv loop to the target surface_id
lifecycle (for example, exit when mux.surface(surface_id) is gone or share the
same shutdown path as the main attach stream), and only spawn it after the
corresponding attach_stream()/attach_frames() setup has succeeded. Also apply
the same lifetime fix in RemoteSession’s notification handling path.

Comment on lines +665 to +682
fn build_run(flags: &FlagMap) -> Result<Value, UsageError> {
let mut value = json!({});
flags.insert_optional_u64(&mut value, "pane")?;
flags.insert_optional_string(&mut value, "cwd");
flags.insert_optional_string(&mut value, "name");
if flags.optional("new-workspace").is_some() {
value["new_workspace"] = json!(true);
}
match (flags.optional("command"), flags.positionals.is_empty()) {
(Some(command), true) => value["command"] = json!(command),
(Some(_), false) => {
return Err(UsageError("--command and argv are mutually exclusive".to_string()));
}
(None, false) => value["argv"] = json!(flags.positionals),
(None, true) => return Err(UsageError("argv or --command is required".to_string())),
}
Ok(value)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider validating --pane/--new-workspace mutual exclusion client-side too.

build_run validates --command vs positional argv mutual exclusivity locally, but not --pane vs --new-workspace (only Command::Run on the server rejects that combination). For consistency and faster feedback (no round trip needed), consider adding the same check here.

♻️ Suggested addition
 fn build_run(flags: &FlagMap) -> Result<Value, UsageError> {
     let mut value = json!({});
     flags.insert_optional_u64(&mut value, "pane")?;
     flags.insert_optional_string(&mut value, "cwd");
     flags.insert_optional_string(&mut value, "name");
     if flags.optional("new-workspace").is_some() {
+        if flags.optional("pane").is_some() {
+            return Err(UsageError("--pane and --new-workspace are mutually exclusive".to_string()));
+        }
         value["new_workspace"] = json!(true);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn build_run(flags: &FlagMap) -> Result<Value, UsageError> {
let mut value = json!({});
flags.insert_optional_u64(&mut value, "pane")?;
flags.insert_optional_string(&mut value, "cwd");
flags.insert_optional_string(&mut value, "name");
if flags.optional("new-workspace").is_some() {
value["new_workspace"] = json!(true);
}
match (flags.optional("command"), flags.positionals.is_empty()) {
(Some(command), true) => value["command"] = json!(command),
(Some(_), false) => {
return Err(UsageError("--command and argv are mutually exclusive".to_string()));
}
(None, false) => value["argv"] = json!(flags.positionals),
(None, true) => return Err(UsageError("argv or --command is required".to_string())),
}
Ok(value)
}
fn build_run(flags: &FlagMap) -> Result<Value, UsageError> {
let mut value = json!({});
flags.insert_optional_u64(&mut value, "pane")?;
flags.insert_optional_string(&mut value, "cwd");
flags.insert_optional_string(&mut value, "name");
if flags.optional("new-workspace").is_some() {
if flags.optional("pane").is_some() {
return Err(UsageError("--pane and --new-workspace are mutually exclusive".to_string()));
}
value["new_workspace"] = json!(true);
}
match (flags.optional("command"), flags.positionals.is_empty()) {
(Some(command), true) => value["command"] = json!(command),
(Some(_), false) => {
return Err(UsageError("--command and argv are mutually exclusive".to_string()));
}
(None, false) => value["argv"] = json!(flags.positionals),
(None, true) => return Err(UsageError("argv or --command is required".to_string())),
}
Ok(value)
}
🤖 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 `@mux/crates/mux-tui/src/cli.rs` around lines 665 - 682, build_run currently
checks --command against positional argv locally, but it does not mirror the
server-side mutual exclusion between --pane and --new-workspace. Update
build_run in cli.rs to validate that these two options cannot be used together,
returning a UsageError before constructing the JSON payload, alongside the
existing command/argv checks.

Comment on lines +16 to +23
fn workspace_has_unread(ws: &crate::session::WorkspaceView) -> bool {
ws.screens
.iter()
.flat_map(|screen| screen.panes.iter())
.flat_map(|pane| pane.tabs.iter())
.any(|tab| tab.notification.is_some_and(|notification| notification.unread))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sidebar unread dot ignores notification severity.

Unlike draw_box/draw_tab_bar in ui/pane.rs, which color by the notification's level via notification_color(...), the sidebar dot always uses notification_info regardless of whether the underlying unread notification is warning or error. This loses the severity cue in the one place meant to summarize unread state per workspace.

♻️ Proposed fix to surface severity in the sidebar dot
-fn workspace_has_unread(ws: &crate::session::WorkspaceView) -> bool {
-    ws.screens
-        .iter()
-        .flat_map(|screen| screen.panes.iter())
-        .flat_map(|pane| pane.tabs.iter())
-        .any(|tab| tab.notification.is_some_and(|notification| notification.unread))
-}
+fn workspace_unread_level(ws: &crate::session::WorkspaceView) -> Option<&'static str> {
+    ws.screens
+        .iter()
+        .flat_map(|screen| screen.panes.iter())
+        .flat_map(|pane| pane.tabs.iter())
+        .filter_map(|tab| tab.notification.filter(|n| n.unread))
+        .map(|n| n.level)
+        .max_by_key(|level| match *level {
+            "error" => 2,
+            "warning" => 1,
+            _ => 0,
+        })
+}
-        if workspace_has_unread(ws) && content_w > 1 {
-            let dot_style =
-                style.fg(app.config.theme.notification_info).add_modifier(Modifier::BOLD);
+        if let Some(level) = workspace_unread_level(ws).filter(|_| content_w > 1) {
+            let color = match level {
+                "error" => app.config.theme.notification_error,
+                "warning" => app.config.theme.notification_warning,
+                _ => app.config.theme.notification_info,
+            };
+            let dot_style = style.fg(color).add_modifier(Modifier::BOLD);
             buf[(0, y)].set_symbol("•").set_style(dot_style);
         }

Also applies to: 86-90

🤖 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 `@mux/crates/mux-tui/src/ui/sidebar.rs` around lines 16 - 23, The sidebar
unread indicator in workspace_has_unread/sidebar rendering currently treats
every unread notification the same and ignores severity. Update the sidebar dot
logic to derive its color from the unread tab’s notification level, using the
same notification_color-based severity mapping already used in ui/pane.rs
(draw_box/draw_tab_bar), so warning/error states are preserved instead of always
showing notification_info. Reference the workspace_has_unread helper and the
sidebar dot rendering path when applying the fix.

Comment on lines +123 to +124
let send_key = cli(&server, &["send-key", "--surface", &surface.to_string(), "enter"]);
assert_success(&send_key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Verify send-key effect with a follow-up read.

The test only asserts send-key exits successfully but doesn't verify the key was actually received by the terminal. The conformance fixture is more thorough — it sends ["h", "i", "enter"] and then checks scrollback for "hi". Consider adding a follow-up copy or read-screen assertion to catch key-encoding regressions.

♻️ Suggested addition
     let send_key = cli(&server, &["send-key", "--surface", &surface.to_string(), "enter"]);
     assert_success(&send_key);
+
+    // Verify the key was received by the terminal.
+    let after_key = wait_for_screen(&server, surface, "\n");
+    // Or use copy --mode scrollback to check for expected output.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let send_key = cli(&server, &["send-key", "--surface", &surface.to_string(), "enter"]);
assert_success(&send_key);
let send_key = cli(&server, &["send-key", "--surface", &surface.to_string(), "enter"]);
assert_success(&send_key);
// Verify the key was received by the terminal.
let after_key = wait_for_screen(&server, surface, "\n");
// Or use copy --mode scrollback to check for expected output.
🤖 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 `@mux/crates/mux-tui/tests/cli.rs` around lines 123 - 124, The send-key test
only checks that the command succeeds, but it does not confirm the key actually
reached the terminal. Update the `send_key` scenario in the CLI test to follow
the `send-key` call with a readback assertion, using the existing test helpers
and the same `surface`/`cli` flow, so it verifies terminal output (for example
via `copy` or `read-screen`) instead of only exit status. Use the surrounding
test case in `cli.rs` as the place to add the follow-up verification and mirror
the conformance fixture’s pattern of sending input then asserting the resulting
screen text.

Comment thread mux/spec/cli.md
Comment on lines +83 to +90
| `wait-for` | implemented | `--surface <id> --pattern <regex> --timeout-ms <n>` | none | none |
| `run` | implemented | `-- <argv...>` or `--command <cmd>` | `--pane <id>`, `--new-workspace`, `--cwd <path>`, `--name <name>` | surface id |
| `send-key` | implemented | `--surface <id> <key>...` | none | none |
| `copy` | implemented | `--surface <id> --mode screen|selection|scrollback` | none | text |
| `ids` | implemented | none | `--kind workspace|screen|pane|surface` | id lines |
| `notify` | implemented | `--title <title> --body <body>` | `--level info|warning|error`, `--surface <id>` | notification id |
| `list-agents` | implemented | none | `--surface <id>`, `--state <state>` | agent lines |
| `report-agent` | implemented | `--surface <id> --state <state> --source socket|hook` | `--session <id>` | none |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Escape the pipe-separated values in the verb table.

The | characters in these cells are being parsed as extra columns, so the table renders incorrectly for copy, ids, notify, and report-agent.

Proposed fix
-| `copy` | implemented | `--surface <id> --mode screen|selection|scrollback` | none | text |
-| `ids` | implemented | none | `--kind workspace|screen|pane|surface` | id lines |
-| `notify` | implemented | `--title <title> --body <body>` | `--level info|warning|error`, `--surface <id>` | notification id |
-| `report-agent` | implemented | `--surface <id> --state <state> --source socket|hook` | `--session <id>` | none |
+| `copy` | implemented | `--surface <id> --mode screen\|selection\|scrollback` | none | text |
+| `ids` | implemented | none | `--kind workspace\|screen\|pane\|surface` | id lines |
+| `notify` | implemented | `--title <title> --body <body>` | `--level info\|warning\|error`, `--surface <id>` | notification id |
+| `report-agent` | implemented | `--surface <id> --state <state> --source socket\|hook` | `--session <id>` | none |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `wait-for` | implemented | `--surface <id> --pattern <regex> --timeout-ms <n>` | none | none |
| `run` | implemented | `-- <argv...>` or `--command <cmd>` | `--pane <id>`, `--new-workspace`, `--cwd <path>`, `--name <name>` | surface id |
| `send-key` | implemented | `--surface <id> <key>...` | none | none |
| `copy` | implemented | `--surface <id> --mode screen|selection|scrollback` | none | text |
| `ids` | implemented | none | `--kind workspace|screen|pane|surface` | id lines |
| `notify` | implemented | `--title <title> --body <body>` | `--level info|warning|error`, `--surface <id>` | notification id |
| `list-agents` | implemented | none | `--surface <id>`, `--state <state>` | agent lines |
| `report-agent` | implemented | `--surface <id> --state <state> --source socket|hook` | `--session <id>` | none |
| `wait-for` | implemented | `--surface <id> --pattern <regex> --timeout-ms <n>` | none | none |
| `run` | implemented | `-- <argv...>` or `--command <cmd>` | `--pane <id>`, `--new-workspace`, `--cwd <path>`, `--name <name>` | surface id |
| `send-key` | implemented | `--surface <id> <key>...` | none | none |
| `copy` | implemented | `--surface <id> --mode screen\|selection\|scrollback` | none | text |
| `ids` | implemented | none | `--kind workspace\|screen\|pane\|surface` | id lines |
| `notify` | implemented | `--title <title> --body <body>` | `--level info\|warning\|error`, `--surface <id>` | notification id |
| `list-agents` | implemented | none | `--surface <id>`, `--state <state>` | agent lines |
| `report-agent` | implemented | `--surface <id> --state <state> --source socket\|hook` | `--session <id>` | none |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 86-86: Table column count
Expected: 5; Actual: 7; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 87-87: Table column count
Expected: 5; Actual: 8; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 88-88: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 88-88: Table column count
Expected: 5; Actual: 7; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 90-90: Table column count
Expected: 5; Actual: 6; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 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 `@mux/spec/cli.md` around lines 83 - 90, The verb table in cli.md is being
parsed incorrectly because the pipe-separated option values are treated as extra
columns. Update the affected rows for copy, ids, notify, and report-agent in the
table so the literal pipe characters inside cells are escaped, keeping the
column structure intact while preserving the existing unique command symbols.

Source: Linters/SAST tools

This branch was previously deployed

1 inactive deployment
Preview – cmux — 6bdfc73b Deployed Jul 8, 2026 by vercel[bot]
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