Multiplex inside a session: split trees and tabs on desktop, tabs on mobile, and a double-Ctrl scratch terminal - #82
Merged
Conversation
…mobile, and a double-Ctrl scratch terminal
Panes are sessions, not a new server concept. The daemon gains two optional,
additive fields — spawn.group links a session under an anchor, and
spawn.ephemeral marks a scratch terminal — plus update.ephemeral to promote a
scratch into a real member ("Keep"). A running ephemeral session lives exactly
as long as its group parent: the registry's reap sweep closes it once the
parent has exited, and an exited scratch is reaped in seconds rather than ten
minutes. Grouped sessions snapshot and revive with their link intact;
ephemeral ones are never snapshotted. The welcome announces a "multiplex"
capability so the web client can feature-detect before drawing any of this;
a plain session keeps exactly today's wire shape, rendering, and revival.
On the web, the terminal route becomes a group host. Desktop renders tabs of
split trees — ⌘D splits the focused pane to the right, ⇧⌘D stacks inside it,
⌥⌘T opens a tab beside the splits (Ctrl+Shift+D / Ctrl+Alt+Shift+D /
Ctrl+Alt+T off a Mac), with draggable dividers whose drags write the DOM
directly and commit once. The arrangement persists per group and device in
localStorage. Mobile flattens every member to a finger-sized tab strip, one
pane on screen, with the visual-viewport pinning taught about in-flow chrome
above the pane. The control strip collapses to one "+" menu carrying all four
verbs and their chords, worn by the surface's top-right pane only.
Double-Ctrl — two bare taps, chords never count — opens the scratch terminal:
an ephemeral session in the current session's directory, in a switcher-styled
modal. Escape dismisses but only detaches; the shell (a dev server, a watch)
keeps running and the next double-Ctrl reattaches to it, until the parent
session ends. The sessions list folds members under their anchor with a pane
count and hides scratches everywhere; the switcher still offers members
individually.
Closes #73.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and the switcher knows the group A theme choice now reaches every terminal in the document, not only the pane whose menu took the click: the storage event only ever fires in other tabs, which was sufficient exactly as long as one terminal per document held — the preference module gains a same-document announcement and every terminal subscribes. The exit overlay is gone. A shell that ends hands the exit to the view above and that view closes it: a split pane folds away, the scratch modal dismisses (typing exit in a scratch now puts it away), a lone session leaves for the dashboard. The one case that stays on screen is a session opened after it had already exited — reading those is what the daemon's retention window is for. ⌥⌘←/→ (Ctrl+Alt+←/→) walk the tabs of the group on screen — trees on a desktop, panes on a phone — in the same modifier family as ⌥⌘T, since every conventional spelling belongs to the browser. The ⌘K switcher leads with a "This session" section naming the group's other terminals, which is the one place they are offered by name now that the sessions list folds them away; pinned siblings keep their Pinned rows so the ⌃⇧1..9 badges never renumber. The list's fold badge says "N terminals" — panes and tabs are per-device renderings of the same group, and the badge should not pick a side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Second round, from live feedback:
Suite: 1450 web tests + lint green, Go untouched this round. |
Every chord in one place — switcher, splits, tabs, scratch, focus mode — spelled for the keyboard at hand, because the pills that used to teach them are gone in a hundred milliseconds on a local daemon and each tooltip teaches one chord. ⌘/ is the palette-help spelling everywhere else; off a Mac it is Ctrl+Shift+/, in the reserved Ctrl+Shift namespace, since plain Ctrl+/ is readline's undo and may not be taken. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Added a keyboard-shortcuts card: a keyboard chip in the control strip, and ⌘/ (Ctrl+Shift+/ off a Mac) toggles it. Lists every chord — switcher, splits, tabs, scratch, focus mode, copy — spelled for the platform. 1456 web tests + lint green. |
A card of keyboard chords is furniture on a phone, so a coarse pointer gets no chip. The ⌘/ listener stays mounted either way: an iPad grows a hardware keyboard without changing its pointer, and the chord should answer it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tabs were shrinkable flex children, so too many of them absorbed the overflow by squishing each other into slivers — a strip that never overflowed had nothing to pan. shrink-0 keeps every tab its natural width and lets the overflow engage, and the selected tab now keeps itself in view, so the cycle chord landing on a scrolled-past tab reads as a move rather than as nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… join Fixes from a second review pass over the multiplex branch, plus the CI flake that predates it. - viewport.ts kept a captured-previous chain per tracker, which breaks the moment trackers dispose out of install order: the last disposer restores a dead tracker's handler, leaving a stale closure pinning a detached pane while the live one goes deaf to the keyboard. The stack is now explicit and shared per viewport; disposal removes exactly its own entry and the slot always belongs to the newest survivor. - The scratch resolution now checks the route at every landing: an answer that arrives after navigating away no longer pops the modal over a different session, and a chord whose parent is not a running session declines to spawn (the daemon's sweep would close the newborn within seconds, in the wrong directory no less). - The split chord's focus ref is cleared when the pane it names closes, so a chord in the gap before the next focusin cannot aim at the dead pane. - Registry.Reap re-reads the ephemeral flag before closing an orphan: a Keep landing between collection and the close no longer loses to the sweep. - splitInTabs returns the same array when it declines, as its contract already promised, and withRatio clamps once at the target split. - The relay transport joins its serve and pairing goroutines before Run returns. Those goroutines write into the device registry (last-seen stamps, pairing entries), and a Run that returned while one was still going left the write racing the caller's teardown - in CI, the test harness deleting the registry directory out from under it, which is the TempDir flake that failed the previous run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #73.
What this is
A session stays one terminal, full bleed — until you ask for more. Then:
+menu. The control strip stays four chips; New session / Split right / Split down / New tab / Scratch live in one menu with their chords beside them, worn by the surface's top-right pane only.Server design (issue's sketch, followed)
Panes are sessions — one PTY, one ring, one revival each. The daemon learns two optional spawn fields (
group,ephemeral) and one update field (ephemeral: false, the Keep edit), all additive:groupparent exits; an exited one is reaped in ~seconds instead of ten minutes; one with no group is left alone.multiplexcap for feature detection.Backward compatibility
GET /api/sessionsunchanged; folding and hiding are client decisions.Testing
go test ./...andgo vetgreen: lifecycle tests for group/ephemeral spawn, parent-death sweep, promotion, snapshot rules, wire round-trips, and daemon-level spawn/update/welcome tests.cd web && pnpm vitest run: 1442 tests green, including the shared golden fixture (extended with the new messages on both sides), the chord state machines, the pane tree (split/prune/reconcile/tabs), the group component across breakpoints, and the Tailwind prose-scanner build guard.Known limits (deliberate, per the issue)
FlueClient.wanted/refOwnerare keyed by session id).🤖 Generated with Claude Code