Merge-time fix for #676 (review nits 4 and 5). sessionCreated forgot a stale
case default inside the try whose catch rolls the registration back; it now
does so after the try, once the session is committed, so the cleanup can
never drop a committed session from the live map. caseDefaultGroup (behind
GET /api/cases/:name/default-group) no longer deletes a stale id as a side
effect: it reports Ungrouped and leaves the value for the next creation to
forget. The two tests that expected the read to clear it now expect it to
write nothing; the creation test still pins the cleanup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Case settings gains a "Default session group" selector (Ungrouped plus every
named tab group of the owner). A new session of the case lands in that group
in the same versioned layout write that registers it, so it is drawn in place
with no Ungrouped-to-group jump.
- Stored per owner and case in state.json (caseDefaultGroups), as the group's
stable id or null for Ungrouped; read and written only through
TabLayoutService under the owner lock.
- Precedence in sessionCreated: explicit tabGroupId, then the parent
placement, then the case default, else Ungrouped. A deleted or foreign id
never fails a spawn and is forgotten only after the creating write lands.
- POST /api/quick-start (local, linked, remote, Docker) and
/api/ralph-loop/start pass their case; POST /api/sessions infers it
lexically from workingDir. The create response carries tabLayout when a
case default placed the session.
- GET/PUT /api/cases/:name/default-group; deleting a case (or an admin
deleting a user's case folder) forgets the value.
- The selector mirrors desktop and phone, reverts on a refused save, and is
redrawn from every adopted tab layout; zh-CN strings added.
With the follow setting off (the default), pinCreatedChild marked each new
child manual where normalization put it: right after the parent's FOLLOWING
block, which no longer holds the siblings pinned before it. A fan-out
therefore showed parent, w3, w2, w1 in the layout and the global
sessionOrder. The pinned child now steps past the siblings (and their
descendants) already after the parent.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review fixes for "place new, child and web tabs into groups".
- Spawned tabs follow their parent only with the new synced setting
spawnedTabsFollowParent (App Settings > Appearance > Tabs, off by default).
TabLayoutService.sessionCreated() reads it fresh through a
childrenFollowParent dep: off, a spawned session lands after its parent and
is marked manual in the same write; on, it follows until moved by hand. The
row menu shows the placement note and "Follow parent again" only when on.
"New session" in a group menu is unaffected. zh-CN label and a
Settings-Reference row.
- applyLegacySessionRank marks a child manual only when the requested order
splits it from its parent's block. A PUT of the current order, or one that
moves a family together, leaves every stored placement alone.
- The browser never echoes a create or delete through PUT /api/session-order,
with or without groups: the layout service already re-projects the order and
broadcasts it, and syncSessionOrder adopts it.
- The placement note in the row menu is plain text described by the menu, not
a focusable menu item.
- A tab that first appears inside a collapsed group briefly marks that group's
header.
- i18n skips .tab-name, so tab names are never translated.
- Tests: legacy-order cases for an unchanged order, a family reorder and a
split; service cases for the setting and for order PUTs; the scheduled
registration test now exercises register/undo; the grouped-create test binds
port 0; one route case runs through the real layout service.
New sessions, spawned child sessions and saved web tabs now land in tab
groups instead of always starting in Ungrouped.
New session in a group:
- The group menu gains "New session" (first item). It expands a collapsed
group and runs ONE launch with the current case and run mode; a launch
already in flight is reported, not queued.
- run({ count, tabGroupId }) threads the group through every launch path
(local POST /api/sessions, remote/docker and external-CLI
/api/quick-start). The count overrides the steppers for that launch only.
- Server: POST /api/sessions and /api/quick-start accept an optional
tabGroupId (Zod, 1-100 chars; null/empty is a 400). The routes pass it to
addSession(), and TabLayoutService.sessionCreated() places the session at
the end of that group in the SAME versioned write that first records it.
The session is added to the live map inside the owner lock (with a
rollback when the write fails), so no queued layout edit can commit it
first. The group is looked up in the creating owner's own layout, so an
unknown, deleted or foreign id is ignored instead of failing the spawn. An
explicit group is a hand placement: a child placed this way is manual.
- The create response carries the committed tabLayout (only when tabGroupId
was sent), adopted before the new tab is drawn. Responses without the
field are unchanged.
Child sessions (parentSessionId / X-Codeman-Parent-Session):
- A child nobody placed follows its parent: the server's existing
normalization already puts it after the parent's rows; the browser now
draws a not-yet-stored child there too (project({ parents })) and, while
groups exist, adopts the server's order instead of echoing its own append
back through PUT /api/session-order (which pinned every child manual).
- The row menu says where a child sits (follows, placed by hand, parent
closed, parent loop) and offers "Follow parent again" only to a hand-placed
child. A child whose parent is gone stands on its own and is never
re-adopted.
Saved web tabs:
- Web tabs are layout refs like sessions; openness stays per device. A
grouped web tab shows (and takes an Alt+N slot) only while open here and
reopens where the layout keeps it. Pinned by tests; no behaviour change.
Unchanged: the flat rail and header strip (markup, HTML5 drag, append and
save on create), the tree semantics and single tab stop, sessionOrder and
Alt+N, and every request that does not send tabGroupId.
Post-merge follow-ups from the deep review of #334 and #335, so they ship in
the same release as the features.
Tab-layout foundation (#335):
- PUT /api/session-order drops unknown/foreign ids again instead of 400ing
the whole write, in both the owner and the admin path (single-user requests
are the synthetic admin, so that path is the one the browser hits). The
frontend debounces its reorder push and swallows errors, so a session
deleted inside the debounce window silently cost the user the entire
reorder - and the endpoint sits on the stable /api/v1 surface, where the
pre-layout server merged leniently.
- A failed mux restore no longer locks explicit deletions into 500s for the
process lifetime: runSessionDeletion and webviewDeleted degrade to
best-effort without layout coordination, while the automated stale sweep
(runStaleSessionCleanup) stays fail-closed.
- sse-events doc comment: no 'suppressed' hook event exists; hooks stay 8.
- registerSessionWithLayout resolves its owner through ownerLayoutKey()
instead of a hardcoded '@single'.
Vertical rail (#334) - all rail-awareness gaps in sidebar-only predicates,
unified behind the new _isVerticalTabList() (sidebar OR rail):
- Drag-reorder read the insertion side from clientX in the rail, so
before/after was effectively arbitrary on vertical rows; the drag-over
indicators now draw as top/bottom edges there like the sidebar's.
- The active tab is scrolled into view in the rail (Alt+N/palette selection
used to leave the row below the fold).
- Floating subagent/ultracode windows anchor to the RIGHT of rail tabs, and
the connector redraw gates (render tail + strip scroll) cover the rail.
- Server-seeded tabOrientation is applied when the async settings load
resolves, not only at boot, so a fresh device shows the rail immediately.
- The pre-paint script stamps data-tab-orientation and --tab-rail-width
(sidebar-wins and solo carve-outs included), removing the flash of the
header strip on every vertical-mode load.
- The session name font defaults to 12px, the sidebar's historical 0.75rem
size, so installs that never touch the new slider are not restyled.
Also documents the rail in CLAUDE.md (second #sessionTabs host, mover
ordering, the axis-predicate rule) and gives tab-rail-resize.js its
@dependency/@loadorder header. Full gate green (6093 tests); the excluded
browser suite was run by hand - only the known environmental failures
(opencode/codex binaries) remain, identical to pristine master.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>