Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
eb4c18c
Add horizontal scrolling layout to cmux-tui
lawrencecchen Jul 24, 2026
b73230c
Make horizontal panes command-driven
lawrencecchen Jul 24, 2026
528cc75
Merge remote-tracking branch 'origin/main' into feat-cmux-tui-horizon…
lawrencecchen Jul 24, 2026
29d5815
Merge remote-tracking branch 'origin/main' into feat-cmux-tui-horizon…
lawrencecchen Jul 25, 2026
0b31618
Add horizontal viewport regression coverage
lawrencecchen Jul 26, 2026
ca89bef
Make horizontal viewport columns resilient and resizable
lawrencecchen Jul 26, 2026
254b462
Make horizontal columns first-class and undoable
lawrencecchen Jul 26, 2026
f016c3c
Merge remote-tracking branch 'origin/main' into feat-cmux-tui-horizon…
lawrencecchen Jul 26, 2026
41466df
Merge remote-tracking branch 'origin/main' into feat-cmux-tui-horizon…
lawrencecchen Jul 26, 2026
f0e4fed
fix(cmux-tui): harden horizontal viewport behavior
lawrencecchen Jul 26, 2026
b22c0d6
fix(cmux-tui): preserve viewport navigation invariants
lawrencecchen Jul 26, 2026
80a1dc1
fix(cmux-tui): harden wide viewport interactions
lawrencecchen Jul 26, 2026
675b518
fix(cmux-tui): close horizontal viewport review gaps
lawrencecchen Jul 26, 2026
ccf9475
fix(cmux-tui): preserve viewport compatibility contracts
lawrencecchen Jul 26, 2026
7ac3ca5
fix(cmux-tui): skip unchanged resize mutations
lawrencecchen Jul 26, 2026
827916a
fix(cmux-tui): harden viewport failure boundaries
lawrencecchen Jul 26, 2026
d130437
Merge remote-tracking branch 'origin/main' into feat-cmux-tui-horizon…
lawrencecchen Jul 26, 2026
fa9a116
fix(cmux-tui): preserve focus and localize layout failures
lawrencecchen Jul 26, 2026
99553b3
test(cmux-tui): cover resize owner collisions
lawrencecchen Jul 26, 2026
eb96597
fix(cmux-tui): isolate resize transaction owners
lawrencecchen Jul 26, 2026
661559a
test(cmux-tui): cover clipped browser frame hot paths
lawrencecchen Jul 26, 2026
0fb8db9
fix(cmux-tui): keep clipped browser editing responsive
lawrencecchen Jul 26, 2026
f9d2da7
test(cmux-tui): repair current smoke expectations
lawrencecchen Jul 26, 2026
5d68426
test(cmux-tui): cover stack selection across layout undo
lawrencecchen Jul 26, 2026
878c9f6
fix(cmux-tui): preserve stack focus through layout undo
lawrencecchen Jul 27, 2026
95ede0c
test(cmux-tui): cover clamped viewport ratio updates
lawrencecchen Jul 27, 2026
f78803a
fix(cmux-tui): preserve authoritative resize state
lawrencecchen Jul 27, 2026
7047d2b
test(cmux-tui): cover wide status labels beside scrollbar
lawrencecchen Jul 27, 2026
51c1e5f
fix(cmux-tui): measure status labels in terminal cells
lawrencecchen Jul 27, 2026
052a92b
test(cmux-tui): cover scrollbar anchors and undo flags
lawrencecchen Jul 27, 2026
003f644
fix(cmux-tui): anchor scrollbar drags and undo flags
lawrencecchen Jul 27, 2026
deb7c67
test(cmux-tui): cover stable resize and emoji geometry
lawrencecchen Jul 27, 2026
c674eb9
fix(cmux-tui): freeze resize geometry and cell widths
lawrencecchen Jul 27, 2026
1d2afd4
test(cmux-tui): cover efficient animated chrome rendering
lawrencecchen Jul 27, 2026
9b9fb7c
fix(cmux-tui): isolate animated viewport painting
lawrencecchen Jul 27, 2026
ac0cd6c
test(cmux-tui): cover browser crops and abandoned resize
lawrencecchen Jul 27, 2026
dfde713
fix(cmux-tui): preserve image crops and resize undo fences
lawrencecchen Jul 27, 2026
79e3d3c
test(cmux-tui): cover zoom focus restoration
lawrencecchen Jul 27, 2026
9f6f987
fix(cmux-tui): align restored zoom and focus
lawrencecchen Jul 27, 2026
64a6c79
test(cmux-tui): cover exact-fit status labels
lawrencecchen Jul 27, 2026
da24864
fix(cmux-tui): render exact-fit status labels
lawrencecchen Jul 27, 2026
9a2641c
test(cmux-tui): cover swept viewport leases
lawrencecchen Jul 27, 2026
6be1410
fix(cmux-tui): lease swept viewport panes
lawrencecchen Jul 27, 2026
9c7755f
test(cmux-tui): cover localized finite width errors
lawrencecchen Jul 27, 2026
6e5dfa9
fix(cmux-tui): localize viewport width errors
lawrencecchen Jul 27, 2026
417e2ce
test(cmux-tui): cover distant jumps and wide projections
lawrencecchen Jul 27, 2026
0fd9ebf
fix(cmux-tui): bound jumps and preserve projections
lawrencecchen Jul 27, 2026
cd125d9
test(cmux-tui): cover additive binding fields
lawrencecchen Jul 27, 2026
3969571
fix(cmux-tui): expose additive binding metadata
lawrencecchen Jul 27, 2026
7553b17
test(cmux-tui): cover bounded localized rendering
lawrencecchen Jul 27, 2026
3b405dd
fix(cmux-tui): bound and localize render inputs
lawrencecchen Jul 27, 2026
9310ff6
Merge remote-tracking branch 'origin/main' into feat-cmux-tui-horizon…
lawrencecchen Jul 27, 2026
0daef13
test(cmux-tui): cover autoreview regressions
lawrencecchen Jul 27, 2026
3f0cf8c
fix(cmux-tui): close autoreview correctness gaps
lawrencecchen Jul 27, 2026
bcaef50
test(cmux-tui): reject invalid SDK pane widths
lawrencecchen Jul 27, 2026
6b1389f
fix(cmux-tui): validate SDK pane widths locally
lawrencecchen Jul 27, 2026
554ac3e
test(cmux-tui): preserve nullable pane width
lawrencecchen Jul 27, 2026
f95abce
fix(cmux-tui): preserve nullable pane width
lawrencecchen Jul 27, 2026
85b0706
test(cmux-tui): require breaking Rust SDK version
lawrencecchen Jul 27, 2026
0fa3427
fix(cmux-tui): version Rust SDK error shape
lawrencecchen Jul 27, 2026
20afbc1
test(cmux-tui): reject invalid SDK viewport widths
lawrencecchen Jul 27, 2026
6ed2812
fix(cmux-tui): validate remaining SDK viewport widths
lawrencecchen Jul 27, 2026
8566569
test(cmux-tui): validate Go viewport resize width
lawrencecchen Jul 27, 2026
b28f621
fix(cmux-tui): validate Go viewport resize width
lawrencecchen Jul 27, 2026
9d81565
test(cmux-tui): cover release version and locale contracts
lawrencecchen Jul 27, 2026
441e89c
fix(cmux-tui): synchronize SDK versions and locale
lawrencecchen Jul 27, 2026
ae192c1
test(cmux-tui): reject unsafe undo previews
lawrencecchen Jul 27, 2026
141b493
fix(cmux-tui): reject unsafe undo previews
lawrencecchen Jul 27, 2026
1ac985a
test(cmux-tui): enforce complete undo result contracts
lawrencecchen Jul 27, 2026
28fe8ef
fix(cmux-tui): centralize layout undo decoding
lawrencecchen Jul 27, 2026
a210835
test(cmux-tui): bound viewport reclip work
lawrencecchen Jul 27, 2026
f4e0d37
fix(cmux-tui): index animated viewport projections
lawrencecchen Jul 27, 2026
eab53db
test(cmux-tui): type layout undo edge failures
lawrencecchen Jul 27, 2026
e1196f5
fix(cmux-tui): type layout undo race failures
lawrencecchen Jul 27, 2026
6f959aa
test(cmux-tui): cover missing browser CSS dimensions
lawrencecchen Jul 27, 2026
f64b55c
fix(cmux-tui): accept missing browser CSS dimensions
lawrencecchen Jul 27, 2026
8b6a168
test(cmux-tui): keep SDK runbook version synchronized
lawrencecchen Jul 27, 2026
038ee72
docs(cmux-tui): update SDK release version
lawrencecchen Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmux-tui/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cmux-tui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ cargo run -p cmux-tui -- attach --session agents --surface <surface-id>

