@@ -0,0 +1,113 @@
|
||||
"use client"
|
||||
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@conusai-kit/ui/primitives"
|
||||
|
||||
// One run, live (umbrella TS5.8): the page's loader reads the run from `agent.run.status` and its view
|
||||
// hands it over as the seed, so the page arrives complete; this island then follows the run's AG-UI
|
||||
// event stream (`agent.run.events`) through TanStack AI's resumable connection
|
||||
// (`agentRunConnection`, `watchAgentRun` from `@conusai-kit/agent/client`) — step states, the
|
||||
// status, a parked approval and the completed run's answer appear without a reload, and no request
|
||||
// polls the run. A dropped stream reconnects with its last offset inside the connection.
|
||||
|
||||
import { agentRunConnection, initialAgentRunView, watchAgentRun } from "@conusai-kit/agent/client"
|
||||
import { AiDisclosureBadge, type ProjectSlugs } from "@conusai-kit/ui"
|
||||
import { type ReactElement, useEffect, useState } from "react"
|
||||
import type { RunView } from "../contract/index.js"
|
||||
import { liveRunState } from "./live-run-state.js"
|
||||
import { RunControls, type RunControlsLabels } from "./run-controls.js"
|
||||
|
||||
export interface LiveRunLabels {
|
||||
readonly status: string
|
||||
readonly progress: string
|
||||
readonly plan: string
|
||||
readonly step: string
|
||||
readonly tool: string
|
||||
readonly needsApproval: string
|
||||
readonly answer: string
|
||||
readonly answerDisclosure: string
|
||||
readonly streamLost: string
|
||||
/** The message of each run status, by status. */
|
||||
readonly statuses: Record<RunView["status"], string>
|
||||
/** The message of each step status, by status. */
|
||||
readonly stepStatuses: Record<RunView["plan"][number]["status"], string>
|
||||
readonly controls: RunControlsLabels
|
||||
}
|
||||
|
||||
export function LiveRun({
|
||||
slugs,
|
||||
seed,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
seed: RunView
|
||||
labels: LiveRunLabels
|
||||
}): ReactElement {
|
||||
const [view, setView] = useState(() => initialAgentRunView(seed.runId))
|
||||
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams({ org: slugs.org, workspace: slugs.workspace, project: slugs.project })
|
||||
const connection = agentRunConnection({ url: `/api/v1/agent.run.events?${query.toString()}` })
|
||||
const stop = new AbortController()
|
||||
void watchAgentRun({ connection, runId: seed.runId, signal: stop.signal, onChange: setView })
|
||||
return () => stop.abort()
|
||||
}, [slugs.org, slugs.workspace, slugs.project, seed.runId])
|
||||
|
||||
const state = liveRunState(seed, view)
|
||||
return (
|
||||
<div data-live="agent.run">
|
||||
<dl>
|
||||
<dt>{labels.status}</dt>
|
||||
<dd role="status">{labels.statuses[state.status]}</dd>
|
||||
</dl>
|
||||
<progress value={state.percent} max={1} aria-label={labels.progress}>
|
||||
{Math.round(state.percent * 100)}%
|
||||
</progress>
|
||||
{state.streamError !== null ? <p role="alert">{labels.streamLost}</p> : null}
|
||||
<RunControls
|
||||
slugs={slugs}
|
||||
run={{ runId: seed.runId, status: state.status, failureCode: state.failureCode }}
|
||||
labels={labels.controls}
|
||||
/>
|
||||
<section aria-labelledby="plan-heading">
|
||||
<h2 id="plan-heading">{labels.plan}</h2>
|
||||
<Table>
|
||||
<TableCaption>{labels.plan}</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="col">{labels.step}</TableHead>
|
||||
<TableHead scope="col">{labels.tool}</TableHead>
|
||||
<TableHead scope="col">{labels.status}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{seed.plan.map((step, index) => {
|
||||
const live = state.steps[index]
|
||||
return (
|
||||
<TableRow key={step.key}>
|
||||
<TableCell>
|
||||
{step.label}
|
||||
{step.needsApproval ? ` (${labels.needsApproval})` : ""}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code>{step.tool}</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{labels.stepStatuses[live?.status ?? step.status]}
|
||||
{live?.error ? `: ${live.error}` : ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
{state.answer !== null ? (
|
||||
<section aria-labelledby="answer-heading">
|
||||
<h2 id="answer-heading">{labels.answer}</h2>
|
||||
<AiDisclosureBadge label={labels.answerDisclosure} provenance={state.answerProvenance} />
|
||||
<p>{state.answer}</p>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user