Skip to content

Transcript

<cae-transcript> shows a conversation with an agent: chat bubbles for the prompts, the answers and the model’s reasoning, lines of the agent’s own log, and a card per tool call that opens on its command and output. A web search lists the pages it returned as links; a page read shows its URL. Register it with @contraagent/elements/transcript/define; the types are in @contraagent/elements/transcript.

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);
}

The conversation comes in sections, one per session with the agent. Above two sections, a picker shows one at a time; each section names itself (title), can add short facts after its name (a model) and a mark of how it ended (verdict: ✓ with the tone done, ✕ otherwise). A section’s head shows its tags (who ran it) and its details: folded texts, code (a command line) or prose (a prompt). A footer after the items says how the section ended, with a list (errors) or a note.

src/transcript/types.ts
export interface TranscriptSection {
id: string;
/** Its name in the section picker. */
title: string;
/** Short facts after the title in the picker (a model). */
facts?: readonly string[];
/** How the section ended, in the picker: ✓ with tone `done`, else ✕; `text` for screen readers, `tip` as its tooltip. */
verdict?: { tone: Tone; text: string; tip?: string };
/** The section is still being written: a dot in the picker. */
active?: boolean;
/** Chips at the head of the section. */
tags?: readonly TranscriptTag[];
/** Folded texts at the head of the section. */
details?: readonly TranscriptDetail[];
items: readonly TranscriptItem[];
/** A box after the items: how the section ended, and a list (errors) or a note. */
footer?: { tone: Tone; title: string; list?: readonly string[]; note?: string };
}

An item is a prompt, a message or reasoning (markdown), an event (a glyph, a title and a detail), a notice, or a tool card:

src/transcript/types.ts
/** One tool call: the card that opens on its start and shows its result. */
export interface ToolCard {
type: 'tool';
id: string;
name: string;
/**
* The card's glyph: `bash`, `read`, `grep`, `glob`, `edit`, `write`, `task`
* or `agent`; any other key shows ⚒. A web search (`query`) and a page read
* (`page`) have their own. Set as `data-tool` on the card.
*/
icon: string;
/** One line: the command, pattern, path or URL. */
summary: string;
/** Why the call was made, beside the summary. */
intent?: string;
/** A web search's query: shown above the pages it returned (`links`). */
query?: string;
/** A page the call read: its URL, a link when it is http(s). */
page?: string;
/** The command in full, above the output (not for a `query` or a `page`). */
body?: string;
/** The result; null while the call runs. */
output: string | null;
status: 'running' | 'ok' | 'error';
/** The pages the call returned: listed as links, and counted beside the summary. */
links?: readonly ToolLink[];
}
export type TranscriptItem =
/** A prompt sent to the agent: its length, the start of it as a tooltip. */
| { type: 'prompt'; id: string; text: string }
/** The agent's answer, in markdown. */
| { type: 'message'; id: string; text: string }
/** The model's reasoning, in markdown. */
| { type: 'reasoning'; id: string; text: string }
/** Something that happened in the session, as a bubble: a glyph, a title and a detail. */
| { type: 'event'; id: string; icon: string; title: string; text?: string }
/** A line of the agent's own log. */
| { type: 'notice'; id: string; text: string }
| ToolCard;

shown picks a section (0-based); null follows the latest. While the reader is at the bottom, new content scrolls into view. Once they scroll up, it stays where they are, and a button counts what arrived below: count is how much the transcript holds (by default its sections and items), so a host that counts events sets it. A section that starts while the reader is scrolled up in the latest one keeps the one they read on screen. live shows the agent working, and a tool call with no result as running; without it, such a call was interrupted.

A host that loads a transcript in pages sets loading until the first page has arrived, and earlier to the number of lines before it that are not loaded: a button then asks for them with cae-load-earlier, and the next sections it gets is the content from further back, which keeps the reader’s place. For another transcript, give it a new element.

memory keeps what the reader opened and where they were: isOpen and setOpen for the tool cards, scroll and setScroll for the position, which is restored once the content has arrived. Without it, both last as long as the element. reveal(id) opens a tool card and scrolls it into view once its section is shown; it returns that section’s index for the host to show.

  • cae-section-change with {index}: the reader picked a section (null: the latest). The host sets shown.
  • cae-section-shown with {index}: the section on screen changed (-1: none), also when a new section keeps the one being read.
  • cae-load-earlier: the reader asked for the content before earlier.

Markdown is turned into HTML by markdown (md of @contraagent/elements/markdown escapes the text); without it, answers and reasoning show as plain text. renderDetail draws an open prose detail. The parts are picker, section, tag, bubble, notice, tool, tool-body and verdict; tagChips and tagStyles draw tags elsewhere in the same look. The words are strings (Strings): the picker’s name and tooltip, the bubbles’ heads, the tool cards’ statuses, the counts.