Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
3ee19ce
Forward mouse input to TUI applications
lawrencecchen Jul 9, 2026
e9845ad
Merge remote-tracking branch 'origin/main' into task-tui-mouse-passth…
lawrencecchen Jul 9, 2026
8b3de53
Harden TUI mouse passthrough
lawrencecchen Jul 9, 2026
71fc0ae
Preserve PTY mouse release state
lawrencecchen Jul 9, 2026
982b45a
Order remote PTY input delivery
lawrencecchen Jul 9, 2026
ef5473d
Guarantee remote mouse release delivery
lawrencecchen Jul 9, 2026
5939e78
Bound remote PTY input buffering
lawrencecchen Jul 9, 2026
6c82650
Add regression coverage for remote input findings
lawrencecchen Jul 9, 2026
f0a377f
Fix reviewed remote PTY input edge cases
lawrencecchen Jul 9, 2026
43377d9
Correct horizontal wheel regression expectations
lawrencecchen Jul 9, 2026
0aa52d9
Align horizontal wheel input and local writes
lawrencecchen Jul 9, 2026
3c76bc5
Cover ambiguous PTY mouse button events
lawrencecchen Jul 10, 2026
1337b58
Keep PTY mouse input off the event loop
lawrencecchen Jul 10, 2026
68d548f
Cover PTY input mutation barrier
lawrencecchen Jul 10, 2026
a87d4f3
Order PTY input before session mutations
lawrencecchen Jul 10, 2026
674d6a6
Cover PTY input backpressure recovery
lawrencecchen Jul 10, 2026
4bada37
Serialize PTY input and session mutations
lawrencecchen Jul 10, 2026
b6c044f
Cover failed PTY mouse press cleanup
lawrencecchen Jul 10, 2026
696c147
Close ordered input review gaps
lawrencecchen Jul 10, 2026
5ba128f
Cover PTY drag preservation after motion failure
lawrencecchen Jul 10, 2026
83e000d
Bound remote PTY operation failures
lawrencecchen Jul 10, 2026
3b53a4e
Complete ordered session failure handling
lawrencecchen Jul 10, 2026
5b9b53a
Coalesce high-rate mouse updates
lawrencecchen Jul 10, 2026
687c0a9
Close mouse queue lifecycle gaps
lawrencecchen Jul 10, 2026
120c09f
Merge remote-tracking branch 'origin/main' into task-tui-mouse-passth…
lawrencecchen Jul 10, 2026
e48ce2e
Make remote mutation recovery authoritative
lawrencecchen Jul 10, 2026
25bb3d0
Order browser completion after tree invalidation
lawrencecchen Jul 10, 2026
db1b5e5
Refresh remote tree snapshots off loop
lawrencecchen Jul 10, 2026
30e49b3
Separate identity and background tree refresh
lawrencecchen Jul 10, 2026
7290780
Close remote input routing gaps
lawrencecchen Jul 10, 2026
9ce5b48
Preserve remote input routing invariants
lawrencecchen Jul 10, 2026
096f34b
Close asynchronous input ordering races
lawrencecchen Jul 10, 2026
4c78a45
Version title events and complete mutations authoritatively
lawrencecchen Jul 10, 2026
14dfa79
Bound UI title and drag hot paths
lawrencecchen Jul 10, 2026
ab86afc
Enforce ordered session lifecycles
lawrencecchen Jul 10, 2026
e6a0ec8
Restore Windows TUI portability
lawrencecchen Jul 10, 2026
12e392c
Finish ordered TUI lifecycle handling
lawrencecchen Jul 10, 2026
b5c7398
Merge remote-tracking branch 'origin/main' into task-tui-mouse-passth…
lawrencecchen Jul 10, 2026
111183c
Close remaining TUI hot-path gaps
lawrencecchen Jul 10, 2026
703b1b6
Harden TUI asynchronous input lifecycle
lawrencecchen Jul 10, 2026
a03490f
Fix browser resize failure handling
lawrencecchen Jul 10, 2026
d3f36c6
Merge remote-tracking branch 'origin/main' into task-tui-mouse-passth…
lawrencecchen Jul 10, 2026
28310ad
Bound remote surface synchronization
lawrencecchen Jul 10, 2026
b8a4633
Harden remote recovery and browser teardown
lawrencecchen Jul 10, 2026
e5d0006
Retry browser resizes with bounded backoff
lawrencecchen Jul 10, 2026
a72aa9c
Preserve reattach input and lifecycle ownership
lawrencecchen Jul 10, 2026
ff3deb4
Close TUI recovery review gaps
lawrencecchen Jul 10, 2026
feef345
Preserve deferred TUI interaction intent
lawrencecchen Jul 10, 2026
1cdd21b
Bound TUI routing and subscriber recovery
lawrencecchen Jul 10, 2026
b783f16
Recover bounded TUI event streams
lawrencecchen Jul 10, 2026
efe04eb
Complete TUI overflow recovery contract
lawrencecchen Jul 10, 2026
18b7e7c
Harden bounded TUI event lifecycle
lawrencecchen Jul 10, 2026
7732068
Close TUI queue recovery gaps
lawrencecchen Jul 10, 2026
286d005
Order TUI recovery and mouse mode state
lawrencecchen Jul 10, 2026
aa08a2e
Bound browser and mouse recovery state
lawrencecchen Jul 10, 2026
eeb7e82
Make TUI mouse snapshots lifetime stable
lawrencecchen Jul 10, 2026
4666f71
Bound TUI attach and deferred routing
lawrencecchen Jul 14, 2026
501ef33
Merge remote-tracking branch 'origin/main' into task-tui-mouse-passth…
lawrencecchen Jul 14, 2026
abf2232
Clean up merged TUI input sender
lawrencecchen Jul 14, 2026
ff2530f
Recover bounded TUI event streams
lawrencecchen Jul 14, 2026
486867c
Scope attach events and preserve pointer state
lawrencecchen Jul 14, 2026
14fcce5
Deliver terminal overflow recovery events
lawrencecchen Jul 14, 2026
94c39ab
Keep scoped overflow on shared transports
lawrencecchen Jul 14, 2026
c099810
Acknowledge TUI subscription recovery
lawrencecchen Jul 14, 2026
593ee4e
Order remote subscription reconciliation
lawrencecchen Jul 15, 2026
b738e3a
Unify bounded TUI transport output
lawrencecchen Jul 15, 2026
6a76fde
Reserve TUI transport capacity for responses
lawrencecchen Jul 15, 2026
e5afd59
Bound and prioritize TUI transport control
lawrencecchen Jul 15, 2026
5777f21
Purge stale TUI frames on stream overflow
lawrencecchen Jul 15, 2026
87582f9
Preserve TUI titles and bound browser frames
lawrencecchen Jul 15, 2026
bbfb669
Scope retained TUI title updates
lawrencecchen Jul 15, 2026
9ae9379
Bound TUI refresh and capture configuration
lawrencecchen Jul 15, 2026
84aca8a
Attribute TUI queue pressure to backlog owners
lawrencecchen Jul 15, 2026
85ec61f
Bound TUI replay and title recovery state
lawrencecchen Jul 15, 2026
d9741a4
Preserve TUI release and socket lifecycles
lawrencecchen Jul 15, 2026
1300602
Bound TUI release recovery and initial attach
lawrencecchen Jul 15, 2026
fee7b4c
Scope TUI pointer routing barriers
lawrencecchen Jul 15, 2026
7bf4e32
Wake TUI streams and cap resize recovery
lawrencecchen Jul 15, 2026
c266bb7
Time out stalled WebSocket clients
lawrencecchen Jul 15, 2026
d17250e
Complete browser resize recovery
lawrencecchen Jul 15, 2026
785345a
Order browser resize completion
lawrencecchen Jul 15, 2026
7c4b4d5
Publish successful browser resize retries
lawrencecchen Jul 15, 2026
3fe196a
Update web frontend for protocol 7
lawrencecchen Jul 15, 2026
bc5b89f
Bound ordered TUI transport waits
lawrencecchen Jul 15, 2026
49deb96
Recover web terminal attachment overflow
lawrencecchen Jul 15, 2026
437440b
Preserve recovery routes and outcomes
lawrencecchen Jul 15, 2026
70ec0c8
Bound attach overflow recovery
lawrencecchen Jul 15, 2026
6f4458e
Test bounded terminal replay
lawrencecchen Jul 15, 2026
727f3c3
Bound terminal state replays
lawrencecchen Jul 15, 2026
c6b8f17
Merge remote-tracking branch 'origin/main' into task-tui-mouse-passth…
lawrencecchen Jul 15, 2026
d3c10af
Document PTY failure wake ownership
lawrencecchen Jul 15, 2026
929cd84
Remove polling from bounded transports
lawrencecchen Jul 15, 2026
0a9a822
Coordinate overflow and resize recovery
lawrencecchen Jul 15, 2026
fd5d6e4
Resolve ambiguous mouse and title recovery
lawrencecchen Jul 15, 2026
2026060
Preserve bounded replay and title locality
lawrencecchen Jul 15, 2026
4a758a7
Reconcile remote size and title ordering
lawrencecchen Jul 15, 2026
c89c094
Queue browser resize and bound replay scans
lawrencecchen Jul 15, 2026
98d5c4f
Bound asynchronous resize recovery
lawrencecchen Jul 15, 2026
44c1e8e
Complete browser resize recovery contract
lawrencecchen Jul 15, 2026
b4521f1
Keep pointer motion off terminal locks
lawrencecchen Jul 15, 2026
30fa9c9
Scope resize recovery to request owners
lawrencecchen Jul 15, 2026
9160af3
Make resize ownership follow accepted work
lawrencecchen Jul 15, 2026
c2f27eb
Close terminal and resize publication races
lawrencecchen Jul 15, 2026
c109013
Preserve browser resize barriers
lawrencecchen Jul 15, 2026
5942158
Own browser resize lifecycle in core
lawrencecchen Jul 15, 2026
3a10b36
Make async completions identity-safe
lawrencecchen Jul 15, 2026
3869725
Cache PTY mouse protocol per surface
lawrencecchen Jul 15, 2026
2091374
Preserve mux events under backpressure
lawrencecchen Jul 15, 2026
f19ead0
Always deliver the paired mouse release
lawrencecchen Jul 15, 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
87 changes: 44 additions & 43 deletions cmux-tui/Cargo.lock

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

