feat(sessions): minimal add row, immediate feedback, whole-row click, Ctrl-K creation #122

Merged
lz merged 11 commits from feat/new-session-affordance into main 2026-09-05 12:06:26 +02:00
Owner

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-slot with two contents: a + session row, and the input that replaces it in place. Both at height: 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: createSession inserts the row as status: 'creating' before the slow worktree + tmux work. The optimistic row is reconciled by name inside refresh(), not by awaiting the POST, because the poll routinely sees the server's creating row while the POST is still in flight — reconciling only in the caller's finally showed the same session twice.

Pending rows are kept out of sessionsFor: every SessionDTO consumer 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-1 most of a 40px row looked clickable and was not. The and the ports chip stopPropagation and 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, same Esc exit, 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, because createSession refuses a container that is not running.

It fixes a shipped bug

Sessions.svelte's name check was SESSION_NAME_REGEX alone, and its character class contained a space — which git refs forbid. So my session was accepted, sent, and refused by the server. The same class admitted feat//dock and feat..dock.

validateGitRefName moves 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 session now produces ref 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 the refresh() prune and the addPending dedupe 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):

  • Add slot 26px and pane 348px, identical across collapsed → open → collapsed; focus lands in the input.
  • Two creates back to back: both rows present as starting, input cleared and focused each time, slot unchanged.
  • my session → toast, 0 POSTs, no row.
  • On an empty dock, each click resolves to exactly one thing: row body → terminal:w1:s1; ports chip → ports:w1:s1 and 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.
  • Palette: newNew session in nexus-main… → crumb + Name the session… + results suppressed + ↵ create / Esc back; feat//x → red ref name cannot contain consecutive slashes; feat/threecreates branch feat/three and its worktree; closes the palette and the starting row appears in the sidebar; Esc backs out of compose before closing.

Notes

  • Pre-existing flake, not from this branch: nexus-statusline.test.sh fails intermittently under full-suite load (expected 3 POSTs, got 0) — 2 of 4 full runs. It passes 3/3 in isolation, and worker/nexus-statusline.sh is not in this diff at all. Worth its own look.
  • AGENTS.md updated: it documented git-ref.ts at the old path.
  • Deliberately not done: a palette-created session does not auto-open its terminal (for most of the creating window 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 in Workers.svelte and needs an answer for a create landing before tmux is up.
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-slot` with two contents: a `+ session` row, and the input that replaces it in place. Both at `height: 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: `createSession` inserts the row as `status: 'creating'` *before* the slow worktree + tmux work. The optimistic row is reconciled by **name** inside `refresh()`, not by awaiting the POST, because the poll routinely sees the server's `creating` row while the POST is still in flight — reconciling only in the caller's `finally` showed the same session twice. Pending rows are kept **out of `sessionsFor`**: every `SessionDTO` consumer 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-1` most of a 40px row looked clickable and was not. The `⋯` and the ports chip `stopPropagation` and 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, same `Esc` exit, 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, because `createSession` refuses a container that is not running. ## It fixes a shipped bug `Sessions.svelte`'s name check was `SESSION_NAME_REGEX` alone, and its character class **contained a space** — which git refs forbid. So `my session` was accepted, sent, and refused by the server. The same class admitted `feat//dock` and `feat..dock`. `validateGitRefName` moves 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 session` now produces `ref 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 the `refresh()` prune and the `addPending` dedupe 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): - Add slot **26px and pane 348px, identical across collapsed → open → collapsed**; focus lands in the input. - Two creates back to back: both rows present as starting, input cleared and focused each time, slot unchanged. - `my session` → toast, **0 POSTs**, no row. - On an empty dock, each click resolves to exactly one thing: row body → `terminal:w1:s1`; ports chip → `ports:w1:s1` **and 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. - Palette: `new` → `New session in nexus-main…` → crumb + `Name the session…` + results suppressed + `↵ create / Esc back`; `feat//x` → red `ref 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; `Esc` backs out of compose before closing. ## Notes - **Pre-existing flake, not from this branch:** `nexus-statusline.test.sh` fails intermittently under full-suite load (`expected 3 POSTs, got 0`) — 2 of 4 full runs. It passes 3/3 in isolation, and `worker/nexus-statusline.sh` is not in this diff at all. Worth its own look. - `AGENTS.md` updated: it documented `git-ref.ts` at the old path. - **Deliberately not done:** a palette-created session does not auto-open its terminal (for most of the `creating` window 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 in `Workers.svelte` and needs an answer for a create landing before tmux is up.
lz added 10 commits 2026-09-04 20:47:01 +02:00
The sidebar's check is a character class that permits a space, which git refs
forbid — so `my session` was accepted, sent, and refused by the server. The
same class admitted `feat//dock` and `feat..dock`.

validateGitRefName moves out of $lib/server (pure string logic, two importers)
so the client runs the same implementation the server does instead of an
approximation of it.
Kept out of sessionsFor: every SessionDTO consumer acts on the id, and a
synthetic row would hand the dock and the menus an id no endpoint knows.
Reconciled by NAME inside refresh(), because the poll routinely sees the
server's own 'creating' row while the POST is still in flight.
The name was the only hit target, so on a short name most of a 40px row looked
clickable and was not. The chips and the ⋯ stop propagation and keep their own
action; the name stays a button as the keyboard path.

Adds the pending variant used by the optimistic add row: named, dotted
'starting', and acting on nothing, because it has no server-side id yet.
The form was mounted on every workspace card permanently: 29px plus a 9px
margin, whether or not anyone was creating anything. It becomes a 26px slot
with two contents and identical heights, so opening it moves nothing.

Creating no longer waits for the server: the row appears at once as starting,
the input clears and keeps focus, and a second name can be typed immediately.
One row per workspace with no single-workspace shortcut — with one workspace
that is one row, on the same code path. Eligibility arrives as a predicate,
like degraded, so this module stays free of the store.
feat(palette): create a session from Ctrl-K
All checks were successful
ci / nexus (pull_request) Successful in 8m36s
ci / images (pull_request) Successful in 18m12s
1a2fa4e1db
A second submode beside drill: same crumb, same Esc exit, results replaced by
a live verdict. Validation runs while you type, which is the argument for
putting creation here — the sidebar can only toast after the round trip.

The new session's terminal is not auto-opened: for most of the 'creating'
window there is no tmux window to attach to.
refactor(sessions): fix what the restyle broke, and stop it happening again
All checks were successful
ci / nexus (pull_request) Successful in 9m36s
ci / images (pull_request) Successful in 11m26s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 7s
1ec74be732
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.
lz merged commit ff458fb454 into main 2026-09-05 12:06:26 +02:00
lz deleted branch feat/new-session-affordance 2026-09-05 12:06:30 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!122
No description provided.