Extension UI
Custom UI
Extensions can interact with users viactx.ui methods and customize how messages/tools render.
For custom components, see TUI components which has copy-paste patterns for:
- Selection dialogs (SelectList)
- Async operations with cancel (BorderedLoader)
- Settings toggles (SettingsList)
- Status indicators (setStatus)
- Working message, visibility, and indicator from accepted prompt startup through active turns (
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - Widgets above/below editor (setWidget)
- Autocomplete providers layered on top of built-in slash/path completion (addAutocompleteProvider)
- Custom footers (setFooter)
Dialogs
RESOURCES disclosure line, never above it.
Timed Dialogs with Countdown
Dialogs support atimeout option that auto-dismisses with a live countdown display:
select()returnsundefinedconfirm()returnsfalseinput()returnsundefined
Manual Dismissal with AbortSignal
For more control (e.g., to distinguish timeout from user cancel), useAbortSignal:
Widgets, Status, and Footer
setToolsExpanded() with the current value is a no-op.
Atomic’s default working indicator keeps the literal one-cell ∀ fixed while following the active theme’s optional workingIndicator tone overrides through a dark → accent → bright/bold → accent → dark ramp every 88ms. Any omitted tones are derived from selected-surface, accent, and text roles. NO_COLOR keeps regular/bold activity without foreground-color escapes, and ATOMIC_REDUCED_MOTION=1 uses a static regular accent ∀ without a timer. Custom working-indicator frames and intervals are rendered verbatim. If you want colors, add them to the frame strings yourself, for example with ctx.ui.theme.fg(...).
These APIs customize presentation only; they do not start work or emit an extension stream event before prompt startup. See Working Indicator Customization for accepted-prompt, pre-stream, and agent-turn handoff timing.
Autocomplete Providers
Usectx.ui.addAutocompleteProvider() to stack custom autocomplete logic on top of the built-in slash-command and path provider.
Typical pattern:
- inspect the text before the cursor
- return your own suggestions when your extension-specific syntax matches
- otherwise delegate to
current.getSuggestions(...) - delegate
applyCompletion(...)unless you need custom insertion behavior
gh issue list and filters them locally for fast #... completion. It requires GitHub CLI (gh) and a GitHub repository checkout.
Custom Components
For complex UI, usectx.ui.custom(). This temporarily replaces the editor with your component until done() is called:
tui- TUI instance (for screen dimensions, focus management)theme- Current theme for stylingkeybindings- App keybinding manager (for checking shortcuts)done(value)- Call to close component and return value
{ signal } to dismiss the custom UI if an operation is aborted; the returned promise rejects with the signal reason.
Custom component handleInput methods must return true when they consume an input and false (or undefined) when they do not. In fullscreen mode, an unhandled viewport key continues to the transcript; remote components also fall through on a failed or timed-out reply.
Custom component handleInput methods must return true when they consume an input and false or undefined when they do not. In fullscreen mode, an unhandled viewport key continues to the transcript; remote components also fall through on a failed or timed-out reply. Return true for a handled key so it is not applied twice.
A handler that returns a promise is judged when it settles: only a resolved true consumes the key, while false, undefined, and a rejection fall through to the viewport. A component with no handleInput declines everything, so viewport keys still scroll the transcript behind it.
Pass { handlesCtrlC: true } when the component binds Ctrl+C itself (cancel, skip, close). In isolated interactive sessions the host otherwise closes a component that owns input on the first Ctrl+C, so that a component which never resolves cannot trap the keyboard. See Interactive callback isolation.
See TUI components for the full component API.
Overlay Mode (Experimental)
Pass{ overlay: true } to render the component as a floating modal on top of existing content, without clearing the screen:
overlayOptions. Use onHandle to control visibility programmatically:
OverlayOptions API and overlay-qa-tests.ts for examples.
Pass { reserveTranscriptRows: true } for a blocking bottom-anchored dialog. A reserving overlay must set overlayOptions.anchor to bottom-left, bottom-center, or bottom-right; row and a nonzero offsetY are rejected because they invalidate the transcript-intersection model. Horizontal placement options remain supported. An overlay is composited over the transcript rather than measured into the layout, so without this option a tall dialog can cover the whole screen and the transcript rows it covers can never be scrolled above it. With it, the host bounds the overlay so at least six transcript rows stay visible. Top and bottom margins limit the wrapper before pi-tui composition, preventing a second fixed-head crop. Numeric and percentage maxHeight values are also resolved before active-row windowing and removed from the options passed to pi-tui. The host computes each visible bottom overlay’s real intersection with the transcript and reserves the connected covered suffix once, so scrolling to the end keeps the newest output readable. A measured height change on mount or resize requests one automatic settling repaint. Margins, overlapping overlays, resize, and temporary visibility changes are reflected each frame. A temporarily hidden overlay — through OverlayHandle.setHidden(true) or a false OverlayOptions.visible result — contributes no intersection until it becomes visible again. Permanent handle removal, closure, and raw host removal release that exact overlay’s registration; the shared reserve remains until its final overlay leaves. Leave the option unset for an overlay that is meant to take the screen, such as a full-screen graph. The built-in ask_user_question dialog sets it.
reserveTranscriptRows always releases configured fullscreen transcript actions and vertical wheel input to the host viewport, including while a nested input has focus. The component keeps all other keyboard and mouse input, including text editing, arrows, confirmation, cancellation, and clicks. This rule applies only to reserving overlays; other focused overlays still receive page and wheel input first and can keep it by returning true.
Bounding a tall dialog means dropping rows, and the host would otherwise have to guess which. Embed OVERLAY_ACTIVE_ROW_MARKER in the line your component most needs kept — the selected row of a list — and the host places what it keeps around that row instead of taking a fixed head, even when the effective maxHeight is only one row. The mark is a zero-width APC sequence that visibleWidth measures as zero, terminated with ST as ECMA-48 requires. The renderer strips it centrally, in the last transform over the composited screen before it is written out, so it never reaches the terminal — from a reserving overlay, an ordinary overlay, an inline mount, a widget, or a workflow stage chat alike. Embed it once per frame; the host uses the first line that carries it. Put it anywhere on that line: a mark buried mid-line is removed just as a trailing one is. The ask_user_question dialog marks every active selectable row, including single- and multi-select options, Next, Submit, Cancel, and inline sentinel rows. Focused pi-tui inputs also anchor the bound through their cursor marker, so arrow keys and text input stay visible on a 16-row terminal.
Custom Editor
Replace the main input editor with a custom implementation (vim mode, emacs mode, etc.):- Extend
CustomEditor(not baseEditor) to get app keybindings (escape to abort, ctrl+d, model switching) - Call
super.handleInput(data)for keys you don’t handle - Editors keep the standalone working row by default. Pass
{ embedWorkingStatus: true }as the fourthCustomEditorconstructor argument to opt into the editor-border spinner. - Factory receives
tui,theme, andkeybindingsfrom the app - Use
ctx.ui.getEditorComponent()beforesetEditorComponent()to wrap the previously configured custom editor - Pass
undefinedto restore default:ctx.ui.setEditorComponent(undefined) - When a custom editor installed through
ctx.ui.setEditorComponent()exposessetAutocompleteMaxVisible(), Atomic initializes it from the activeautocompleteMaxVisiblesetting.
Message Rendering
Register a custom renderer for messages with yourcustomType:
pi.sendMessage():
Theme Colors
All render functions receive atheme object. See Themes for creating custom themes and the full color palette.