docs/superpowers/specs/2026-09-05-native-tiling-design.md
Ref: Size: 20.0 KiB History
# Native tiling — design and delivery plan
2026-09-05. Status: the native tiling milestone is implemented, reviewed, and
validated through Sprint 5; the user accepted the final recorded trial on
2026-09-05 (“Trial passes! Good work”). No changes were requested. Separate
Sprint 4 demo approval was not given; the user explicitly authorized advancing
to Sprint 5. Sprint 3 was accepted after its mouse-focused recording.
Execution: [delegated work packages and review gates](../plans/2026-09-05-native-tiling.md).
The [interaction ownership refactor](../plans/2026-09-05-native-interaction-debt.md)
keeps these behaviors: SDL adapts events in `frame.zig`; `interaction.zig` owns
mode transitions, menus and pending actions.
## Agreed requirements
- Sway-like directional splitting within a GUI window. Select horizontal or
vertical first; the layout changes only when a new pane is inserted.
- Insert through a host picker, followed by a session picker offering existing
sessions and creation of a new session.
- GUI layout persistence is independent of terminal mux's layout and pane choices.
- Both clients use the same daemons and session catalogue. Keep terminal mux's
interaction model separate; native-client learning may inform it later.
- Design pane ownership and persistence to accommodate tabs later. The initial
deliverable has one tab internally, without a tab bar or tab commands.
- Multiple hosts and pane resizing are required for the first useful release.
- Detaching a pane and ending its session are distinct actions.
- An unreachable host retains its pane and displays its state; it is recoverable.
- Rearranging panes is a stretch goal.
This design supersedes the single-session limit and native-pane prerequisite in
the 2026-09-04 native-client spec. It does not require migrating the terminal
wall and browser to a common controller before native tiling can ship.
## Workspace and future tabs
Ownership is explicit from the first sprint:
```text
GUI window: SDL/GL resources, shared font atlas, workspace
Workspace: ordered tabs, active tab ID
Tab: stable ID, split tree, focused pane ID, pending split
Pane: stable ID, target/session identity, connection and terminal grid
```
The window owns rendering resources; each tab owns its layout and panes. A pane
belongs to exactly one tab and owns its session attachment. Pane IDs are unique
within the workspace and independent of array positions, so asynchronous replies
cannot be routed by the active tab or a reused slot. Tab and pane actions take
explicit IDs. Cancellation generations also distinguish replaced attachments.
Only the active tab receives terminal input and supplies rectangles for drawing.
Future tab switching changes visibility, not session ownership: hidden panes keep
their connections and last valid PTY dimensions, without sending zero-sized
resizes. They continue applying bounded output updates without requiring window
repaints. Activation recalculates geometry at the current display scale and paints
the latest state. An exited session or failed connection belongs to its pane even
when its tab is inactive; it must not terminate the window or steal focus.
Closing a pane changes its tab's tree. An empty tab remains an Add pane surface.
Closing the window detaches every tab's panes. Future tab-close behavior will be
specified with the tab UI; do not make it an alias for ending remote sessions.
Keep global keyboard modes transient and cancel them on a future tab switch;
focus and pending insertion belong to their tab. Picker operations capture their
originating tab and pane and must never insert into whichever tab happens to be
active when a network reply arrives.
Implement only the ownership boundary needed for one tab now. Tab creation,
switching, reordering, and background-tab scheduling tests arrive with that feature;
they are not prerequisites for the two-pane deliverable.
## Proposed interaction defaults
The defaults below are proposals to trial, rather than additional requirements.
Use side-by-side and above/below in the interface: `v` puts the new pane below,
matching Sway's vertical split, while `b` puts the new pane beside the focus. Show the
pending direction at its originating pane so the orientation is unambiguous.
Direction selection arms the focused pane for one insertion, immediately opens
the host picker and changes no geometry. The picker captures that pane and direction. Host selection
opens its sessions; New session requests a name and creates it explicitly.
Only committing a session selection inserts the pane and focuses it. Cancelling
either level before committing creates no pane or session. Esc in sessions returns to hosts;
Esc in hosts dismisses the picker. Keep the pending direction visible until
insertion or explicit cancellation. Start with side-by-side as the default.
The pending split remains bound to its original pane if focus moves; its preview
stays there. Re-arming replaces that choice, and removing the pane cancels it.
Initially, split the armed leaf into two equally sized children. Other panes
retain their allocated space. Repeated insertion semantics can be adjusted after
the first ergonomic trial; do not silently rebalance unrelated branches.
Sprint 2 picker interactions (proposed before implementation; recorded demo
accepted by the user on 2026-09-05):
prefix + Enter opens hosts; arrows or `j/k` move selection, Enter chooses a row.
The shared host catalogue supplies hosts, with an Add host row opening a target
editor. Include the current pane's target even if it is absent from that catalogue.
The session list offers existing names and a New session row opening a name
editor. Editors accept text and Backspace; Enter confirms and Esc returns without
submitting. Escape from sessions returns to hosts; from hosts it closes the
picker, preserving the armed split. Escape outside the picker cancels that split.
The popup owns keyboard and mouse input while open; underlying panes keep painting.
Discovery errors, invalid selections, and creation refusals stay in the picker
and leave existing pane geometry unchanged. Choosing a listed existing session
commits its pane; a subsequent attachment failure remains visible in that pane,
following Sprint 1's lifecycle behavior. A successful creation reply also commits
insertion, followed by join-only attachment. If a resize or allocation failure
prevents insertion after creation succeeded, report the created session so the
user can select it again; never end it automatically.
Selecting an existing session uses a zero-size join-only attach, then claims
the pane size after admission. This policy also applies to reconnects and resyncs;
a vanished session is never recreated. Explicit creation uses a separate atomic
create-only request: a name collision is an error, never an attachment or resize.
An older daemon can still serve existing sessions, but may ignore creation requests.
A timeout does not distinguish that case from a lost reply. Once Enter submits
creation, cancellation or timeout may leave its outcome unknown; report that and
allow a fresh session listing, without automatic retry or destructive rollback.
Keyboard prefix for the ergonomic trial: `Ctrl+\`, matching terminal mux.
Press it twice to forward `Ctrl+\` to a terminal application. The native client
attaches directly to daemon sessions; nesting terminal mux is not a design requirement.
The prefix opens a small command hint strip; its next key is consumed by the GUI.
| After prefix | Action |
| --- | --- |
| `v` | Open host picker for above/below insertion (new pane below) |
| `b` | Open host picker for side-by-side insertion (new pane beside) |
| Enter | Open host picker for insertion |
| `h` / `j` / `k` / `l`, or arrows | Focus left / down / up / right |
| `r` | Enter resize mode; `h/j/k/l` or arrows resize, Esc/Enter exits |
| `d` | Detach and remove the focused pane |
| `x` | End the focused session through the daemon's existing end workflow |
| `p` | Open Pane actions (Retry, Choose session, Detach) for the focused pane |
| Esc | Cancel the command and pending split |
The split and movement bindings above reserve `h/j/k/l` for directional
movement, with `v` and `b` selecting split direction. A 2026-09-07 hands-on
trial aligned their meanings with Sway: `v` below and `b` beside.
The subsequent hands-on ergonomic trial makes `v`/`b` open host selection
immediately; a second prefix + Enter is unnecessary. Cancelling the host picker
retains the armed direction, and Esc outside the picker cancels it.
Pressing the prefix twice forwards its normal terminal keymap encoding once and
exits command mode. Ordinary input
goes only to the focused terminal; picker and resize-mode input never leaks to it.
Mouse click focuses a pane, and dragging a divider resizes it. Terminal mouse
reporting remains a separate feature. Pane headers identify host, session, focus,
and connection state; both detach and end actions must be clearly labelled.
### Sprint 3 resize trial
The following trial defaults were accepted through recorded-demo approval on
2026-09-05. Prefix + `r` enters a visibly labelled resize mode. `h`/Left and `l`/Right
move the focused pane's nearest ancestor vertical divider left/right by one cell
width; `k`/Up and `j`/Down move its nearest ancestor horizontal divider up/down by
one cell height. Direction moves the divider in screen coordinates regardless of
which child is focused. Repeated keys repeat the step. Enter/Esc exits; modal
keys and text do not reach the terminal. A missing divider on that axis is a no-op.
Mouse dividers have a six-logical-pixel total grab area, converted using the
window's drawable density. Overlaps choose the nearest divider centerline, then
the deeper divider on a tie. Dragging retains the pointer's initial offset so
pressing near a divider does not jump it. Releasing ends the drag. Focus loss or
a window/scale geometry change ends the drag while retaining its last applied
position. Capture must be released on every exit path.
Both paths update the same relative split weights and clamp against the recursive
minimum footprint. A leaf reserves two cell widths and two terminal rows plus
its header, matching the daemon's minimum accepted session size. Ordinary window
resizing clamps the displayed cut
without rewriting its stored proportion. A window smaller than the tree's minimum
footprint retains pane identities and weights and disables explicit divider changes
until there is room again. Every resulting pane content size is sent to its PTY.
Weights belong to the tabs-aware native model; saving them arrives in Sprint 4.
## Lifecycle and persistence
Closing the OS window saves the workspace and detaches all panes. It does not
end their sessions. Explicit pane detach removes that leaf from the saved layout;
its sibling fills the freed area. Closing the last pane leaves an empty tab
with an Add pane action. An ordinary shell exit leaves an exited pane with recovery
actions; it must not terminate the entire GUI or remove other hosts' panes. This
also applies to a one-pane GUI, superseding the viewer's shell-exit-code propagation.
End session uses the existing daemon request and its response, including its
existing handling of other attached clients. A failure to end keeps the pane and
shows the reason. Transport cancellation/GUI shutdown is not session termination:
the current session pump's `quit` and `detach` both stop the attachment.
Sprint 4 sends End on the pane's own attachment so the daemon excludes that
attachment from its other-client count. Refusal offers an explicit **End for all
clients** action with **Cancel** selected; no automatic force or retry of End.
A lost or timed-out reply reports an unknown outcome. Retry establishes a fresh
attachment before another End may be issued. Recovery and confirmation retain
their originating pane and attachment generation; confirmations identify the
session and target and cannot follow a later focus change to a different pane.
The chord-ergonomics trial shows a pending End in its originating pane header,
without a progress popup. Normal input and GUI controls remain available; repeat
End requests are blocked until the outcome arrives. Existing save-failure
warnings retain header priority, so they can temporarily hide the pending marker.
Use a separate, versioned GUI state file at
`$XDG_STATE_HOME/mux/native-workspace.json`. The initial schema has an ordered
`tabs` collection and `active_tab_id`, even while only one tab is exposed. Persist
each tab's stable ID, split tree, relative weights, panes, and focus. Pane records
hold stable IDs, explicit host/transport references, and resolved session names.
Pending commands and picker state are transient. Do not write terminal mux's
layout file. Save atomically after
committed changes; preserve unreadable state rather than overwriting it with an
empty workspace. Save divider changes when a drag finishes. A saved host reference
must survive removal from the shared host catalogue.
The version 1 document stores `active_tab_id`, `next_pane_id`, and one entry in
`tabs`. Each tab stores its ID, focus, owned pane targets/session names, and the
tree's root, nodes, relative weights, and next divider ID. Transient modes and
attachment state are excluded. Filesystem target references are made absolute
when saved. Unsupported versions/tabs, invalid identities, malformed trees, and
unreadable files disable writes for the run while allowing a fallback picker.
A sibling `.lock` file holds exclusive ownership across atomic JSON replacement.
State and temporary files use mode 0600; a newly created parent uses 0700.
No-argument `muxg` restores the GUI workspace; first launch presents the picker.
An explicit CLI target opens a temporary workspace without replacing the default
saved workspace. For this milestone, allow one writer for the default workspace;
a second no-argument launch should report that it is already open. Multiple named
workspaces and multiple persistent windows are deferred.
Unreachable panes retain their identity, position, and last available content,
with a clear offline indication. Keep other panes usable. Offer Retry and Choose
session in place, plus Detach. Use bounded background reconnect attempts. Missing
or exited sessions remain visible; restoration must not silently replace a missing
session with a newly created shell. Check the existing attach/create semantics
before implementing this guarantee and add a narrow protocol capability if needed.
The authorized [SSH follow-up](../plans/2026-09-05-ssh-error-classification.md)
adds a stop condition to automatic reconnect: a recognized SSH authentication
refusal retains the pane and its diagnostic but requires explicit Retry after
credentials are corrected. This applies during restoration and after connection
loss. An unavailable host or unclassified announcement EOF remains recoverable
through bounded-backoff reconnect; EOF alone never proves authentication refusal.
Confirmed: GUI and terminal clients can deliberately select the same daemon
session from a shared catalogue, while their layouts and automatic pane choices
remain independent. Sharing a session retains the daemon's existing resize and
attachment semantics; GUI layout isolation does not create a separate PTY.
## Implementation boundaries
Reuse `client.session_pump` per pane, `client.listSessions`, session creation and
`client.endSession`, plus existing target resolution and host catalogue support.
Discovery and lifecycle requests run off the SDL thread with cancellation and
generation checks so stale picker/retry results cannot mutate a replacement pane.
One unreachable or flooding host must not block input or drawing on another.
Keep the GUI workspace controller independent of SDL and terminal wall policy.
Use the existing `client.layout.Tree` geometry where it fits: it already supports
split-right, split-below, weights, removal, and directional neighbors. Its current
cell-based rails and placement need evaluation for horizontal dividers and pixel
hit-testing; use a narrow adapter or extension with existing-client regressions.
Do not import the terminal wall picker or inherit its automatic pane selection.
The controller owns per-tab focus; the SDL frame loop routes events to the active
tab and computes physical pixel rectangles. Render that tab's panes with the
existing shared font/atlas, clip content to each pane,
draw only the focused cursor, and resize each PTY to its own content rectangle.
Retain whole-frame clearing initially. Apply display-scale changes consistently
to every pane, header, divider, and picker. Enforce minimum sizes during splits
and divider drags; shrinking the OS window must not discard saved pane identity.
If the existing layout cannot fit a tiny window, retain its minimum footprint and
clip it to the viewport; never send zero PTY dimensions. Refuse new splits that
cannot fit. For each frame, copy visible cells and their text under individual
pump locks, then prepare all glyphs and generate all UVs from that frozen data.
Revise build rule 8 and architecture comments to permit this explicit model while
retaining the SDL boundary and separation from the terminal wall. No broad wall
policy extraction is a prerequisite.
## Small functional sprints
1. **Two real panes.** Introduce workspace/tab/pane ownership with one tab and a
pure split controller. Trial `v`/`b` insertion using explicit targets, focus via
prefix + `h/j/k/l` or mouse click, and route input/resize independently.
Demo two isolated daemons simultaneously, including one flooding and one idle.
Retain the current direct-target smoke test through a temporary workspace.
2. **Insertion workflow.** Connect pending direction to host and session pickers,
explicit creation, and expanded keyboard hints. Demo nested splits across
hosts; cancelling creates nothing; failed discovery keeps existing panes usable.
Trial the direction semantics and shortcuts here before expanding them.
3. **Resizing.** Add divider dragging and keyboard resize mode with minimum sizes.
Demo nested pane resizing, correct per-session PTY sizes, clipped glyphs, and
a high-DPI scale change. Assert old pixels disappear after shrinking regions.
4. **Lifecycle and restore.** Add separate native persistence, detach/end actions,
exited/offline pane recovery, and bounded shutdown. Demo close/reopen preserving
multiple hosts, an unavailable host returning, a missing session remaining
recoverable, and unchanged terminal mux layout state.
5. **Release review.** Exercise the full multi-host workflow, keyboard ergonomics,
failure isolation, default-workspace ownership, malformed saved state, and
high-DPI rendering. Review and commit the completed milestone.
Each sprint must leave runnable code with a concrete GUI demo. Delegate bounded
implementation and a separate adversarial review; they resolve findings directly
before completion. Reserve explicit work at the end of every sprint to deduplicate,
refactor, and clean up technical debt in the integrated code. Review ownership,
interfaces, repeated logic, and temporary scaffolding; complete cleanup before
final validation and commit. Record any justified remaining debt and its follow-up
trigger. Keep native and terminal interaction policy separate while simplifying
shared mechanics where appropriate. Run acceptance on the refactored result.
Use focused state/lifecycle tests and actual framebuffer and
daemon-size checks. Run broader repository checks when shared code changes.
Do not add ceremony or broad test runs without a specific integration risk.
First-sprint acceptance includes exact layout and focused-input tests, distinct
typed markers checked in each daemon grid and framebuffer, clipping and erasure
after resize, and each daemon reporting its own pane's PTY dimensions. Flood or
disconnect one daemon while the other continues accepting input and painting;
closing the GUI leaves sessions alive. Check native Wayland at the display's real
scale. Isolated local daemons establish connection independence; a separate real
remote-host check is needed to claim SSH/QUIC end-to-end validation, with any
untested transport explicitly reported. Review and commit the runnable sprint.
Rearranging panes, drag-and-drop movement, tab UI, container focus/reparenting,
multiple persistent windows, and terminal-client tiling redesign are later work.