feat: per-case default session group

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.
This commit is contained in:
Aamer Akhter committed 2026-10-22 03:42:42 -04:00
1 parent 39077281d1
commit 74c980413a
24 files changed
+1362 -26

No files matched your search

+20
View File
@@ -778,6 +778,26 @@ deleted group, another user's group) is ignored and the session lands where it
otherwise would, never a `400`. A malformed value (`null`, an empty string, a
non-string) is a `400 INVALID_INPUT`. Requests without the field are unchanged.
### A case's default session group
- `GET /api/v1/cases/:name/default-group` → `{ caseName, groupId, groups }`: `groupId` is a
group id from the caller's own tab layout or `null` (Ungrouped), `groups` the caller's
current named groups (`{ id, name }`, in layout order). A stored id whose group was
deleted reads back as `null` and is forgotten.
- `PUT /api/v1/cases/:name/default-group` with `{ "groupId": string | null }` saves it. An
id not in the caller's layout is a `400 INVALID_INPUT` that leaves the old value in place.
Both answer `404 NOT_FOUND` for a case the caller cannot see (the same rules as
`GET /api/v1/cases`). The value is stored per owner and case, so in multi-user mode it is
never shared and needs no admin role. A new session of the case is placed in that group in
its creating write when nothing stronger applies; the order is: an explicit `tabGroupId`,
then the session it was spawned from (`parentSessionId`), then the case default, else
Ungrouped. `POST /api/v1/quick-start` uses its `caseName` (local, linked, remote and Docker
cases); `POST /api/v1/sessions` uses the case whose folder `workingDir` is (a direct child of
the caller's case space, or a linked case's exact path). When a case default places the
session, the create response carries `tabLayout` as it does for `tabGroupId`. Deleting the
case forgets the value; existing sessions never move.
## Approvals Inbox
Cross-session queue of prompts waiting on a human (permission dialogs,