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>
125 lines
5.1 KiB
Svelte
125 lines
5.1 KiB
Svelte
<script lang="ts">
|
|
// Floating-window content for a task/session — self-contained per
|
|
// sessionId via chat.ts's chatFor()/loadSessionChat()/sendSessionMessage()
|
|
// and workspace.ts's workspaceFor()/startSessionWorkspace(), so several of
|
|
// these can be open (and independently live) at once.
|
|
import { onDestroy, onMount } from 'svelte'
|
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
|
import { chatFor, loadSessionChat, sendSessionMessage, cancelSessionStream, stopSessionPolling, dismissError, chatErrors } from '$lib/stores/chat'
|
|
import { activityLogFor } from '$lib/stores/activity'
|
|
import { workspaceFor, startSessionWorkspace } from '$lib/stores/workspace'
|
|
import ChatThread from '$lib/components/ChatThread.svelte'
|
|
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
|
|
|
let { sessionId }: { sessionId: string } = $props()
|
|
|
|
// Svelte's `$store` auto-subscription only works on a plain identifier
|
|
// bound directly to a store, not a member expression — chatFor() returns
|
|
// an object of stores, so pull each one out into its own identifier here.
|
|
// sessionId is a stable prop (one per window mount, never changes), so
|
|
// capturing it at init is safe and intended.
|
|
// eslint-disable-next-line svelte/valid-compile
|
|
const chat = chatFor(sessionId)
|
|
const chatMessages = chat.messages
|
|
const chatStreaming = chat.streaming
|
|
const chatConnectionState = chat.connectionState
|
|
const chatError = chat.error
|
|
const chatNotFound = chat.notFound
|
|
// eslint-disable-next-line svelte/valid-compile
|
|
const sessionActivityLog = activityLogFor(sessionId)
|
|
// Started here (rather than left to TaskContextPanel's own onMount) so the
|
|
// workspace is already tracking touched entities/plan/questions before the
|
|
// context rail ever mounts — it needs that live even while the rail stays
|
|
// hidden (see hasContext below).
|
|
// eslint-disable-next-line svelte/valid-compile
|
|
const workspace = workspaceFor(sessionId)
|
|
// eslint-disable-next-line svelte/valid-compile
|
|
const touchedEntities = workspace.touched
|
|
// eslint-disable-next-line svelte/valid-compile
|
|
const openQuestion = workspace.openQuestion
|
|
let loading = $state(true)
|
|
|
|
// The context rail (Scope/Activity) is only worth its screen space once
|
|
// there's something in it — a brand-new task otherwise opens to an empty
|
|
// "entities appear here" placeholder next to an equally empty activity
|
|
// list. Show it the moment either has real content, and keep it shown
|
|
// from then on (no flicker back to hidden if e.g. touched entities later
|
|
// expire). An open question does NOT gate this anymore — it renders
|
|
// inline in the chat thread itself (see ChatThread's `question` prop
|
|
// below), not in this rail.
|
|
let hasContext = $state(false)
|
|
$effect(() => {
|
|
if (!hasContext && ($sessionActivityLog.length > 0 || $touchedEntities.length > 0)) {
|
|
hasContext = true
|
|
}
|
|
})
|
|
|
|
// startSessionWorkspace's cleanup is registered via onDestroy below rather
|
|
// than returned from this callback — onMount ignores a returned function
|
|
// once the callback is async (its return value is a Promise, not the
|
|
// cleanup itself).
|
|
const stopWorkspace = startSessionWorkspace(sessionId)
|
|
|
|
onMount(async () => {
|
|
await loadSessionChat(sessionId)
|
|
loading = false
|
|
})
|
|
|
|
onDestroy(() => {
|
|
stopSessionPolling(sessionId)
|
|
stopWorkspace()
|
|
})
|
|
|
|
// Resizable right rail — sized smaller by default since task windows open
|
|
// narrower than the full page.
|
|
let railSize = $state(24)
|
|
</script>
|
|
|
|
<div class="flex h-full min-h-0">
|
|
{#if loading}
|
|
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">Loading…</div>
|
|
{:else if $chatNotFound}
|
|
<div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center">
|
|
<p class="text-sm text-muted-foreground">Task not found.</p>
|
|
<p class="text-xs text-muted-foreground/70">It may have been deleted.</p>
|
|
</div>
|
|
{:else if hasContext}
|
|
<Splitpanes theme="oikos-theme" dblClickSplitter={false}>
|
|
<Pane>
|
|
<ChatThread
|
|
messages={$chatMessages}
|
|
streaming={$chatStreaming}
|
|
connectionState={$chatConnectionState}
|
|
error={$chatError}
|
|
chatErrors={$chatErrors}
|
|
activityLog={sessionActivityLog}
|
|
{sessionId}
|
|
question={$openQuestion}
|
|
onSend={(text) => sendSessionMessage(sessionId, text)}
|
|
onCancel={() => cancelSessionStream(sessionId)}
|
|
onReconnect={() => loadSessionChat(sessionId)}
|
|
onDismissError={dismissError}
|
|
/>
|
|
</Pane>
|
|
<Pane bind:size={railSize} minSize={18} maxSize={40}>
|
|
<TaskContextPanel {sessionId} />
|
|
</Pane>
|
|
</Splitpanes>
|
|
{:else}
|
|
<ChatThread
|
|
messages={$chatMessages}
|
|
streaming={$chatStreaming}
|
|
connectionState={$chatConnectionState}
|
|
error={$chatError}
|
|
chatErrors={$chatErrors}
|
|
activityLog={sessionActivityLog}
|
|
{sessionId}
|
|
question={$openQuestion}
|
|
onSend={(text) => sendSessionMessage(sessionId, text)}
|
|
onCancel={() => cancelSessionStream(sessionId)}
|
|
onReconnect={() => loadSessionChat(sessionId)}
|
|
onDismissError={dismissError}
|
|
/>
|
|
{/if}
|
|
</div>
|