Skip to content

Render Agent session entries with Markdown components #800

Description

@taras

Story

As an XMD author, I want Agent conversation entries rendered through ordinary Markdown components, so session viewers and interactive REPLs can share a configurable presentation without sharing their transports or lifecycles.

Example

Repository-local components can change the presentation for one message kind:

<Session.Message.Tool entry={entry} />

The initial default kinds are:

  • Session.Message.User
  • Session.Message.Assistant
  • Session.Message.Thought
  • Session.Message.Tool
  • Session.Message.Permission
  • Session.Message.Status
  • Session.Message.Unknown

A Tail viewer and an ACP-backed REPL both normalize their own source events before invoking these components. XMD supplies one immutable entry prop whose exact kind selects the component.

Current gap

#774 established that provider files can be observed read-only, and XMD already receives conversation updates while it owns an ACP prompt. These sources have different schemas, timing, authority, and lifecycles. Without one presentation boundary, Tail and REPL would duplicate normalization-facing rendering behavior or expose provider records to authored components.

Accepted evidence

#799, implemented on open PR #805, establishes that @bomb.sh/tty is viable as a zero-I/O rendering and input layer under both Deno source and deno compile. It also establishes that XMD's generator model can keep ingesting immutable snapshots while Markdown rendering is blocked, discard stale projection work, preserve entry-anchored viewport state, and render only the newest pending state.

The production design preserves those conclusions, not #799's private entry type, test-only props, narrow Markdown lexer, mutation controls, evidence record, byte counts, barrier implementation, development-dependency placement, or proof-harness permissions.

Contract

The presentation layer accepts immutable snapshots of provider-neutral logical entries. Each entry has a stable identity, one presentation kind, normalized state and normalized content. Provider adapters retain raw records and private fields; authored renderers receive only the XMD entry.

An unsupported but safely framed provider record becomes an unknown entry instead of disappearing. A source whose schema cannot safely establish record boundaries or session identity refuses before presentation.

Each conversation view has one XMD-owned update loop and one stable internal region per logical entry. XMD owns normalized entry state, stable entry identity, replacement, coalescing, viewport anchoring, follow mode, and unread state. A streaming snapshot replaces only its entry's region; settlement replaces the same region with its final presentation without duplicating neighboring entries.

Source ingestion is independent of rendering. If a component blocks, snapshots continue to enter the in-memory projection. Stale projection work is discarded, pending work coalesces, and release renders the newest immutable state instead of replaying every intermediate revision.

Viewport position is anchored to entry identity rather than terminal rows. Resize and relayout preserve that anchor, history order, entry identities, follow mode, and unread state.

Message presentation uses ordinary Markdown components and the established component-resolution tiers. Each standard kind has a package default, and a repository-local component of the same name overrides that default alone. This Story does not add a renderer-specific capability restriction or a per-instance renderer map.

@bomb.sh/tty owns zero-I/O layout, input decoding, resize processing, and ANSI output for cells changed since the preceding frame. XMD applies decoded input and size changes to its own viewport state. Bombshell does not own normalized entries, entry identity, replacement, coalescing, viewport anchoring, follow mode, unread state, provider observation, ACP, Agent authority, PTYs, terminal emulation, child processes, or Grid lifecycle.

Rendered frames, streaming revisions, cursor, selection, scroll position, follow mode, and unread state are ephemeral. This presentation layer journals none of them. A consumer may retain one final normalized turn for its separate replay contract; doing so does not retain or restore the presentation state.

Tail and REPL share this presentation contract without sharing transports, stores, file watchers, ACP connections, or lifecycle authority. Each view owns and tears down its own ingestion and projection work.

Default Markdown components and every Bombshell WASM or other runtime asset use the canonical compiled-asset path in scripts/lib/compile.ts. Local builds, releases, and clean verification consume that one list; no Tail or REPL integration creates another compile inventory.

Normalized entry contract

Every entry is a recursively immutable JSON value. It has:

  • a non-empty opaque id, stable for the logical entry within one conversation source;
  • a kind selecting user, assistant, thought, tool, permission, status, or unknown;
  • a state selecting pending, active, completed, failed, or cancelled; and
  • an immutable content array.

Content values are exactly:

type SessionEntryContent =
  | { type: "text"; text: string }
  | { type: "media"; mediaType: string; data: string; alt?: string }
  | { type: "data"; value: Json }
  | { type: "diff"; oldText?: string; newText: string }
  | { type: "terminal"; text: string; exitCode?: number };

