a73x

d903a815

fix: align the native command prefix with mux

a73x   2026-09-05 07:47

Commit message
fix: align the native command prefix with mux

README.md
Old New
@@ -54,7 +54,7 @@ The first native tiling sprint supports one staged second target:
54 # Local second daemon: --next-target '--sock /tmp/second.sock' --next-session logs 54 # Local second daemon: --next-target '--sock /tmp/second.sock' --next-session logs
55 ``` 55 ```
56 56
57 Press **Ctrl+Shift+Space**, then **v** for side by side or **b** for above/below. 57 Press `Ctrl+\`, then **v** for side by side or **b** for above/below.
58 This shows the intended split. Press the prefix again, then **Enter**, to insert 58 This shows the intended split. Press the prefix again, then **Enter**, to insert
59 the staged target. Cancellation creates no second pane or session. The split stays 59 the staged target. Cancellation creates no second pane or session. The split stays
60 on the pane where it was armed, even if focus moves before insertion. 60 on the pane where it was armed, even if focus moves before insertion.
RETRO.md
Old New
@@ -0,0 +1,60 @@
1 # Sprint retrospectives
2
3 ## Native tiling — Sprint 1, 2026-09-05
4
5 Delivered two independent terminal panes, split selection, keyboard/mouse focus,
6 per-pane PTY sizing, and failure isolation. Refactoring and adversarial review
7 completed before acceptance. Native integration, 200% Wayland, and real SSH/QUIC
8 tests passed; the working demo is ready for ergonomic trial.
9
10 ### What worked
11
12 - Two real panes exercised ownership, rendering, input, and failure isolation
13 before adding picker complexity. The deliverable was concrete and testable.
14 - Reusing one implementer and one adversarial reviewer kept responsibility clear.
15 Review found command-repeat leakage and the PTY signal-handling requirement for
16 testing Ctrl+\ passthrough.
17 - Mandatory cleanup consolidated geometry and ownership before final validation.
18 - Automated checks established correctness; the user's trial exposed ergonomic
19 questions that those checks could not settle.
20
21 ### What to change
22
23 - The implementation chose Ctrl+Shift+Space without explicitly settling the
24 prefix with the user. The trial selected `Ctrl+\` for consistency with terminal
25 mux. Nesting mux is not a design requirement. Make unresolved interaction
26 choices explicit before implementing them.
27 - Explain temporary demo controls at handoff. `--next-target` and `--next-session`
28 stage one host/session pair; they are scaffolding until the picker exists.
29 - Keep delegation bounded: reuse the pair, assign narrow ownership, and exchange
30 concrete findings. Avoid repeated broad reviews or automatic skill ceremony.
31
32 ### Actions for Sprint 2
33
34 Owner: the sprint lead, with implementation and review delegated as appropriate.
35 At the end of Sprint 2, mark completed items and record evidence or carry-forward
36 reasons below. Passing a test does not by itself complete an ergonomic action.
37
38 - [ ] Settle unresolved picker interactions before implementation. Record the
39 agreed keys, back/cancel behavior, and existing/new session flow in the spec.
40 - [ ] Demo host → existing/new session → insertion with the user. Verify that
41 the picker supplies the target without requiring staged CLI flags, and capture
42 the user's feedback on direction selection, cancellation, and focus.
43 - [ ] Make connection setup and discovery cancellable. Verify stalled dialing,
44 slow replies, cancellation, shutdown, and stale results after target changes;
45 inherited synchronous Unix connect and DNS resolution need explicit treatment.
46 - [ ] Resolve attach-existing versus create semantics. Test a session vanishing
47 between listing and attachment, and document behavior with older daemons.
48 - [ ] Keep the implementation/reviewer pair bounded and complete the refactoring
49 package before final acceptance. Record concrete findings resolved, duplicated
50 or obsolete code removed, and any remaining debt with its next owner/package.
51
52 ### Sprint 2 follow-up
53
54 To complete at the next retrospective:
55
56 - Which actions were completed, and what evidence demonstrates that?
57 - Which actions remain open, why, and where will they be addressed?
58 - Did the user trial expose another assumption that should have been settled
59 before implementation?
60 - Did delegation and cleanup improve the result enough to justify their cost?
docs/superpowers/plans/2026-09-05-native-tiling.md
Old New
@@ -131,7 +131,7 @@ This is a usable substitute for session selection in sprint 1;
131 sprint 2 replaces target acquisition with the picker while retaining insertion. 131 sprint 2 replaces target acquisition with the picker while retaining insertion.
132 The target should be selectable from real CLI arguments, not only a test FIFO. 132 The target should be selectable from real CLI arguments, not only a test FIFO.
133 133
134 Implement Ctrl+Shift+Space command mode, visible pending direction, `h/j/k/l` 134 Implement Ctrl+\ command mode, visible pending direction, `h/j/k/l`
135 and arrow focus, click focus, cancellation, and prefix passthrough. Consume both 135 and arrow focus, click focus, cancellation, and prefix passthrough. Consume both
136 key events and associated SDL text events correctly: commands must not leak bytes 136 key events and associated SDL text events correctly: commands must not leak bytes
137 into a terminal or suppress subsequent ordinary text. Window resize remains live. 137 into a terminal or suppress subsequent ordinary text. Window resize remains live.
@@ -269,7 +269,9 @@ Validation completed:
269 Unix/Unix and Unix/QUIC pane pairs are covered. The measured quiet-pane input 269 Unix/Unix and Unix/QUIC pane pairs are covered. The measured quiet-pane input
270 reached an actual painted frame in 81 ms during a neighbouring flood; window 270 reached an actual painted frame in 81 ms during a neighbouring flood; window
271 p99 was 72 microseconds against the 20 ms budget. Prefix passthrough produced 271 p99 was 72 microseconds against the 20 ms budget. Prefix passthrough produced
272 exactly NUL followed by A through a real PTY (`od`: `00 41`). 272 exactly NUL followed by A through a real PTY (`od`: `00 41`) with the original
273 trial prefix. The ergonomic follow-up uses `Ctrl+\`; its repeated prefix sends
274 `0x1c`, verified with `od` as `1c 41` with PTY signal processing disabled.
273 - Native Wayland: 1698×2760 framebuffer at 849×1380 logical size, 200% density; 275 - Native Wayland: 1698×2760 framebuffer at 849×1380 logical size, 200% density;
274 both daemon sizes, independent coloured pixels, and a non-origin pane click 276 both daemon sizes, independent coloured pixels, and a non-origin pane click
275 converted from physical pixels to logical input coordinates passed. 277 converted from physical pixels to logical input coordinates passed.
@@ -296,6 +298,16 @@ running. Logs: `/tmp/muxg-tiling-real-remote.log`; local frame artifacts:
296 Sprint commit: `feat: add independent native terminal panes`. The next gate is 298 Sprint commit: `feat: add independent native terminal panes`. The next gate is
297 the user's ergonomic trial before Sprint 2 picker work. 299 the user's ergonomic trial before Sprint 2 picker work.
298 300
301 Ergonomic follow-up: the user selected `Ctrl+\` to match terminal mux. Updated
302 the handler, event hook, documentation, and existing passthrough checks; the
303 implementer and reviewer agreed with no outstanding findings. ReleaseSafe native
304 tests (25), native integration checkpoints (10 + 14), and `make ci` passed.
305 The first CI attempt hit an intermittent existing CLI pager-wheel assertion;
306 the isolated mouse group and full rerun passed without CLI changes. Logs:
307 `/tmp/muxg-prefix-native.log`, `/tmp/muxg-prefix-e2e.log`, and
308 `/tmp/muxg-prefix-ci-recheck.log`. Sprint lessons and checkable next-sprint
309 actions live separately in [RETRO.md](../../../RETRO.md).
310
299 Retained debt for package 2A: transport opening still has inherited synchronous 311 Retained debt for package 2A: transport opening still has inherited synchronous
300 Unix connect and DNS resolution before cancellable waits. Broadcast cancellation 312 Unix connect and DNS resolution before cancellable waits. Broadcast cancellation
301 improves multi-pane shutdown but does not make those stages interruptible. Extend 313 improves multi-pane shutdown but does not make those stages interruptible. Extend
docs/superpowers/specs/2026-09-05-native-tiling-design.md
Old New
@@ -83,8 +83,9 @@ Initially, split the armed leaf into two equally sized children. Other panes
83 retain their allocated space. Repeated insertion semantics can be adjusted after 83 retain their allocated space. Repeated insertion semantics can be adjusted after
84 the first ergonomic trial; do not silently rebalance unrelated branches. 84 the first ergonomic trial; do not silently rebalance unrelated branches.
85 85
86 Keyboard prefix for the ergonomic trial: **Ctrl+Shift+Space**. It avoids Sway's usual Super 86 Keyboard prefix for the ergonomic trial: `Ctrl+\`, matching terminal mux.
87 bindings and the existing terminal mux prefix, including mux nested in a pane. 87 Press it twice to forward `Ctrl+\` to a terminal application. The native client
88 attaches directly to daemon sessions; nesting terminal mux is not a design requirement.
88 The prefix opens a small command hint strip; its next key is consumed by the GUI. 89 The prefix opens a small command hint strip; its next key is consumed by the GUI.
89 90
90 | After prefix | Action | 91 | After prefix | Action |
src/gui/frame.zig
Old New
@@ -94,7 +94,7 @@ pub fn parseHook(line: []const u8) ?Hook {
94 if (std.mem.startsWith(u8, line, "text:")) return .{ .text = line["text:".len..] }; 94 if (std.mem.startsWith(u8, line, "text:")) return .{ .text = line["text:".len..] };
95 if (std.mem.startsWith(u8, line, "key:")) { 95 if (std.mem.startsWith(u8, line, "key:")) {
96 const name = line["key:".len..]; 96 const name = line["key:".len..];
97 if (std.mem.eql(u8, name, "prefix")) return .{ .key = .{ .code = c.SDLK_SPACE, .mods = c.SDL_KMOD_CTRL | c.SDL_KMOD_SHIFT } }; 97 if (std.mem.eql(u8, name, "prefix")) return .{ .key = .{ .code = c.SDLK_BACKSLASH, .mods = c.SDL_KMOD_CTRL } };
98 if (name.len == 1 and std.mem.indexOfScalar(u8, "hjklvb", name[0]) != null) return .{ .key = .{ .code = name[0] } }; 98 if (name.len == 1 and std.mem.indexOfScalar(u8, "hjklvb", name[0]) != null) return .{ .key = .{ .code = name[0] } };
99 inline for (.{ .{ "enter", c.SDLK_RETURN }, .{ "tab", c.SDLK_TAB }, .{ "escape", c.SDLK_ESCAPE }, .{ "backspace", c.SDLK_BACKSPACE }, .{ "up", c.SDLK_UP }, .{ "down", c.SDLK_DOWN }, .{ "left", c.SDLK_LEFT }, .{ "right", c.SDLK_RIGHT } }) |pair| { 99 inline for (.{ .{ "enter", c.SDLK_RETURN }, .{ "tab", c.SDLK_TAB }, .{ "escape", c.SDLK_ESCAPE }, .{ "backspace", c.SDLK_BACKSPACE }, .{ "up", c.SDLK_UP }, .{ "down", c.SDLK_DOWN }, .{ "left", c.SDLK_LEFT }, .{ "right", c.SDLK_RIGHT } }) |pair| {
100 if (std.mem.eql(u8, name, pair[0])) return .{ .key = .{ .code = pair[1] } }; 100 if (std.mem.eql(u8, name, pair[0])) return .{ .key = .{ .code = pair[1] } };
@@ -308,7 +308,7 @@ const Events = struct {
308 self.suppress_text = true; 308 self.suppress_text = true;
309 return true; 309 return true;
310 } 310 }
311 const prefix = key == c.SDLK_SPACE and ev.key.mod & c.SDL_KMOD_CTRL != 0 and ev.key.mod & c.SDL_KMOD_SHIFT != 0 and ev.key.mod & (c.SDL_KMOD_ALT | c.SDL_KMOD_GUI | c.SDL_KMOD_MODE) == 0; 311 const prefix = key == c.SDLK_BACKSLASH and ev.key.mod & c.SDL_KMOD_CTRL != 0 and ev.key.mod & (c.SDL_KMOD_ALT | c.SDL_KMOD_GUI | c.SDL_KMOD_MODE) == 0;
312 if (prefix) { 312 if (prefix) {
313 self.suppress_text = true; 313 self.suppress_text = true;
314 if (!ev.key.repeat) { 314 if (!ev.key.repeat) {
@@ -748,12 +748,18 @@ test "command key text is consumed and subsequent ordinary text is not suppresse
748 var events: Events = .{ .rt = &rt, .win = undefined, .wake = &wake, .hook = null, .cache = undefined, .base_font_px = 16, .metrics = .{ .cell_w = 8, .cell_h = 16 }, .fb_w = 960, .fb_h = 600 }; 748 var events: Events = .{ .rt = &rt, .win = undefined, .wake = &wake, .hook = null, .cache = undefined, .base_font_px = 16, .metrics = .{ .cell_w = 8, .cell_h = 16 }, .fb_w = 960, .fb_h = 600 };
749 var event = std.mem.zeroes(c.SDL_Event); 749 var event = std.mem.zeroes(c.SDL_Event);
750 event.key.type = c.SDL_EVENT_KEY_DOWN; 750 event.key.type = c.SDL_EVENT_KEY_DOWN;
751 event.key.key = c.SDLK_SPACE; 751 event.key.key = c.SDLK_BACKSLASH;
752 event.key.mod = c.SDL_KMOD_CTRL | c.SDL_KMOD_SHIFT; 752 for ([_]u16{ 0, c.SDL_KMOD_SHIFT, c.SDL_KMOD_CTRL | c.SDL_KMOD_ALT, c.SDL_KMOD_CTRL | c.SDL_KMOD_GUI, c.SDL_KMOD_CTRL | c.SDL_KMOD_MODE }) |mods| {
753 event.key.mod = mods;
754 try std.testing.expect(try events.handle(event));
755 try std.testing.expect(!events.command_mode);
756 }
757 // Bare Ctrl activates the prefix; Shift is also accepted below.
758 event.key.mod = c.SDL_KMOD_CTRL;
753 try std.testing.expect(try events.handle(event)); 759 try std.testing.expect(try events.handle(event));
754 try std.testing.expect(events.command_mode and events.suppress_text); 760 try std.testing.expect(events.command_mode and events.suppress_text);
755 event.text.type = c.SDL_EVENT_TEXT_INPUT; 761 event.text.type = c.SDL_EVENT_TEXT_INPUT;
756 event.text.text = " "; 762 event.text.text = "\\";
757 try std.testing.expect(try events.handle(event)); 763 try std.testing.expect(try events.handle(event));
758 event = std.mem.zeroes(c.SDL_Event); 764 event = std.mem.zeroes(c.SDL_Event);
759 event.key.type = c.SDL_EVENT_KEY_DOWN; 765 event.key.type = c.SDL_EVENT_KEY_DOWN;
@@ -778,16 +784,16 @@ test "command key text is consumed and subsequent ordinary text is not suppresse
778 event.key.key = c.SDLK_X; 784 event.key.key = c.SDLK_X;
779 try std.testing.expect(try events.handle(event)); 785 try std.testing.expect(try events.handle(event));
780 try std.testing.expect(!events.suppress_text); 786 try std.testing.expect(!events.suppress_text);
781 // Repeated prefix exits mode; its shared keymap encoding is NUL. 787 // Repeated prefix exits mode; its shared keymap encoding is 0x1c.
782 // The integration fixture verifies the emitted byte count with a real PTY. 788 // The integration fixture verifies the emitted byte count with a real PTY.
783 event.key.key = c.SDLK_SPACE; 789 event.key.key = c.SDLK_BACKSLASH;
784 event.key.mod = c.SDL_KMOD_CTRL | c.SDL_KMOD_SHIFT; 790 event.key.mod = c.SDL_KMOD_CTRL | c.SDL_KMOD_SHIFT;
785 try std.testing.expect(try events.handle(event)); 791 try std.testing.expect(try events.handle(event));
786 try std.testing.expect(events.command_mode); 792 try std.testing.expect(events.command_mode);
787 try std.testing.expect(try events.handle(event)); 793 try std.testing.expect(try events.handle(event));
788 try std.testing.expect(!events.command_mode); 794 try std.testing.expect(!events.command_mode);
789 var bytes: [keymap.max_seq_len]u8 = undefined; 795 var bytes: [keymap.max_seq_len]u8 = undefined;
790 try std.testing.expectEqualSlices(u8, &.{0}, keymap.encode(keyEvent(c.SDLK_SPACE, event.key.mod).?, &bytes)); 796 try std.testing.expectEqualSlices(u8, &.{0x1c}, keymap.encode(keyEvent(c.SDLK_BACKSLASH, event.key.mod).?, &bytes));
791 } 797 }
792 798
793 test "later pane atlas growth precedes earlier pane UV generation" { 799 test "later pane atlas growth precedes earlier pane UV generation" {
test/native_tiling.py
Old New
@@ -321,12 +321,12 @@ def normal_scenario(rig, axis):
321 rig.ok("window resize, neighbour-safe clearing, and mouse focus work") 321 rig.ok("window resize, neighbour-safe clearing, and mouse focus work")
322 322
323 if axis == "v": 323 if axis == "v":
324 rig.shell("stty -echo -icanon min 1 time 0; printf 'RAW-%s\\n' READY; " 324 rig.shell("stty -echo -icanon -isig min 1 time 0; printf 'RAW-%s\\n' READY; "
325 "od -An -tx1 -N2; stty sane; printf 'RAW-%s\\n' DONE") 325 "od -An -tx1 -N2; stty sane; printf 'RAW-%s\\n' DONE")
326 rig.wait_marker(second, "right", "RAW-READY") 326 rig.wait_marker(second, "right", "RAW-READY")
327 rig.send("key:prefix", "key:prefix", "text:A") 327 rig.send("key:prefix", "key:prefix", "text:A")
328 rig.wait_marker(second, "right", "RAW-DONE") 328 rig.wait_marker(second, "right", "RAW-DONE")
329 require("00 41" in rig.dump(second, "right"), "prefix passthrough did not send one NUL then A") 329 require("1c 41" in rig.dump(second, "right"), "prefix passthrough did not send one 0x1c then A")
330 rig.ok("double prefix sends the terminal encoding exactly once; ordinary input follows") 330 rig.ok("double prefix sends the terminal encoding exactly once; ordinary input follows")
331 # A bounded flood with an external stop signal. During the frame-count 331 # A bounded flood with an external stop signal. During the frame-count
332 # interval only nonforcing state observations are allowed, never captures. 332 # interval only nonforcing state observations are allowed, never captures.