Terminal
experimentalStreamed command output that behaves like a real terminal: ANSI colours, progress-bar rewrites, stick-to-bottom, live clock and exit-code badge.
Finished command
minimal
~/acme/webexit 0
Streaming test run
`pnpm test` streams vitest output line by line (live clock, cursor, stick-to-bottom), ends with one failing test and exit 1.
full
Run tests~/acme/web
Collapsed build log
A Next.js build collapsed to the last 9 lines; one line is a \r-rewritten progress counter.
state
Build~/acme/web23.4sexit 0
Dev server with Stop
A Button in `actions` stops a dev server that keeps logging.
composition
Dev server~/acme/web
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/terminal.jsonCopies the source into components/kura/terminal with its dependencies (kura-theme, icon-button, kura-format, status-glyph, use-controllable-state, use-elapsed, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- Show a shell command an agent ran (pnpm test, git status, a build) with its output, while it runs and after.
- Stream a child process's stdout (AsyncIterable<string>) or a growing log string into the UI.
- Long logs where the tail matters: collapse to the last N lines with “Show all N lines”.
- A dev-server or watcher log with a Stop control in the header.
Reach for something else when
- Source code or a snippet to copy — use code-block.
- Reviewing file edits — use diff-view.
- A tool call whose output is structured JSON — use tool-call.
- Interactive shells that take keyboard input — this renders output only (no PTY, no input line).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| command | string | — | The command, shown on the prompt line (`$ pnpm test`) and in the accessible name. |
| lines | readonly string[] | string | — | Output as lines or one growing string. ANSI SGR (colours 30–37/90–97, 256/truecolour approximated, bold, dim, italic, underline, inverse) and cursor controls (\r, erase line, cursor up, backspace) are rendered; OSC titles/links are stripped. Append-only updates are parsed incrementally. |
| stream | AsyncIterable<string> | — | Output as a stream of chunks; takes precedence over `lines`. Chunks may split lines and escape sequences. Status is “running” until it ends. |
| onStreamEnd | () => void | — | Called once `stream` has ended. |
| status | "idle" | "running" | "done" | "failed" | — | Lifecycle. Default: from exitCode (0 → done, otherwise failed), "running" while a stream is open, else "idle" (no glyph). |
| exitCode | number | null | — | Exit code, shown as a badge once finished (red when non-zero). |
| startedAt | Date | number | — | Start time for the live clock. Default: when status became running. |
| duration | number | — | Final duration in ms for persisted runs. Default: measured from running → finished in this session. |
| cwd | string | — | Working directory in the header, e.g. "~/acme/web". |
| title | React.ReactNode | — | Header title, e.g. "Run tests". Default: the cwd, or "Terminal". |
| prompt | string | "$" | Prompt glyph before the command. |
| maxHeight | number | 320 | Viewport height in px. Output sticks to the bottom while the user is there; scrolling up shows “Jump to latest” / “N new lines”. |
| maxLines | number | — | Collapse to the last N lines with a “Show all N lines” control at the top. |
| expanded | boolean | — | Showing every line past maxLines (controlled). |
| defaultExpanded | boolean | false | Initial expanded state when uncontrolled. |
| onExpandedChange | (expanded: boolean) => void | — | Called when “Show all” / “Show last N” is used. |
| wrap | boolean | true | Soft-wrap long lines. |
| copyable | boolean | true | Show the copy button: copies `$ command` plus the plain output (no escape codes). |
| onCopy | (text: string) => void | — | Called with the copied text after a successful copy. |
| header | boolean | true | Show the header row. |
| actions | React.ReactNode | — | Extra header controls before the copy button, e.g. a Stop Button. |
All native attributes of the root element, className and ref pass through.
Anatomy
- Terminal`data-status="idle" | "running" | "done" | "failed"`, `data-state="collapsed" | "expanded"` when maxLines applies.
data-slot="terminal" - HeaderStatus glyph, title (`terminal-title`), cwd, live clock / duration (`terminal-elapsed`), exit badge (`terminal-exit-code`), actions, copy.
data-slot="terminal-header" - Output`role="log"` scroll viewport (aria-live off), keyboard-scrollable; prompt line (`terminal-command`) and lines (`terminal-line`).
data-slot="terminal-output" - CursorBlock cursor at the live edge while running.
data-slot="terminal-cursor" - Expand“Show all N lines” row when collapsed (`terminal-collapse` to collapse again).
data-slot="terminal-expand" - JumpFloating “Jump to latest” / “N new lines” pill when scrolled away from the bottom.
data-slot="terminal-jump"
Accessibility
- Role:
log - The output is a role=log region named “Output of <command>” with aria-live off, so streaming never floods a screen reader.
- A separate polite status announces only transitions: “pnpm test running”, “pnpm test failed, exit code 1”, and copy confirmation.
- The viewport is focusable (Tab) and scrolls with arrow keys / Page Up / Page Down.
- Status is text as well as colour: the exit badge reads “exit 1”; the glyph is decorative next to it.
- Reduced motion: The cursor stops pulsing, the running glyph stops spinning, and the copy icon swap and pill entrance become plain fades. Jump to latest still scrolls smoothly only if the browser allows it.
| Keys | Action |
|---|---|
| Tab | Focus the output, then the expand, jump, action and copy buttons |
| Arrow keys / Page Up / Page Down | Scroll the focused output |
| Enter / Space | Activate Show all / Jump to latest / Copy |
Notes for agents
- Pass raw process output; don't strip ANSI codes. Escape sequences split across chunks are buffered.
- For a growing string, only append — replacing earlier text triggers a full re-parse (still correct, just slower).
- Keep the same Terminal mounted from running → done so the measured duration works; for history pass `duration`.
- `stream` is consumed once; create a new iterable to restart. Set `exitCode` in onStreamEnd to finish with a badge.
- Colours map to Kura tokens; override any of them with --kura-terminal-{red,green,yellow,blue,magenta,cyan,white,gray,black} on a parent.
- Helpers exported alongside: parseAnsi(text), stripAnsi(text), AnsiScreen (incremental).