The default session is `main`. Default sockets live at `$TMPDIR/cmux-tui-<uid>/<session>.sock`; use `--socket <path>` for an explicit path. Detach from an attached TUI with prefix `d`, which is `Ctrl-b d` by default.

Pane layout stays tiled by default. Press `Ctrl-b g` to append a terminal to the right at two-thirds of the viewport width. The existing layout keeps its width, so a continuous horizontal scrollbar appears in the status bar. Focusing a pane reveals it with an animated viewport movement. `Alt-n` reapplies Zellij's automatic layout inside the focused horizontal column. `Ctrl-b U` undoes the latest structural layout action on the focused screen; undoing pane creation asks for confirmation before closing the pane.

`attach --surface <id>` attaches one PTY terminal by numeric or short surface id. It uses the full host terminal without the sidebar, status bar, pane border, or other tabs.

Packaged builds can run as `npx cmux`. The optional machine rail lets that local client switch among the current session, other Unix sockets, and sessions reached through SSH. It is disabled by default and activates when `machine_sidebar.enabled` is true or `machines` contains a valid entry in `cmux-tui.json`. `npx cmux --cloud` composes those local targets with the Cloud catalog and enables temporary machine connections without sending local SSH details to Cloud. The client uses noninteractive SSH with strict host-key checking and the remote `cmux-tui relay --session <name>` transport primitive, so the remote headless session, trusted host key, authentication key, and binary must already exist. See [Machines and remote sessions](docs/machines.md).
Expand Down
2 changes: 1 addition & 1 deletion cmux-tui/bindings/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ cmux-sdk-vX.Y.Z
Historical releases used `mux-sdk-vX.Y.Z`; the publish workflows still accept
that prefix so old release history remains connected. These tags are separate
from app release tags such as `vX.Y.Z`. Current SDK package versions are
`0.3.0`.
`0.4.0`.

