@bastani/atomic 0.9.19-alpha.6 → 0.9.19-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/index.bundle.mjs +22 -3
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +19 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/builtin/adversarial-verification.js +2 -2
- package/dist/builtin/workflows/builtin/{chunk-vhmnzdj9.js → chunk-1jth021m.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-p8jjwfg6.js → chunk-2377r57g.js} +4 -2
- package/dist/builtin/workflows/builtin/{chunk-hbnwv56f.js → chunk-31j2gs7h.js} +2 -2
- package/dist/builtin/workflows/builtin/{chunk-n8zysf1t.js → chunk-ngkqkzej.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-1nbq96fh.js → chunk-nkrafh9s.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-1kx4aggw.js → chunk-wf741bv3.js} +2 -2
- package/dist/builtin/workflows/builtin/{chunk-t6x2d78s.js → chunk-z29qbehr.js} +1 -1
- package/dist/builtin/workflows/builtin/generate-and-filter.js +2 -2
- package/dist/builtin/workflows/builtin/goal.js +3 -3
- package/dist/builtin/workflows/builtin/index.js +7 -7
- package/dist/builtin/workflows/builtin/ralph.js +3 -3
- package/dist/builtin/workflows/builtin/tournament.js +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +590 -256
- package/dist/builtin/workflows/src/index.js +67 -19
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +2 -0
- package/dist/modes/interactive/components/task-live-transcript.d.ts +1 -1
- package/dist/modes/interactive/components/task-live-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-live-transcript.js +19 -11
- package/dist/modes/interactive/components/task-live-transcript.js.map +1 -1
- package/docs/background-tasks.md +1 -1
- package/docs/build.md +51 -0
- package/docs/changelog.mdx +8 -8
- package/docs/compaction/reference.md +267 -0
- package/docs/compaction.md +36 -219
- package/docs/computer-use.md +35 -0
- package/docs/containerization.md +2 -0
- package/docs/custom-provider/api-reference.md +145 -0
- package/docs/custom-provider/oauth.md +131 -0
- package/docs/custom-provider/override.md +34 -0
- package/docs/custom-provider/registration.md +163 -0
- package/docs/custom-provider/streaming.md +214 -0
- package/docs/custom-provider.md +43 -607
- package/docs/development.md +76 -0
- package/docs/docs.json +199 -77
- package/docs/environment-variables.md +1 -1
- package/docs/extensions/api-reference.md +1001 -0
- package/docs/extensions/authoring.md +676 -0
- package/docs/extensions/events.md +793 -0
- package/docs/extensions/examples.md +94 -0
- package/docs/extensions/ui.md +470 -0
- package/docs/extensions.md +270 -2768
- package/docs/getting-started/authentication.md +55 -0
- package/docs/getting-started/first-session.md +172 -0
- package/docs/getting-started/installation.md +154 -0
- package/docs/getting-started/project-instructions.md +43 -0
- package/docs/guides/configuration.md +65 -0
- package/docs/guides/intercom.md +24 -0
- package/docs/guides/non-interactive.md +33 -0
- package/docs/guides/subagents.md +24 -0
- package/docs/guides/workflows.md +27 -0
- package/docs/guides.md +41 -0
- package/docs/index.md +13 -45
- package/docs/intercom/operations.md +178 -0
- package/docs/intercom/reference.md +146 -0
- package/docs/intercom.md +54 -259
- package/docs/json.md +17 -0
- package/docs/llama-cpp.md +5 -0
- package/docs/models/artificial-analysis-index.md +89 -0
- package/docs/models/evals.md +2 -2
- package/docs/models/model-selection.md +3 -3
- package/docs/models/pareto-efficiency.md +2 -2
- package/docs/models/reference.md +601 -0
- package/docs/models.md +45 -550
- package/docs/packages/authoring.md +88 -0
- package/docs/packages/reference.md +43 -0
- package/docs/packages.md +28 -107
- package/docs/programmatic.md +29 -0
- package/docs/prompt-templates.md +11 -0
- package/docs/providers/reference.md +23 -0
- package/docs/providers.md +10 -15
- package/docs/quickstart.md +44 -238
- package/docs/reference/cli.md +252 -0
- package/docs/reference.md +52 -0
- package/docs/rpc/examples.md +64 -0
- package/docs/rpc/extension-ui.md +198 -0
- package/docs/rpc/protocol.md +1341 -0
- package/docs/rpc.md +109 -1346
- package/docs/sdk/reference.md +1090 -0
- package/docs/sdk.md +56 -1002
- package/docs/session-format.md +2 -0
- package/docs/sessions.md +6 -10
- package/docs/settings.md +6 -45
- package/docs/skills/authoring.md +95 -0
- package/docs/skills/reference.md +60 -0
- package/docs/skills.md +20 -122
- package/docs/subagents/authoring.md +37 -0
- package/docs/subagents/reference.md +60 -0
- package/docs/subagents.md +39 -95
- package/docs/terminal-setup.md +1 -1
- package/docs/themes/reference.md +176 -0
- package/docs/themes.md +21 -135
- package/docs/tmux.md +5 -0
- package/docs/tools/edit.md +5 -0
- package/docs/tools.md +5 -0
- package/docs/tui/reference.md +343 -0
- package/docs/tui.md +23 -286
- package/docs/usage.md +26 -209
- package/docs/web-access.md +2 -0
- package/docs/windows.md +2 -1
- package/docs/workflows/api-reference.md +11 -4
- package/docs/workflows/authoring.md +4 -2
- package/docs/workflows/builtins.md +2 -2
- package/docs/workflows/operations.md +15 -10
- package/docs/workflows/reliable-design.md +21 -3
- package/docs/workflows/verification.md +8 -0
- package/npm-shrinkwrap.json +32 -32
- package/package.json +3 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Extension examples
|
|
3
|
+
description: Runnable extension examples shipped with Atomic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extension examples
|
|
7
|
+
|
|
8
|
+
## Examples Reference
|
|
9
|
+
|
|
10
|
+
All examples in [examples/extensions/](https://github.com/bastani-inc/atomic/tree/main/packages/coding-agent/examples/extensions).
|
|
11
|
+
|
|
12
|
+
| Example | Description | Key APIs |
|
|
13
|
+
|---------|-------------|----------|
|
|
14
|
+
| **Tools** |||
|
|
15
|
+
| `hello.ts` | Minimal tool registration | `registerTool` |
|
|
16
|
+
| `question.ts` | Width-wrapped single-question custom UI with option descriptions and typed answers | `registerTool`, `ui.custom` |
|
|
17
|
+
| `questionnaire.ts` | Width-wrapped multi-step wizard with tab navigation and typed answers | `registerTool`, `ui.custom` |
|
|
18
|
+
| `todo.ts` | Stateful tool with persistence | `registerTool`, `appendEntry`, `renderResult`, session events |
|
|
19
|
+
| `dynamic-tools.ts` | Register tools after startup and during commands | `registerTool`, `session_start`, `registerCommand` |
|
|
20
|
+
| `structured-output.ts` | Opt-in schema-specific `structured_output` tool using the canonical factory | `createStructuredOutputTool`, `registerTool`, terminating tool results |
|
|
21
|
+
| `truncated-tool.ts` | Output truncation example | `registerTool`, `truncateHead` |
|
|
22
|
+
| `tool-override.ts` | Override built-in read tool | `registerTool` (same name as built-in) |
|
|
23
|
+
| **Commands** |||
|
|
24
|
+
| `pirate.ts` | Modify system prompt per-turn | `registerCommand`, `before_agent_start` |
|
|
25
|
+
| `summarize.ts` | Conversation summary command | `registerCommand`, `ui.custom` |
|
|
26
|
+
| `handoff.ts` | Cross-provider model handoff | `registerCommand`, `ui.editor`, `ui.custom` |
|
|
27
|
+
| `qna.ts` | Q&A with custom UI | `registerCommand`, `ui.custom`, `setEditorText` |
|
|
28
|
+
| `send-user-message.ts` | Inject user messages | `registerCommand`, `sendUserMessage` |
|
|
29
|
+
| `reload-runtime.ts` | Reload command and LLM tool handoff | `registerCommand`, `ctx.reload()`, `sendUserMessage` |
|
|
30
|
+
| `shutdown-command.ts` | Graceful shutdown command | `registerCommand`, `shutdown()` |
|
|
31
|
+
| **Events & Gates** |||
|
|
32
|
+
| `permission-gate.ts` | Block dangerous commands | `on("tool_call")`, `ui.confirm` |
|
|
33
|
+
| `protected-paths.ts` | Block writes to specific paths | `on("tool_call")` |
|
|
34
|
+
| `confirm-destructive.ts` | Confirm session changes | `on("session_before_switch")`, `on("session_before_fork")` |
|
|
35
|
+
| `dirty-repo-guard.ts` | Warn on dirty git repo | `on("session_before_*")`, `exec` |
|
|
36
|
+
| `input-transform.ts` | Transform user input | `on("input")` |
|
|
37
|
+
| `input-transform-streaming.ts` | Streaming-aware input transform | `on("input")`, `streamingBehavior` |
|
|
38
|
+
| `project-trust.ts` | Decide or defer project trust from a user/global or CLI extension | `on("project_trust")`, trust UI, required trust result |
|
|
39
|
+
| `model-status.ts` | React to model changes | `on("model_select")`, `setStatus` |
|
|
40
|
+
| `provider-payload.ts` | Inspect payloads and provider response headers | `on("before_provider_request")`, `on("after_provider_response")` |
|
|
41
|
+
| `system-prompt-header.ts` | Display system prompt info | `on("agent_start")`, `getSystemPrompt` |
|
|
42
|
+
| `claude-rules.ts` | Load rules from files | `on("session_start")`, `on("before_agent_start")` |
|
|
43
|
+
| `prompt-customizer.ts` | Add context-aware tool guidance using `systemPromptOptions` | `on("before_agent_start")`, `BuildSystemPromptOptions` |
|
|
44
|
+
| `file-trigger.ts` | File watcher triggers messages | `sendMessage` |
|
|
45
|
+
| **Compaction & Sessions** |||
|
|
46
|
+
| `custom-compaction.ts` | Offline compacted-text override | `on("session_before_compact")` |
|
|
47
|
+
| `trigger-compact.ts` | Trigger compaction manually | `compact()` |
|
|
48
|
+
| `git-checkpoint.ts` | Git stash on turns | `on("turn_start")`, `on("session_before_fork")`, `exec` |
|
|
49
|
+
| `auto-commit-on-exit.ts` | Commit on shutdown | `on("session_shutdown")`, `exec` |
|
|
50
|
+
| **UI Components** |||
|
|
51
|
+
| `status-line.ts` | Footer status indicator | `setStatus`, session events |
|
|
52
|
+
| `working-indicator.ts` | Customize the Working indicator used during prompt startup and active turns | `setWorkingIndicator`, `registerCommand` |
|
|
53
|
+
| `github-issue-autocomplete.ts` | Add `#1234` issue completions on top of built-in autocomplete by preloading recent open issues from `gh issue list` | `addAutocompleteProvider`, `on("session_start")`, `exec` |
|
|
54
|
+
| `custom-footer.ts` | Replace footer entirely | `registerCommand`, `setFooter` |
|
|
55
|
+
| `custom-header.ts` | Replace startup header | `on("session_start")`, `setHeader` |
|
|
56
|
+
| `modal-editor.ts` | Vim-style modal editor | `setEditorComponent`, `CustomEditor` |
|
|
57
|
+
| `rainbow-editor.ts` | Custom editor styling | `setEditorComponent` |
|
|
58
|
+
| `widget-placement.ts` | Widget above/below editor | `setWidget` |
|
|
59
|
+
| `overlay-test.ts` | Overlay components | `ui.custom` with overlay options |
|
|
60
|
+
| `overlay-qa-tests.ts` | Comprehensive overlay tests | `ui.custom`, all overlay options |
|
|
61
|
+
| `notify.ts` | Simple notifications | `ui.notify` |
|
|
62
|
+
| `timed-confirm.ts` | Dialogs with timeout | `ui.confirm` with timeout/signal |
|
|
63
|
+
| `mac-system-theme.ts` | Auto-switch theme | `setTheme`, `exec` |
|
|
64
|
+
| **Complex Extensions** |||
|
|
65
|
+
| `plan-mode/` | Full plan mode implementation | All event types, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |
|
|
66
|
+
| `preset.ts` | Saveable presets (model, tools, thinking) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |
|
|
67
|
+
| `tools.ts` | Toggle tools on/off UI | `registerCommand`, `setActiveTools`, `SettingsList`, session events |
|
|
68
|
+
| **Remote & Sandbox** |||
|
|
69
|
+
| `ssh.ts` | SSH remote execution | `registerFlag`, `on("user_bash")`, `on("before_agent_start")`, tool operations |
|
|
70
|
+
| `interactive-shell.ts` | Persistent shell session | `on("user_bash")` |
|
|
71
|
+
| `sandbox/` | Sandboxed tool execution | Tool operations |
|
|
72
|
+
| `gondolin/` | Route built-in tools and `!` commands into a Gondolin micro-VM | Tool operations, built-in tool overrides, `on("user_bash")` |
|
|
73
|
+
| `subagent/` | Spawn sub-agents | `registerTool`, `exec` |
|
|
74
|
+
| **Games** |||
|
|
75
|
+
| `snake.ts` | Snake game | `registerCommand`, `ui.custom`, keyboard handling |
|
|
76
|
+
| `space-invaders.ts` | Space Invaders game | `registerCommand`, `ui.custom` |
|
|
77
|
+
| `doom-overlay/` | Doom in overlay | `ui.custom` with overlay |
|
|
78
|
+
| **Providers** |||
|
|
79
|
+
| `custom-provider-anthropic/` | Custom Anthropic proxy | `registerProvider` |
|
|
80
|
+
| `custom-provider-gitlab-duo/` | GitLab Duo integration | `registerProvider` with OAuth |
|
|
81
|
+
| **Messages & Communication** |||
|
|
82
|
+
| `message-renderer.ts` | Custom message rendering | `registerMessageRenderer`, `sendMessage` |
|
|
83
|
+
| `event-bus.ts` | Inter-extension events | `pi.events` |
|
|
84
|
+
| **Session Metadata** |||
|
|
85
|
+
| `session-name.ts` | Name sessions for selector | `setSessionName`, `getSessionName` |
|
|
86
|
+
| `bookmark.ts` | Bookmark entries for /tree | `setLabel` |
|
|
87
|
+
| **Misc** |||
|
|
88
|
+
| `inline-bash.ts` | Inline bash in tool calls | `on("tool_call")` |
|
|
89
|
+
| `bash-spawn-hook.ts` | Adjust bash command, cwd, and env before execution | `createBashTool`, `spawnHook` |
|
|
90
|
+
| `with-deps/` | Extension with npm dependencies | Package structure with `package.json` |
|
|
91
|
+
|
|
92
|
+
## Next steps
|
|
93
|
+
|
|
94
|
+
Use the [Extension API reference](/extensions/api-reference) to check the context properties and method contracts used by these examples.
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Extension UI
|
|
3
|
+
description: Render custom UI from an extension.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extension UI
|
|
7
|
+
|
|
8
|
+
## Custom UI
|
|
9
|
+
|
|
10
|
+
Extensions can interact with users via `ctx.ui` methods and customize how messages/tools render.
|
|
11
|
+
|
|
12
|
+
**For custom components, see [TUI components](/tui)** which has copy-paste patterns for:
|
|
13
|
+
- Selection dialogs (SelectList)
|
|
14
|
+
- Async operations with cancel (BorderedLoader)
|
|
15
|
+
- Settings toggles (SettingsList)
|
|
16
|
+
- Status indicators (setStatus)
|
|
17
|
+
- Working message, visibility, and indicator from accepted prompt startup through active turns (`setWorkingMessage`, `setWorkingVisible`, `setWorkingIndicator`)
|
|
18
|
+
- Widgets above/below editor (setWidget)
|
|
19
|
+
- Autocomplete providers layered on top of built-in slash/path completion (addAutocompleteProvider)
|
|
20
|
+
- Custom footers (setFooter)
|
|
21
|
+
|
|
22
|
+
### Dialogs
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Select from options
|
|
26
|
+
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
|
|
27
|
+
|
|
28
|
+
// Confirm dialog
|
|
29
|
+
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
|
|
30
|
+
|
|
31
|
+
// Text input
|
|
32
|
+
const name = await ctx.ui.input("Name:", "placeholder");
|
|
33
|
+
|
|
34
|
+
// Multi-line editor
|
|
35
|
+
const text = await ctx.ui.editor("Edit:", "prefilled text");
|
|
36
|
+
|
|
37
|
+
// Notification (non-blocking)
|
|
38
|
+
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Notifications emitted while extensions load or startup is in progress always appear below the startup `RESOURCES` disclosure line, never above it.
|
|
42
|
+
|
|
43
|
+
#### Timed Dialogs with Countdown
|
|
44
|
+
|
|
45
|
+
Dialogs support a `timeout` option that auto-dismisses with a live countdown display:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
|
|
49
|
+
const confirmed = await ctx.ui.confirm(
|
|
50
|
+
"Timed Confirmation",
|
|
51
|
+
"This dialog will auto-cancel in 5 seconds. Confirm?",
|
|
52
|
+
{ timeout: 5000 }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (confirmed) {
|
|
56
|
+
// User confirmed
|
|
57
|
+
} else {
|
|
58
|
+
// User cancelled or timed out
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Return values on timeout:**
|
|
63
|
+
- `select()` returns `undefined`
|
|
64
|
+
- `confirm()` returns `false`
|
|
65
|
+
- `input()` returns `undefined`
|
|
66
|
+
|
|
67
|
+
#### Manual Dismissal with AbortSignal
|
|
68
|
+
|
|
69
|
+
For more control (e.g., to distinguish timeout from user cancel), use `AbortSignal`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
74
|
+
|
|
75
|
+
const confirmed = await ctx.ui.confirm(
|
|
76
|
+
"Timed Confirmation",
|
|
77
|
+
"This dialog will auto-cancel in 5 seconds. Confirm?",
|
|
78
|
+
{ signal: controller.signal }
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
|
|
83
|
+
if (confirmed) {
|
|
84
|
+
// User confirmed
|
|
85
|
+
} else if (controller.signal.aborted) {
|
|
86
|
+
// Dialog timed out
|
|
87
|
+
} else {
|
|
88
|
+
// User cancelled (pressed Escape or selected "No")
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
See [examples/extensions/timed-confirm.ts](https://github.com/bastani-inc/atomic/blob/main/packages/coding-agent/examples/extensions/timed-confirm.ts) for complete examples.
|
|
93
|
+
|
|
94
|
+
### Widgets, Status, and Footer
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Status in footer (persistent until cleared)
|
|
98
|
+
ctx.ui.setStatus("my-ext", "Processing...");
|
|
99
|
+
ctx.ui.setStatus("my-ext", undefined); // Clear
|
|
100
|
+
|
|
101
|
+
// Working loader customization (active from accepted prompt startup through the agent turn)
|
|
102
|
+
ctx.ui.setWorkingMessage("Thinking deeply...");
|
|
103
|
+
ctx.ui.setWorkingMessage(); // Restore default
|
|
104
|
+
ctx.ui.setWorkingVisible(false); // Hide the built-in working indicator entirely
|
|
105
|
+
ctx.ui.setWorkingVisible(true); // Show the built-in working indicator
|
|
106
|
+
|
|
107
|
+
// Working indicator customization (same lifecycle; see TUI Pattern 4b)
|
|
108
|
+
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] }); // Static dot
|
|
109
|
+
ctx.ui.setWorkingIndicator({
|
|
110
|
+
frames: [
|
|
111
|
+
ctx.ui.theme.fg("dim", "·"),
|
|
112
|
+
ctx.ui.theme.fg("muted", "•"),
|
|
113
|
+
ctx.ui.theme.fg("accent", "●"),
|
|
114
|
+
ctx.ui.theme.fg("muted", "•"),
|
|
115
|
+
],
|
|
116
|
+
intervalMs: 120,
|
|
117
|
+
});
|
|
118
|
+
ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicator
|
|
119
|
+
ctx.ui.setWorkingIndicator(); // Restore the default one-cell ∀ luminance ramp
|
|
120
|
+
// The working status uses a standalone row by default. A CustomEditor can opt
|
|
121
|
+
// into placing it in the top border with { embedWorkingStatus: true }.
|
|
122
|
+
|
|
123
|
+
// Widget above editor (default)
|
|
124
|
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
|
125
|
+
// Widget below editor
|
|
126
|
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
|
|
127
|
+
ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
|
|
128
|
+
ctx.ui.setWidget("my-widget", undefined); // Clear
|
|
129
|
+
|
|
130
|
+
// Custom footer (replaces built-in footer entirely)
|
|
131
|
+
ctx.ui.setFooter((tui, theme) => ({
|
|
132
|
+
render(width) { return [theme.fg("dim", "Custom footer")]; },
|
|
133
|
+
invalidate() {},
|
|
134
|
+
}));
|
|
135
|
+
ctx.ui.setFooter(undefined); // Restore built-in footer
|
|
136
|
+
|
|
137
|
+
// Terminal title
|
|
138
|
+
ctx.ui.setTitle("atomic - my-project");
|
|
139
|
+
|
|
140
|
+
// Editor text
|
|
141
|
+
ctx.ui.setEditorText("Prefill text");
|
|
142
|
+
const current = ctx.ui.getEditorText();
|
|
143
|
+
|
|
144
|
+
// Paste into editor (triggers paste handling, including collapse for large content)
|
|
145
|
+
ctx.ui.pasteToEditor("pasted content");
|
|
146
|
+
|
|
147
|
+
// Stack custom autocomplete behavior on top of the built-in provider
|
|
148
|
+
ctx.ui.addAutocompleteProvider((current) => ({
|
|
149
|
+
async getSuggestions(lines, line, col, options) {
|
|
150
|
+
const beforeCursor = (lines[line] ?? "").slice(0, col);
|
|
151
|
+
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
|
|
152
|
+
if (!match) {
|
|
153
|
+
return current.getSuggestions(lines, line, col, options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
prefix: `#${match[1] ?? ""}`,
|
|
158
|
+
items: [{ value: "#2983", label: "#2983", description: "Extension API for autocomplete" }],
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
applyCompletion(lines, line, col, item, prefix) {
|
|
162
|
+
return current.applyCompletion(lines, line, col, item, prefix);
|
|
163
|
+
},
|
|
164
|
+
shouldTriggerFileCompletion(lines, line, col) {
|
|
165
|
+
return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;
|
|
166
|
+
},
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
// Tool output expansion
|
|
170
|
+
const wasExpanded = ctx.ui.getToolsExpanded();
|
|
171
|
+
ctx.ui.setToolsExpanded(true);
|
|
172
|
+
ctx.ui.setToolsExpanded(wasExpanded);
|
|
173
|
+
|
|
174
|
+
// Custom editor (vim mode, emacs mode, etc.)
|
|
175
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
|
|
176
|
+
const currentEditor = ctx.ui.getEditorComponent();
|
|
177
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
|
|
178
|
+
new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))
|
|
179
|
+
);
|
|
180
|
+
ctx.ui.setEditorComponent(undefined); // Restore default editor
|
|
181
|
+
|
|
182
|
+
// Theme management (see themes.md for creating themes)
|
|
183
|
+
const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
|
|
184
|
+
const lightTheme = ctx.ui.getTheme("light"); // Load without switching
|
|
185
|
+
const result = ctx.ui.setTheme("light"); // Switch by name
|
|
186
|
+
if (!result.success) {
|
|
187
|
+
ctx.ui.notify(`Failed: ${result.error}`, "error");
|
|
188
|
+
}
|
|
189
|
+
ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
|
|
190
|
+
ctx.ui.theme.fg("accent", "styled text"); // Access current theme
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Calling `setToolsExpanded()` with the current value is a no-op.
|
|
194
|
+
|
|
195
|
+
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(...)`.
|
|
196
|
+
|
|
197
|
+
These APIs customize presentation only; they do not start work or emit an extension stream event before prompt startup. See [Working Indicator Customization](/tui#pattern-4b-working-indicator-customization) for accepted-prompt, pre-stream, and agent-turn handoff timing.
|
|
198
|
+
|
|
199
|
+
### Autocomplete Providers
|
|
200
|
+
|
|
201
|
+
Use `ctx.ui.addAutocompleteProvider()` to stack custom autocomplete logic on top of the built-in slash-command and path provider.
|
|
202
|
+
|
|
203
|
+
Typical pattern:
|
|
204
|
+
|
|
205
|
+
- inspect the text before the cursor
|
|
206
|
+
- return your own suggestions when your extension-specific syntax matches
|
|
207
|
+
- otherwise delegate to `current.getSuggestions(...)`
|
|
208
|
+
- delegate `applyCompletion(...)` unless you need custom insertion behavior
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
pi.on("session_start", (_event, ctx) => {
|
|
212
|
+
ctx.ui.addAutocompleteProvider((current) => ({
|
|
213
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
214
|
+
const line = lines[cursorLine] ?? "";
|
|
215
|
+
const beforeCursor = line.slice(0, cursorCol);
|
|
216
|
+
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
|
|
217
|
+
if (!match) {
|
|
218
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
prefix: `#${match[1] ?? ""}`,
|
|
223
|
+
items: [
|
|
224
|
+
{ value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" },
|
|
225
|
+
{ value: "#2753", label: "#2753", description: "Reload stale resource settings" },
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
231
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
235
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
236
|
+
},
|
|
237
|
+
}));
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
See [github-issue-autocomplete.ts](https://github.com/bastani-inc/atomic/blob/main/packages/coding-agent/examples/extensions/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.
|
|
242
|
+
|
|
243
|
+
### Custom Components
|
|
244
|
+
|
|
245
|
+
For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
249
|
+
|
|
250
|
+
class ConfirmPrompt implements Component {
|
|
251
|
+
render(width: number): string[] {
|
|
252
|
+
return new Text("Enter Confirm · Escape Cancel", 1, 1).render(width);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
invalidate(): void {}
|
|
256
|
+
|
|
257
|
+
handleInput(data: string): boolean {
|
|
258
|
+
if (data === "\r") {
|
|
259
|
+
this.done(true);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
if (data === "\x1b") {
|
|
263
|
+
this.done(false);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
constructor(private readonly done: (value: boolean) => void) {}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = await ctx.ui.custom<boolean>((_tui, _theme, _keybindings, done) => {
|
|
273
|
+
return new ConfirmPrompt(done);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (result) {
|
|
277
|
+
// User pressed Enter
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The callback receives:
|
|
282
|
+
- `tui` - TUI instance (for screen dimensions, focus management)
|
|
283
|
+
- `theme` - Current theme for styling
|
|
284
|
+
- `keybindings` - App keybinding manager (for checking shortcuts)
|
|
285
|
+
- `done(value)` - Call to close component and return value
|
|
286
|
+
|
|
287
|
+
Pass `{ signal }` to dismiss the custom UI if an operation is aborted; the returned promise rejects with the signal reason.
|
|
288
|
+
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.
|
|
289
|
+
|
|
290
|
+
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.
|
|
291
|
+
|
|
292
|
+
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.
|
|
293
|
+
|
|
294
|
+
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](/extensions#interactive-callback-isolation).
|
|
295
|
+
|
|
296
|
+
See [TUI components](/tui) for the full component API.
|
|
297
|
+
|
|
298
|
+
#### Overlay Mode (Experimental)
|
|
299
|
+
|
|
300
|
+
Pass `{ overlay: true }` to render the component as a floating modal on top of existing content, without clearing the screen:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
const result = await ctx.ui.custom<string | null>(
|
|
304
|
+
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
|
|
305
|
+
{ overlay: true }
|
|
306
|
+
);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
For advanced positioning (anchors, margins, percentages, responsive visibility), pass `overlayOptions`. Use `onHandle` to control visibility programmatically:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const result = await ctx.ui.custom<string | null>(
|
|
313
|
+
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
|
|
314
|
+
{
|
|
315
|
+
overlay: true,
|
|
316
|
+
overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
|
|
317
|
+
onHandle: (handle) => { /* handle.setHidden(true/false) */ }
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
See [TUI components](/tui) for the full `OverlayOptions` API and [overlay-qa-tests.ts](https://github.com/bastani-inc/atomic/blob/main/packages/coding-agent/examples/extensions/overlay-qa-tests.ts) for examples.
|
|
323
|
+
|
|
324
|
+
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.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
const result = await ctx.ui.custom<string | null>(
|
|
328
|
+
(tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
|
|
329
|
+
{
|
|
330
|
+
overlay: true,
|
|
331
|
+
reserveTranscriptRows: true,
|
|
332
|
+
overlayOptions: { anchor: "bottom-center", width: "100%" },
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
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`.
|
|
338
|
+
|
|
339
|
+
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.
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { OVERLAY_ACTIVE_ROW_MARKER } from "@bastani/atomic";
|
|
343
|
+
|
|
344
|
+
render(width: number): string[] {
|
|
345
|
+
return this.items.map((item, index) =>
|
|
346
|
+
index === this.selected ? `${this.row(item, width)}${OVERLAY_ACTIVE_ROW_MARKER}` : this.row(item, width),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Custom Editor
|
|
352
|
+
|
|
353
|
+
Replace the main input editor with a custom implementation (vim mode, emacs mode, etc.):
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { CustomEditor, type ExtensionAPI } from "@bastani/atomic";
|
|
357
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
358
|
+
|
|
359
|
+
class VimEditor extends CustomEditor {
|
|
360
|
+
private mode: "normal" | "insert" = "insert";
|
|
361
|
+
|
|
362
|
+
handleInput(data: string): boolean {
|
|
363
|
+
if (matchesKey(data, "escape") && this.mode === "insert") {
|
|
364
|
+
this.mode = "normal";
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (this.mode === "normal" && data === "i") {
|
|
368
|
+
this.mode = "insert";
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
return super.handleInput(data); // App keybindings + text editing
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default function (pi: ExtensionAPI) {
|
|
376
|
+
pi.on("session_start", (_event, ctx) => {
|
|
377
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
|
|
378
|
+
new VimEditor(tui, theme, keybindings)
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Key points:**
|
|
385
|
+
- Extend `CustomEditor` (not base `Editor`) to get app keybindings (escape to abort, ctrl+d, model switching)
|
|
386
|
+
- Call `super.handleInput(data)` for keys you don't handle
|
|
387
|
+
- Editors keep the standalone working row by default. Pass `{ embedWorkingStatus: true }` as the fourth `CustomEditor` constructor argument to opt into the editor-border spinner.
|
|
388
|
+
- Factory receives `tui`, `theme`, and `keybindings` from the app
|
|
389
|
+
- Use `ctx.ui.getEditorComponent()` before `setEditorComponent()` to wrap the previously configured custom editor
|
|
390
|
+
- Pass `undefined` to restore default: `ctx.ui.setEditorComponent(undefined)`
|
|
391
|
+
- When a custom editor installed through `ctx.ui.setEditorComponent()` exposes `setAutocompleteMaxVisible()`, Atomic initializes it from the active `autocompleteMaxVisible` setting.
|
|
392
|
+
|
|
393
|
+
To compose with another extension that already replaced the editor, capture the previous factory before setting yours:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
const previous = ctx.ui.getEditorComponent();
|
|
397
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
|
|
398
|
+
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
|
|
399
|
+
);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
See [TUI components](/tui) Pattern 7 for a complete example with mode indicator.
|
|
403
|
+
|
|
404
|
+
### Message Rendering
|
|
405
|
+
|
|
406
|
+
Register a custom renderer for messages with your `customType`:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
410
|
+
|
|
411
|
+
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
|
|
412
|
+
const { expanded, outputPad } = options;
|
|
413
|
+
let text = theme.fg("accent", `[${message.customType}] `);
|
|
414
|
+
text += message.content;
|
|
415
|
+
|
|
416
|
+
if (expanded && message.details) {
|
|
417
|
+
text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return new Text(text, outputPad, 0);
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Messages are sent via `pi.sendMessage()`:
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
pi.sendMessage({
|
|
428
|
+
customType: "my-extension", // Matches registerMessageRenderer
|
|
429
|
+
content: "Status update",
|
|
430
|
+
display: true, // Show in TUI
|
|
431
|
+
details: { ... }, // Available in renderer
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Theme Colors
|
|
436
|
+
|
|
437
|
+
All render functions receive a `theme` object. See [Themes](/themes) for creating custom themes and the full color palette.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// Foreground colors
|
|
441
|
+
theme.fg("toolTitle", text) // Tool names
|
|
442
|
+
theme.fg("accent", text) // Highlights
|
|
443
|
+
theme.fg("success", text) // Success (green)
|
|
444
|
+
theme.fg("error", text) // Errors (red)
|
|
445
|
+
theme.fg("warning", text) // Warnings (yellow)
|
|
446
|
+
theme.fg("muted", text) // Secondary text
|
|
447
|
+
theme.fg("dim", text) // Tertiary text
|
|
448
|
+
|
|
449
|
+
// Text styles
|
|
450
|
+
theme.bold(text)
|
|
451
|
+
theme.italic(text)
|
|
452
|
+
theme.strikethrough(text)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
For syntax highlighting in custom tool renderers:
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
import { highlightCode, getLanguageFromPath } from "@bastani/atomic";
|
|
459
|
+
|
|
460
|
+
// Highlight code with explicit language
|
|
461
|
+
const highlighted = highlightCode("const x = 1;", "typescript", theme);
|
|
462
|
+
|
|
463
|
+
// Auto-detect language from file path
|
|
464
|
+
const lang = getLanguageFromPath("/path/to/file.rs"); // "rust"
|
|
465
|
+
const highlighted = highlightCode(code, lang, theme);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Next steps
|
|
469
|
+
|
|
470
|
+
Try the runnable [extension examples](/extensions/examples), and use the [Extension API reference](/extensions/api-reference) for context and method contracts.
|