feat(web): operator questions inline in chat, mascot reactions scoped to the focused task
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:
@@ -202,8 +202,12 @@ export const BEHAVIORS: Record<BehaviorId, BehaviorDef> = {
|
||||
id: 'idle',
|
||||
// Periodic blink: enter() sets blinkUntil to the START of the next
|
||||
// blink window (2–6s away). anim() returns 'blink' when we're past
|
||||
// that start but within 150ms of it.
|
||||
// that start but within 150ms of it. A chatter bubble (see enter()
|
||||
// below) takes over the sprite for as long as it's showing — the
|
||||
// mouth-flap 'talk' loop reads as the mascot actually saying the line
|
||||
// instead of just standing there while text happens to appear above it.
|
||||
anim: (rt) => {
|
||||
if (rt.bubbleText) return 'talk'
|
||||
const now = performance.now()
|
||||
if (now >= rt.blinkUntil && now < rt.blinkUntil + 150) return 'blink'
|
||||
return 'idle'
|
||||
@@ -353,6 +357,27 @@ export const BEHAVIORS: Record<BehaviorId, BehaviorDef> = {
|
||||
next: () => 'idle',
|
||||
minMs: REACT_DEFAULT_MS,
|
||||
maxMs: REACT_DEFAULT_MS
|
||||
},
|
||||
|
||||
// Continuous engagement with the focused task window's active turn — not
|
||||
// a timed pulse like `react` above. Entered/exited directly by
|
||||
// MascotLayer's busy-state callback (see stimuli.ts's attachStimuli,
|
||||
// second callback), which also keeps rt.busyTalking current every time
|
||||
// the phase flips. No `weight` — never auto-picked by the idle selector,
|
||||
// same as dragged/falling/land.
|
||||
busy: {
|
||||
id: 'busy',
|
||||
anim: (rt) => (rt.busyTalking ? 'talk' : 'react-think'),
|
||||
tick: () => {
|
||||
// Stationary — just displays whichever sprite busyTalking selects.
|
||||
},
|
||||
// Only reached if something calls next() on it directly, which nothing
|
||||
// does in practice: MascotLayer forces 'idle' itself the moment
|
||||
// stimuli.ts reports the turn ended. Falling back to 'idle' here is
|
||||
// just a safe default, not the real exit path.
|
||||
next: () => 'idle',
|
||||
minMs: 0,
|
||||
maxMs: 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user