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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user