/** * The conversation map: what was asked, what the agent did about it, what it * delegated, where it failed, and what CFC said — read top to bottom. * * Vertical because a conversation is: turns follow turns, a delegation nests * under the call that made it, and a reader scans down rather than sideways. * Every node leads to its step, so this is also how you jump around a run. */ import { html, LitElement, nothing, type TemplateResult } from "lit"; import type { ConsoleCellLabelsSummary } from "../cell-labels.ts"; import type { ConsoleFlow, ConsoleFlowNode } from "./api.ts"; import "./cell-chip.ts"; /** How long a numeric run has to be before it reads as a channel. */ const WIDE_NUMERIC_RUN = 32; /** * What the run's per-cell labels amount to, in one line. * * A cell drawn with no atoms says one thing under a run whose space was read * and another under a run where nobody asked, and the chip cannot tell those * apart on its own. The header carries the difference, so an empty chip is * read against a stated regime rather than as an absence of secrets. * * "So many of so many carry one" is a count over a reading that covered every * cell it names. Where a cell of the run was read only in part, the count is * a floor — another label may sit at a path nothing was read at — so the line * leads with the reading being partial rather than with a total. */ export const labelRegime = (labels: ConsoleCellLabelsSummary): string => { if (labels.status === "absent") { return "cell labels not read"; } if (labels.status === "unavailable") { return `cell labels unavailable${ labels.detail === undefined ? "" : ` · ${labels.detail}` }`; } const carry = `${labels.cellsLabelled} of ${labels.cellsRead} carry one`; return labels.cellsPartial === 0 ? `cell labels read · ${carry}` : `cell labels read in part · ${carry}, ${labels.cellsPartial} only partly read`; }; export class ConsoleFlowView extends LitElement { static override properties = { flow: { attribute: false }, focusStep: { attribute: false }, focusRunId: { attribute: false }, }; declare flow: ConsoleFlow | undefined; /** The step the timeline is on, marked here so the two stay in step. */ declare focusStep: number | undefined; /** The run that step belongs to, since a child's steps number from zero. */ declare focusRunId: string | undefined; protected override createRenderRoot(): HTMLElement { return this; } #node(node: ConsoleFlowNode): TemplateResult { const here = node.step === this.focusStep && (this.focusRunId === undefined || node.runId === this.focusRunId); const wide = (node.longestNumericRun ?? 0) >= WIDE_NUMERIC_RUN; return html`
  • ${node.reads.length === 0 && node.produces.length === 0 && node.entersScope.length === 0 ? nothing : html`
    ${node.reads.map((cell) => html` reads ${cell.as === undefined ? nothing : html`${cell.as}`} ` )} ${node.produces.map((cell) => html` makes ` )} ${node.entersScope.map((cell) => html` in scope ` )}
    `} ${node.text === undefined ? nothing : html`
    ${node.text}
    `} ${node.children.length === 0 ? nothing : html` `}
  • `; } protected override render(): TemplateResult { const flow = this.flow; if (flow === undefined) { return html`

    Reading…

    `; } if (flow.turns.length === 0) { return html`

    This run did nothing to map.

    `; } return html`
    ${flow.cfc === undefined ? nothing : html` ${flow.cfc.posture ?? "first-party"} · labels ${flow.cfc.flowLabels} · ${flow.cfc.enforcementMode} `} ${flow.cellLabels === undefined ? nothing : html` ${labelRegime(flow.cellLabels)} `} ${flow.failures === 0 ? nothing : html`${flow.failures} failed`} ${flow.denials === 0 ? nothing : html`${flow.denials} denied`} ${flow.unwiredPatterns === 0 ? nothing : html` ${flow.unwiredPatterns} unwired `}
    ${flow.turns.map((turn) => html`
    ` )}
    `; } } customElements.define("console-flow-view", ConsoleFlowView);