fix(web): follow a blank session's preset switch in the slash catalog

Presets own the rows that decide what a session's `/` menu contains, but
both browser catalogs cache per session and had no invalidation edge for a
recompose: `commands/changed` is registry-wide and recomposing registers
nothing, so the menu kept serving the composition the session no longer ran.

The host stream now frames the logged `agent-preset/selected` commit as
`host/session-preset-changed`; the runtime bridges it to the typed
`session/preset-changed` event, `ui-command` soft-refreshes that session's
directory key and `ui-skill` invalidates its catalog entry.

Reaching the host on a second switch was a separate defect: the list-row
identity guard compared every summary field except `agentPreset`, and the
merge keeps the row's `updatedAt`, so a switched row looked unchanged and
served its cached instance forever. The hero chip compares the pick against
that row, so switching back to the creation-time preset sent no RPC at all.
This commit is contained in:
Yichen Jiang
2026-08-10 14:35:13 +08:00
parent 6648ff984a
commit 259d998455
36 changed files with 433 additions and 34 deletions

View File

@@ -17,7 +17,9 @@
* Catalog fetches are cached per session (the small twin of the ui-command
* directory): the per-keystroke candidates re-poll filters a settled
* snapshot locally, so one session costs one RPC. The scope-birth warm hook
* prewarms the session's key; connection/reset clears everything — the host
* prewarms the session's key; a preset switch drops that one key (the
* catalog is the preset's, and a blank session may switch after the warm);
* connection/reset clears everything — the host
* catalog may differ across generations. A shared in-flight fetch
* deliberately outlives any single menu interaction: closing the menu must
* not kill the prewarm other consumers will hit, so it carries its own
@@ -174,6 +176,9 @@ export function apply(ctx: ClientContext): void {
},
}
const slash = ctx.get('slash') as SlashServiceContract
// A preset decides which skill providers an agent reads, so a switched
// session's cached catalog belongs to the composition it no longer runs.
ctx.on('session/preset-changed', invalidate)
ctx.on('connection/reset', clearAll)
ctx.effect(() => {
const unregister = slash.registerSource(source)