Skip to content
Merged
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
205 commits
Select commit Hold shift + click to select a range
ceecddb
test(tui): cover command-k history clearing
lawrencecchen Jul 22, 2026
44f3819
feat(tui): add familiar terminal shortcuts
lawrencecchen Jul 22, 2026
e91f530
fix(tui): request command-modified key events
lawrencecchen Jul 23, 2026
d29954b
test(tui): preserve prompt when clearing history
lawrencecchen Jul 23, 2026
c63c43a
fix(tui): preserve prompt when clearing history
lawrencecchen Jul 23, 2026
89046ac
test(tui): cover shortcut review regressions
lawrencecchen Jul 23, 2026
b63c249
fix(tui): close shortcut review gaps
lawrencecchen Jul 23, 2026
3703e1e
test(tui): cover autoreview clear-history regressions
lawrencecchen Jul 23, 2026
4810146
fix(tui): make history clearing protocol-safe
lawrencecchen Jul 23, 2026
3fb122a
test(tui): cover final shortcut review regressions
lawrencecchen Jul 23, 2026
3d33c5f
fix(tui): keep terminal clears surface-local
lawrencecchen Jul 23, 2026
1869b3d
test(tui): cover final keyboard review findings
lawrencecchen Jul 23, 2026
a24572a
fix(tui): preserve terminal keyboard semantics
lawrencecchen Jul 23, 2026
ae903da
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 23, 2026
6c8623c
test(tui): cover shifted shortcut review regressions
lawrencecchen Jul 23, 2026
e4449d1
fix(tui): normalize enhanced shifted keys
lawrencecchen Jul 23, 2026
604ac27
test(tui): cover layout and wrapped prompt regressions
lawrencecchen Jul 23, 2026
444d861
fix(tui): preserve layout and live prompt semantics
lawrencecchen Jul 23, 2026
89adc4d
test(tui): cover associated keyboard semantics
lawrencecchen Jul 23, 2026
b451d8d
fix(tui): normalize enhanced keyboard input
lawrencecchen Jul 23, 2026
03d7a41
test(tui): cover final prompt safety regressions
lawrencecchen Jul 23, 2026
973fa06
fix(tui): harden prompt keyboard startup
lawrencecchen Jul 23, 2026
e258fb5
test(tui): cover consumed keyboard regressions
lawrencecchen Jul 23, 2026
80b84de
fix(tui): honor consumed keyboard text
lawrencecchen Jul 23, 2026
b253054
test(tui): cover final clear-history review gaps
lawrencecchen Jul 23, 2026
11156c5
fix(tui): close final clear-history review gaps
lawrencecchen Jul 23, 2026
d9505c3
test(tui): cover final autoreview regressions
lawrencecchen Jul 23, 2026
b84f6b5
fix(tui): resolve final autoreview blockers
lawrencecchen Jul 23, 2026
94ae1a5
test(tui): cover serialized prompt redraw
lawrencecchen Jul 23, 2026
d65a373
fix(tui): serialize prompt-safe history clear
lawrencecchen Jul 23, 2026
09cf616
test(tui): read alternate-screen readiness replay
lawrencecchen Jul 23, 2026
5a1ff7b
test(tui): cover consumed Option and Ctrl-Shift metadata
lawrencecchen Jul 23, 2026
00bb454
fix(tui): preserve enhanced keyboard identity
lawrencecchen Jul 23, 2026
35d780e
test(tui): cover authoritative terminal safety
lawrencecchen Jul 23, 2026
4b48cd9
fix(tui): make terminal shortcut routing authoritative
lawrencecchen Jul 23, 2026
66e37a4
test(tui): cover authoritative shortcut encoding
lawrencecchen Jul 24, 2026
7755117
fix(tui): encode shortcut fallback authoritatively
lawrencecchen Jul 24, 2026
12de9c7
test(tui): cover safe shortcut fallback protocol
lawrencecchen Jul 24, 2026
e98140e
fix(tui): validate authoritative key fallback
lawrencecchen Jul 24, 2026
cfe1743
test(tui): cover streaming shortcut safety
lawrencecchen Jul 24, 2026
cdb3536
fix(tui): make shortcut fallback streaming-safe
lawrencecchen Jul 24, 2026
cdac6a7
test(tui): reject pre-submit prompt redraws
lawrencecchen Jul 24, 2026
c51a22a
fix(tui): sequence prompt and keyboard ownership
lawrencecchen Jul 24, 2026
d34d30f
test(tui): cover prompt lifecycle safety
lawrencecchen Jul 24, 2026
d976806
fix(tui): fail closed on prompt lifecycle
lawrencecchen Jul 24, 2026
be62632
test(tui): preserve prompt during history clear
lawrencecchen Jul 24, 2026
6767c05
fix(tui): clear history without shell input
lawrencecchen Jul 24, 2026
baa4b50
test(tui): rely on executable binding conformance
lawrencecchen Jul 24, 2026
138c33b
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 24, 2026
ee010fd
fix(tui): preserve clear history lock order
lawrencecchen Jul 24, 2026
5935601
test(tui): cover terminal shortcut delivery barriers
lawrencecchen Jul 24, 2026
29c75ae
fix(tui): preserve alt shortcuts and await host clear
lawrencecchen Jul 24, 2026
1d12f04
test(tui): preserve modified associated key events
lawrencecchen Jul 24, 2026
93e19c2
fix(tui): retain modified associated key events
lawrencecchen Jul 24, 2026
084ca2f
test(tui): reject lossy extended modifiers
lawrencecchen Jul 24, 2026
9fdd5ca
fix(tui): preserve shortcuts at host boundary
lawrencecchen Jul 24, 2026
0af8388
test(tui): cover explicit option routing and compact input
lawrencecchen Jul 24, 2026
ef847d2
fix(tui): harden terminal shortcut routing
lawrencecchen Jul 24, 2026
323a816
test(tui): clear visible rows without prompt metadata
lawrencecchen Jul 24, 2026
34bf90a
fix(tui): clear completed rows without prompt metadata
lawrencecchen Jul 24, 2026
0b259f2
test(tui): cover terminal-standard clear shortcut
lawrencecchen Jul 24, 2026
1063418
fix(tui): honor terminal-standard clear shortcut
lawrencecchen Jul 24, 2026
6aecf1e
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 24, 2026
293e421
fix(macOS): import dock shell activity type
lawrencecchen Jul 24, 2026
587b973
fix(macOS): make session snapshot types explicit
lawrencecchen Jul 24, 2026
437581d
fix(macOS): return resume policy explicitly
lawrencecchen Jul 24, 2026
a07d3ba
test(tui): cover host keyboard protocol edge cases
lawrencecchen Jul 24, 2026
d22bd0e
fix(tui): validate host keyboard capabilities
lawrencecchen Jul 24, 2026
770cc67
test(tui): cover bounded terminal response reads
lawrencecchen Jul 24, 2026
6e191ee
fix(tui): bound host keyboard capability probe
lawrencecchen Jul 24, 2026
d89a527
test(tui): cover remaining shortcut edge cases
lawrencecchen Jul 24, 2026
06e1cfe
fix(tui): preserve shortcut routing invariants
lawrencecchen Jul 24, 2026
02f2cc8
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 24, 2026
972874d
test(web): avoid leaking feedback environment mock
lawrencecchen Jul 24, 2026
b7bd9cd
test(tui): expose legacy CSI-u shifted characters
lawrencecchen Jul 24, 2026
e7b44cc
fix(tui): normalize legacy CSI-u shift input
lawrencecchen Jul 24, 2026
fc3caf7
fix(macOS): restore dock checks on current main
lawrencecchen Jul 24, 2026
5cd476e
test(tui): preserve per-event keyboard metadata
lawrencecchen Jul 24, 2026
dd8c2f8
fix(tui): trust per-event keyboard metadata
lawrencecchen Jul 24, 2026
e600b67
fix(macOS): isolate dock snapshot filtering
lawrencecchen Jul 24, 2026
78eb2ab
test(tui): cover fallback payload queue accounting
lawrencecchen Jul 24, 2026
8753d16
fix(tui): bound fallback payload queue memory
lawrencecchen Jul 24, 2026
6eedef7
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 24, 2026
d117cf6
test(ci): cover TestFlight variant identities
lawrencecchen Jul 24, 2026
4b28f98
test(macOS): import checklist setting in sidebar fixture
lawrencecchen Jul 24, 2026
61eea81
fix(macOS): repair sidebar suspension integration
lawrencecchen Jul 24, 2026
f81e832
test(tui): cover clear-history preservation boundaries
lawrencecchen Jul 25, 2026
b9ea624
fix(tui): make clear-history delivery total
lawrencecchen Jul 25, 2026
63ba123
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 25, 2026
b7080b4
test(tui): cover Caps Lock Alt shortcut routing
lawrencecchen Jul 25, 2026
f1e14b3
fix(tui): preserve Alt shortcuts under Caps Lock
lawrencecchen Jul 25, 2026
3bdd0f4
fix(macOS): isolate feature flag default evaluation
lawrencecchen Jul 25, 2026
daafda2
test(tui): cover intermediate clear-history peers
lawrencecchen Jul 25, 2026
21ef16a
fix(tui): preserve intermediate remote clear support
lawrencecchen Jul 25, 2026
a59a199
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 25, 2026
890a065
test(tui): cover non-atomic remote shortcut routing
lawrencecchen Jul 26, 2026
e2fe93c
fix(tui): require atomic remote shortcut routing
lawrencecchen Jul 26, 2026
f45fa33
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
699b352
test(tui): cover unencodable clear fallback
lawrencecchen Jul 26, 2026
5cb0fa0
fix(tui): report undeliverable clear shortcuts
lawrencecchen Jul 26, 2026
5329765
test(tui): require bounded keyboard protocol probe
lawrencecchen Jul 26, 2026
2f97e6c
fix(tui): bound host keyboard protocol probe
lawrencecchen Jul 26, 2026
3c5be36
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
20ddb4c
test(tui): preserve meaningful shift chords
lawrencecchen Jul 26, 2026
94d6c26
fix(tui): retain meaningful shift modifiers
lawrencecchen Jul 26, 2026
dc72a6b
test(tui): cover enhanced non-ascii shift binding
lawrencecchen Jul 26, 2026
a1a4908
fix(tui): align enhanced shift shortcut identity
lawrencecchen Jul 26, 2026
145bf45
test(tui): reject non-authoritative clear fallback
lawrencecchen Jul 26, 2026
1915880
fix(tui): reject non-authoritative clear fallback
lawrencecchen Jul 26, 2026
6cf2b69
test(tui): cover private screen mode restore semantics
lawrencecchen Jul 26, 2026
172b953
fix(tui): track private screen modes independently
lawrencecchen Jul 26, 2026
159a52a
test(tui): reject ambiguous option shortcuts
lawrencecchen Jul 26, 2026
8e8f8fe
fix(tui): fail closed on ambiguous option input
lawrencecchen Jul 26, 2026
7ed4df3
test(tui): preserve empty option CSI-u events
lawrencecchen Jul 26, 2026
d9dd22c
fix(tui): preserve ambiguous CSI-u input
lawrencecchen Jul 26, 2026
5963830
test(tui): isolate prompt tracking from control strings
lawrencecchen Jul 26, 2026
578bd00
fix(tui): isolate prompt tracking control strings
lawrencecchen Jul 26, 2026
cbc12f7
test(tui): align alt CSI-u parser expectations
lawrencecchen Jul 26, 2026
0f363ae
test(tui): satisfy control string test lint
lawrencecchen Jul 26, 2026
2c1d994
test(tui): keep ctrl-l child-owned
lawrencecchen Jul 26, 2026
1e9fbdb
fix(tui): keep ctrl-l child-owned
lawrencecchen Jul 26, 2026
25a72dc
test(tui): preserve metadata-free multiline input
lawrencecchen Jul 26, 2026
b31924d
fix(tui): preserve metadata-free visible input
lawrencecchen Jul 26, 2026
14ac324
test(tui): align ctrl-l shortcut expectations
lawrencecchen Jul 26, 2026
f003de6
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
fcbbf5f
test(tui): cover enhanced shortcut review gaps
lawrencecchen Jul 26, 2026
e07e00a
fix(tui): preserve enhanced shortcut semantics
lawrencecchen Jul 26, 2026
e759af1
test(tui): exercise child-owned ctrl-l directly
lawrencecchen Jul 26, 2026
d39fb02
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
63a9609
test(tui): preserve fallback base layout identity
lawrencecchen Jul 26, 2026
2a8f54c
fix(tui): preserve fallback layout metadata
lawrencecchen Jul 26, 2026
6126559
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
b13c18e
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
539acd6
test(tui): reject clears inside partial VT streams
lawrencecchen Jul 26, 2026
b1a09d0
fix(tui): guard clear at VT stream boundaries
lawrencecchen Jul 26, 2026
320ef24
test(tui): retain clear intent across partial VT output
lawrencecchen Jul 26, 2026
40ba492
fix(tui): defer clear until VT boundary
lawrencecchen Jul 26, 2026
b54db01
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
ef7fc66
test(tui): cover final clear-history review gaps
lawrencecchen Jul 26, 2026
8dd8439
fix(tui): fail closed and settle clear UI
lawrencecchen Jul 26, 2026
04a9a4b
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
12ba765
test(tui): cover metadata fallback and progress wakes
lawrencecchen Jul 26, 2026
a81719f
fix(tui): preserve metadata-free clear fallback
lawrencecchen Jul 26, 2026
5277033
test(tui): cover prompt continuation in history
lawrencecchen Jul 26, 2026
9ddd1b6
fix(tui): guard prompt continuations in history
lawrencecchen Jul 26, 2026
8c118a7
test(tui): cover Unicode VT boundaries and ambiguous clears
lawrencecchen Jul 26, 2026
a40d177
fix(tui): track string UTF-8 and ambiguous clears
lawrencecchen Jul 26, 2026
2efc551
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
c294f1d
test(tui): distinguish deterministic clear failures
lawrencecchen Jul 26, 2026
713aa3c
fix(tui): preserve clear failure delivery certainty
lawrencecchen Jul 26, 2026
e25f60e
test(tui): cover clear coalescing and transport ambiguity
lawrencecchen Jul 26, 2026
6292ef5
fix(tui): bound clear retries and delivery certainty
lawrencecchen Jul 26, 2026
fc92cf6
test(tui): preserve fallback repeats without timeout backlog
lawrencecchen Jul 26, 2026
171c1a1
fix(tui): share stalled clear wait budgets
lawrencecchen Jul 26, 2026
1ebefad
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
4f83e5b
test(tui): cover repeated prompt markers in history
lawrencecchen Jul 26, 2026
cedeb78
fix(tui): preserve prompts spanning history
lawrencecchen Jul 26, 2026
4392664
test(tui): cover output racing clear timeout release
lawrencecchen Jul 26, 2026
bd749ee
fix(tui): rearm clear waits after raced output
lawrencecchen Jul 26, 2026
5f360ad
test(tui): cover fallback key WebSocket budget
lawrencecchen Jul 26, 2026
a8bbc1f
fix(tui): bound fallback keys to transport budget
lawrencecchen Jul 26, 2026
bd1cc52
test(tui): cover delayed clear and enhanced key regressions
lawrencecchen Jul 26, 2026
d8fc15c
fix(tui): preserve enhanced input and newer clear state
lawrencecchen Jul 26, 2026
fa08155
test(tui): cover clear queue and selection races
lawrencecchen Jul 26, 2026
b92c2d8
fix(tui): isolate clear operations per surface
lawrencecchen Jul 26, 2026
4b23edc
test(tui): cover enhanced text and remote clear blocking
lawrencecchen Jul 26, 2026
3e5ff20
fix(tui): keep remote clear off connection readers
lawrencecchen Jul 26, 2026
7d4d527
Merge remote-tracking branch 'origin/main' into feat-tui-terminal-sho…
lawrencecchen Jul 26, 2026
56723cc
test(tui): cover scheduler resource and ordering bounds
lawrencecchen Jul 26, 2026
faebb16
fix(tui): bound and order clear schedulers
lawrencecchen Jul 26, 2026
8d5d332
test(tui): cover global clear limits and host certainty
lawrencecchen Jul 26, 2026
9a130ec
fix(tui): bound server clears and preserve host failures
lawrencecchen Jul 26, 2026
5231001
test(tui): cover clear liveness and alt certainty
lawrencecchen Jul 26, 2026
a6befd6
fix(tui): isolate and bound terminal clear work
lawrencecchen Jul 26, 2026
05cf6d1
test(tui): cover review closeout regressions
lawrencecchen Jul 26, 2026
ec5cfe1
fix(tui): retain control admission and clear prefixes safely
lawrencecchen Jul 26, 2026
7fb734e
test(tui): cover partial host control writes
lawrencecchen Jul 26, 2026
b895624
fix(tui): preserve ambiguous host control delivery
lawrencecchen Jul 26, 2026
058a60d
test(tui): cover ambiguous clear lane safety
lawrencecchen Jul 26, 2026
6b60c61
fix(tui): quarantine ambiguous input lanes
lawrencecchen Jul 26, 2026
de7d681
test(tui): cover graceful drain and rejection certainty
lawrencecchen Jul 27, 2026
75800fb
fix(tui): drain accepted relay requests safely
lawrencecchen Jul 27, 2026
abae547
test(tui): cover input lane session ownership
lawrencecchen Jul 27, 2026
de979f4
fix(tui): scope input lanes to session generation
lawrencecchen Jul 27, 2026
23cb08c
test(tui): cover dead keys and press recovery
lawrencecchen Jul 27, 2026
f6c7fc8
fix(tui): preserve composition and recovery releases
lawrencecchen Jul 27, 2026
19927b5
docs(tui): clarify relay response ordering
lawrencecchen Jul 27, 2026
df49796
test(tui): cover local press recovery release
lawrencecchen Jul 27, 2026
6a75b65
fix(tui): retain ambiguous press recovery release
lawrencecchen Jul 27, 2026
6bb7e5a
test(tui): cover clear admission and input localization
lawrencecchen Jul 27, 2026
0406eb7
fix(tui): admit runnable clears and localize input failures
lawrencecchen Jul 27, 2026
691b11b
test(tui): preserve real alt without associated text
lawrencecchen Jul 27, 2026
5424ada
fix(tui): resolve ambiguous option input explicitly
lawrencecchen Jul 27, 2026
fb1c6b8
test(tui): cover saturated clear lane ordering
lawrencecchen Jul 27, 2026
663d588
fix(tui): preserve clear lane order at worker cap
lawrencecchen Jul 27, 2026
6378183
test(tui): cover clear fallback lifecycle gaps
lawrencecchen Jul 27, 2026
4330382
fix(tui): close clear fallback lifecycle gaps
lawrencecchen Jul 27, 2026
b36318c
test(tui): expose legacy clear fallback capability
lawrencecchen Jul 27, 2026
682a732
fix(tui): gate clear shortcut by active surface
lawrencecchen Jul 27, 2026
cee98aa
test(tui): cover blocking clear barrier bypass
lawrencecchen Jul 27, 2026
4a23c0c
fix(tui): fence blocking clear barrier bypasses
lawrencecchen Jul 27, 2026
88a148c
test(tui): require lock-free clear capability reads
lawrencecchen Jul 27, 2026
556ce70
fix(tui): cache clear capability outside writer lock
lawrencecchen Jul 27, 2026
2a07832
test(tui): require localized option mode warning
lawrencecchen Jul 27, 2026
7ad3afc
fix(tui): localize invalid option mode warning
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
4 changes: 4 additions & 0 deletions .github/workflows/cmux-tui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ jobs:
working-directory: cmux-tui
run: cargo test --workspace --locked

