diff --git a/web/claw/README.md b/web/claw/README.md index 7037157..f29eb09 100644 --- a/web/claw/README.md +++ b/web/claw/README.md @@ -119,9 +119,11 @@ Two consequences worth knowing before changing this code: - **Sessions are created in place.** The sidebar's `+` form posts `POST /v1/sessions` with a client-validated name (`[A-Za-z0-9._-]{1,64}`), switches the chat to the new key, and - lets 400/409 land in the error banner. The phone client's status - keys (`phone:…`) stay out of the inventory; the full list stays on - the wire. + lets 400/409 land in the error banner. The inventory lists only the + resident prefixes (`interactive:`, `task:`, `calendar:`) — the ones the + message, event and history routes answer for. The phone's status keys + (`phone:…`), the coding jobs' `job:#` keys and forge keys + stay out, and the full list stays on the wire. - **Deleting a session is a decision, not a click.** The row's delete button only names the key; the shell's dialog spells out that the turn history goes with it, and `DELETE /v1/sessions/{key}` runs on diff --git a/web/claw/app/app.vue b/web/claw/app/app.vue index f56fe40..f0d56ee 100644 --- a/web/claw/app/app.vue +++ b/web/claw/app/app.vue @@ -5,7 +5,7 @@ // state is in-memory; only the token and the active session persist. // The claw owns the conversation and the config, so this page is a // mirror, not a store. -import { ClawError, clawHealth, isInteractive, useClaw } from '~/composables/useClaw' +import { ClawError, clawHealth, isInteractive, isResidentKey, useClaw } from '~/composables/useClaw' import type { AgentIdentity, AgentImageKind, @@ -339,7 +339,7 @@ function closeDeleteSession(): void { * empty key opens no stream (see `resubscribe`). */ function nextActiveKey(gone: string): string { const remaining = sessions.value.filter((session) => session.key !== gone) - const next = remaining.find((session) => isInteractive(session.key)) ?? remaining[0] + const next = remaining.find((session) => isResidentKey(session.key)) return next?.key ?? '' } diff --git a/web/claw/app/components/SessionsSidebar.vue b/web/claw/app/components/SessionsSidebar.vue index ec23550..1602234 100644 --- a/web/claw/app/components/SessionsSidebar.vue +++ b/web/claw/app/components/SessionsSidebar.vue @@ -7,7 +7,7 @@ // new key lands in the inventory. The delete button asks nothing itself // — the confirmation, which names the transcript that goes with the // session, is the shell's. -import { SESSION_NAME_PATTERN } from '~/composables/useClaw' +import { SESSION_NAME_PATTERN, isResidentKey } from '~/composables/useClaw' import type { ClawSession } from '~/composables/useClaw' const props = defineProps<{ @@ -32,10 +32,15 @@ interface Group { const groups = computed(() => { const byKey = (a: ClawSession, b: ClawSession) => a.key.localeCompare(b.key) - // `phone::` keys carry the phone client's status - // reporting (battery, network, dnd), not conversation; the inventory + // Only the resident prefixes are chat sessions: `interactive:` is the + // live chat surface, `task:` a scheduled task's own turn, `calendar:` a + // reminder's. The rest of the inventory has no transcript to open — + // `phone::` is status reporting, `job:#` + // is the coding jobs' bookkeeping, a forge key is a thread the handler + // reports on — and the daemon's message, event and history routes + // refuse those prefixes, so a row for one is a dead end. The inventory // hides them rather than the API narrowing. - const conversable = props.sessions.filter((session) => !session.key.startsWith('phone:')) + const conversable = props.sessions.filter((session) => isResidentKey(session.key)) const interactive = conversable .filter((session) => session.key.startsWith('interactive:')) .sort(byKey) diff --git a/web/claw/app/composables/useClaw.ts b/web/claw/app/composables/useClaw.ts index 6aebaab..c9ea681 100644 --- a/web/claw/app/composables/useClaw.ts +++ b/web/claw/app/composables/useClaw.ts @@ -248,6 +248,18 @@ export function isInteractive(key: string): boolean { return key.startsWith(INTERACTIVE_PREFIX) } +/** The prefixes the chat surface answers for: the daemon's + * `is_resident_key` (`crates/rhema-claw/src/interactive.rs`). Every other + * key in the inventory — `phone:` status reporting, `job:#`, + * a forge thread — has no transcript: the message, event and history + * routes 400 on it. */ +const RESIDENT_PREFIXES = ['interactive:', 'task:', 'calendar:'] + +/** Whether `key` is one the transcript surface can open. */ +export function isResidentKey(key: string): boolean { + return RESIDENT_PREFIXES.some((prefix) => key.startsWith(prefix)) +} + /** The session name `POST /v1/sessions` accepts; the key becomes * `interactive:`. Anything else is a 400. */ export const SESSION_NAME_PATTERN = /^[A-Za-z0-9._-]{1,64}$/