fix(web): the session inventory lists only the keys the chat can open
The sidebar grouped everything that was not `interactive:` under "other keys", minus the phone's status keys. That included the coding jobs' `job:<instance>#<id>` keys and forge keys — and those are dead ends: the message, event and history routes answer only for the resident prefixes (`is_resident_key`), so opening one 400s and the row sits there looking busy with nothing inside. A finished job's key was worse than useless while its dispatch spun: `reacquire`, forever. `isResidentKey` names the rule once, beside `isInteractive` — `interactive:`, `task:`, `calendar:` — and the inventory filters on it. That leaves the phone's keys out as before, keeps scheduled tasks (their turns have real transcripts), and stops offering keys the chat surface cannot address. The shell's `nextActiveKey` follows the same rule: after a delete the chat moves to the next key it can open, or to none. The full list stays on the wire — the API's inventory is the dispatcher's, not the client's, and the delete route still answers for any key with a record. Consequence worth naming: a `job:` or forge key has no row now, so it cannot be deleted from the sidebar either.
This commit is contained in:
1 parent
bb475856a0
commit
b3bb026fab
4 files changed
+28
-9
No files matched your search
+5
-3
@@ -119,9 +119,11 @@ Two consequences worth knowing before changing this code:
|
|||||||
- **Sessions are created in place.** The sidebar's `+` form posts
|
- **Sessions are created in place.** The sidebar's `+` form posts
|
||||||
`POST /v1/sessions` with a client-validated name
|
`POST /v1/sessions` with a client-validated name
|
||||||
(`[A-Za-z0-9._-]{1,64}`), switches the chat to the new key, and
|
(`[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
|
lets 400/409 land in the error banner. The inventory lists only the
|
||||||
keys (`phone:…`) stay out of the inventory; the full list stays on
|
resident prefixes (`interactive:`, `task:`, `calendar:`) — the ones the
|
||||||
the wire.
|
message, event and history routes answer for. The phone's status keys
|
||||||
|
(`phone:…`), the coding jobs' `job:<instance>#<id>` 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
|
- **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
|
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
|
turn history goes with it, and `DELETE /v1/sessions/{key}` runs on
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// state is in-memory; only the token and the active session persist.
|
// 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
|
// The claw owns the conversation and the config, so this page is a
|
||||||
// mirror, not a store.
|
// mirror, not a store.
|
||||||
import { ClawError, clawHealth, isInteractive, useClaw } from '~/composables/useClaw'
|
import { ClawError, clawHealth, isInteractive, isResidentKey, useClaw } from '~/composables/useClaw'
|
||||||
import type {
|
import type {
|
||||||
AgentIdentity,
|
AgentIdentity,
|
||||||
AgentImageKind,
|
AgentImageKind,
|
||||||
@@ -339,7 +339,7 @@ function closeDeleteSession(): void {
|
|||||||
* empty key opens no stream (see `resubscribe`). */
|
* empty key opens no stream (see `resubscribe`). */
|
||||||
function nextActiveKey(gone: string): string {
|
function nextActiveKey(gone: string): string {
|
||||||
const remaining = sessions.value.filter((session) => session.key !== gone)
|
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 ?? ''
|
return next?.key ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// new key lands in the inventory. The delete button asks nothing itself
|
// new key lands in the inventory. The delete button asks nothing itself
|
||||||
// — the confirmation, which names the transcript that goes with the
|
// — the confirmation, which names the transcript that goes with the
|
||||||
// session, is the shell's.
|
// 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'
|
import type { ClawSession } from '~/composables/useClaw'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -32,10 +32,15 @@ interface Group {
|
|||||||
|
|
||||||
const groups = computed<Group[]>(() => {
|
const groups = computed<Group[]>(() => {
|
||||||
const byKey = (a: ClawSession, b: ClawSession) => a.key.localeCompare(b.key)
|
const byKey = (a: ClawSession, b: ClawSession) => a.key.localeCompare(b.key)
|
||||||
// `phone:<device>:<topic>` keys carry the phone client's status
|
// Only the resident prefixes are chat sessions: `interactive:` is the
|
||||||
// reporting (battery, network, dnd), not conversation; the inventory
|
// 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:<device>:<topic>` is status reporting, `job:<instance>#<id>`
|
||||||
|
// 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.
|
// 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
|
const interactive = conversable
|
||||||
.filter((session) => session.key.startsWith('interactive:'))
|
.filter((session) => session.key.startsWith('interactive:'))
|
||||||
.sort(byKey)
|
.sort(byKey)
|
||||||
|
|||||||
@@ -248,6 +248,18 @@ export function isInteractive(key: string): boolean {
|
|||||||
return key.startsWith(INTERACTIVE_PREFIX)
|
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:<instance>#<id>`,
|
||||||
|
* 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
|
/** The session name `POST /v1/sessions` accepts; the key becomes
|
||||||
* `interactive:<name>`. Anything else is a 400. */
|
* `interactive:<name>`. Anything else is a 400. */
|
||||||
export const SESSION_NAME_PATTERN = /^[A-Za-z0-9._-]{1,64}$/
|
export const SESSION_NAME_PATTERN = /^[A-Za-z0-9._-]{1,64}$/
|
||||||
|
|||||||
Reference in new issue
Block a user