user, assistant, and thought add no fields. tool adds a required human title and a category: read, edit, delete, move, search, execute, think, fetch, switch-mode, or other. permission adds a required title, display-only options with allow-once, allow-always, reject-once, or reject-always dispositions, and an optional selected display value. status may add a title. unknown adds a required safe label.

Each Session.Message.* component receives exactly one required prop named entry, containing the complete entry variant for its kind. No sibling props or implicit body are supplied.

The entry identifier is source-local presentation identity, not durable or cross-source identity, and grants no authority. Entry order, view revision, source and provider identity, session identity, file paths, credentials, terminal identifiers, permission option identifiers, and interaction authority stay outside the entry. data is JSON deliberately constructed by the normalizer, never a copied provider object, raw tool input or output, metadata, or extension payload. A diff carries no path and terminal content carries no terminal identity.

Production ownership

The first production implementation remains private to @executablemd/cli; #800 creates no @executablemd/session, @executablemd/session-ui, or @executablemd/ui package.

The seven Markdown defaults live under:

packages/cli/src/components/Session/Message/

Their filenames are User.md, Assistant.md, Thought.md, Tool.md, Permission.md, Status.md, and Unknown.md. Normalized entry types and validation, component registration, the update loop, view state, and Bombshell projection live under:

packages/cli/src/session-ui/

These are private modules, not public @executablemd/cli library exports. They receive source streams and terminal input/output capabilities as arguments and do not read command flags, process globals, provider files, or ACP connections themselves.

The CLI imports core and ACP; neither lower package imports CLI. ACP exposes its own transport information and the CLI converts it into normalized entries. Tail separately converts provider-file records at the CLI boundary. Tail, REPL, Grid, and command modules may depend on session-ui; session-ui depends on none of them.

The Tail or REPL caller owns its watcher or ACP connection. A presentation operation owns only its update loop, subscription, and projection children in its Effection scope. Cancellation dismantles those children, while the source owner performs its own teardown.

All current production consumers are CLI-hosted. A non-CLI production consumer is the condition for extracting this boundary into a workspace package. Until then, the CLI remains the only release unit and publishes no speculative package.

Acceptance

  • Every standard entry kind has a default Markdown component and a repository-local override replaces only that kind.
  • Tail fixtures and controlled ACP events that describe the same semantic conversation produce the same kind-specific component input except for source-local opaque identity.
  • Each component receives exactly one entry prop containing its recursively immutable, kind-narrowed entry; malformed variants and unknown fields refuse.
  • Raw provider records, file paths, credentials, terminal identifiers, permission option identifiers, and unrecognized private payloads never reach authored components.
  • Production presentation modules exist only under packages/cli/src/session-ui, the Markdown defaults exist only under packages/cli/src/components/Session/Message, and core and ACP import neither private path.
  • @executablemd/cli exposes no public library export for the private UI boundary and remains the only release unit added by this Story.
  • An unknown safely framed record remains visible as Session.Message.Unknown; an unsafe or identity-ambiguous source refuses.
  • Streaming snapshots replace only their entry's stable region, and settlement replaces that region with its final presentation.
  • A signal-controlled blocked renderer does not stop source ingestion; stale work is discarded and release renders the latest coalesced snapshot.
  • Two independent views do not share a watcher, ACP connection, live store, cursor, or lifecycle.
  • Full-history rendering, initial positioning at the newest entry, follow suspension, unread indication, return-to-end, and resize satisfy the behavior proved by Test incremental Agent session rendering with Markdown components #799.
  • Source and deno compile entrypoints produce the same normalized component selection and viewport behavior with every required component and Bombshell asset embedded through the canonical compile inputs.
  • Negative controls fail if provider records leak through, entry identity changes during streaming, neighboring regions are replaced, ingestion waits for rendering, stale projection is committed, or a compiled asset is omitted.

Relationships

Out of scope

  • Discovering, reading, following, or writing provider files.
  • Opening ACP connections or sending, cancelling, or answering an Agent.
  • REPL evaluation, journals, replay, or resource restoration.
  • PTYs, terminal emulation, tmux, or Grid providers.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    UXUser-facing usability and interaction improvementsdocumentsExecutable documents, authored workflows, and reader-facing document behaviorenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions