114 lines
4.4 KiB
TypeScript
114 lines
4.4 KiB
TypeScript
"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>
|
|
)
|
|
}
|