Files
oikos/web/src/lib/components/GoalHeader.svelte
dtoro 991e7d0900 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>
2026-07-11 13:52:36 +02:00

39 lines
1.7 KiB
Svelte

<script lang="ts">
import { currentTask } from '$lib/stores/workspace'
import { Badge } from '$lib/components/ui/badge'
type StatusStyle = { label: string; dot: string; pulse: boolean; variant: 'default' | 'secondary' | 'destructive' | 'outline' }
function statusStyle(status: string | undefined, outcome: string | undefined): StatusStyle {
switch (status) {
case 'awaiting_input':
return { label: 'Needs your input', dot: 'bg-warning', pulse: true, variant: 'secondary' }
case 'done':
return outcome === 'partial'
? { label: 'Done · partial', dot: 'bg-warning', pulse: false, variant: 'secondary' }
: { label: 'Done', dot: 'bg-success', pulse: false, variant: 'default' }
case 'failed':
return { label: 'Failed', dot: 'bg-destructive', pulse: false, variant: 'destructive' }
case 'planning':
return { label: 'Planning', dot: 'bg-primary', pulse: true, variant: 'secondary' }
case 'executing':
return { label: 'Executing', dot: 'bg-primary', pulse: true, variant: 'secondary' }
default:
return { label: 'Active', dot: 'bg-muted-foreground', pulse: false, variant: 'outline' }
}
}
</script>
{#if $currentTask}
{@const st = statusStyle($currentTask.status, $currentTask.outcome)}
<div class="flex shrink-0 flex-col gap-1.5 border-b px-3 py-2.5">
<div class="flex items-center gap-1.5">
<span class="size-2 rounded-full {st.dot} {st.pulse ? 'animate-pulse' : ''}"></span>
<Badge variant={st.variant} class="text-[10px]">{st.label}</Badge>
</div>
<p class="text-sm font-medium leading-snug">
{$currentTask.goal || $currentTask.title || 'Untitled task'}
</p>
</div>
{/if}