feat(tasks): phase 6 — live TaskContextPanel (goal, plan, question, entities)
Replaces the chat right rail's ad-hoc Digest+Graph stack with a single
TaskContextPanel that renders the task's live working state, driven by the
always-on events stream (not the per-turn chat SSE) so it keeps updating
during server-side auto-continuation/resume:
- GoalHeader: goal + status pill (planning/executing/awaiting_input/done/
failed), sourced from the sessions list.
- PlanProgress: ordered steps with live status icons + progress bar, hydrated
via new GET /sessions/{id}/plan; clicking a step with a target opens its
EntitySheet (no fake "jump to transcript" — bits-ui Collapsible content
isn't force-mounted, so a DOM-scroll jump would silently no-op for
collapsed tool groups).
- OperatorQuestion: the pinned structured question card (prompt/why/entity
chips/option buttons/free-text), hydrated via new GET /sessions/{id}/
questions; answering POSTs to the existing answer endpoint.
- SessionGraph upgraded to a live entity panel: entity.touched pulses the
node (animated ring) and shows "Now touching <slug>"; health.changed shows
a transient diff badge for touched entities.
- SessionDigest gains a success/failure/partial outcome banner and now also
refetches when the task's status changes, not just on session switch.
Two bugs found and fixed while wiring this up:
- workspace.ts's status-refresh trigger only covered goal.set/task.status;
question.raised/answered didn't refresh the sessions list, so GoalHeader's
pill went stale after answering via the panel (resumeSession runs entirely
server-side — no client 'done' event to piggyback a refresh on). Now every
status-affecting event triggers the (debounced) refetch.
- Forgot to rebuild the nomos container after adding the /plan and
/questions endpoints, so they silently fell through to the old default GET
handler — caught via a live curl diff against the running container,
not a code read.
Verified end-to-end against the live stack: goal/plan/question all update
without a reload as the agent works; answering a question via the panel
resumes the agent and the header pill correctly flips to Executing;
entity.touched pulses the live graph.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { fetchSessionDigest, type SessionDigest } from '$lib/api'
|
||||
import { currentSession, streaming } from '$lib/stores/chat'
|
||||
import { currentTask } from '$lib/stores/workspace'
|
||||
import { Badge } from '$lib/components/ui/badge'
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||||
import SparklesIcon from '@lucide/svelte/icons/sparkles'
|
||||
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
|
||||
import CircleXIcon from '@lucide/svelte/icons/circle-x'
|
||||
|
||||
let digest = $state<SessionDigest | null>(null)
|
||||
let open = $state(false)
|
||||
let loadedFor = $state<string | null>(null)
|
||||
// Keyed on session id AND status: a task that completes mid-view (via
|
||||
// resumeSession running server-side, with $streaming never true here) must
|
||||
// still refetch once outcome/summary land, not just on session switch.
|
||||
let loadedKey = $state<string | null>(null)
|
||||
|
||||
// Reload the digest whenever the session changes or a stream finishes —
|
||||
// "what did this session actually do" is only meaningful once executions
|
||||
// have had a chance to land.
|
||||
// Reload the digest whenever the session changes, the task's status changes
|
||||
// (e.g. it just completed), or a stream finishes — "what did this session
|
||||
// actually do" is only meaningful once executions have had a chance to land.
|
||||
$effect(() => {
|
||||
const sid = $currentSession
|
||||
const busy = $streaming
|
||||
const status = $currentTask?.status ?? ''
|
||||
if (!sid || busy) return
|
||||
if (loadedFor === sid) return
|
||||
loadedFor = sid
|
||||
const key = `${sid}:${status}`
|
||||
if (loadedKey === key) return
|
||||
loadedKey = key
|
||||
fetchSessionDigest(sid).then((d) => (digest = d))
|
||||
})
|
||||
|
||||
@@ -30,6 +38,23 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $currentTask?.outcome}
|
||||
<div
|
||||
class="flex items-start gap-2 border-b px-3 py-2 text-xs {$currentTask.outcome === 'failure'
|
||||
? 'bg-destructive/5 text-destructive'
|
||||
: $currentTask.outcome === 'partial'
|
||||
? 'bg-warning/5 text-warning'
|
||||
: 'bg-success/5 text-success'}"
|
||||
>
|
||||
{#if $currentTask.outcome === 'failure'}
|
||||
<CircleXIcon class="mt-0.5 size-3.5 shrink-0" />
|
||||
{:else}
|
||||
<CircleCheckIcon class="mt-0.5 size-3.5 shrink-0" />
|
||||
{/if}
|
||||
<span class="leading-snug">{$currentTask.summary || `Task ${$currentTask.outcome}.`}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if digest && digest.total_executions > 0}
|
||||
<div class="border-b">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user