feat(web): operator questions inline in chat, mascot reactions scoped to the focused task
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

The pending operator-question card now renders inline in ChatThread (the
newest thing in the conversation) instead of in the context rail — it's
part of the chat, not a separate side panel, and the panel's hasContext
gate no longer needs to special-case it.

The desktop mascot's reactions are now entirely about whichever task
window has focus, not fleet-wide events: thinking/talking is a new
continuous `busy` behavior that tracks the focused session's own
streaming state (thinking before any text arrives, talking once it
does — using the previously-unwired peep/talk sprite), eureka fires with
the actual knowledge title that was recorded, happy fires with the
task's own completion summary, and alarmed now means "this task needs
your OK" (an operator question was raised) rather than a fleet-wide
critical/signal event.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 10:46:46 +02:00
parent 6b6bfe1fd8
commit e5a81241b7
12 changed files with 246 additions and 136 deletions

View File

@@ -3,12 +3,19 @@
// opened from Knowledge Base, chat, or anywhere else land in the same
// floating window layer, with several windows open side by side, rather than
// each page owning its own single-entity sidebar/sheet.
import { derived, type Readable } from 'svelte/store'
import { createManager, createDesktop, wmStore } from '@surdeddd/wmkit/svelte'
import { persist } from '@surdeddd/wmkit/persist'
import { appById, appWindowId } from '$lib/apps'
import { sessions } from '$lib/stores/chat'
import { heading } from '$lib/tasks'
// Window ids for a task/session's chat window are namespaced `session:<id>`
// — see openTaskWindow below. Shared here (rather than each file redeclaring
// its own copy) since both WindowLayer.svelte and focusedSessionId below
// need to parse it.
export const SESSION_PREFIX = 'session:'
export const wm = createManager({ defaultSize: { width: 480, height: 560 } })
export const dk = createDesktop(wm, {
// topEdge:'maximize' + preview gives the classic drag-to-top-maximizes
@@ -23,6 +30,15 @@ export const dk = createDesktop(wm, {
})
export const wmState = wmStore(wm)
// The session id backing whichever task/chat window currently has focus, or
// null when no task window is focused (Tasks app, an entity window, or
// nothing at all). The desktop mascot's stimuli (stimuli.ts) key off this so
// its reactions track the task the operator is actually looking at, rather
// than firing for every session fleet-wide.
export const focusedSessionId: Readable<string | null> = derived(wmState, ($s) =>
$s.focusedId?.startsWith(SESSION_PREFIX) ? $s.focusedId.slice(SESSION_PREFIX.length) : null
)
// Layout survives reloads: every window id is self-describing (app:<id>,
// session:<id>, or a bare entity slug — see EntityDesktop/WindowLayer's
// content branch), so a hydrated window needs no extra bookkeeping to know