## One-time registry setup

Expand Down
59 changes: 59 additions & 0 deletions cmux-tui/bindings/conformance/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,65 @@
}
]
},
{
"name": "layout-undo-preview-and-confirm",
"requires": { "commands": ["new-pane", "undo-layout"] },
"steps": [
{
"type": "command",
"request": { "id": 1, "cmd": "new-workspace", "name": "conf-undo", "cols": 80, "rows": 24 },
"expect": { "id": 1, "ok": true },
"match": "partial"
},
{
"type": "command",
"request": { "id": 2, "cmd": "ids", "kind": "pane" },
"expect": { "id": 2, "ok": true },
"match": "partial",
"bind": { "pane0": "data.ids[-1].id" }
},
{
"type": "command",
"request": { "id": 3, "cmd": "new-pane", "pane": "$pane0" },
"expect": { "id": 3, "ok": true },
"match": "partial"
},
{
"type": "command",
"request": { "id": 4, "cmd": "ids", "kind": "pane" },
"expect": { "id": 4, "ok": true },
"match": "partial",
"bind": { "pane1": "data.ids[-1].id" }
},
{
"type": "command",
"request": { "id": 5, "cmd": "undo-layout", "pane": "$pane1" },
"expect": {
"id": 5,
"ok": true,
"data": {
"undone": false,
"confirmation_required": true,
"closes_panes": ["$pane1"]
}
},
"match": "partial",
"bind": { "undo_revision": "data.revision" }
},
{
"type": "command",
"request": {
"id": 6,
"cmd": "undo-layout",
"pane": "$pane1",
"revision": "$undo_revision",
"confirm_close": true
},
"expect": { "id": 6, "ok": true, "data": { "undone": true } },
"match": "partial"
}
]
},
{
"name": "subscribe-tree-changed",
"steps": [
Expand Down
10 changes: 8 additions & 2 deletions cmux-tui/bindings/conformance/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,15 @@ def dispatch(client: CmuxClient, cmd: str, params: Dict[str, Any]) -> Any:
"new-browser-tab": client.new_browser_tab,
"new-workspace": client.new_workspace,
"new-screen": client.new_screen,
"new-pane": client.new_pane,
"new-pane-right": client.new_pane_right,
"split": client.split,
"set-ratio": client.set_ratio,
"set-split-ratio": client.set_split_ratio,
"set-viewport-pane-width": client.set_viewport_pane_width,
"undo-layout": lambda **kw: client.undo_layout(
kw["pane"], kw.get("revision") if kw.get("confirm_close") else None
),
"pane-neighbor": client.pane_neighbor,
"focus-direction": client.focus_direction,
"swap-pane": client.swap_pane,
Expand Down Expand Up @@ -374,7 +380,7 @@ def wait_contains(client: CmuxClient, request: Dict[str, Any], path: str, needle
def get_path(value: Any, path: str) -> Any:
current = value
for part in path.split("."):
match = re.fullmatch(r"([A-Za-z0-9_-]+)(?:\[(\d+)\])?", part)
match = re.fullmatch(r"([A-Za-z0-9_-]+)(?:\[(-?\d+)\])?", part)
if not match:
raise FixtureFailure(f"bad JSON path segment {part!r}")
key = match.group(1)
Expand All @@ -383,7 +389,7 @@ def get_path(value: Any, path: str) -> Any:
current = current[key]
if match.group(2) is not None:
index = int(match.group(2))
if not isinstance(current, list) or index >= len(current):
if not isinstance(current, list) or index >= len(current) or index < -len(current):
raise FixtureFailure(f"path {path!r} missing index {index}")
current = current[index]
return current
Expand Down
147 changes: 140 additions & 7 deletions cmux-tui/bindings/go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"math"
"net"
"os"
"path/filepath"
Expand Down Expand Up @@ -36,10 +37,62 @@ func validateWorkspaceSelector(workspace *uint64, key *string) error {
return nil
}

func validateViewportPaneWidth(width float32) error {
value := float64(width)
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0.1 || value > 1.0 {
return fmt.Errorf(
"%w: viewport pane width must be between 0.1 and 1.0",
ErrInvalidArgument,
)
}
return nil
}

type layoutUndoWire struct {
Undone *bool `json:"undone"`
ConfirmationRequired *bool `json:"confirmation_required"`
Screen *uint64 `json:"screen"`
Revision *uint64 `json:"revision"`
ClosesPanes *[]uint64 `json:"closes_panes"`
}

func decodeLayoutUndoResult(wire layoutUndoWire) (LayoutUndoResult, error) {
if wire.Screen == nil {
return nil, &decodeError{msg: "layout undo response omitted screen"}
}
if wire.Revision == nil {
return nil, &decodeError{msg: "layout undo response omitted revision"}
}

switch {
case wire.Undone != nil &&
*wire.Undone &&
(wire.ConfirmationRequired == nil || !*wire.ConfirmationRequired):
return LayoutUndoUndone{Screen: *wire.Screen, Revision: *wire.Revision}, nil
case wire.Undone != nil &&
!*wire.Undone &&
wire.ConfirmationRequired != nil &&
*wire.ConfirmationRequired:
if wire.ClosesPanes == nil {
return nil, &decodeError{
msg: "layout undo confirmation closes_panes must be an array of pane IDs",
}
}
return LayoutUndoConfirmationRequired{
Screen: *wire.Screen, Revision: *wire.Revision, ClosesPanes: *wire.ClosesPanes,
}, nil
default:
return nil, &decodeError{
msg: "layout undo response does not contain exactly one valid outcome",
}
}
}

type CommandError struct {
Message string
ID any
Delivery ErrorDelivery
Message string
ID any
ErrorCode string
Delivery ErrorDelivery
}

func (e *CommandError) Error() string { return e.Message }
Expand All @@ -59,11 +112,13 @@ func commandErrorFromResponse(response map[string]any) *CommandError {
if msg == "" {
msg = "unknown error"
}
code, _ := response["error_code"].(string)
delivery, _ := response["error_delivery"].(string)
return &CommandError{
Message: msg,
ID: response["id"],
Delivery: ErrorDelivery(delivery),
Message: msg,
ID: response["id"],
ErrorCode: code,
Delivery: ErrorDelivery(delivery),
}
}

Expand Down Expand Up @@ -440,6 +495,29 @@ func (c *Client) NewPane(ctx context.Context, pane uint64, opts NewPaneOptions)
return result, c.request(ctx, "new-pane", params, &result)
}

func (c *Client) NewPaneRight(ctx context.Context, pane uint64, opts NewPaneRightOptions) (SurfaceResult, error) {
if opts.Width != nil {
if err := validateViewportPaneWidth(*opts.Width); err != nil {
return SurfaceResult{}, err
}
}
if err := c.requireCapability(ctx, "viewport-splits-v1", "viewport panes"); err != nil {
return SurfaceResult{}, err
}
params := map[string]any{"pane": pane}
if opts.Width != nil {
params["width"] = *opts.Width
}
if opts.Cols != nil {
params["cols"] = *opts.Cols
}
if opts.Rows != nil {
params["rows"] = *opts.Rows
}
var result SurfaceResult
return result, c.request(ctx, "new-pane-right", params, &result)
}

func (c *Client) Split(ctx context.Context, pane uint64, dir string, opts SplitOptions) (SurfaceResult, error) {
params := commandMap(opts)
params["pane"] = pane
Expand All @@ -453,10 +531,65 @@ func (c *Client) SetRatio(ctx context.Context, pane uint64, dir string, ratio fl
}

func (c *Client) SetSplitRatio(ctx context.Context, split uint64, ratio float32) error {
return c.setSplitRatio(ctx, split, ratio, nil)
}

// SetSplitRatioInTransaction coalesces samples that share one client-scoped transaction.
func (c *Client) SetSplitRatioInTransaction(ctx context.Context, split uint64, ratio float32, transaction uint64) error {
return c.setSplitRatio(ctx, split, ratio, &transaction)
}

func (c *Client) setSplitRatio(ctx context.Context, split uint64, ratio float32, transaction *uint64) error {
if err := c.requireProtocol(ctx, 8, "set-split-ratio"); err != nil {
return err
}
return c.request(ctx, "set-split-ratio", map[string]any{"split": split, "ratio": ratio}, nil)
params := map[string]any{"split": split, "ratio": ratio}
if transaction != nil {
params["transaction"] = *transaction
}
return c.request(ctx, "set-split-ratio", params, nil)
}

func (c *Client) SetViewportPaneWidth(ctx context.Context, pane uint64, width float32) error {
return c.setViewportPaneWidth(ctx, pane, width, nil)
}

// SetViewportPaneWidthInTransaction coalesces samples that share one client-scoped transaction.
func (c *Client) SetViewportPaneWidthInTransaction(ctx context.Context, pane uint64, width float32, transaction uint64) error {
return c.setViewportPaneWidth(ctx, pane, width, &transaction)
}

func (c *Client) setViewportPaneWidth(ctx context.Context, pane uint64, width float32, transaction *uint64) error {
if err := validateViewportPaneWidth(width); err != nil {
return err
}
if err := c.requireCapability(ctx, "viewport-column-resize-v1", "viewport pane resizing"); err != nil {
return err
}
params := map[string]any{"pane": pane, "width": width}
if transaction != nil {
params["transaction"] = *transaction
}
return c.request(ctx, "set-viewport-pane-width", params, nil)
}

// UndoLayout previews the latest layout undo when confirmationRevision is nil.
// To confirm a pane-closing undo, pass the exact revision returned by
// LayoutUndoConfirmationRequired.
func (c *Client) UndoLayout(ctx context.Context, pane uint64, confirmationRevision *uint64) (LayoutUndoResult, error) {
if err := c.requireCapability(ctx, "layout-undo-v1", "layout undo"); err != nil {
return nil, err
}
params := map[string]any{"pane": pane}
if confirmationRevision != nil {
params["revision"] = *confirmationRevision
params["confirm_close"] = true
}
var wire layoutUndoWire
if err := c.request(ctx, "undo-layout", params, &wire); err != nil {
return nil, err
}
return decodeLayoutUndoResult(wire)
}

func (c *Client) SetDefaultColors(ctx context.Context, fg, bg *string) error {
Expand Down
Loading