3eaad756
refactor: unify native layout planning before resize sprint
a73x 2026-09-05 09:22
Commit message
docs/native-sprint-workflow.md
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,86 @@ | |||
| 1 | # Native client sprint workflow | ||
| 2 | |||
| 3 | Use this formula for each native client sprint. The tiling | ||
| 4 | [spec](superpowers/specs/2026-09-05-native-tiling-design.md) defines behavior; | ||
| 5 | the [delivery plan](superpowers/plans/2026-09-05-native-tiling.md) breaks it into | ||
| 6 | packages. Keep one sprint active, ending in a working demo and explicit acceptance. | ||
| 7 | |||
| 8 | 1. **Review and scope.** Read the previous sprint's actions in [RETRO.md](../RETRO.md). | ||
| 9 | Define the smallest useful deliverable, its acceptance scenarios, and what | ||
| 10 | belongs to later sprints. Settle material interaction choices before UI work; | ||
| 11 | record proposed defaults as proposals until the user accepts them. | ||
| 12 | 2. **Tidy the starting point.** Inspect the code the sprint will touch. Make small, | ||
| 13 | behavior-preserving refactors that simplify the planned work, review them, | ||
| 14 | run affected checks and `make check`, and commit separately. No cleanup quota: | ||
| 15 | if the code is ready, record that. Substantial refactors become explicit | ||
| 16 | packages; unrelated debt goes into the backlog. | ||
| 17 | 3. **Assign a bounded pair.** Reuse one implementer and one independent adversarial | ||
| 18 | reviewer. Give them the package, relevant files, ownership boundaries, | ||
| 19 | acceptance criteria, and each other's names. Root owns integration, independent | ||
| 20 | acceptance, user communication, and commits. Delegate another task only when | ||
| 21 | it can progress independently. Load skills only where they help the work. | ||
| 22 | 4. **Build a functional slice.** Establish the smallest model/interface contract, | ||
| 23 | then connect actual input, rendering, and daemon behavior. Keep changes small | ||
| 24 | enough to review. Avoid expanding into later sprint features. | ||
| 25 | 5. **Resolve concrete review findings.** The reviewer supplies a failing scenario, | ||
| 26 | consequence, and expected result directly to the implementer. The implementer | ||
| 27 | returns a fix and evidence, or a reason the finding does not apply. Root | ||
| 28 | adjudicates disagreement against requirements and observed behavior. Agreement | ||
| 29 | is not a substitute for independent validation. | ||
| 30 | 6. **Validate through real boundaries.** Drive ordinary SDL events into an isolated | ||
| 31 | GUI using `test/native_tiling.py`'s Rig. Compare actual framebuffer pixels and | ||
| 32 | independently queried daemon state, including every pane's PTY dimensions. | ||
| 33 | Exercise failure paths and event ordering. Observation helpers must not repair | ||
| 34 | the state under test. Use real Wayland at high DPI and a scale transition; | ||
| 35 | exercise SSH/QUIC when changes affect their behavior. Create and clean up only | ||
| 36 | owned fixture sessions and daemons. | ||
| 37 | 7. **Refactor the integrated result.** Reserve this as work, even after the opening | ||
| 38 | cleanup. Remove duplication, superseded paths, unnecessary state, and temporary | ||
| 39 | scaffolding exposed by implementation. Review the result again. For retained | ||
| 40 | debt, record location, consequence, reason, and next owner or trigger. Correctness | ||
| 41 | failures cannot be deferred past acceptance. | ||
| 42 | 8. **Freeze and run final gates.** Stop source edits before compilation. Run the | ||
| 43 | required repository and native gates on the final result, retaining logs: | ||
| 44 | |||
| 45 | ```sh | ||
| 46 | export ZIG_GLOBAL_CACHE_DIR=/tmp/muxg-zig-cache | ||
| 47 | make check | ||
| 48 | make ci | ||
| 49 | deps/zig/zig build native native-test -Doptimize=ReleaseSafe | ||
| 50 | make native-e2e | ||
| 51 | ``` | ||
| 52 | |||
| 53 | `make ci` excludes native tests. Use the pinned compiler. A sandbox denial is | ||
| 54 | not a product failure: run required local socket/PTY checks with the appropriate | ||
| 55 | authorized permissions. Rerun affected gates after meaningful changes; do not | ||
| 56 | repeatedly run broad suites without a new reason. | ||
| 57 | 9. **Record and share the actual demo.** Use Rig with `MUXG_VIDEODRIVER=wayland` | ||
| 58 | and the current compositor environment. Create fixtures, focus the owned GUI, | ||
| 59 | and drive the same user controls being delivered. Capture the actual compositor | ||
| 60 | window continuously; on Sway, find its rectangle by PID with `swaymsg -t get_tree` | ||
| 61 | and pipe repeated `grim -g GEOMETRY -s 1 -t ppm -` frames to ffmpeg. Encode H.264, | ||
| 62 | `yuv420p`, even dimensions, and `+faststart`, without audio. Stop recording | ||
| 63 | before closing the window so another application cannot enter the recording. | ||
| 64 | A slideshow of screenshots is useful evidence but is not a screen recording. | ||
| 65 | |||
| 66 | For a user on the same tailnet, serve only the demo page/video through a loopback | ||
| 67 | HTTP server with byte-range support, then use Tailscale Serve: | ||
| 68 | |||
| 69 | ```sh | ||
| 70 | tailscale serve status | ||
| 71 | tailscale serve --bg http://127.0.0.1:18765 | ||
| 72 | ``` | ||
| 73 | |||
| 74 | Check existing routes before changing them; preserve unrelated services. Use | ||
| 75 | the private HTTPS URL printed by Serve. Verify playback/HTTP range responses, | ||
| 76 | retain the recording outside temporary fixture directories, and record the | ||
| 77 | server's teardown command. Funnel and audio are unnecessary for this workflow. | ||
| 78 | 10. **Close honestly.** Inspect the final diff, commit validated work, and provide | ||
| 79 | the demo, controls, test results, and material limits. User demo acceptance is | ||
| 80 | distinct from passing automation. Update the spec/plan status and RETRO.md with | ||
| 81 | resolved findings and checkable next-sprint actions. Mark acceptance only when | ||
| 82 | given; a recorded-demo approval does not imply hands-on feedback. Then begin | ||
| 83 | the next authorized sprint with step 1. | ||
| 84 | |||
| 85 | Sprint 2 established the recorded Sway/private Serve demo route. Sprint 3 adds the | ||
| 86 | opening cleanup pass by user request; keep the closing cleanup as well. | ||
docs/superpowers/plans/2026-09-05-native-tiling.md
| Old | New | ||
|---|---|---|---|
| @@ -1,10 +1,14 @@ | |||
| 1 | # Native tiling — delegated delivery plan | 1 | # Native tiling — delegated delivery plan |
| 2 | 2 | ||
| 3 | Design authority: [native tiling spec](../specs/2026-09-05-native-tiling-design.md). | 3 | Design authority: [native tiling spec](../specs/2026-09-05-native-tiling-design.md). |
| 4 | Status: Sprint 2 complete; implementation, review, validation, and user demo acceptance passed. | 4 | Status: Sprint 2 accepted; Sprint 3 underway, beginning with reviewed cleanup. |
| 5 | 5 | ||
| 6 | ## Working agreement | 6 | ## Working agreement |
| 7 | 7 | ||
| 8 | Follow the reusable [sprint workflow](../../native-sprint-workflow.md), including | ||
| 9 | a bounded pre-sprint cleanup pass and separate validated cleanup commit before | ||
| 10 | feature changes. The closing refactoring package remains mandatory. | ||
| 11 | |||
| 8 | Keep one sprint active. The root agent owns integration, user updates, acceptance | 12 | Keep one sprint active. The root agent owns integration, user updates, acceptance |
| 9 | evidence, and commits. Assign one implementer and one independent adversarial | 13 | evidence, and commits. Assign one implementer and one independent adversarial |
| 10 | reviewer by default. Reuse those agents across packages; do not spawn a new agent | 14 | reviewer by default. Reuse those agents across packages; do not spawn a new agent |
src/gui/workspace.zig
| Old | New | ||
|---|---|---|---|
| @@ -136,6 +136,14 @@ const Tree = struct { | |||
| 136 | .empty => unreachable, | 136 | .empty => unreachable, |
| 137 | }; | 137 | }; |
| 138 | } | 138 | } |
| 139 | fn layout(self: *const Tree, width: u32, height: u32, m: Metrics) Layout { | ||
| 140 | var out: Layout = .{}; | ||
| 141 | if (self.root) |root| { | ||
| 142 | const min = self.minimum(root, m); | ||
| 143 | self.flatten(root, .{ .w = @max(width, min.w), .h = @max(height, min.h) }, .{ .w = width, .h = height }, m, &out); | ||
| 144 | } | ||
| 145 | return out; | ||
| 146 | } | ||
| 139 | fn flatten(self: *const Tree, at: u8, rect: Rect, viewport: Rect, m: Metrics, out: *Layout) void { | 147 | fn flatten(self: *const Tree, at: u8, rect: Rect, viewport: Rect, m: Metrics, out: *Layout) void { |
| 140 | switch (self.nodes[at]) { | 148 | switch (self.nodes[at]) { |
| 141 | .leaf => |id| { | 149 | .leaf => |id| { |
| @@ -204,12 +212,7 @@ pub const Workspace = struct { | |||
| 204 | return null; | 212 | return null; |
| 205 | } | 213 | } |
| 206 | pub fn layout(self: *Workspace, width: u32, height: u32, m: Metrics) Layout { | 214 | pub fn layout(self: *Workspace, width: u32, height: u32, m: Metrics) Layout { |
| 207 | var out: Layout = .{}; | 215 | return self.tab().tree.layout(width, height, m); |
| 208 | if (self.tab().tree.root) |root| { | ||
| 209 | const min = self.tab().tree.minimum(root, m); | ||
| 210 | self.tab().tree.flatten(root, .{ .w = @max(width, min.w), .h = @max(height, min.h) }, .{ .w = width, .h = height }, m, &out); | ||
| 211 | } | ||
| 212 | return out; | ||
| 213 | } | 216 | } |
| 214 | pub fn arm(self: *Workspace, direction: Direction) void { | 217 | pub fn arm(self: *Workspace, direction: Direction) void { |
| 215 | if (self.tab().focus) |id| self.tab().pending = .{ .pane = id, .direction = direction }; | 218 | if (self.tab().focus) |id| self.tab().pending = .{ .pane = id, .direction = direction }; |
| @@ -255,9 +258,7 @@ pub const Workspace = struct { | |||
| 255 | var tree = t.tree; | 258 | var tree = t.tree; |
| 256 | const id = self.next_pane_id; | 259 | const id = self.next_pane_id; |
| 257 | try tree.insert(origin, id, direction); | 260 | try tree.insert(origin, id, direction); |
| 258 | var flat: Layout = .{}; | 261 | const flat = tree.layout(width, height, m); |
| 259 | const min = tree.minimum(tree.root.?, m); | ||
| 260 | tree.flatten(tree.root.?, .{ .w = @max(width, min.w), .h = @max(height, min.h) }, .{ .w = width, .h = height }, m, &flat); | ||
| 261 | return .{ .tree = tree, .placement = flat.get(id).? }; | 262 | return .{ .tree = tree, .placement = flat.get(id).? }; |
| 262 | } | 263 | } |
| 263 | /// Call immediately after preparing the attachment; no other model edits | 264 | /// Call immediately after preparing the attachment; no other model edits |