- name: crossterm parser tests
working-directory: cmux-tui
run: cargo test --manifest-path vendor/crossterm/Cargo.toml --lib
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

- name: TUI smoke test (scripted pty)
working-directory: cmux-tui
run: |
Expand Down
9 changes: 6 additions & 3 deletions Sources/ControlSurfaceResumeTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,12 @@ extension TerminalController {
content.apply(to: alert, presentingWindow: nil)

switch alert.runModal() {
case .alertFirstButtonReturn: .auto
case .alertSecondButtonReturn: .prompt
default: .manual
case .alertFirstButtonReturn:
return .auto
case .alertSecondButtonReturn:
return .prompt
default:
return .manual
}
}

Expand Down
1 change: 1 addition & 0 deletions Sources/DockSplitStore+RestoredAgentLifecycle.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxWorkspaces
import Foundation

extension DockSplitStore {
Expand Down
14 changes: 8 additions & 6 deletions Sources/DockSplitStore+SessionSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ extension DockSplitStore {
)
}
let persistedPanelIds = Set(panelSnapshots.map(\.id))
let sourceWorkspaceIdsByPanelId = Dictionary(uniqueKeysWithValues: panelSnapshots.compactMap {
panel in
guard let transfer = detachedSurfaceTransfersByPanelId[panel.id] else { return nil }
return (panel.id, transfer.sessionRestoreWorkspaceId)
})
let sourceWorkspaceIdsByPanelId: [UUID: UUID] = Dictionary(
uniqueKeysWithValues: panelSnapshots.compactMap {
panel in
guard let transfer = detachedSurfaceTransfersByPanelId[panel.id] else { return nil }
return (panel.id, transfer.sessionRestoreWorkspaceId)
}
)
let layout = layoutCodec.pruned(
rawLayout,
keeping: persistedPanelIds
Expand Down Expand Up @@ -329,7 +331,7 @@ extension DockSplitStore {
let expectedSessionId = resumeBinding?.isAgentHookBinding == true
? resumeBinding?.checkpointId
: restorableAgent?.sessionId
let relevantObservation = observation.flatMap { entry in
let relevantObservation: RestorableAgentSessionIndex.Entry? = observation.flatMap { entry in
guard entry.snapshot.kind == expectedKind, entry.snapshot.sessionId == expectedSessionId else {
return nil
}
Expand Down
2 changes: 0 additions & 2 deletions cmux-tui/Cargo.lock

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

7 changes: 7 additions & 0 deletions cmux-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"crates/cmux-tui-machine-protocol",
"crates/cmux-tui",
]
exclude = ["vendor/crossterm"]

[workspace.package]
edition = "2024"
Expand Down Expand Up @@ -54,6 +55,12 @@ fs4 = "1.1.0"
sha2 = "0.10"
zeroize = "1.9"

[patch.crates-io]
# Crossterm 0.29 discards Kitty's shifted key, base-layout key, and associated
# text while parsing CSI-u input. cmux needs those fields to preserve keyboard
# layouts across its shortcut layer and nested terminals.
crossterm = { path = "vendor/crossterm" }

[profile.release]
lto = "thin"

Expand Down
68 changes: 68 additions & 0 deletions cmux-tui/bindings/conformance/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,74 @@
}
]
},
{
"name": "clear-history-round-trip",
"requires": { "commands": ["clear-history"] },
"steps": [
{
"type": "command",
"request": { "id": 1, "cmd": "new-workspace", "name": "conf-clear-history", "cols": 80, "rows": 24 },
"expect": { "id": 1, "ok": true },
"match": "partial",
"bind": { "surface0": "data.surface" }
},
{
"type": "command",
"request": { "id": 2, "cmd": "clear-history", "surface": "$surface0" },
"expect": { "id": 2, "ok": true, "data": {} },
"match": "exact"
}
]
},
{
"name": "clear-history-key-fallback-round-trip",
"requires": {
"commands": ["clear-history"],
"capabilities": ["clear-history-v1", "clear-history-key-v1"]
},
"steps": [
{
"type": "command",
"request": { "id": 1, "cmd": "new-workspace", "name": "conf-clear-history-key", "cols": 80, "rows": 24 },
"expect": { "id": 1, "ok": true },
"match": "partial",
"bind": { "surface0": "data.surface" }
},
{
"type": "command",
"request": {
"id": 2,
"cmd": "clear-history",
"surface": "$surface0",
"fallback_key": {
"key": "k",
"mods": {
"shift": false,
"control": false,
"alt": false,
"super": true,
"caps_lock": false,
"num_lock": false
},
"consumed_mods": {
"shift": false,
"control": false,
"alt": false,
"super": false,
"caps_lock": false,
"num_lock": false
},
"utf8": "",
"unshifted_codepoint": "k",
"action": "press",
"macos_option_as_alt": true
}
},
"expect": { "id": 2, "ok": true, "data": {} },
"match": "exact"
}
]
},
{
"name": "overflow-event-schema",
"steps": [
Expand Down
38 changes: 35 additions & 3 deletions cmux-tui/bindings/conformance/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@

sys.path.insert(0, str(PYTHON_BINDING))

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


Expand Down Expand Up @@ -184,17 +190,25 @@ def execute_command(client: CmuxClient, request: Dict[str, Any]) -> Dict[str, An

def check_requires(fixture: Dict[str, Any], socket_path: str) -> None:
commands = fixture.get("requires", {}).get("commands", [])
if not commands:
capabilities = fixture.get("requires", {}).get("capabilities", [])
if not commands and not capabilities:
return
unsupported: List[str] = []
with CmuxClient(socket_path=socket_path, timeout=3.0) as client:
if capabilities:
advertised = set(client.identify().capabilities)
unsupported.extend(
f"capability:{capability}"
for capability in capabilities
if capability not in advertised
)
for command in commands:
response = client.request(command)
error = str(response.get("error", ""))
if response.get("ok") is False and "unknown variant" in error:
unsupported.append(command)
if unsupported:
raise FixtureSkipped(f"server lacks required command(s): {', '.join(unsupported)}")
raise FixtureSkipped(f"server lacks required feature(s): {', '.join(unsupported)}")


def dispatch(client: CmuxClient, cmd: str, params: Dict[str, Any]) -> Any:
Expand All @@ -208,6 +222,10 @@ def dispatch(client: CmuxClient, cmd: str, params: Dict[str, Any]) -> Any:
"export-layout": client.export_layout,
"apply-layout": client.apply_layout,
"send": lambda **kw: client.send(kw["surface"], text=kw.get("text"), bytes_data=kw.get("bytes")),
"clear-history": lambda **kw: client.clear_history(
kw["surface"],
fallback_key=terminal_key_input(kw.get("fallback_key")),
),
"read-screen": client.read_screen,
"vt-state": client.vt_state,
"new-tab": client.new_tab,
Expand Down Expand Up @@ -253,6 +271,20 @@ def dispatch(client: CmuxClient, cmd: str, params: Dict[str, Any]) -> Any:
return mapping[cmd](**params)


def terminal_key_input(value: Any) -> Optional[TerminalKeyInput]:
if value is None:
return None
return TerminalKeyInput(
key=value["key"],
mods=TerminalModifiers.from_wire(value["mods"]),
consumed_mods=TerminalModifiers.from_wire(value["consumed_mods"]),
utf8=value["utf8"],
unshifted_codepoint=value.get("unshifted_codepoint"),
action=value.get("action"),
macos_option_as_alt=value["macos_option_as_alt"],
)


def result_to_data(result: Any) -> Any:
if dataclasses.is_dataclass(result):
data = dataclasses.asdict(result)
Expand Down
3 changes: 3 additions & 0 deletions cmux-tui/bindings/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ fmt.Println(screen.Text)
`NewClient` uses `CMUX_TUI_SOCKET` when set, then legacy `CMUX_MUX_SOCKET`, then
the default session socket path.

`ClearHistoryWithFallback` accepts a `TerminalKeyInput` and requires the server
capability `clear-history-key-v1`.

## E2E

```bash
Expand Down
28 changes: 28 additions & 0 deletions cmux-tui/bindings/go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,34 @@ func (c *Client) Send(ctx context.Context, surface uint64, opts SendOptions) err
return c.request(ctx, "send", params, nil)
}

func (c *Client) ClearHistory(ctx context.Context, surface uint64) error {
if err := c.requireCapability(ctx, "clear-history-v1", "clear-history"); err != nil {
return err
}
return c.request(ctx, "clear-history", map[string]any{"surface": surface}, nil)
}

func (c *Client) ClearHistoryWithFallback(
ctx context.Context,
surface uint64,
fallbackKey TerminalKeyInput,
) error {
if err := c.requireCapability(ctx, "clear-history-v1", "clear-history"); err != nil {
return err
}
if err := c.requireCapability(
ctx,
"clear-history-key-v1",
"clear-history key fallback",
); err != nil {
return err
}
return c.request(ctx, "clear-history", map[string]any{
"surface": surface,
"fallback_key": fallbackKey,
}, nil)
}

func (c *Client) ReadScreen(ctx context.Context, surface uint64) (ReadScreenResult, error) {
var result ReadScreenResult
return result, c.request(ctx, "read-screen", map[string]any{"surface": surface}, &result)
Expand Down
106 changes: 106 additions & 0 deletions cmux-tui/bindings/go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,112 @@ func TestSetSplitRatioRejectsServersOlderThanProtocolEight(t *testing.T) {
}
}

func TestClearHistoryRequiresCapability(t *testing.T) {
protocol := uint32(9)
client := &Client{protocol: &protocol}
err := client.ClearHistory(context.Background(), 7)
if err == nil || !errors.Is(err, ErrProtocolMismatch) {
t.Fatalf("ClearHistory() error = %v, want protocol mismatch", err)
}
}

func TestClearHistorySendsCapabilityGatedWireCommand(t *testing.T) {
clientConn, serverConn := net.Pipe()
defer serverConn.Close()
protocol := uint32(9)
client := &Client{
timeout: time.Second,
conn: &jsonLineConn{conn: clientConn, reader: bufio.NewReader(clientConn)},
protocol: &protocol,
capabilities: map[string]struct{}{"clear-history-v1": {}},
}
defer client.Close()

requests := make(chan map[string]any, 1)
go func() {
decoder := json.NewDecoder(serverConn)
encoder := json.NewEncoder(serverConn)
var request map[string]any
if decoder.Decode(&request) != nil {
return
}
requests <- request
_ = encoder.Encode(map[string]any{"id": request["id"], "ok": true, "data": map[string]any{}})
}()

if err := client.ClearHistory(context.Background(), 7); err != nil {
t.Fatalf("ClearHistory() error = %v", err)
}
request := <-requests
if request["cmd"] != "clear-history" || request["surface"] != float64(7) {
t.Fatalf("ClearHistory() request = %#v", request)
}
}

func TestClearHistoryFallbackRequiresCapabilityAndPreservesKey(t *testing.T) {
protocol := uint32(9)
codepoint := "k"
action := TerminalKeyPress
fallback := TerminalKeyInput{
Key: TerminalKeyK,
Mods: TerminalModifiers{Super: true},
UnshiftedCodepoint: &codepoint,
Action: &action,
MacOSOptionAsAlt: true,
}
client := &Client{
protocol: &protocol,
capabilities: map[string]struct{}{"clear-history-v1": {}},
}
err := client.ClearHistoryWithFallback(context.Background(), 7, fallback)
if err == nil || !errors.Is(err, ErrProtocolMismatch) {
t.Fatalf("ClearHistoryWithFallback() error = %v, want protocol mismatch", err)
}

clientConn, serverConn := net.Pipe()
defer serverConn.Close()
client = &Client{
timeout: time.Second,
conn: &jsonLineConn{conn: clientConn, reader: bufio.NewReader(clientConn)},
protocol: &protocol,
capabilities: map[string]struct{}{
"clear-history-v1": {},
"clear-history-key-v1": {},
},
}
defer client.Close()

requests := make(chan map[string]any, 1)
go func() {
decoder := json.NewDecoder(serverConn)
encoder := json.NewEncoder(serverConn)
var request map[string]any
if decoder.Decode(&request) != nil {
return
}
requests <- request
_ = encoder.Encode(map[string]any{"id": request["id"], "ok": true, "data": map[string]any{}})
}()

if err := client.ClearHistoryWithFallback(context.Background(), 7, fallback); err != nil {
t.Fatalf("ClearHistoryWithFallback() error = %v", err)
}
request := <-requests
key, ok := request["fallback_key"].(map[string]any)
if !ok {
t.Fatalf("fallback_key = %#v", request["fallback_key"])
}
mods, ok := key["mods"].(map[string]any)
if !ok ||
key["key"] != "k" ||
mods["super"] != true ||
key["unshifted_codepoint"] != "k" ||
key["action"] != "press" ||
key["macos_option_as_alt"] != true {
t.Fatalf("ClearHistoryWithFallback() fallback_key = %#v", key)
}
}

func TestSetSplitRatioAcceptsNewerAdditiveProtocols(t *testing.T) {
protocol := uint32(9)
client := &Client{protocol: &protocol}
Expand Down
Loading