1 change: 1 addition & 0 deletions cmux-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ tungstenite = { version = "0.29", default-features = false, features = ["handsha
uds_windows = "1.2"
regex = "1"
bindgen = "0.72"
smallvec = "1"

[profile.release]
lto = "thin"
Expand Down
91 changes: 89 additions & 2 deletions cmux-tui/bindings/conformance/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
"type": "command",
"request": { "id": 1, "cmd": "identify" },
"expect": { "id": 1, "ok": true, "data": { "app": "cmux-tui", "protocol": 6 } },
"expect": { "id": 1, "ok": true, "data": { "app": "cmux-tui", "protocol": 7 } },
"match": "partial"
}
]
Expand All @@ -18,7 +18,7 @@
{
"type": "command",
"request": { "id": 1, "cmd": "ping" },
"expect": { "id": 1, "ok": true, "data": { "ok": true, "protocol": 6 } },
"expect": { "id": 1, "ok": true, "data": { "ok": true, "protocol": 7 } },
"match": "partial"
},
{
Expand Down Expand Up @@ -191,6 +191,82 @@
}
]
},
{
"name": "subscribe-title-changed-carries-title",
"steps": [
{
"type": "command",
"request": { "id": 1, "cmd": "new-workspace", "name": "conf-title", "cols": 80, "rows": 24 },
"expect": { "id": 1, "ok": true },
"match": "partial",
"bind": { "surface0": "data.surface" }
},
{
"type": "stream",
"name": "events",
"request": { "id": 2, "cmd": "subscribe" },
"expect": { "id": 2, "ok": true },
"match": "partial"
},
{
"type": "command",
"request": { "id": 3, "cmd": "send", "surface": "$surface0", "text": "printf '\\033]2;conformance title\\007'; sleep 30\r" },
"expect": { "id": 3, "ok": true },
"match": "partial"
},
{
"type": "expect_events",
"stream": "events",
"expect": [
{ "event": "title-changed", "surface": "$surface0", "title": "conformance title" }
]
},
{
"type": "command",
"request": { "id": 4, "cmd": "send", "surface": "$surface0", "text": "\u0003" },
"expect": { "id": 4, "ok": true },
"match": "partial"
}
]
},
{
"name": "attach-resize-carries-replay",
"steps": [
{
"type": "command",
"request": { "id": 1, "cmd": "new-workspace", "name": "conf-attach-resize", "cols": 80, "rows": 24 },
"expect": { "id": 1, "ok": true },
"match": "partial",
"bind": { "surface0": "data.surface" }
},
{
"type": "stream",
"name": "attach",
"request": { "cmd": "attach-surface", "surface": "$surface0" },
"expect": { "ok": true },
"match": "partial"
},
{
"type": "command",
"request": { "id": 2, "cmd": "resize-surface", "surface": "$surface0", "cols": 100, "rows": 31 },
"expect": { "id": 2, "ok": true, "data": { "accepted": true } },
"match": "partial"
},
{
"type": "expect_events",
"stream": "attach",
"expect": [
{
"event": "resized",
"surface": "$surface0",
"cols": 100,
"rows": 31,
"replay": { "__match__": "nonempty-string" }
}
]
}
]
},
{
"name": "send-read-screen-round-trip",
"steps": [
Expand All @@ -216,6 +292,17 @@
}
]
},
{
"name": "overflow-event-schema",
"steps": [
{
"type": "parse_event",
"event": { "event": "overflow", "error": "subscriber fell behind", "scope": "surface", "surface": 7 },
"expect": { "event": "overflow", "error": "subscriber fell behind", "scope": "surface", "surface": 7 },
"match": "partial"
}
]
},
{
"name": "implemented-verbs-json",
"requires": {
Expand Down
19 changes: 16 additions & 3 deletions cmux-tui/bindings/conformance/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
sys.path.insert(0, str(PYTHON_BINDING))

from cmux import CommandError, CmuxClient, TimeoutError as CmuxTimeoutError # noqa: E402
from cmux.client import _parse_event # noqa: E402


class FixtureFailure(Exception):
Expand Down Expand Up @@ -118,7 +119,11 @@ def run_fixture(fixture: Dict[str, Any], defaults: Dict[str, Any], socket_path:
variables: Dict[str, Any] = {}
streams: Dict[str, Any] = {}
timeout_ms = int(fixture.get("timeout_ms", defaults.get("timeout_ms", 5000)))
with CmuxClient(socket_path=socket_path, timeout=max(timeout_ms / 1000.0, 1.0)) as client:
with CmuxClient(
socket_path=socket_path,
timeout=max(timeout_ms / 1000.0, 1.0),
allow_protocol_v6_attach=True,
) as client:
try:
for step in fixture.get("steps", []):
step_timeout = int(step.get("timeout_ms", timeout_ms))
Expand All @@ -143,9 +148,12 @@ def run_step(
bind_variables(response, step.get("bind", {}), variables)
elif step_type == "stream":
request = substitute(step["request"], variables)
if request.get("cmd") != "subscribe":
if request.get("cmd") == "subscribe":
stream = client.subscribe_with_request(request)
elif request.get("cmd") == "attach-surface":
stream = client.attach_surface(int(request["surface"]))
else:
raise FixtureFailure(f"unsupported stream command {request.get('cmd')}")
stream = client.subscribe_with_request(request)
streams[step["name"]] = stream
response = stream.response
assert_match(response, substitute(step.get("expect", {}), variables), step.get("match", "partial"))
Expand All @@ -156,6 +164,9 @@ def run_step(
elif step_type == "wait_contains":
request = substitute(step["request"], variables)
wait_contains(client, request, step["path"], step["contains"], timeout_ms / 1000.0)
elif step_type == "parse_event":
event = dataclasses.asdict(_parse_event(substitute(step["event"], variables)))
assert_match(event, substitute(step.get("expect", {}), variables), step.get("match", "partial"))
else:
raise FixtureFailure(f"unknown step type {step_type}")

Expand Down Expand Up @@ -278,6 +289,8 @@ def assert_match(actual: Any, expected: Any, mode: str) -> None:


def partial_match(actual: Any, expected: Any) -> bool:
if expected == {"__match__": "nonempty-string"}:
return isinstance(actual, str) and bool(actual)
if isinstance(expected, dict):
if not isinstance(actual, dict):
return False
Expand Down
2 changes: 1 addition & 1 deletion cmux-tui/bindings/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ assemble_prompt() {
echo "- This is a temporary output directory. The harness swaps it into cmux-tui/bindings/$lang/ only after generation succeeds."
echo "- Regeneration must overwrite stale generated files inside the temporary output directory."
echo "- Follow the style sheet exactly."
echo "- Implement protocol-v5 commands marked implemented, plus consumer-side implemented move commands."
echo "- Implement protocol-v7 commands and events marked implemented, plus consumer-side implemented move commands."
echo "- Preserve command names, params, result shapes, event names, and error behavior from the spec."
echo "- Do not implement proposed commands unless the style sheet explicitly asks for version-gated stubs."
echo
Expand Down
28 changes: 23 additions & 5 deletions cmux-tui/bindings/go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
Expand Down Expand Up @@ -307,8 +308,10 @@ func (c *Client) RenameWorkspace(ctx context.Context, workspace uint64, name str
return c.request(ctx, "rename-workspace", map[string]any{"workspace": workspace, "name": name}, nil)
}

func (c *Client) ResizeSurface(ctx context.Context, surface uint64, cols, rows uint16) error {
return c.request(ctx, "resize-surface", map[string]any{"surface": surface, "cols": cols, "rows": rows}, nil)
func (c *Client) ResizeSurface(ctx context.Context, surface uint64, cols, rows uint16) (ResizeSurfaceResult, error) {
var result ResizeSurfaceResult
err := c.request(ctx, "resize-surface", map[string]any{"surface": surface, "cols": cols, "rows": rows}, &result)
return result, err
}

func (c *Client) FocusPane(ctx context.Context, pane uint64) error {
Expand Down Expand Up @@ -352,7 +355,7 @@ func (c *Client) AttachSurface(ctx context.Context, surface uint64) (*Stream, er
}
protocol = &info.Protocol
}
if *protocol > 6 || (*protocol > 5 && !c.allowProtocolV6Attach) {
if *protocol > 7 || (*protocol > 5 && !c.allowProtocolV6Attach) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow zero-value Go clients to attach to v7 servers

With the server now advertising protocol 7, callers using the normal zero-value Options path leave allowProtocolV6Attach false, so AttachSurface rejects every current server here before opening even PTY attach streams. The Go binding now parses the newer attach events, and the e2e only passes because it explicitly opts in; default Go clients should accept v7 or default this option to true.

Useful? React with 👍 / 👎.

return nil, &protocolError{msg: fmt.Sprintf("unsupported attach protocol %d", *protocol)}
}
return c.openStream(ctx, map[string]any{"id": c.nextRequestID(), "cmd": "attach-surface", "surface": surface})
Expand Down Expand Up @@ -398,29 +401,44 @@ type Stream struct {
conn *jsonLineConn
timeout time.Duration
buffered []Event
closed atomic.Bool
}

func (s *Stream) Close() error {
if !s.closed.CompareAndSwap(false, true) {
return nil
}
return s.conn.Close()
}

func (s *Stream) Recv(ctx context.Context) (Event, error) {
if s.closed.Load() {
return nil, io.EOF
}
if len(s.buffered) > 0 {
event := s.buffered[0]
s.buffered = s.buffered[1:]
return event, nil
return s.finishTerminal(event), nil
}
for {
value, err := s.conn.Recv(ctx, s.timeout)
if err != nil {
return nil, err
}
if _, ok := value["event"].(string); ok {
return parseEvent(value), nil
return s.finishTerminal(parseEvent(value)), nil
}
}
}

func (s *Stream) finishTerminal(event Event) Event {
switch event.(type) {
case DetachedEvent, OverflowEvent:
_ = s.Close()
}
return event
}

type jsonLineConn struct {
conn net.Conn
reader *bufio.Reader
Expand Down
Loading