Capability: surfaces-and-renderers
A terminal and a web page on one harness instance at the same time, both A2A clients and nothing else, both rendering the A2UI 0.9.1 basic catalog and sending its actions back.
What it is
Where a user meets the harness. The surface and renderer entities describe what can be drawn; the A2UI extension is how the agent draws it; the TUI (Textual) and the web renderer (React 19 on the official A2UI renderer) are the two implementations. Lives in tiny_harness/interaction/ and renderers/web/.
Current behaviour
- A surface SHALL declare its modality (text only) and renderer capabilities; a renderer SHALL declare which message and artifact kinds it renders, with MCP Apps (
text/html;profile=mcp-app) declarable later without an interface change. - The harness SHALL implement the A2UI A2A extension at 0.9.1 (
https://a2ui.org/a2a-extension/a2ui/v0.9.1; the 1.0 SDKs were not available), advertising the basic catalog id andacceptsInlineCatalogs: falsein the card. The schemas are vendored undertiny_harness/interaction/a2ui/schemas/with attribution. emit_uiSHALL validate the server messages against the schemas and emit them as aTaskArtifactUpdateEventnameda2uiwithapplication/a2ui+jsonparts; the workflow records the surfaces and components the task created.- WHEN a user action arrives as a message with an
application/a2ui+jsonpart THEN intake SHALL validate it against the task's surface registry and discard an action naming a surface or component the task did not create; a genuine action resumes the task with[A2UI action] <name> on surface <id> from <component>and its context. - Both renderers SHALL render every component of the basic catalog (Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, ChoicePicker, Slider); the TUI renders Video and AudioPlayer as the typed placeholder, and any kind a renderer cannot render SHALL be shown as a placeholder naming the kind, never dropped.
- The TUI SHALL be a Textual app with the conversation on the left, Task / Plan / Trace tabs on the right and the composer below, every action on a key; it streams the reply and keeps a
SubscribeToTaskstream per open task, so a second surface sees the same events.tiny-harness tui --url <server>starts it; baretiny-harnessstarts it hosting the harness from the default configuration. - The TUI SHALL show
STATE · sub-statein the top bar (e.g.INPUT_REQUIRED · awaiting-next-message), bindctrl+dto Complete andctrl+cto Cancel (both in the footer, acting only on a non-terminal task, otherwise a notice), offer inline/completion from the card's commands with their descriptions under the composer, list sub-tasks in the Task tab (enteropens one,f4returns to the parent), and follow the open task'sSubscribeToTaskstream once a message's own stream ends, so a reply from another surface and each sub-task row's state (relayed by the parent) show without a reload or polling. A sub-task is read once for its row's goal and followed only when opened. Conversation entries render as plain text (no Rich markup). - The TUI SHALL assert a participant on every message and action it sends, as the
X-Participant-Idheader and theparticipant_idmetadata:--participant <id>, else the OS user name. IF neither yields a non-empty printable-ASCII id (a blank--participant, no OS user name, or a non-ASCII or control character, which the header cannot carry intact) THENtiny-harness tuiSHALL exit 2 naming--participantbefore it connects. The composer shows the asserted id (Enter to send as <id>). The id is self-asserted (decision-003); the server's refusal of a message that asserts nobody is unchanged. - The web renderer SHALL be a Vite + React 19 + TypeScript (strict) app on shadcn (Tailwind v4, the shadcn/ui primitives and the chat components
MessageScroller,Message,BubbleandMarker) that reaches the server only through the official A2A JavaScript SDK (@a2a-js/sdk, protocol 1.0: card resolution, the JSON-RPC transport,sendMessageStream,resubscribeTask,cancelTask), addingX-Participant-Id, folds the SDK's events into a pure model, takes the harness URL from?server=, the browser's remembered choice or its own origin (a "Harness" field in the header changes it), attaches to an existing task with?task=, keeps aSubscribeToTasksubscription open after a stream ends in a non-terminal state (reopened after a pause, since the SDK ends a stream at every final event) so a reply from another surface shows up, is published on the docs site under/ui/by the docs workflow, and renders cards with@a2ui/react'sMessageProcessorand the basic catalog; it is served under/uiwhenui_dirpoints at its build and is covered by Playwright visual and axe-core accessibility tests. - The web renderer SHALL show a state badge and, while
INPUT_REQUIRED, a sub-state badge; a Complete (primary) and a Cancel task (ghost) button, disabled when the task is terminal or none is open; a "← Parent" link on a sub-task; sub-task rows that open?task=<id>in place; and, when the composer starts with/, a WAI-ARIA combobox listbox of the card's commands (arrows move, Tab or Enter accepts, Escape closes).?fixture=controlsruns it against a scripted harness for the visual tests. - WHEN two surfaces are connected to one task THEN both SHALL receive the same updates, completion and cancellation included.
Design
design.md § Surfaces and renderers, design.md § UI/UX design and the prototypes under docs/specs/issue-3/design/.
History
| Work item | What changed | Links |
|---|---|---|
| issue-3 | A2UI 0.9.1, surface and renderer entities, the Textual TUI (Layer 7); the web renderer (Layer 8) | spec, PR #13, PR #14 |
| issue-20 | The TUI asserts a participant (--participant, else the OS user name) so it can send at all | spec, PR #21 |
| issue-25 | TUI and web Complete/Cancel controls, the sub-state in the top bar and badge, / command completion, sub-task open and back; bare tiny-harness opens the TUI | spec, PR #26, decision-007 |