"use client" // The project's runs, live (umbrella TS6.4 and TS6.6): the page's view renders the first page and hands it // over as the seed, so the page arrives complete; this keeps it current through the organization's live // stream -- a run another member starts appears, and each run's status and progress move as its steps // commit, without a reload. The rows come from `agent.run.getMany`, under the same permission as the list, // so the stream itself never carries a run's goal or plan. import type { ProjectSlugs } from "@conusai-kit/ui" import { liveCollection, useDataClient, useLive } from "@conusai-kit/ui/data" import { PageSection, type ResourceColumn, ResourceList } from "@conusai-kit/ui/patterns" import { type ReactElement, useState } from "react" import { RUN_COLLECTION, type RunView } from "../contract/index.js" export interface RunListLabels { readonly heading: string /** What to say when there is no run. */ readonly empty: string readonly goal: string readonly status: string readonly progress: string readonly startedBy: string /** The message of each run status, by status. */ readonly statuses: Record } export function useLiveRuns(slugs: ProjectSlugs, seed: readonly RunView[]): readonly RunView[] { const client = useDataClient() // One collection for the life of the page; the seed is the first render's. const [runs] = useState(() => liveCollection({ client, org: slugs.org, topic: { ws: slugs.workspace, project: slugs.project }, entity: RUN_COLLECTION.entity, key: RUN_COLLECTION.key, seed, list: { procedure: RUN_COLLECTION.list, input: { ...slugs }, rows: (out) => (out as { runs: RunView[] }).runs, }, getMany: { procedure: RUN_COLLECTION.getMany, input: { ...slugs }, rows: (out) => (out as { items: RunView[] }).items, }, }), ) // Newest first, as the list procedure orders them. const { data, isReady } = useLive(runs, (q, rows) => q.from({ run: rows }).orderBy(({ run }: { run: RunView }) => run.createdAt, "desc"), ) // Until the collection is ready the server's rows are the answer, so the first paint matches it. return isReady ? data : seed } export function RunList({ slugs, seed, labels, }: { slugs: ProjectSlugs seed: readonly RunView[] labels: RunListLabels }): ReactElement { const rows = useLiveRuns(slugs, seed) const pageHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/agent` const columns: ResourceColumn[] = [ { id: "goal", header: labels.goal, wrap: true }, { id: "status", header: labels.status, cell: (run) => labels.statuses[run.status] }, { id: "percent", header: labels.progress, cell: (run) => ( {Math.round(run.percent * 100)}% ), }, { id: "actorId", header: labels.startedBy }, ] return ( run.runId} rowHref={(run) => `${pageHref}/${run.runId}`} rowLabel={(run) => run.goal} empty={{ title: labels.empty }} /> ) }