feat(sessions): minimal add row, immediate feedback, whole-row click, Ctrl-K creation #122
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/new-session-affordance"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Built via the full SDD flow — spec (
docs/superpowers/specs/2026-09-04-…) and plan (docs/superpowers/plans/2026-09-04-…) are both in the diff, each reviewed before the next started.What changes
1. The add control shrinks and opens on demand. The form was mounted on every workspace card permanently — measured at 29px + 9px margin = 38px, whether or not anyone was creating anything. It becomes a 26px
.add-slotwith two contents: a+ sessionrow, and the input that replaces it in place. Both atheight: 100%of the same box, and the 3px left border is reserved transparent on the collapsed row, so opening it moves nothing. Net saving is ~10px per card; the larger win is that a text input is no longer sitting open on every card.2. Creating gives feedback immediately. Submit no longer waits for the server. The row appears at once as starting, the input clears and keeps focus, and the add row stays open — so a second name can be typed straight away.
"Starting" is not invented:
createSessioninserts the row asstatus: 'creating'before the slow worktree + tmux work. The optimistic row is reconciled by name insiderefresh(), not by awaiting the POST, because the poll routinely sees the server'screatingrow while the POST is still in flight — reconciling only in the caller'sfinallyshowed the same session twice.Pending rows are kept out of
sessionsFor: everySessionDTOconsumer acts on the id, and a synthetic row would hand the dock and the menus an id no endpoint knows. They render inert — no terminal, no⋯, no context menu.3. The whole row opens the terminal. The name was the only hit target, so on a short name like
fix-1most of a 40px row looked clickable and was not. The⋯and the ports chipstopPropagationand keep their own action; the name stays a real<button>as the keyboard path.4. Ctrl-K can create a session. A second submode beside
drill— same crumb, sameEscexit, results replaced by a live verdict. One row per workspace, no single-workspace shortcut: with one workspace that is one row, on the same code path. A workspace that is still booting is not offered, becausecreateSessionrefuses a container that is not running.It fixes a shipped bug
Sessions.svelte's name check wasSESSION_NAME_REGEXalone, and its character class contained a space — which git refs forbid. Somy sessionwas accepted, sent, and refused by the server. The same class admittedfeat//dockandfeat..dock.validateGitRefNamemoves out of$lib/server(pure string logic, two importers) into$lib/git-ref.ts, so the client runs the same implementation the server does rather than an approximation. git's checks run first, because its messages name the offending character. Verified live:my sessionnow producesref name cannot contain ' 'and zero POSTs.Verification
Gates: typecheck 4900 files / 0 errors, lint clean, 122 files / 1220 tests.
19 new unit tests (
session-name, store pendings, palette index). The store tests were negative-controlled — disabling therefresh()prune and theaddPendingdedupe each failed exactly one intended test — because the implementation predated them and TDD ordering wasn't available.Driven in a real browser (the suite is
environment: 'node', so this is the only place the UI is exercised):my session→ toast, 0 POSTs, no row.terminal:w1:s1; ports chip →ports:w1:s1and no terminal;⋯→ menu and zero panels. Testing these against an already-open terminal would have been inconclusive, since a leaked click would merely refocus it.new→New session in nexus-main…→ crumb +Name the session…+ results suppressed +↵ create / Esc back;feat//x→ redref name cannot contain consecutive slashes;feat/three→creates branch feat/three and its worktree;↵closes the palette and the starting row appears in the sidebar;Escbacks out of compose before closing.Notes
nexus-statusline.test.shfails intermittently under full-suite load (expected 3 POSTs, got 0) — 2 of 4 full runs. It passes 3/3 in isolation, andworker/nexus-statusline.shis not in this diff at all. Worth its own look.AGENTS.mdupdated: it documentedgit-ref.tsat the old path.creatingwindow there is no tmux window to attach to); creating a session while its workspace is still booting stays out of scope — that is the{#if ready}gate inWorkers.svelteand needs an answer for a create landing before tmux is up.Four regressions and six cleanups from a /simplify pass. The regressions all come from one mistake: renaming markup that other components reach into by CSS selector. - MainSplit's focusNewSession queried `form.session-add input`, which this branch renamed and made conditional. The ⋯ menu's "New session…" fell through to "Sessions appear once the workspace has finished starting" on healthy workspaces, from every surface that menu reaches. It is the SECOND time this exact handler has broken this way — the last time a restyle deleted `.sessions-mount`. So the fix is not another selector: `adding` moves into a store the menu sets, and readiness is now asked of the store instead of inferred from whether a DOM query found anything. - The 10s poll's skip-while-typing guard queried the same dead selector, so a full refresh fan-out fired on every tick mid-type. It now keys off a declared data attribute. - The palette rendered its results list UNDER the compose verdict: "Nothing waiting. Type to search." on an empty name, "No match for feat/x" once one was typed. Verdict and results are now either/or. - The palette hardcoded agent_teams_enabled: true, ignoring the operator's default_agent_teams_enabled that the sidebar honours. The last one is a symptom of the create sequence being written twice, so it moves into workspaces.createSession() — the same shape openShell() already uses, and now unit-testable in the node suite where neither component was. Also: dotClass mapped the server's own 'creating' status to a grey stopped dot, so the handover from the optimistic row read amber → grey → green; the third copy of the session-name regex (in the API route, still carrying the space git forbids) now imports the shared one; the palette's canCreateSession takes the WorkerDTO its only caller already holds, making the call site `isReady`; the unreachable `creating` re-entrancy guard is gone; and the verdict's three-branch block is one derived.