Skip to main content

Extension UI

Custom UI

Extensions can interact with users via ctx.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

Notifications emitted while extensions load or startup is in progress always appear below the startup RESOURCES disclosure line, never above it.

Timed Dialogs with Countdown

Dialogs support a timeout option that auto-dismisses with a live countdown display:
Return values on timeout:
  • select() returns undefined
  • confirm() returns false
  • input() returns undefined

Manual Dismissal with AbortSignal

For more control (e.g., to distinguish timeout from user cancel), use AbortSignal:
See examples/extensions/timed-confirm.ts for complete examples.
Calling setToolsExpanded() with the current value is a no-op. The default indicator is ∀. ATOMIC_REDUCED_MOTION=1 makes it static, and NO_COLOR disables foreground colors. Custom frames and intervals render as supplied; add colors with ctx.ui.theme.fg(...) if wanted. 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

Use ctx.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
See github-issue-autocomplete.ts for a complete example that preloads the latest open GitHub issues with 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, use ctx.ui.custom(). This temporarily replaces the editor with your component until done() is called:
The callback receives:
  • tui - TUI instance (for screen dimensions, focus management)
  • theme - Current theme for styling
  • keybindings - App keybinding manager (for checking shortcuts)
  • done(value) - Call to close component and return value
Pass { 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. 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:
For advanced positioning (anchors, margins, percentages, responsive visibility), pass overlayOptions. Use onHandle to control visibility programmatically:
See TUI components for the full OverlayOptions API and overlay-qa-tests.ts for examples. Pass { reserveTranscriptRows: true } for a blocking bottom dialog that must leave at least six transcript rows visible. Set overlayOptions.anchor to bottom-left, bottom-center, or bottom-right; row and nonzero offsetY are rejected. Horizontal placement, margins, and numeric or percentage maxHeight remain supported. Reservation updates on resize and visibility changes and ends when the overlay closes. Leave it unset for a full-screen overlay. The built-in ask_user_question dialog uses it.
A component mounted with 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. Embed OVERLAY_ACTIVE_ROW_MARKER once per frame on the line that should stay visible when the dialog is cropped, usually the selected row. The first marked line wins, even with maxHeight: 1; the marker has zero visible width and is removed before terminal output. Focused inputs also keep their cursor row visible.

Custom Editor

Replace the main input editor with a custom implementation (vim mode, emacs mode, etc.):
Key points:
  • Extend CustomEditor (not base Editor) 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 fourth CustomEditor constructor argument to opt into the editor-border spinner.
  • Factory receives tui, theme, and keybindings from the app
  • Use ctx.ui.getEditorComponent() before setEditorComponent() to wrap the previously configured custom editor
  • Pass undefined to restore default: ctx.ui.setEditorComponent(undefined)
  • When a custom editor installed through ctx.ui.setEditorComponent() exposes setAutocompleteMaxVisible(), Atomic initializes it from the active autocompleteMaxVisible setting.
To compose with another extension that already replaced the editor, capture the previous factory before setting yours:
See TUI components Pattern 7 for a complete example with mode indicator.

Message Rendering

Register a custom renderer for messages with your customType:
Messages are sent via pi.sendMessage():

Theme Colors

All render functions receive a theme object. See Themes for creating custom themes and the full color palette.
For syntax highlighting in custom tool renderers:
To label a read call’s line range from offset/limit arguments, use formatReadLineRange. It accepts numbers, numeric strings, and null, and returns an empty string for missing or malformed ranges:

Next steps

Try the runnable extension examples, and use the Extension API reference for context and method contracts.