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:
2026-07-11 13:52:36 +02:00
parent 413bf54daf
commit 991e7d0900
11 changed files with 652 additions and 13 deletions

View File

@@ -44,6 +44,51 @@ export async function deleteSession(sessionId: string): Promise<boolean> {
return res.ok
}
export interface PlanStep {
id: string
seq: number
title: string
detail: string
status: 'pending' | 'running' | 'done' | 'failed' | 'skipped' | 'blocked'
execution_id?: string
target_slug?: string
started_at?: string
finished_at?: string
}
export async function fetchPlan(sessionId: string): Promise<PlanStep[]> {
const res = await fetch(`${BASE}/sessions/${sessionId}/plan`)
if (!res.ok) return []
const data = await res.json()
return data.steps ?? []
}
export interface SessionQuestion {
id: string
prompt: string
context: { why?: string; options?: string[]; entities?: string[] }
status: 'open' | 'answered' | 'dismissed'
answer?: string
created_at: string
answered_at?: string
}
export async function fetchQuestions(sessionId: string): Promise<SessionQuestion[]> {
const res = await fetch(`${BASE}/sessions/${sessionId}/questions`)
if (!res.ok) return []
const data = await res.json()
return data.questions ?? []
}
export async function answerQuestion(sessionId: string, questionId: string, answer: string): Promise<boolean> {
const res = await fetch(`${BASE}/sessions/${sessionId}/questions/${questionId}/answer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answer })
})
return res.ok
}
export interface ChatEvent {
type: string
data: any