/** * One run, read whole. The timeline is the default reading — the run step by * step, with what went into each call and what came back — and the other panes * are the same run seen from a different angle. */ import { html, LitElement, nothing, type TemplateResult } from "lit"; import { type ConsoleRunDetail, readRun, readRunFile } from "./api.ts"; import "./steps-view.ts"; /** Which pane of the open run is showing. */ type Pane = "timeline" | "patterns" | "tool-outputs" | "artifacts"; const prettyJson = (text: string): string => { try { return JSON.stringify(JSON.parse(text), null, 2); } catch { // A payload that is not JSON is still worth showing as it stands. return text; } }; export class ConsoleRunView extends LitElement { static override properties = { runId: { attribute: false }, detail: { attribute: false }, pane: { attribute: false }, focusStep: { attribute: false }, rawName: { attribute: false }, rawText: { attribute: false }, error: { attribute: false }, }; declare runId: string | undefined; declare detail: ConsoleRunDetail | undefined; declare pane: Pane; /** The step the map asked the timeline to show. */ declare focusStep: number | undefined; declare rawName: string | undefined; declare rawText: string | undefined; declare error: string | undefined; /** * Which read of a run is the current one. A running turn re-reads on every * tool completion and a click can switch runs mid-read, so two reads are * routinely in flight and the network is free to answer them in either order; * only the newest one is allowed to say what the pane shows. */ #reads = 0; constructor() { super(); this.pane = "timeline"; } protected override createRenderRoot(): HTMLElement { return this; } protected override willUpdate(changed: Map): void { if (changed.has("runId") && this.runId !== changed.get("runId")) { this.rawName = undefined; this.rawText = undefined; this.pane = "timeline"; void this.refresh(); } } /** Re-reads the open run, which a running turn's new steps arrive through. */ async refresh(): Promise { const read = ++this.#reads; const runId = this.runId; if (runId === undefined) { this.detail = undefined; return; } try { const detail = await readRun(runId); if (read !== this.#reads) { return; } this.detail = detail; this.error = undefined; } catch (error) { if (read !== this.#reads) { return; } this.error = error instanceof Error ? error.message : String(error); } } /** * Reads one file into the raw pane. What was asked for is remembered whole — * the run, the pane, and the name — because a second click while the first * file is still being read is what the pane is for, and a file that arrives * after the selection moved on is no longer what is showing. */ async #showRaw( kind: "artifacts" | "tool-outputs", name: string, ): Promise { const detail = this.detail; if (detail === undefined) { return; } const runId = detail.summary.runId; const pane = this.pane; this.rawName = name; this.rawText = undefined; const stillShowing = (): boolean => this.rawName === name && this.pane === pane && this.detail?.summary.runId === runId; try { const text = prettyJson(await readRunFile(runId, kind, name)); if (stillShowing()) { this.rawText = text; } } catch (error) { if (stillShowing()) { this.rawText = error instanceof Error ? error.message : String(error); } } } #patterns(detail: ConsoleRunDetail): TemplateResult { const { patternAttempts, searches, feedback, pieces } = detail.lens; if ( patternAttempts.length === 0 && searches.length === 0 && feedback.length === 0 && pieces.length === 0 ) { return html`

This run did no pattern work.

`; } return html` ${searches.map((search) => html`
searched ${search.query ?? ""}
${search.hits.length === 0 ? html`
no matches
` : html`
    ${search.hits.map((hit) => html`
  • ${hit.description ?? hit.patternId ?? "unnamed"} ${hit.score === undefined ? nothing : html` ${hit.score.toFixed(2)} `}
  • ` )}
`}
` )} ${patternAttempts.map((attempt, index) => html`
attempt ${index + 1} ${attempt.status} ${attempt.patternId === undefined ? nothing : html`${attempt.patternId}`} ${attempt.inputNames.length === 0 ? nothing : html` inputs: ${attempt.inputNames.join(", ")} `}
${attempt.message === undefined ? nothing : html`
${attempt.message}
`} ${attempt.source === undefined ? nothing : html`
${attempt.source}
`}
` )} ${pieces.map((piece) => html`
named ${piece.slug ?? ""}
${piece.url === undefined ? nothing : html` ${piece.url} `}
` )} ${feedback.map((record) => html`
feedback ${record.verdict ?? ""} ${record.patternId ?? ""}
${record.note === undefined ? nothing : html`
${record.note}
`}
` )} `; } #files( kind: "artifacts" | "tool-outputs", names: readonly string[], ): TemplateResult { if (names.length === 0) { return html`

This run wrote none.

`; } return html`
${names.map((name) => html` ` )}
${this.rawName === undefined || !names.includes(this.rawName) ? nothing : html`
${this.rawText ?? "reading…"}
`} `; } protected override render(): TemplateResult { if (this.runId === undefined) { return html`

Choose a run to read what it did, step by step.

`; } if (this.error !== undefined) { return html`

${this.error}

`; } const detail = this.detail; if (detail === undefined) { return html`

Reading…

`; } const tab = (pane: Pane, label: string) => html` `; return html`
${detail.summary.title ?? detail.summary.runId}
${detail.summary.runId} ${detail.summary.model === undefined ? nothing : html`· ${detail.summary.model}`} ${detail.summary.terminalReason === undefined ? nothing : html`· ${detail.summary.terminalReason}`}
${detail.summary.failure === undefined ? nothing : html`
${detail.summary.failure.kind}: ${detail.summary.failure.detail}
`}
${tab("timeline", `Timeline (${detail.steps.length})`)} ${tab("patterns", "Patterns")} ${tab( "tool-outputs", `Tool outputs (${detail.toolOutputNames.length})`, )} ${tab("artifacts", `Artifacts (${detail.artifactNames.length})`)}
${this.pane === "timeline" ? html` ` : this.pane === "patterns" ? this.#patterns(detail) : this.pane === "tool-outputs" ? this.#files("tool-outputs", detail.toolOutputNames) : this.#files("artifacts", detail.artifactNames)}
`; } } customElements.define("console-run-view", ConsoleRunView);