Skip to content

Element gallery

Every element on this page is live. The views of @contraagent/ui show the demo pipeline’s run (<ca-monitor> reads an export of it); the generic elements of @contraagent/elements show the hand-written data of their examples. What an element sends shows under it.

The whole monitor: header, run switcher, tabs, filters and the views, live. It owns the data (MonitorStore) and the view route (ViewRouter); every view below it only renders what it is given and sends ca-* events back up.

<ca-monitor> on a page of its own

The run as a graph of its flow, one layer at a time (<cae-layered-graph> draws it). A layer shows the kinds of the tasks its function created (a kind node each, joined to the ones it waits for) and the functions it called (a function node each, holding their tasks); nodes created in one loop are drawn in a loop box, with its last pass ("up to 3 passes") and, when its units ran different numbers of passes, how many ran each ("1 pass: 4 | 2 passes: 1"); a nested loop's box is inside it, and a dotted edge inside each box, from its last layer back to its first, is the return to its next pass. A kind node names who runs its tasks (✦ and an accent: an agent task; ⚙: a Python task); a function node and a loop box carry the mark of an inner view (their layer, their passes). Inside a node, one mark per unit, at the same place in every node, so a unit can be followed across the layer: a task in a kind node, a flow call in a function node; in a loop box, the unit's latest pass, with the number of passes it ran when more than 1. A mark of several tasks shows the most severe state among them (failed, running, not started, waiting, skipped, done); a flow call's mark counts every task of it and of the flow calls it started, and its own error (raised or closed) as failed. A node's count is its tasks done of its tasks, with the others by state, and a function node names its flow calls that stopped with an error. A problem has a square mark (failed: red; not started: hatched), every other state a round one. Above 200 marks, a node shows a bar of its counts and names its failed marks. A solid edge waits for an output; a dashed edge was created after a result; an edge from outside the layer starts at a named stub. A click selects a mark, a node or a loop box (sel); the card beside it shows its facts and links to where it leads: a mark to its task detail, a kind node to its tasks, a function node to its layer, a loop box to its passes. A double click, or Enter, follows the first of them. Hovering or focusing a mark highlights its unit and the tasks joined to it, as the selected mark does. The graph opens on the first layer with more than one node (a static flow's kinds), or on a single function node whose flow call raised; a layer the run does not have opens that one, with a line naming it, and the view route is set right in place (a function named by its qualified name alone opens its layer when the run reaches it by one flow path). A loop's passes are a row per unit and a column per node and pass, a nested loop's passes as a group of columns inside each pass of the outer one, the units sortable by number of passes or by status (<cae-mark-matrix>). The breadcrumb, shown on every layer below the top (a loop named by its file and line), and Escape go back up. On a narrow screen the graph runs top to bottom. "Graph | Table" changes the view; in a loop's passes, "Graph" opens the layer the loop is in, with its box selected. A height set on the element (or given it by a flex or grid container) is the graph's, less the outline's line above it: the drawing fits in it.

The transcript of one task: every round (one session of the coding agent, with its command line and prompt), chat bubbles, collapsible tool cards (a web search shows its query and the pages it returned as links, a web page read its URL as a link), tailed live. A round is a try (a new session) or a fix-up (the same session, sent back because its output failed its checks); give it the task's journal rows (.rows) and each round says how it ended. It draws them with <cae-transcript>. Give it .events to show a saved transcript, or run + stream (and base-url) to tail one from the monitor. A new .events that starts with the same event objects as the last one is that transcript grown: its new events are added below and what is drawn stays. Sends ca-navigate with round when the reader picks a round, and a non-bubbling ca-rounds (RoundsChangedDetail) whenever its rounds or the shown round change.

One task: who ran it, every finished attempt, and four views of it: the transcript (live), its input (params, upstream outputs, declared inputs, the prompt), its output (files against their contracts, the result, who uses it) and its definition. Sends ca-navigate with view or round, and ca-open-task for a linked task.

What defines each kind of task in a run: a list of kinds, and the selected one's definition. Fetches /definitions itself; while the run is live and a kind has not recorded its definition yet, it asks again. A pick sends ca-navigate {def}.

Every finished attempt as a bar as long as it took, on its kind's lane, the running ones growing until now (the last six hours of the run), as <cae-lanes> (lanesFromJournal).

The run in numbers, as <cae-stat-tiles>: tasks and how they ended (with plan; from the journal alone otherwise), attempts, failed attempts, agent time, retries, corrections and model switches (kpiTiles). The parts of <cae-stat-tiles> are its own: a page styles them as ca-kpis::part(tile).

The run's history: every finished attempt, newest first (journal.jsonl), with a chip per outcome that filters by it (ca-filter). A row opens the task's detail, with a click or with Enter.

Every task of the run, one row each (deriveRun(...).rows: the plan joined with the journal, so a task that never ran is a row too): what happened to it, its attempts, time, tokens and cost. A row opens the task's detail (ca-open-task), with a click or with Enter.

The files each task must write (OutputContract) and read (InputContract), problems first, each checked field by field against its schema. Links open the task (ca-open-task).

A pipeline’s own view: the demo run’s output files as a tree, each file’s code beside it (examples/panels/files.py of the Python package).

A pipeline's own view drawn from its typed body (PanelBody): tiles, a table, charts, a status grid, notes, code, trees, and blocks of them one under another, each under its title (folded when the body says collapsed). A tree whose nodes hold code shows the selected node's code beside it; a node with a link leads to a body of the panel (its anchor), a run panel's tab or a task. A body of another shape, inside blocks, is shown as a JSON tree.

A CI pipeline’s page built from the elements below: a theme switch, stat tiles, a sortable table and a layered graph with a detail panel.

Source: web/packages/elements/docs/examples/ci-pipeline.ts
web/packages/elements/docs/examples/ci-pipeline.ts
// A whole page from the generic elements: one run of a made-up CI pipeline.
// A theme switch, stat tiles, a sortable table of jobs, and a layered graph of
// the steps (a shard is a series; the end-to-end tests repeat in a retry loop),
// with a detail panel of the page's own beside them. The page holds the data
// and the state (the theme, the selection, the order), and answers the events.
import '@contraagent/elements/segmented/define'; // <cae-segmented>
import '@contraagent/elements/tiles/define'; // <cae-stat-tiles>
import '@contraagent/elements/data-table/define'; // <cae-data-table>
import '@contraagent/elements/graph/define'; // <cae-layered-graph>
import { adoptThemes } from '@contraagent/elements/themes';
import { fmtDur, pill, type StateStyles } from '@contraagent/elements';
import { sortRows, type CaeDataTable, type DataColumn } from '@contraagent/elements/data-table';
import type { GraphInput, GraphMark, GraphNodeInput } from '@contraagent/elements/graph';
import type { StatTile } from '@contraagent/elements/tiles';
// ---- The data: a step is a node, a job is one run of a step (or of one shard of it).
type State = 'done' | 'failed' | 'running' | 'queued';
interface Job {
id: string;
step: string;
/** The shard ('1' to '3'), or null for a step that runs once. */
shard: string | null;
/** The pass of the retry loop it ran in (1 outside the loop). */
pass: number;
state: State;
seconds: number;
runner: string;
}
const steps = [
{ id: 'lint', label: 'lint', loop: false },
{ id: 'unit', label: 'unit tests', loop: false },
{ id: 'build', label: 'build', loop: false },
{ id: 'e2e', label: 'e2e tests', loop: true },
{ id: 'triage', label: 'triage failures', loop: true },
{ id: 'deploy', label: 'deploy preview', loop: false },
];
const shards = ['1', '2', '3'];
const job = (step: string, shard: string | null, pass: number, state: State, seconds: number, runner: string): Job => ({
id: `${step}${shard ? `/${shard}` : ''}#${pass}`,
step,
shard,
pass,
state,
seconds,
runner,
});
const jobs: Job[] = [
job('lint', null, 1, 'done', 41, 'small-1'),
job('unit', '1', 1, 'done', 188, 'large-1'),
job('unit', '2', 1, 'done', 203, 'large-2'),
job('unit', '3', 1, 'done', 176, 'large-3'),
job('build', null, 1, 'done', 315, 'large-1'),
job('e2e', '1', 1, 'done', 402, 'large-2'),
job('e2e', '2', 1, 'failed', 377, 'large-3'),
job('e2e', '3', 1, 'done', 395, 'large-1'),
job('triage', null, 1, 'done', 12, 'small-1'),
job('e2e', '2', 2, 'done', 381, 'large-2'),
job('triage', null, 2, 'done', 9, 'small-1'),
job('deploy', null, 1, 'running', 74, 'deploy-1'),
];
// ---- What each state looks like: a word, a tone, a shape (the legend follows this order).
const states: StateStyles = {
done: { label: 'done', tone: 'done', shape: 'round', rank: 0 },
queued: { label: 'queued', tone: 'muted', shape: 'ring', rank: 1 },
running: { label: 'running', tone: 'running', shape: 'round', rank: 2 },
failed: { label: 'failed', tone: 'failed', shape: 'square', rank: 3 },
};
const toneOf = (s: State) => states[s]!.tone;
const jobName = (j: Job) => `${steps.find((s) => s.id === j.step)!.label}${j.shard ? ` shard ${j.shard}` : ''}`;
// ---- The graph: a mark per shard (or one loose mark), the last pass's state, the passes as a badge.
/** The id of a mark: a step's shard, or the step's single job. */
const markId = (j: Job) => `${j.step}/${j.shard ?? '-'}`;
function mark(runs: Job[]): GraphMark {
const last = runs[runs.length - 1]!;
const passes = runs.length > 1 ? `${runs.length} passes` : '1 pass';
return { id: markId(last), state: last.state, label: `${jobName(last)} | ${last.state} | ${passes}`, tip: jobName(last), badge: runs.length > 1 ? String(runs.length) : undefined };
}
function node(step: (typeof steps)[number]): GraphNodeInput {
const own = jobs.filter((j) => j.step === step.id);
const runsOf = (shard: string | null) => own.filter((j) => j.shard === shard);
const sharded = own.some((j) => j.shard);
const marks = sharded ? shards.map((s) => (runsOf(s).length ? mark(runsOf(s)) : null)) : [];
const loose = sharded ? [] : [mark(runsOf(null))];
// `counts` and `progress` ("2/3") default to what the marks say.
// A job that failed in an earlier pass is not in the marks: the line under them names it.
const failed = own.filter((j) => j.state === 'failed').length;
return {
id: step.id,
label: step.label,
cluster: step.loop ? 'retry' : null,
marks,
loose,
notes: failed ? [{ text: `${failed} failed ${failed === 1 ? 'job' : 'jobs'}`, tone: 'failed' }] : [],
};
}
const graph: GraphInput = {
series: shards,
nodes: steps.map(node),
clusters: [{ id: 'retry', parent: null, label: 'retry loop', note: 'up to 3 passes', cycle: true }],
edges: [
{ from: 'lint', to: 'build', style: 'solid', weight: 1, ranks: true },
{ from: 'unit', to: 'build', style: 'solid', weight: 3, ranks: true },
{ from: 'build', to: 'e2e', style: 'solid', weight: 3, ranks: true },
{ from: 'e2e', to: 'triage', style: 'solid', weight: 3, ranks: true },
// The next pass: from triage back to the tests. It does not set the layers.
{ from: 'triage', to: 'e2e', style: 'solid', weight: 1, ranks: false },
{ from: 'triage', to: 'deploy', style: 'solid', weight: 1, ranks: true },
],
stubs: [{ id: 'push', label: 'push to main', to: 'lint', style: 'solid', weight: 1 }],
};
// ---- The table: a row per job; every column sorts.
const columns: DataColumn<Job>[] = [
{ id: 'job', header: 'Job', rowHeader: true, cell: jobName, sortable: true },
{ id: 'pass', header: 'Pass', align: 'end', width: '72px', cell: (j) => j.pass, compare: (a, b) => a.pass - b.pass },
{ id: 'state', header: 'State', width: '120px', cell: (j) => pill({ text: j.state, tone: toneOf(j.state) }), compare: (a, b) => states[a.state]!.rank - states[b.state]!.rank },
{ id: 'time', header: 'Time', align: 'end', width: '96px', cell: (j) => fmtDur(j.seconds), compare: (a, b) => a.seconds - b.seconds },
{ id: 'runner', header: 'Runner', width: '120px', cell: (j) => j.runner, sortable: true },
];
// ---- The tiles.
const failed = jobs.filter((j) => j.state === 'failed');
const passes = Math.max(...jobs.map((j) => j.pass));
const count = (s: State) => jobs.filter((j) => j.state === s).length;
const tiles: StatTile[] = [
{ id: 'jobs', label: 'Jobs', value: jobs.length, sub: [[`${count('done')} done`, `${count('running')} running`]] },
{ id: 'failed', label: 'Failed jobs', value: failed.length, tone: failed.length ? 'failed' : undefined, sub: [failed.length ? failed.map((j) => `${jobName(j)}, pass ${j.pass}`) : ['All jobs passed']] },
{ id: 'passes', label: 'Passes of the retry loop', value: passes, sub: [['Up to 3']] },
{ id: 'time', label: 'Job time', value: fmtDur(jobs.reduce((t, j) => t + j.seconds, 0)), tip: 'Sum over all jobs' },
];
// ---- The page's own styles. Its colours are the theme's, so the page changes with the switch.
const css = `
.ci-page {
background: var(--ca-bg);
color: var(--ca-text);
font: 14px/1.45 system-ui, sans-serif;
padding: 16px;
display: grid;
gap: 16px;
container-type: inline-size;
}
.ci-page header { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.ci-page h2 { font-size: 18px; margin: 0; flex: 1; color: var(--ca-text); }
.ci-page h3 { font-size: 13px; font-weight: 600; margin: 0 0 8px; color: var(--ca-text-secondary); }
.ci-page .body { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr) 260px; align-items: start; }
.ci-page .main { display: grid; gap: 16px; min-width: 0; }
.ci-page aside { background: var(--ca-card); border: 1px solid var(--ca-line); border-radius: 8px; padding: 12px; position: sticky; top: 0; }
.ci-page dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0; }
.ci-page dt { color: var(--ca-text-muted); }
.ci-page dd { margin: 0; }
@container (max-width: 760px) { .ci-page .body { grid-template-columns: minmax(0, 1fr); } }
`;
// ---- The page: build it, then answer the events.
export function mount(host: HTMLElement): void {
adoptThemes(document);
host.dataset['caTheme'] = 'contraagent-dark';
const page = document.createElement('div');
page.className = 'ci-page';
page.innerHTML = `<style>${css}</style>
<header><h2>web-app | pipeline 482 | main</h2></header>
<div class="body">
<div class="main">
<section><h3>Pipeline</h3></section>
<section></section>
</div>
<aside aria-live="polite"><h3>Selected</h3><div class="detail">No selection</div></aside>
</div>`;
const [graphBox, tableBox] = page.querySelectorAll('section');
const detail = page.querySelector<HTMLElement>('.detail')!;
// The theme: `data-ca-theme` on the box that holds the page.
const theme = document.createElement('cae-segmented');
theme.label = 'Theme';
theme.options = [
{ value: 'contraagent-dark', label: 'Dark' },
{ value: 'contraagent-light', label: 'Light' },
{ value: 'high-contrast', label: 'High contrast' },
];
theme.value = 'contraagent-dark';
theme.addEventListener('cae-change', (e) => {
theme.value = e.detail.value;
host.dataset['caTheme'] = e.detail.value;
});
page.querySelector('header')!.append(theme);
const stats = document.createElement('cae-stat-tiles');
stats.columns = 4;
stats.tiles = tiles;
page.querySelector('header')!.after(stats);
const table = document.createElement('cae-data-table') as CaeDataTable<Job>;
table.caption = 'Jobs of pipeline 482';
table.columns = columns;
table.rowKey = (j) => j.id;
table.rows = jobs;
table.addEventListener('cae-sort', (e) => {
table.sort = e.detail;
table.rows = sortRows(jobs, columns, e.detail);
});
tableBox!.append(table);
const view = document.createElement('cae-layered-graph');
view.key = 'pipeline-482'; // a new key is a new graph: the frame shows all of it again
view.graph = graph;
view.states = states;
view.strings = { cycle: 'next pass', badge: 'number of passes' };
graphBox!.append(view);
/** The selection: the graph shows it, the panel describes it. */
const select = (id: string | null) => {
view.selected = id;
detail.replaceChildren(describe(id));
};
view.addEventListener('cae-select', (e) => select(e.detail.id));
// A row of the table selects its job's mark in the graph.
table.addEventListener('cae-activate', (e) => select(markId(jobs.find((j) => j.id === e.detail.id)!)));
host.replaceChildren(page);
}
/** The panel's content for a mark, a node or the loop. */
function describe(id: string | null): Node {
const runs = jobs.filter((j) => markId(j) === id);
const step = steps.find((s) => s.id === id);
let title = 'No selection';
let rows: [string, string][] = [];
if (runs.length) {
const last = runs[runs.length - 1]!;
title = jobName(last);
rows = [['State', last.state], ['Passes', String(runs.length)], ...runs.map((j): [string, string] => [`Pass ${j.pass}`, `${j.state} | ${fmtDur(j.seconds)} | ${j.runner}`])];
} else if (step) {
const own = jobs.filter((j) => j.step === step.id);
title = step.label;
rows = [['Jobs', String(own.length)], ['Failed', String(own.filter((j) => j.state === 'failed').length)], ['Time', fmtDur(own.reduce((t, j) => t + j.seconds, 0))]];
} else if (id === 'retry') {
title = 'retry loop';
rows = [['Passes', `${passes} of up to 3`], ['Rerun', jobs.filter((j) => j.pass > 1 && j.shard).map(jobName).join(', ')]];
}
const box = document.createElement('div');
const h = document.createElement('strong');
h.textContent = title;
const dl = document.createElement('dl');
for (const [k, v] of rows) {
const dt = document.createElement('dt');
const dd = document.createElement('dd');
dt.textContent = k;
dd.textContent = v;
dl.append(dt, dd);
}
box.append(h, dl);
return box;
}
Source: web/packages/elements/docs/examples/data-table.ts
web/packages/elements/docs/examples/data-table.ts
// A data table: hand-written rows, one sortable column, a row's action. The
// host holds the rows and the order, and answers the events.
import '@contraagent/elements/data-table/define';
import { sortRows, type CaeDataTable, type DataColumn } from '@contraagent/elements/data-table';
import { pill } from '@contraagent/elements';
interface Job {
id: string;
name: string;
minutes: number;
state: 'done' | 'failed' | 'running';
}
const jobs: Job[] = [
{ id: 'j1', name: 'Build', minutes: 4, state: 'done' },
{ id: 'j2', name: 'Unit tests', minutes: 11, state: 'failed' },
{ id: 'j3', name: 'Deploy preview', minutes: 2, state: 'running' },
];
const columns: DataColumn<Job>[] = [
{ id: 'name', header: 'Job', rowHeader: true, cell: (j) => j.name },
{ id: 'minutes', header: 'Minutes', align: 'end', width: '96px', cell: (j) => j.minutes, compare: (a, b) => a.minutes - b.minutes },
{ id: 'state', header: 'State', width: '120px', cell: (j) => pill({ text: j.state, tone: j.state }) },
];
export function mount(host: HTMLElement): void {
const table = document.createElement('cae-data-table') as CaeDataTable<Job>;
table.caption = 'Jobs';
table.columns = columns;
table.rowKey = (j) => j.id;
table.rows = jobs;
table.addEventListener('cae-sort', (e) => {
table.sort = e.detail;
table.rows = sortRows(jobs, columns, e.detail);
});
const shown = document.createElement('p');
table.addEventListener('cae-activate', (e) => (shown.textContent = `Opened: ${e.detail.id}`));
host.replaceChildren(table, shown);
}
Source: web/packages/elements/docs/examples/graph.ts
web/packages/elements/docs/examples/graph.ts
// A layered graph: a hand-written build pipeline of three steps, the last two in a
// cluster that repeats (a retry loop), one mark per service in each step. The host
// keeps the selection and the order, and answers the events.
import '@contraagent/elements/graph/define';
import type { GraphInput, GraphMark, GraphNodeInput } from '@contraagent/elements/graph';
import type { StateStyles } from '@contraagent/elements/primitives';
const states: StateStyles = {
ok: { label: 'ok', tone: 'done', shape: 'round', rank: 0 },
busy: { label: 'busy', tone: 'running', shape: 'round', rank: 2 },
broken: { label: 'broken', tone: 'failed', shape: 'square', rank: 3 },
queued: { label: 'queued', tone: 'neutral', shape: 'ring', rank: 1 },
};
const services = ['api', 'web', 'worker'];
/** One mark per service: its id is unique in the graph, so it is what the graph selects. */
const marks = (step: string, looks: string[]): GraphMark[] =>
services.map((s, i) => ({ id: `${step}#${s}`, state: looks[i] ?? 'queued', label: `${step} · ${s} | ${looks[i] ?? 'queued'}`, tip: `${step} · ${s}` }));
/** A step: its counts and its header's count ("2/3") come from its marks. */
const node = (id: string, label: string, looks: string[], cluster: string | null): GraphNodeInput => ({ id, label, cluster, marks: marks(id, looks) });
const graph: GraphInput = {
series: services,
nodes: [
node('build', 'build', ['ok', 'ok', 'ok'], null),
node('test', 'test', ['ok', 'broken', 'ok'], 'retry'),
node('deploy', 'deploy', ['busy', 'queued', 'ok'], 'retry'),
],
clusters: [{ id: 'retry', parent: null, label: 'retry', note: 'up to 3 rounds', cycle: true }],
edges: [
{ from: 'build', to: 'test', style: 'solid', weight: 3, ranks: true },
{ from: 'test', to: 'deploy', style: 'solid', weight: 3, ranks: true },
// The next round: back from deploy to test; it does not set the layers.
{ from: 'deploy', to: 'test', style: 'solid', weight: 1, ranks: false },
],
stubs: [{ id: 'commit', label: 'commit', to: 'build', style: 'solid', weight: 1 }],
};
export function mount(host: HTMLElement): void {
const el = document.createElement('cae-layered-graph');
el.graph = graph;
el.states = states;
el.key = 'pipeline';
el.strings = { cycle: 'next round' };
const card = document.createElement('div');
card.slot = 'inspector';
el.append(card);
const shown = document.createElement('p');
el.addEventListener('cae-select', (e) => {
el.selected = e.detail.id;
card.innerHTML = e.detail.id ? `<h4>${e.detail.id}</h4>` : '';
});
el.addEventListener('cae-activate', (e) => (shown.textContent = `Opened: ${e.detail.id}`));
host.replaceChildren(el, shown);
}
Source: web/packages/elements/docs/examples/matrix.ts
web/packages/elements/docs/examples/matrix.ts
// A mark matrix: a hand-written week of nightly checks, a row per service, a column
// per check under a group per night. The host sorts the rows and keeps the selection.
import '@contraagent/elements/matrix/define';
import type { MarkMatrixInput, MatrixRowInput } from '@contraagent/elements/matrix';
import type { StateStyles } from '@contraagent/elements/primitives';
const states: StateStyles = {
pass: { label: 'pass', tone: 'done', shape: 'round', rank: 0 },
fail: { label: 'fail', tone: 'failed', shape: 'square', rank: 2 },
skip: { label: 'skip', tone: 'muted', shape: 'round', rank: 1 },
};
const nights = ['Mon', 'Tue'];
const checks = ['lint', 'unit'];
const results: Record<string, string[]> = { api: ['pass', 'pass', 'pass', 'fail'], web: ['pass', 'skip', 'pass', 'pass'], worker: ['fail', 'fail', 'pass', 'pass'] };
const rows: MatrixRowInput[] = Object.entries(results).map(([service, rs]) => ({
id: service,
label: service,
value: rs.filter((r) => r === 'fail').length,
strong: rs.includes('fail'),
cells: rs.map((state, i) => {
const at = `${nights[Math.floor(i / 2)]} ${checks[i % 2]}`;
return { id: `${service}|${i}`, state, label: `${service} | ${at} | ${state}`, tip: `${service}, ${at}` };
}),
}));
const byFailures = (rs: readonly MatrixRowInput[]) => [...rs].sort((a, b) => Number(b.value) - Number(a.value));
const matrix = (sort: string): MarkMatrixInput => ({
caption: 'Nightly checks',
label: 'Nightly checks by service',
facts: ['2 nights', '3 services'],
head: [
nights.map((n) => ({ id: n, label: n, span: 2, rows: 1, type: 'group' as const })),
nights.flatMap((n) => checks.map((c) => ({ id: `${n}|${c}`, label: c, span: 1, rows: 1, type: 'column' as const }))),
],
columns: nights.flatMap((n) => checks.map((c) => ({ id: `${n}|${c}`, groups: [n] }))),
rows: sort === 'failures' ? byFailures(rows) : rows,
value: { label: 'Failures' },
sorts: [
{ id: 'name', label: 'Name' },
{ id: 'failures', label: 'Failures' },
],
sort,
});
export function mount(host: HTMLElement): void {
const el = document.createElement('cae-mark-matrix');
el.matrix = matrix('name');
el.states = states;
el.strings = { rowsHeader: 'Service' };
el.addEventListener('cae-sort', (e) => (el.matrix = matrix(e.detail.id)));
el.addEventListener('cae-select', (e) => (el.selected = e.detail.id));
host.replaceChildren(el);
}
Source: web/packages/elements/docs/examples/tiles.ts
web/packages/elements/docs/examples/tiles.ts
// Stat tiles: hand-written figures, one with a tone and a link. The host holds
// the figures and answers `cae-action`.
import '@contraagent/elements/tiles/define';
import type { StatTile } from '@contraagent/elements/tiles';
const tiles: StatTile[] = [
{ id: 'jobs', label: 'Jobs', value: 24, sub: [[{ text: '22 passed' }, { text: '2 failed', tone: 'failed' }]] },
{
id: 'failed',
label: 'Failed jobs',
value: 2,
tone: 'failed',
sub: [['Unit tests', 'Lint']],
action: { id: 'list', label: 'Failed jobs in the log' },
},
{ id: 'time', label: 'Build time', value: '14m 05s', sub: [['Longest: Unit tests']], tip: 'Sum over all jobs' },
{ id: 'queued', label: 'Queued', value: 0, sub: [['No jobs waiting']] },
];
export function mount(host: HTMLElement): void {
const grid = document.createElement('cae-stat-tiles');
grid.columns = 4;
grid.tiles = tiles;
const shown = document.createElement('p');
grid.addEventListener('cae-action', (e) => (shown.textContent = `Action: ${e.detail.action} of ${e.detail.id}`));
host.replaceChildren(grid, shown);
}
Source: web/packages/elements/docs/examples/lanes.ts
web/packages/elements/docs/examples/lanes.ts
// Lanes: hand-written jobs on a time axis, a lane per stage, one job running.
// The host names the looks of the bars; their labels are the legend.
import '@contraagent/elements/lanes/define';
import type { Lane, LaneStyle } from '@contraagent/elements/lanes';
const barStyles: Record<string, LaneStyle> = {
passed: { label: 'passed', color: 'var(--ca-status-done)' },
failed: { label: 'failed', color: 'var(--ca-status-failed)', pattern: 'hatched' },
running: { label: 'running', color: 'var(--ca-status-running)', pattern: 'striped' },
};
export function mount(host: HTMLElement): void {
const now = Date.now();
const min = 60e3;
const lanes: Lane[] = [
{
id: 'build',
label: 'Build',
bars: [
{ id: 'b1', start: now - 9 * min, end: now - 6 * min, style: 'failed', label: 'Build #1\nfailed' },
{ id: 'b2', start: now - 6 * min, end: now - 3 * min, style: 'passed', label: 'Build #2\npassed' },
],
},
{
id: 'test',
label: 'Test',
bars: [
{ id: 't1', start: now - 3 * min, end: null, style: 'running', label: 'Unit tests\nrunning' },
{ id: 't2', start: now - 2.5 * min, end: now - 1 * min, style: 'passed', label: 'Lint\npassed' },
],
},
];
const el = document.createElement('cae-lanes');
el.lanes = lanes;
el.barStyles = barStyles;
el.live = true;
el.omitted = 3;
el.strings = { omitted: (n) => `${n} older jobs not shown` };
host.replaceChildren(el);
}
Source: web/packages/elements/docs/examples/charts.ts
web/packages/elements/docs/examples/charts.ts
// Charts: a CI pipeline's minutes by day and stage (stacked columns), its queue
// time (lines), its builds by weekday and hour (a heatmap), how its nightly builds
// ended (a state grid) and its test funnel (horizontal bars). The host gives each
// chart its unit's format.
import '@contraagent/elements/charts/define';
import type { ChartSeries } from '@contraagent/elements/charts';
const days = ['10-08', '10-09', '10-10', '10-11', '10-12', '10-13', '10-14'];
const minutes: ChartSeries[] = [
{ name: 'build', values: [42, 38, 51, 12, 9, 47, 44] },
{ name: 'test', values: [65, 70, 58, 20, null, 61, 66] },
{ name: 'deploy', values: [8, 8, 9, 2, 2, 10, 8] },
];
export function mount(host: HTMLElement): void {
const columns = document.createElement('cae-column-chart');
columns.label = 'Minutes by day';
columns.x = days;
columns.series = minutes;
columns.stacked = true;
columns.format = (v) => `${v} min`;
const lines = document.createElement('cae-line-chart');
lines.label = 'Queue time';
lines.x = days;
lines.series = [
{ name: 'median', values: [1.2, 1.4, 0.9, 0.3, 0.2, 1.8, 1.1] },
{ name: 'slowest', values: [6, 9, 4, 1, 1, 12, 7] },
];
lines.format = (v) => `${v} min`;
const heat = document.createElement('cae-heatmap');
heat.label = 'Builds by weekday and hour';
heat.x = ['9', '10', '11', '12', '13', '14', '15', '16', '17'];
heat.y = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
// A count per weekday and hour; Wednesday at 13 has no record (null: an empty cell).
heat.values = heat.y.map((_, r) => heat.x.map((_, c) => (r === 2 && c === 4 ? null : (r * 7 + c * 3) % 9)));
heat.strings = { scale: (low, high) => `${low} to ${high} builds` };
const bars = document.createElement('cae-bar-chart');
bars.label = 'Tests this week';
bars.bars = [
{ label: 'written', value: 1240 },
{ label: 'run', value: 1180 },
{ label: 'passed', value: 1162 },
{ label: 'flaky', value: 18, tone: 'attention' },
];
const nightly = document.createElement('cae-state-grid');
nightly.label = 'Nightly builds by weekday and hour';
nightly.x = ['0', '1', '2', '3'];
nightly.y = ['Mon', 'Tue'];
const passed = { label: 'passed', tone: 'done' as const };
// A state per weekday and hour: its word, its tone, and a fact for the tooltip; null: no build.
nightly.cells = [
[passed, { label: 'failed', tone: 'failed', tip: 'build 4127' }, passed, null],
[passed, passed, { label: 'skipped', tone: 'muted' }, passed],
];
host.replaceChildren(columns, lines, heat, nightly, bars);
}
Source: web/packages/elements/docs/examples/check-table.ts
web/packages/elements/docs/examples/check-table.ts
// A check table: hand-written fields of a settings file, each checked against
// what it must be. The host judges the values; the table shows the verdicts.
import '@contraagent/elements/check-table/define';
import type { CheckRow } from '@contraagent/elements/check-table';
const rows: CheckRow[] = [
{ name: 'name', expected: 'string', hasValue: true, value: 'release-build', verdict: 'pass', verdictLabel: 'passed' },
{ name: 'retries', expected: 'integer', hasValue: true, value: '3', verdict: 'fail', verdictLabel: 'wrong type', tip: 'Expected integer' },
{ name: 'targets', expected: 'list[str]', hasValue: true, value: ['linux', 'macos', 'windows', 'wasm'], verdict: 'pass', verdictLabel: 'passed' },
{ name: 'cache', expected: 'object', optional: true, hasValue: false, verdict: 'none', verdictLabel: 'absent (optional)' },
{ name: 'colour', expected: 'not in the schema', extra: true, hasValue: true, value: 'blue', verdict: 'warn', verdictLabel: 'extra field' },
];
export function mount(host: HTMLElement): void {
const table = document.createElement('cae-check-table');
table.rows = rows;
table.strings = { name: 'Field', actual: 'In the file' };
host.replaceChildren(table);
}
Source: web/packages/elements/docs/examples/transcript.ts
web/packages/elements/docs/examples/transcript.ts
// A conversation with a support bot in two sessions: the first ended with a
// failed lookup, the second answered. The host holds the sections and decides
// what a reader's pick means: here the picked session is simply shown.
import '@contraagent/elements/transcript/define';
import { md } from '@contraagent/elements/markdown';
import type { TranscriptSection } from '@contraagent/elements/transcript';
const sections: TranscriptSection[] = [
{
id: 's1',
title: 'Session 1',
verdict: { tone: 'failed', text: 'failed', tip: 'The order service did not answer' },
tags: [
{ label: 'bot', value: 'helpdesk' },
{ label: 'model', value: 'small-2', look: 'mono' },
],
items: [
{ type: 'prompt', id: 'p1', text: 'Where is order 1042?' },
{ type: 'reasoning', id: 'r1', text: 'Look the order up first, then answer with its **status**.' },
{ type: 'tool', id: 't1', name: 'orders.get', icon: 'read', summary: 'order 1042', output: 'timeout after 30s', status: 'error' },
],
footer: { tone: 'failed', title: 'The order service did not answer' },
},
{
id: 's2',
title: 'Session 2',
facts: ['small-2'],
verdict: { tone: 'done', text: 'answered' },
items: [
{ type: 'event', id: 'e1', icon: '↻', title: 'Request sent again', text: 'after a timeout' },
{
type: 'tool',
id: 't2',
name: 'web_search',
icon: 'search',
summary: 'carrier tracking 1042',
query: 'carrier tracking 1042',
links: [
{ title: 'Track a parcel', url: 'https://example.com/track' },
{ title: 'Delivery times', url: 'https://example.org/delivery' },
],
output: 'Track a parcel https://example.com/track\nDelivery times https://example.org/delivery',
status: 'ok',
},
{ type: 'notice', id: 'n1', text: 'cache hit: orders/1042' },
{ type: 'message', id: 'm1', text: 'Order **1042** left the warehouse on Monday and arrives on *Thursday*.' },
],
},
];
export function mount(host: HTMLElement): void {
const t = document.createElement('cae-transcript');
t.sections = sections;
t.markdown = md;
t.strings = { sections: 'Sessions', sectionsTip: 'One session per conversation with the bot' };
t.style.height = '420px';
t.addEventListener('cae-section-change', (e) => (t.shown = e.detail.index));
host.replaceChildren(t);
}
Source: web/packages/elements/docs/examples/code.ts
web/packages/elements/docs/examples/code.ts
// Code: a hand-written script, numbered from line 12, two of its lines marked; and
// the same element on a text too long to show whole.
import '@contraagent/elements/code/define';
const script = `def build(target: str, jobs: int = 4) -> bool:
"""Build one target; True when it passed."""
result = run(["make", f"-j{jobs}", target])
if result.returncode != 0:
log.error("build of %s failed", target) # the log names the target
return False
return True
`;
export function mount(host: HTMLElement): void {
const code = document.createElement('cae-code');
code.code = script;
code.language = 'python';
code.path = 'ci/build.py';
code.start = 12;
code.highlight = [15, [16, 17]];
const long = document.createElement('cae-code');
long.code = Array.from({ length: 40 }, (_, i) => `step ${i + 1}: ok`).join('\n');
long.path = 'logs/steps.txt';
long.maxLines = 6;
long.style.marginTop = '12px';
host.replaceChildren(code, long);
}
Source: web/packages/elements/docs/examples/tree.ts
web/packages/elements/docs/examples/tree.ts
// A tree of a CI pipeline's files, paired with a code element: selecting a file shows
// its text. The host holds the selection and answers `cae-select`.
import '@contraagent/elements/code/define';
import '@contraagent/elements/tree/define';
import type { TreeItem } from '@contraagent/elements/tree';
const files: Record<string, string> = {
'ci/build.py': 'def build(target: str) -> bool:\n return run(["make", target]).returncode == 0\n',
'ci/jobs.yaml': 'jobs:\n unit: { needs: [build] }\n lint: { needs: [] }\n',
'ci/tests/test_build.py': 'def test_build():\n assert build("all")\n',
'README.md': 'The CI pipeline of the project.\n',
};
const items: TreeItem[] = [
{
id: 'ci',
name: 'ci',
children: [
{ id: 'ci/build.py', name: 'build.py' },
{ id: 'ci/jobs.yaml', name: 'jobs.yaml', badge: 'changed', tone: 'attention' },
{ id: 'ci/tests', name: 'tests', open: false, children: [{ id: 'ci/tests/test_build.py', name: 'test_build.py', badge: 'failed', tone: 'failed' }] },
],
},
{ id: 'README.md', name: 'README.md' },
];
export function mount(host: HTMLElement): void {
const tree = document.createElement('cae-tree');
tree.items = items;
tree.label = 'Files';
const code = document.createElement('cae-code');
const show = (id: string) => {
tree.selected = id;
code.code = files[id] ?? '';
code.path = id;
code.language = id;
};
tree.addEventListener('cae-select', (e) => e.detail.id && show(e.detail.id));
show('ci/build.py');
const pair = document.createElement('div');
pair.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; align-items: start';
pair.append(tree, code);
host.replaceChildren(pair);
}
Source: web/packages/elements/docs/examples/segmented.ts
web/packages/elements/docs/examples/segmented.ts
// A segmented control: one choice among a few. The host holds the value and
// answers `cae-change`; the words are the host's.
import '@contraagent/elements/segmented/define';
export function mount(host: HTMLElement): void {
const control = document.createElement('cae-segmented');
control.label = 'Period';
control.options = [
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month', title: 'The last 30 days' },
];
control.value = 'week';
const shown = document.createElement('p');
shown.textContent = `Showing: ${control.value}`;
control.addEventListener('cae-change', (e) => (shown.textContent = `Showing: ${e.detail.value}`));
host.replaceChildren(control, shown);
}
Source: web/packages/elements/docs/examples/status.ts
web/packages/elements/docs/examples/status.ts
// Statuses, a legend and a row of facts, drawn by the primitives into an element
// of your own: its styles start from `base` (the tokens and the status rules),
// plus `legendStyles` for the legend.
import { base, factLine, legend, legendStyles, pill, statusWord } from '@contraagent/elements';
import { LitElement, html } from 'lit';
class BuildSummary extends LitElement {
static override styles = [base, legendStyles];
override render() {
return html`<p>
${pill({ text: 'passed', tone: 'done' })} ${pill({ text: 'failed', tone: 'failed', title: 'exit code 2' })}
${pill({ text: 'running', tone: 'running' })} ${statusWord({ text: 'queued', tone: 'attention' })}
</p>
<p class="meta words">${factLine(['12 jobs', { text: '1 failed', tone: 'failed' }, '4m 05s'])}</p>
${legend(
[
{ label: 'passed', tone: 'done', shape: 'round' },
{ label: 'failed', tone: 'failed', shape: 'square' },
{ label: 'queued', tone: 'attention', shape: 'ring' },
],
{ label: 'Job states' },
)}`;
}
}
export function mount(host: HTMLElement): void {
if (!customElements.get('build-summary')) customElements.define('build-summary', BuildSummary);
host.replaceChildren(document.createElement('build-summary'));
}