indusagi-coding-agent 0.1.0

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.
Files changed (240) hide show
  1. package/CHANGELOG.md +2249 -0
  2. package/README.md +546 -0
  3. package/dist/cli/args.js +282 -0
  4. package/dist/cli/config-selector.js +30 -0
  5. package/dist/cli/file-processor.js +78 -0
  6. package/dist/cli/list-models.js +91 -0
  7. package/dist/cli/session-picker.js +31 -0
  8. package/dist/cli.js +10 -0
  9. package/dist/config.js +158 -0
  10. package/dist/core/agent-session.js +2097 -0
  11. package/dist/core/auth-storage.js +278 -0
  12. package/dist/core/bash-executor.js +211 -0
  13. package/dist/core/compaction/branch-summarization.js +241 -0
  14. package/dist/core/compaction/compaction.js +606 -0
  15. package/dist/core/compaction/index.js +6 -0
  16. package/dist/core/compaction/utils.js +137 -0
  17. package/dist/core/diagnostics.js +1 -0
  18. package/dist/core/event-bus.js +24 -0
  19. package/dist/core/exec.js +70 -0
  20. package/dist/core/export-html/ansi-to-html.js +248 -0
  21. package/dist/core/export-html/index.js +221 -0
  22. package/dist/core/export-html/template.css +905 -0
  23. package/dist/core/export-html/template.html +54 -0
  24. package/dist/core/export-html/template.js +1549 -0
  25. package/dist/core/export-html/tool-renderer.js +56 -0
  26. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  27. package/dist/core/export-html/vendor/marked.min.js +6 -0
  28. package/dist/core/extensions/index.js +8 -0
  29. package/dist/core/extensions/loader.js +395 -0
  30. package/dist/core/extensions/runner.js +499 -0
  31. package/dist/core/extensions/types.js +31 -0
  32. package/dist/core/extensions/wrapper.js +101 -0
  33. package/dist/core/footer-data-provider.js +133 -0
  34. package/dist/core/index.js +8 -0
  35. package/dist/core/keybindings.js +140 -0
  36. package/dist/core/messages.js +122 -0
  37. package/dist/core/model-registry.js +454 -0
  38. package/dist/core/model-resolver.js +309 -0
  39. package/dist/core/package-manager.js +1142 -0
  40. package/dist/core/prompt-templates.js +250 -0
  41. package/dist/core/resource-loader.js +569 -0
  42. package/dist/core/sdk.js +225 -0
  43. package/dist/core/session-manager.js +1078 -0
  44. package/dist/core/settings-manager.js +430 -0
  45. package/dist/core/skills.js +339 -0
  46. package/dist/core/system-prompt.js +136 -0
  47. package/dist/core/timings.js +24 -0
  48. package/dist/core/tools/bash.js +226 -0
  49. package/dist/core/tools/edit-diff.js +242 -0
  50. package/dist/core/tools/edit.js +145 -0
  51. package/dist/core/tools/find.js +205 -0
  52. package/dist/core/tools/grep.js +238 -0
  53. package/dist/core/tools/index.js +60 -0
  54. package/dist/core/tools/ls.js +117 -0
  55. package/dist/core/tools/path-utils.js +52 -0
  56. package/dist/core/tools/read.js +165 -0
  57. package/dist/core/tools/truncate.js +204 -0
  58. package/dist/core/tools/write.js +77 -0
  59. package/dist/index.js +41 -0
  60. package/dist/main.js +565 -0
  61. package/dist/migrations.js +260 -0
  62. package/dist/modes/index.js +7 -0
  63. package/dist/modes/interactive/components/armin.js +328 -0
  64. package/dist/modes/interactive/components/assistant-message.js +86 -0
  65. package/dist/modes/interactive/components/bash-execution.js +155 -0
  66. package/dist/modes/interactive/components/bordered-loader.js +47 -0
  67. package/dist/modes/interactive/components/branch-summary-message.js +41 -0
  68. package/dist/modes/interactive/components/compaction-summary-message.js +42 -0
  69. package/dist/modes/interactive/components/config-selector.js +458 -0
  70. package/dist/modes/interactive/components/countdown-timer.js +27 -0
  71. package/dist/modes/interactive/components/custom-editor.js +61 -0
  72. package/dist/modes/interactive/components/custom-message.js +80 -0
  73. package/dist/modes/interactive/components/diff.js +132 -0
  74. package/dist/modes/interactive/components/dynamic-border.js +19 -0
  75. package/dist/modes/interactive/components/extension-editor.js +96 -0
  76. package/dist/modes/interactive/components/extension-input.js +54 -0
  77. package/dist/modes/interactive/components/extension-selector.js +70 -0
  78. package/dist/modes/interactive/components/footer.js +213 -0
  79. package/dist/modes/interactive/components/index.js +31 -0
  80. package/dist/modes/interactive/components/keybinding-hints.js +60 -0
  81. package/dist/modes/interactive/components/login-dialog.js +138 -0
  82. package/dist/modes/interactive/components/model-selector.js +253 -0
  83. package/dist/modes/interactive/components/oauth-selector.js +91 -0
  84. package/dist/modes/interactive/components/scoped-models-selector.js +262 -0
  85. package/dist/modes/interactive/components/session-selector-search.js +145 -0
  86. package/dist/modes/interactive/components/session-selector.js +698 -0
  87. package/dist/modes/interactive/components/settings-selector.js +250 -0
  88. package/dist/modes/interactive/components/show-images-selector.js +33 -0
  89. package/dist/modes/interactive/components/skill-invocation-message.js +44 -0
  90. package/dist/modes/interactive/components/theme-selector.js +43 -0
  91. package/dist/modes/interactive/components/thinking-selector.js +45 -0
  92. package/dist/modes/interactive/components/tool-execution.js +608 -0
  93. package/dist/modes/interactive/components/tree-selector.js +892 -0
  94. package/dist/modes/interactive/components/user-message-selector.js +109 -0
  95. package/dist/modes/interactive/components/user-message.js +15 -0
  96. package/dist/modes/interactive/components/visual-truncate.js +32 -0
  97. package/dist/modes/interactive/interactive-mode.js +3576 -0
  98. package/dist/modes/interactive/theme/dark.json +85 -0
  99. package/dist/modes/interactive/theme/light.json +84 -0
  100. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  101. package/dist/modes/interactive/theme/theme.js +938 -0
  102. package/dist/modes/print-mode.js +96 -0
  103. package/dist/modes/rpc/rpc-client.js +390 -0
  104. package/dist/modes/rpc/rpc-mode.js +448 -0
  105. package/dist/modes/rpc/rpc-types.js +7 -0
  106. package/dist/utils/changelog.js +86 -0
  107. package/dist/utils/clipboard-image.js +116 -0
  108. package/dist/utils/clipboard.js +58 -0
  109. package/dist/utils/frontmatter.js +25 -0
  110. package/dist/utils/git.js +5 -0
  111. package/dist/utils/image-convert.js +34 -0
  112. package/dist/utils/image-resize.js +180 -0
  113. package/dist/utils/mime.js +25 -0
  114. package/dist/utils/photon.js +120 -0
  115. package/dist/utils/shell.js +164 -0
  116. package/dist/utils/sleep.js +16 -0
  117. package/dist/utils/tools-manager.js +186 -0
  118. package/docs/compaction.md +390 -0
  119. package/docs/custom-provider.md +538 -0
  120. package/docs/development.md +69 -0
  121. package/docs/extensions.md +1733 -0
  122. package/docs/images/doom-extension.png +0 -0
  123. package/docs/images/interactive-mode.png +0 -0
  124. package/docs/images/tree-view.png +0 -0
  125. package/docs/json.md +79 -0
  126. package/docs/keybindings.md +162 -0
  127. package/docs/models.md +193 -0
  128. package/docs/packages.md +163 -0
  129. package/docs/prompt-templates.md +67 -0
  130. package/docs/providers.md +147 -0
  131. package/docs/rpc.md +1048 -0
  132. package/docs/sdk.md +957 -0
  133. package/docs/session.md +412 -0
  134. package/docs/settings.md +216 -0
  135. package/docs/shell-aliases.md +13 -0
  136. package/docs/skills.md +226 -0
  137. package/docs/terminal-setup.md +65 -0
  138. package/docs/themes.md +295 -0
  139. package/docs/tree.md +219 -0
  140. package/docs/tui.md +887 -0
  141. package/docs/windows.md +17 -0
  142. package/examples/README.md +25 -0
  143. package/examples/extensions/README.md +192 -0
  144. package/examples/extensions/antigravity-image-gen.ts +414 -0
  145. package/examples/extensions/auto-commit-on-exit.ts +49 -0
  146. package/examples/extensions/bookmark.ts +50 -0
  147. package/examples/extensions/claude-rules.ts +86 -0
  148. package/examples/extensions/confirm-destructive.ts +59 -0
  149. package/examples/extensions/custom-compaction.ts +115 -0
  150. package/examples/extensions/custom-footer.ts +65 -0
  151. package/examples/extensions/custom-header.ts +73 -0
  152. package/examples/extensions/custom-provider-anthropic/index.ts +605 -0
  153. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  154. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  155. package/examples/extensions/custom-provider-gitlab-duo/index.ts +350 -0
  156. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  157. package/examples/extensions/custom-provider-gitlab-duo/test.ts +83 -0
  158. package/examples/extensions/dirty-repo-guard.ts +56 -0
  159. package/examples/extensions/doom-overlay/README.md +46 -0
  160. package/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
  161. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  162. package/examples/extensions/doom-overlay/doom/build.sh +152 -0
  163. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
  164. package/examples/extensions/doom-overlay/doom-component.ts +133 -0
  165. package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  166. package/examples/extensions/doom-overlay/doom-keys.ts +105 -0
  167. package/examples/extensions/doom-overlay/index.ts +74 -0
  168. package/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  169. package/examples/extensions/event-bus.ts +43 -0
  170. package/examples/extensions/file-trigger.ts +41 -0
  171. package/examples/extensions/git-checkpoint.ts +53 -0
  172. package/examples/extensions/handoff.ts +151 -0
  173. package/examples/extensions/hello.ts +25 -0
  174. package/examples/extensions/inline-bash.ts +94 -0
  175. package/examples/extensions/input-transform.ts +43 -0
  176. package/examples/extensions/interactive-shell.ts +196 -0
  177. package/examples/extensions/mac-system-theme.ts +47 -0
  178. package/examples/extensions/message-renderer.ts +60 -0
  179. package/examples/extensions/modal-editor.ts +86 -0
  180. package/examples/extensions/model-status.ts +31 -0
  181. package/examples/extensions/notify.ts +25 -0
  182. package/examples/extensions/overlay-qa-tests.ts +882 -0
  183. package/examples/extensions/overlay-test.ts +151 -0
  184. package/examples/extensions/permission-gate.ts +34 -0
  185. package/examples/extensions/pirate.ts +47 -0
  186. package/examples/extensions/plan-mode/README.md +65 -0
  187. package/examples/extensions/plan-mode/index.ts +341 -0
  188. package/examples/extensions/plan-mode/utils.ts +168 -0
  189. package/examples/extensions/preset.ts +399 -0
  190. package/examples/extensions/protected-paths.ts +30 -0
  191. package/examples/extensions/qna.ts +120 -0
  192. package/examples/extensions/question.ts +265 -0
  193. package/examples/extensions/questionnaire.ts +428 -0
  194. package/examples/extensions/rainbow-editor.ts +88 -0
  195. package/examples/extensions/sandbox/index.ts +318 -0
  196. package/examples/extensions/sandbox/package-lock.json +92 -0
  197. package/examples/extensions/sandbox/package.json +19 -0
  198. package/examples/extensions/send-user-message.ts +97 -0
  199. package/examples/extensions/session-name.ts +27 -0
  200. package/examples/extensions/shutdown-command.ts +63 -0
  201. package/examples/extensions/snake.ts +344 -0
  202. package/examples/extensions/space-invaders.ts +561 -0
  203. package/examples/extensions/ssh.ts +220 -0
  204. package/examples/extensions/status-line.ts +40 -0
  205. package/examples/extensions/subagent/README.md +172 -0
  206. package/examples/extensions/subagent/agents/planner.md +37 -0
  207. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  208. package/examples/extensions/subagent/agents/scout.md +50 -0
  209. package/examples/extensions/subagent/agents/worker.md +24 -0
  210. package/examples/extensions/subagent/agents.ts +127 -0
  211. package/examples/extensions/subagent/index.ts +964 -0
  212. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  213. package/examples/extensions/subagent/prompts/implement.md +10 -0
  214. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  215. package/examples/extensions/summarize.ts +196 -0
  216. package/examples/extensions/timed-confirm.ts +70 -0
  217. package/examples/extensions/todo.ts +300 -0
  218. package/examples/extensions/tool-override.ts +144 -0
  219. package/examples/extensions/tools.ts +147 -0
  220. package/examples/extensions/trigger-compact.ts +40 -0
  221. package/examples/extensions/truncated-tool.ts +193 -0
  222. package/examples/extensions/widget-placement.ts +17 -0
  223. package/examples/extensions/with-deps/index.ts +36 -0
  224. package/examples/extensions/with-deps/package-lock.json +31 -0
  225. package/examples/extensions/with-deps/package.json +22 -0
  226. package/examples/sdk/01-minimal.ts +22 -0
  227. package/examples/sdk/02-custom-model.ts +50 -0
  228. package/examples/sdk/03-custom-prompt.ts +55 -0
  229. package/examples/sdk/04-skills.ts +46 -0
  230. package/examples/sdk/05-tools.ts +56 -0
  231. package/examples/sdk/06-extensions.ts +88 -0
  232. package/examples/sdk/07-context-files.ts +40 -0
  233. package/examples/sdk/08-prompt-templates.ts +47 -0
  234. package/examples/sdk/09-api-keys-and-oauth.ts +48 -0
  235. package/examples/sdk/10-settings.ts +38 -0
  236. package/examples/sdk/11-sessions.ts +48 -0
  237. package/examples/sdk/12-full-control.ts +82 -0
  238. package/examples/sdk/13-codex-oauth.ts +37 -0
  239. package/examples/sdk/README.md +144 -0
  240. package/package.json +85 -0
@@ -0,0 +1,1733 @@
1
+ > indusagi can create extensions. Ask it to build one for your use case.
2
+
3
+ # Extensions
4
+
5
+ Extensions are TypeScript modules that extend indusagi's behavior. They can subscribe to lifecycle events, register custom tools callable by the LLM, add commands, and more.
6
+
7
+ **Key capabilities:**
8
+ - **Custom tools** - Register tools the LLM can call via `indusagi.registerTool()`
9
+ - **Event interception** - Block or modify tool calls, inject context, customize compaction
10
+ - **User interaction** - Prompt users via `ctx.ui` (select, confirm, input, notify)
11
+ - **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()` for complex interactions
12
+ - **Custom commands** - Register commands like `/mycommand` via `indusagi.registerCommand()`
13
+ - **Session persistence** - Store state that survives restarts via `indusagi.appendEntry()`
14
+ - **Custom rendering** - Control how tool calls/results and messages appear in TUI
15
+
16
+ **Example use cases:**
17
+ - Permission gates (confirm before `rm -rf`, `sudo`, etc.)
18
+ - Git checkpointing (stash at each turn, restore on branch)
19
+ - Path protection (block writes to `.env`, `node_modules/`)
20
+ - Custom compaction (summarize conversation your way)
21
+ - Conversation summaries (see `summarize.ts` example)
22
+ - Interactive tools (questions, wizards, custom dialogs)
23
+ - Stateful tools (todo lists, connection pools)
24
+ - External integrations (file watchers, webhooks, CI triggers)
25
+ - Games while you wait (see `snake.ts` example)
26
+
27
+ See [examples/extensions/](../examples/extensions/) for working implementations.
28
+
29
+ ## Table of Contents
30
+
31
+ - [Quick Start](#quick-start)
32
+ - [Extension Locations](#extension-locations)
33
+ - [Available Imports](#available-imports)
34
+ - [Writing an Extension](#writing-an-extension)
35
+ - [Extension Styles](#extension-styles)
36
+ - [Events](#events)
37
+ - [Lifecycle Overview](#lifecycle-overview)
38
+ - [Session Events](#session-events)
39
+ - [Agent Events](#agent-events)
40
+ - [Tool Events](#tool-events)
41
+ - [ExtensionContext](#extensioncontext)
42
+ - [ExtensionCommandContext](#extensioncommandcontext)
43
+ - [ExtensionAPI Methods](#extensionapi-methods)
44
+ - [State Management](#state-management)
45
+ - [Custom Tools](#custom-tools)
46
+ - [Custom UI](#custom-ui)
47
+ - [Error Handling](#error-handling)
48
+ - [Mode Behavior](#mode-behavior)
49
+ - [Examples Reference](#examples-reference)
50
+
51
+ ## Quick Start
52
+
53
+ Create `~/.indusagi/agent/extensions/my-extension.ts`:
54
+
55
+ ```typescript
56
+ import type { ExtensionAPI } from "indusagi-coding-agent";
57
+ import { Type } from "@sinclair/typebox";
58
+
59
+ export default function (indusagi: ExtensionAPI) {
60
+ // React to events
61
+ indusagi.on("session_start", async (_event, ctx) => {
62
+ ctx.ui.notify("Extension loaded!", "info");
63
+ });
64
+
65
+ indusagi.on("tool_call", async (event, ctx) => {
66
+ if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
67
+ const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
68
+ if (!ok) return { block: true, reason: "Blocked by user" };
69
+ }
70
+ });
71
+
72
+ // Register a custom tool
73
+ indusagi.registerTool({
74
+ name: "greet",
75
+ label: "Greet",
76
+ description: "Greet someone by name",
77
+ parameters: Type.Object({
78
+ name: Type.String({ description: "Name to greet" }),
79
+ }),
80
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
81
+ return {
82
+ content: [{ type: "text", text: `Hello, ${params.name}!` }],
83
+ details: {},
84
+ };
85
+ },
86
+ });
87
+
88
+ // Register a command
89
+ indusagi.registerCommand("hello", {
90
+ description: "Say hello",
91
+ handler: async (args, ctx) => {
92
+ ctx.ui.notify(`Hello ${args || "world"}!`, "info");
93
+ },
94
+ });
95
+ }
96
+ ```
97
+
98
+ Test with `--extension` (or `-e`) flag:
99
+
100
+ ```bash
101
+ indusagi -e ./my-extension.ts
102
+ ```
103
+
104
+ ## Extension Locations
105
+
106
+ > **Security:** Extensions run with your full system permissions and can execute arbitrary code. Only install from sources you trust.
107
+
108
+ Extensions are auto-discovered from:
109
+
110
+ | Location | Scope |
111
+ |----------|-------|
112
+ | `~/.indusagi/agent/extensions/*.ts` | Global (all projects) |
113
+ | `~/.indusagi/agent/extensions/*/index.ts` | Global (subdirectory) |
114
+ | `.indusagi/extensions/*.ts` | Project-local |
115
+ | `.indusagi/extensions/*/index.ts` | Project-local (subdirectory) |
116
+
117
+ Additional paths via `settings.json`:
118
+
119
+ ```json
120
+ {
121
+ "packages": [
122
+ "npm:@foo/bar@1.0.0",
123
+ "git:github.com/user/repo@v1"
124
+ ],
125
+ "extensions": [
126
+ "/path/to/local/extension.ts",
127
+ "/path/to/local/extension/dir"
128
+ ]
129
+ }
130
+ ```
131
+
132
+ To share extensions via npm or git as indusagi packages, see [packages.md](packages.md).
133
+
134
+ ## Available Imports
135
+
136
+ | Package | Purpose |
137
+ |---------|---------|
138
+ | `indusagi-coding-agent` | Extension types (`ExtensionAPI`, `ExtensionContext`, events) |
139
+ | `@sinclair/typebox` | Schema definitions for tool parameters |
140
+ | `indusagi-ai` | AI utilities (`StringEnum` for Google-compatible enums) |
141
+ | `indusagi-tui` | TUI components for custom rendering |
142
+
143
+ npm dependencies work too. Add a `package.json` next to your extension (or in a parent directory), run `npm install`, and imports from `node_modules/` are resolved automatically.
144
+
145
+ Node.js built-ins (`node:fs`, `node:path`, etc.) are also available.
146
+
147
+ ## Writing an Extension
148
+
149
+ An extension exports a default function that receives `ExtensionAPI`:
150
+
151
+ ```typescript
152
+ import type { ExtensionAPI } from "indusagi-coding-agent";
153
+
154
+ export default function (indusagi: ExtensionAPI) {
155
+ // Subscribe to events
156
+ indusagi.on("event_name", async (event, ctx) => {
157
+ // ctx.ui for user interaction
158
+ const ok = await ctx.ui.confirm("Title", "Are you sure?");
159
+ ctx.ui.notify("Done!", "success");
160
+ ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
161
+ ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
162
+ });
163
+
164
+ // Register tools, commands, shortcuts, flags
165
+ indusagi.registerTool({ ... });
166
+ indusagi.registerCommand("name", { ... });
167
+ indusagi.registerShortcut("ctrl+x", { ... });
168
+ indusagi.registerFlag("my-flag", { ... });
169
+ }
170
+ ```
171
+
172
+ Extensions are loaded via [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation.
173
+
174
+ ### Extension Styles
175
+
176
+ **Single file** - simplest, for small extensions:
177
+
178
+ ```
179
+ ~/.indusagi/agent/extensions/
180
+ └── my-extension.ts
181
+ ```
182
+
183
+ **Directory with index.ts** - for multi-file extensions:
184
+
185
+ ```
186
+ ~/.indusagi/agent/extensions/
187
+ └── my-extension/
188
+ ├── index.ts # Entry point (exports default function)
189
+ ├── tools.ts # Helper module
190
+ └── utils.ts # Helper module
191
+ ```
192
+
193
+ **Package with dependencies** - for extensions that need npm packages:
194
+
195
+ ```
196
+ ~/.indusagi/agent/extensions/
197
+ └── my-extension/
198
+ ├── package.json # Declares dependencies and entry points
199
+ ├── package-lock.json
200
+ ├── node_modules/ # After npm install
201
+ └── src/
202
+ └── index.ts
203
+ ```
204
+
205
+ ```json
206
+ // package.json
207
+ {
208
+ "name": "my-extension",
209
+ "dependencies": {
210
+ "zod": "^3.0.0",
211
+ "chalk": "^5.0.0"
212
+ },
213
+ "indusagi": {
214
+ "extensions": ["./src/index.ts"]
215
+ }
216
+ }
217
+ ```
218
+
219
+ Run `npm install` in the extension directory, then imports from `node_modules/` work automatically.
220
+
221
+ ## Events
222
+
223
+ ### Lifecycle Overview
224
+
225
+ ```
226
+ indusagi starts
227
+
228
+ └─► session_start
229
+
230
+
231
+ user sends prompt ─────────────────────────────────────────┐
232
+ │ │
233
+ ├─► (extension commands checked first, bypass if found) │
234
+ ├─► input (can intercept, transform, or handle) │
235
+ ├─► (skill/template expansion if not handled) │
236
+ ├─► before_agent_start (can inject message, modify system prompt)
237
+ ├─► agent_start │
238
+ │ │
239
+ │ ┌─── turn (repeats while LLM calls tools) ───┐ │
240
+ │ │ │ │
241
+ │ ├─► turn_start │ │
242
+ │ ├─► context (can modify messages) │ │
243
+ │ │ │ │
244
+ │ │ LLM responds, may call tools: │ │
245
+ │ │ ├─► tool_call (can block) │ │
246
+ │ │ │ tool executes │ │
247
+ │ │ └─► tool_result (can modify) │ │
248
+ │ │ │ │
249
+ │ └─► turn_end │ │
250
+ │ │
251
+ └─► agent_end │
252
+
253
+ user sends another prompt ◄────────────────────────────────┘
254
+
255
+ /new (new session) or /resume (switch session)
256
+ ├─► session_before_switch (can cancel)
257
+ └─► session_switch
258
+
259
+ /fork
260
+ ├─► session_before_fork (can cancel)
261
+ └─► session_fork
262
+
263
+ /compact or auto-compaction
264
+ ├─► session_before_compact (can cancel or customize)
265
+ └─► session_compact
266
+
267
+ /tree navigation
268
+ ├─► session_before_tree (can cancel or customize)
269
+ └─► session_tree
270
+
271
+ /model or Ctrl+P (model selection/cycling)
272
+ └─► model_select
273
+
274
+ exit (Ctrl+C, Ctrl+D)
275
+ └─► session_shutdown
276
+ ```
277
+
278
+ ### Session Events
279
+
280
+ See [session.md](session.md) for session storage internals and the SessionManager API.
281
+
282
+ #### session_start
283
+
284
+ Fired on initial session load.
285
+
286
+ ```typescript
287
+ indusagi.on("session_start", async (_event, ctx) => {
288
+ ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
289
+ });
290
+ ```
291
+
292
+ #### session_before_switch / session_switch
293
+
294
+ Fired when starting a new session (`/new`) or switching sessions (`/resume`).
295
+
296
+ ```typescript
297
+ indusagi.on("session_before_switch", async (event, ctx) => {
298
+ // event.reason - "new" or "resume"
299
+ // event.targetSessionFile - session we're switching to (only for "resume")
300
+
301
+ if (event.reason === "new") {
302
+ const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
303
+ if (!ok) return { cancel: true };
304
+ }
305
+ });
306
+
307
+ indusagi.on("session_switch", async (event, ctx) => {
308
+ // event.reason - "new" or "resume"
309
+ // event.previousSessionFile - session we came from
310
+ });
311
+ ```
312
+
313
+ #### session_before_fork / session_fork
314
+
315
+ Fired when forking via `/fork`.
316
+
317
+ ```typescript
318
+ indusagi.on("session_before_fork", async (event, ctx) => {
319
+ // event.entryId - ID of the entry being forked from
320
+ return { cancel: true }; // Cancel fork
321
+ // OR
322
+ return { skipConversationRestore: true }; // Fork but don't rewind messages
323
+ });
324
+
325
+ indusagi.on("session_fork", async (event, ctx) => {
326
+ // event.previousSessionFile - previous session file
327
+ });
328
+ ```
329
+
330
+ #### session_before_compact / session_compact
331
+
332
+ Fired on compaction. See [compaction.md](compaction.md) for details.
333
+
334
+ ```typescript
335
+ indusagi.on("session_before_compact", async (event, ctx) => {
336
+ const { preparation, branchEntries, customInstructions, signal } = event;
337
+
338
+ // Cancel:
339
+ return { cancel: true };
340
+
341
+ // Custom summary:
342
+ return {
343
+ compaction: {
344
+ summary: "...",
345
+ firstKeptEntryId: preparation.firstKeptEntryId,
346
+ tokensBefore: preparation.tokensBefore,
347
+ }
348
+ };
349
+ });
350
+
351
+ indusagi.on("session_compact", async (event, ctx) => {
352
+ // event.compactionEntry - the saved compaction
353
+ // event.fromExtension - whether extension provided it
354
+ });
355
+ ```
356
+
357
+ #### session_before_tree / session_tree
358
+
359
+ Fired on `/tree` navigation. See [tree.md](tree.md) for tree navigation concepts.
360
+
361
+ ```typescript
362
+ indusagi.on("session_before_tree", async (event, ctx) => {
363
+ const { preparation, signal } = event;
364
+ return { cancel: true };
365
+ // OR provide custom summary:
366
+ return { summary: { summary: "...", details: {} } };
367
+ });
368
+
369
+ indusagi.on("session_tree", async (event, ctx) => {
370
+ // event.newLeafId, oldLeafId, summaryEntry, fromExtension
371
+ });
372
+ ```
373
+
374
+ #### session_shutdown
375
+
376
+ Fired on exit (Ctrl+C, Ctrl+D, SIGTERM).
377
+
378
+ ```typescript
379
+ indusagi.on("session_shutdown", async (_event, ctx) => {
380
+ // Cleanup, save state, etc.
381
+ });
382
+ ```
383
+
384
+ ### Agent Events
385
+
386
+ #### before_agent_start
387
+
388
+ Fired after user submits prompt, before agent loop. Can inject a message and/or modify the system prompt.
389
+
390
+ ```typescript
391
+ indusagi.on("before_agent_start", async (event, ctx) => {
392
+ // event.prompt - user's prompt text
393
+ // event.images - attached images (if any)
394
+ // event.systemPrompt - current system prompt
395
+
396
+ return {
397
+ // Inject a persistent message (stored in session, sent to LLM)
398
+ message: {
399
+ customType: "my-extension",
400
+ content: "Additional context for the LLM",
401
+ display: true,
402
+ },
403
+ // Replace the system prompt for this turn (chained across extensions)
404
+ systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
405
+ };
406
+ });
407
+ ```
408
+
409
+ #### agent_start / agent_end
410
+
411
+ Fired once per user prompt.
412
+
413
+ ```typescript
414
+ indusagi.on("agent_start", async (_event, ctx) => {});
415
+
416
+ indusagi.on("agent_end", async (event, ctx) => {
417
+ // event.messages - messages from this prompt
418
+ });
419
+ ```
420
+
421
+ #### turn_start / turn_end
422
+
423
+ Fired for each turn (one LLM response + tool calls).
424
+
425
+ ```typescript
426
+ indusagi.on("turn_start", async (event, ctx) => {
427
+ // event.turnIndex, event.timestamp
428
+ });
429
+
430
+ indusagi.on("turn_end", async (event, ctx) => {
431
+ // event.turnIndex, event.message, event.toolResults
432
+ });
433
+ ```
434
+
435
+ #### context
436
+
437
+ Fired before each LLM call. Modify messages non-destructively. See [session.md](session.md) for message types.
438
+
439
+ ```typescript
440
+ indusagi.on("context", async (event, ctx) => {
441
+ // event.messages - deep copy, safe to modify
442
+ const filtered = event.messages.filter(m => !shouldPrune(m));
443
+ return { messages: filtered };
444
+ });
445
+ ```
446
+
447
+ ### Model Events
448
+
449
+ #### model_select
450
+
451
+ Fired when the model changes via `/model` command, model cycling (`Ctrl+P`), or session restore.
452
+
453
+ ```typescript
454
+ indusagi.on("model_select", async (event, ctx) => {
455
+ // event.model - newly selected model
456
+ // event.previousModel - previous model (undefined if first selection)
457
+ // event.source - "set" | "cycle" | "restore"
458
+
459
+ const prev = event.previousModel
460
+ ? `${event.previousModel.provider}/${event.previousModel.id}`
461
+ : "none";
462
+ const next = `${event.model.provider}/${event.model.id}`;
463
+
464
+ ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
465
+ });
466
+ ```
467
+
468
+ Use this to update UI elements (status bars, footers) or perform model-specific initialization when the active model changes.
469
+
470
+ ### Tool Events
471
+
472
+ #### tool_call
473
+
474
+ Fired before tool executes. **Can block.**
475
+
476
+ ```typescript
477
+ indusagi.on("tool_call", async (event, ctx) => {
478
+ // event.toolName - "bash", "read", "write", "edit", etc.
479
+ // event.toolCallId
480
+ // event.input - tool parameters
481
+
482
+ if (shouldBlock(event)) {
483
+ return { block: true, reason: "Not allowed" };
484
+ }
485
+ });
486
+ ```
487
+
488
+ #### tool_result
489
+
490
+ Fired after tool executes. **Can modify result.**
491
+
492
+ ```typescript
493
+ import { isBashToolResult } from "indusagi-coding-agent";
494
+
495
+ indusagi.on("tool_result", async (event, ctx) => {
496
+ // event.toolName, event.toolCallId, event.input
497
+ // event.content, event.details, event.isError
498
+
499
+ if (isBashToolResult(event)) {
500
+ // event.details is typed as BashToolDetails
501
+ }
502
+
503
+ // Modify result:
504
+ return { content: [...], details: {...}, isError: false };
505
+ });
506
+ ```
507
+
508
+ ### User Bash Events
509
+
510
+ #### user_bash
511
+
512
+ Fired when user executes `!` or `!!` commands. **Can intercept.**
513
+
514
+ ```typescript
515
+ indusagi.on("user_bash", (event, ctx) => {
516
+ // event.command - the bash command
517
+ // event.excludeFromContext - true if !! prefix
518
+ // event.cwd - working directory
519
+
520
+ // Option 1: Provide custom operations (e.g., SSH)
521
+ return { operations: remoteBashOps };
522
+
523
+ // Option 2: Full replacement - return result directly
524
+ return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
525
+ });
526
+ ```
527
+
528
+ ### Input Events
529
+
530
+ #### input
531
+
532
+ Fired when user input is received, after extension commands are checked but before skill and template expansion. The event sees the raw input text, so `/skill:foo` and `/template` are not yet expanded.
533
+
534
+ **Processing order:**
535
+ 1. Extension commands (`/cmd`) checked first - if found, handler runs and input event is skipped
536
+ 2. `input` event fires - can intercept, transform, or handle
537
+ 3. If not handled: skill commands (`/skill:name`) expanded to skill content
538
+ 4. If not handled: prompt templates (`/template`) expanded to template content
539
+ 5. Agent processing begins (`before_agent_start`, etc.)
540
+
541
+ ```typescript
542
+ indusagi.on("input", async (event, ctx) => {
543
+ // event.text - raw input (before skill/template expansion)
544
+ // event.images - attached images, if any
545
+ // event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
546
+
547
+ // Transform: rewrite input before expansion
548
+ if (event.text.startsWith("?quick "))
549
+ return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
550
+
551
+ // Handle: respond without LLM (extension shows its own feedback)
552
+ if (event.text === "ping") {
553
+ ctx.ui.notify("pong", "info");
554
+ return { action: "handled" };
555
+ }
556
+
557
+ // Route by source: skip processing for extension-injected messages
558
+ if (event.source === "extension") return { action: "continue" };
559
+
560
+ // Intercept skill commands before expansion
561
+ if (event.text.startsWith("/skill:")) {
562
+ // Could transform, block, or let pass through
563
+ }
564
+
565
+ return { action: "continue" }; // Default: pass through to expansion
566
+ });
567
+ ```
568
+
569
+ **Results:**
570
+ - `continue` - pass through unchanged (default if handler returns nothing)
571
+ - `transform` - modify text/images, then continue to expansion
572
+ - `handled` - skip agent entirely (first handler to return this wins)
573
+
574
+ Transforms chain across handlers. See [input-transform.ts](../examples/extensions/input-transform.ts).
575
+
576
+ ## ExtensionContext
577
+
578
+ Every handler receives `ctx: ExtensionContext`:
579
+
580
+ ### ctx.ui
581
+
582
+ UI methods for user interaction. See [Custom UI](#custom-ui) for full details.
583
+
584
+ ### ctx.hasUI
585
+
586
+ `false` in print mode (`-p`), JSON mode, and RPC mode. Always check before using `ctx.ui`.
587
+
588
+ ### ctx.cwd
589
+
590
+ Current working directory.
591
+
592
+ ### ctx.sessionManager
593
+
594
+ Read-only access to session state. See [session.md](session.md) for the full SessionManager API and entry types.
595
+
596
+ ```typescript
597
+ ctx.sessionManager.getEntries() // All entries
598
+ ctx.sessionManager.getBranch() // Current branch
599
+ ctx.sessionManager.getLeafId() // Current leaf entry ID
600
+ ```
601
+
602
+ ### ctx.modelRegistry / ctx.model
603
+
604
+ Access to models and API keys.
605
+
606
+ ### ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
607
+
608
+ Control flow helpers.
609
+
610
+ ### ctx.shutdown()
611
+
612
+ Request a graceful shutdown of indusagi.
613
+
614
+ - **Interactive mode:** Deferred until the agent becomes idle (after processing all queued steering and follow-up messages).
615
+ - **RPC mode:** Deferred until the next idle state (after completing the current command response, when waiting for the next command).
616
+ - **Print mode:** No-op. The process exits automatically when all prompts are processed.
617
+
618
+ Emits `session_shutdown` event to all extensions before exiting. Available in all contexts (event handlers, tools, commands, shortcuts).
619
+
620
+ ```typescript
621
+ indusagi.on("tool_call", (event, ctx) => {
622
+ if (isFatal(event.input)) {
623
+ ctx.shutdown();
624
+ }
625
+ });
626
+ ```
627
+
628
+ ### ctx.getContextUsage()
629
+
630
+ Returns current context usage for the active model. Uses last assistant usage when available, then estimates tokens for trailing messages.
631
+
632
+ ```typescript
633
+ const usage = ctx.getContextUsage();
634
+ if (usage && usage.tokens > 100_000) {
635
+ // ...
636
+ }
637
+ ```
638
+
639
+ ### ctx.compact()
640
+
641
+ Trigger compaction without awaiting completion. Use `onComplete` and `onError` for follow-up actions.
642
+
643
+ ```typescript
644
+ ctx.compact({
645
+ customInstructions: "Focus on recent changes",
646
+ onComplete: (result) => {
647
+ ctx.ui.notify("Compaction completed", "info");
648
+ },
649
+ onError: (error) => {
650
+ ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
651
+ },
652
+ });
653
+ ```
654
+
655
+ ## ExtensionCommandContext
656
+
657
+ Command handlers receive `ExtensionCommandContext`, which extends `ExtensionContext` with session control methods. These are only available in commands because they can deadlock if called from event handlers.
658
+
659
+ ### ctx.waitForIdle()
660
+
661
+ Wait for the agent to finish streaming:
662
+
663
+ ```typescript
664
+ indusagi.registerCommand("my-cmd", {
665
+ handler: async (args, ctx) => {
666
+ await ctx.waitForIdle();
667
+ // Agent is now idle, safe to modify session
668
+ },
669
+ });
670
+ ```
671
+
672
+ ### ctx.newSession(options?)
673
+
674
+ Create a new session:
675
+
676
+ ```typescript
677
+ const result = await ctx.newSession({
678
+ parentSession: ctx.sessionManager.getSessionFile(),
679
+ setup: async (sm) => {
680
+ sm.appendMessage({
681
+ role: "user",
682
+ content: [{ type: "text", text: "Context from previous session..." }],
683
+ timestamp: Date.now(),
684
+ });
685
+ },
686
+ });
687
+
688
+ if (result.cancelled) {
689
+ // An extension cancelled the new session
690
+ }
691
+ ```
692
+
693
+ ### ctx.fork(entryId)
694
+
695
+ Fork from a specific entry, creating a new session file:
696
+
697
+ ```typescript
698
+ const result = await ctx.fork("entry-id-123");
699
+ if (!result.cancelled) {
700
+ // Now in the forked session
701
+ }
702
+ ```
703
+
704
+ ### ctx.navigateTree(targetId, options?)
705
+
706
+ Navigate to a different point in the session tree:
707
+
708
+ ```typescript
709
+ const result = await ctx.navigateTree("entry-id-456", {
710
+ summarize: true,
711
+ customInstructions: "Focus on error handling changes",
712
+ replaceInstructions: false, // true = replace default prompt entirely
713
+ label: "review-checkpoint",
714
+ });
715
+ ```
716
+
717
+ Options:
718
+ - `summarize`: Whether to generate a summary of the abandoned branch
719
+ - `customInstructions`: Custom instructions for the summarizer
720
+ - `replaceInstructions`: If true, `customInstructions` replaces the default prompt instead of being appended
721
+ - `label`: Label to attach to the branch summary entry (or target entry if not summarizing)
722
+
723
+ ## ExtensionAPI Methods
724
+
725
+ ### indusagi.on(event, handler)
726
+
727
+ Subscribe to events. See [Events](#events) for event types and return values.
728
+
729
+ ### indusagi.registerTool(definition)
730
+
731
+ Register a custom tool callable by the LLM. See [Custom Tools](#custom-tools) for full details.
732
+
733
+ ```typescript
734
+ import { Type } from "@sinclair/typebox";
735
+ import { StringEnum } from "indusagi-ai";
736
+
737
+ indusagi.registerTool({
738
+ name: "my_tool",
739
+ label: "My Tool",
740
+ description: "What this tool does",
741
+ parameters: Type.Object({
742
+ action: StringEnum(["list", "add"] as const),
743
+ text: Type.Optional(Type.String()),
744
+ }),
745
+
746
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
747
+ // Stream progress
748
+ onUpdate?.({ content: [{ type: "text", text: "Working..." }] });
749
+
750
+ return {
751
+ content: [{ type: "text", text: "Done" }],
752
+ details: { result: "..." },
753
+ };
754
+ },
755
+
756
+ // Optional: Custom rendering
757
+ renderCall(args, theme) { ... },
758
+ renderResult(result, options, theme) { ... },
759
+ });
760
+ ```
761
+
762
+ ### indusagi.sendMessage(message, options?)
763
+
764
+ Inject a custom message into the session.
765
+
766
+ ```typescript
767
+ indusagi.sendMessage({
768
+ customType: "my-extension",
769
+ content: "Message text",
770
+ display: true,
771
+ details: { ... },
772
+ }, {
773
+ triggerTurn: true,
774
+ deliverAs: "steer",
775
+ });
776
+ ```
777
+
778
+ **Options:**
779
+ - `deliverAs` - Delivery mode:
780
+ - `"steer"` (default) - Interrupts streaming. Delivered after current tool finishes, remaining tools skipped.
781
+ - `"followUp"` - Waits for agent to finish. Delivered only when agent has no more tool calls.
782
+ - `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
783
+ - `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
784
+
785
+ ### indusagi.sendUserMessage(content, options?)
786
+
787
+ Send a user message to the agent. Unlike `sendMessage()` which sends custom messages, this sends an actual user message that appears as if typed by the user. Always triggers a turn.
788
+
789
+ ```typescript
790
+ // Simple text message
791
+ indusagi.sendUserMessage("What is 2+2?");
792
+
793
+ // With content array (text + images)
794
+ indusagi.sendUserMessage([
795
+ { type: "text", text: "Describe this image:" },
796
+ { type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
797
+ ]);
798
+
799
+ // During streaming - must specify delivery mode
800
+ indusagi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
801
+ indusagi.sendUserMessage("And then summarize", { deliverAs: "followUp" });
802
+ ```
803
+
804
+ **Options:**
805
+ - `deliverAs` - Required when agent is streaming:
806
+ - `"steer"` - Interrupts after current tool, remaining tools skipped
807
+ - `"followUp"` - Waits for agent to finish all tools
808
+
809
+ When not streaming, the message is sent immediately and triggers a new turn. When streaming without `deliverAs`, throws an error.
810
+
811
+ See [send-user-message.ts](../examples/extensions/send-user-message.ts) for a complete example.
812
+
813
+ ### indusagi.appendEntry(customType, data?)
814
+
815
+ Persist extension state (does NOT participate in LLM context).
816
+
817
+ ```typescript
818
+ indusagi.appendEntry("my-state", { count: 42 });
819
+
820
+ // Restore on reload
821
+ indusagi.on("session_start", async (_event, ctx) => {
822
+ for (const entry of ctx.sessionManager.getEntries()) {
823
+ if (entry.type === "custom" && entry.customType === "my-state") {
824
+ // Reconstruct from entry.data
825
+ }
826
+ }
827
+ });
828
+ ```
829
+
830
+ ### indusagi.setSessionName(name)
831
+
832
+ Set the session display name (shown in session selector instead of first message).
833
+
834
+ ```typescript
835
+ indusagi.setSessionName("Refactor auth module");
836
+ ```
837
+
838
+ ### indusagi.getSessionName()
839
+
840
+ Get the current session name, if set.
841
+
842
+ ```typescript
843
+ const name = indusagi.getSessionName();
844
+ if (name) {
845
+ console.log(`Session: ${name}`);
846
+ }
847
+ ```
848
+
849
+ ### indusagi.setLabel(entryId, label)
850
+
851
+ Set or clear a label on an entry. Labels are user-defined markers for bookmarking and navigation (shown in `/tree` selector).
852
+
853
+ ```typescript
854
+ // Set a label
855
+ indusagi.setLabel(entryId, "checkpoint-before-refactor");
856
+
857
+ // Clear a label
858
+ indusagi.setLabel(entryId, undefined);
859
+
860
+ // Read labels via sessionManager
861
+ const label = ctx.sessionManager.getLabel(entryId);
862
+ ```
863
+
864
+ Labels persist in the session and survive restarts. Use them to mark important points (turns, checkpoints) in the conversation tree.
865
+
866
+ ### indusagi.registerCommand(name, options)
867
+
868
+ Register a command.
869
+
870
+ ```typescript
871
+ indusagi.registerCommand("stats", {
872
+ description: "Show session statistics",
873
+ handler: async (args, ctx) => {
874
+ const count = ctx.sessionManager.getEntries().length;
875
+ ctx.ui.notify(`${count} entries`, "info");
876
+ }
877
+ });
878
+ ```
879
+
880
+ Optional: add argument auto-completion for `/command ...`:
881
+
882
+ ```typescript
883
+ import type { AutocompleteItem } from "indusagi-tui";
884
+
885
+ indusagi.registerCommand("deploy", {
886
+ description: "Deploy to an environment",
887
+ getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
888
+ const envs = ["dev", "staging", "prod"];
889
+ const items = envs.map((e) => ({ value: e, label: e }));
890
+ const filtered = items.filter((i) => i.value.startsWith(prefix));
891
+ return filtered.length > 0 ? filtered : null;
892
+ },
893
+ handler: async (args, ctx) => {
894
+ ctx.ui.notify(`Deploying: ${args}`, "info");
895
+ },
896
+ });
897
+ ```
898
+
899
+ ### indusagi.registerMessageRenderer(customType, renderer)
900
+
901
+ Register a custom TUI renderer for messages with your `customType`. See [Custom UI](#custom-ui).
902
+
903
+ ### indusagi.registerShortcut(shortcut, options)
904
+
905
+ Register a keyboard shortcut. See [keybindings.md](keybindings.md) for the shortcut format and built-in keybindings.
906
+
907
+ ```typescript
908
+ indusagi.registerShortcut("ctrl+shift+p", {
909
+ description: "Toggle plan mode",
910
+ handler: async (ctx) => {
911
+ ctx.ui.notify("Toggled!");
912
+ },
913
+ });
914
+ ```
915
+
916
+ ### indusagi.registerFlag(name, options)
917
+
918
+ Register a CLI flag.
919
+
920
+ ```typescript
921
+ indusagi.registerFlag("plan", {
922
+ description: "Start in plan mode",
923
+ type: "boolean",
924
+ default: false,
925
+ });
926
+
927
+ // Check value
928
+ if (indusagi.getFlag("--plan")) {
929
+ // Plan mode enabled
930
+ }
931
+ ```
932
+
933
+ ### indusagi.exec(command, args, options?)
934
+
935
+ Execute a shell command.
936
+
937
+ ```typescript
938
+ const result = await indusagi.exec("git", ["status"], { signal, timeout: 5000 });
939
+ // result.stdout, result.stderr, result.code, result.killed
940
+ ```
941
+
942
+ ### indusagi.getActiveTools() / indusagi.getAllTools() / indusagi.setActiveTools(names)
943
+
944
+ Manage active tools.
945
+
946
+ ```typescript
947
+ const active = indusagi.getActiveTools(); // ["read", "bash", "edit", "write"]
948
+ const all = indusagi.getAllTools(); // [{ name: "read", description: "Read file contents..." }, ...]
949
+ const names = all.map(t => t.name); // Just names if needed
950
+ indusagi.setActiveTools(["read", "bash"]); // Switch to read-only
951
+ ```
952
+
953
+ ### indusagi.setModel(model)
954
+
955
+ Set the current model. Returns `false` if no API key is available for the model. See [models.md](models.md) for configuring custom models.
956
+
957
+ ```typescript
958
+ const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
959
+ if (model) {
960
+ const success = await indusagi.setModel(model);
961
+ if (!success) {
962
+ ctx.ui.notify("No API key for this model", "error");
963
+ }
964
+ }
965
+ ```
966
+
967
+ ### indusagi.getThinkingLevel() / indusagi.setThinkingLevel(level)
968
+
969
+ Get or set the thinking level. Level is clamped to model capabilities (non-reasoning models always use "off").
970
+
971
+ ```typescript
972
+ const current = indusagi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
973
+ indusagi.setThinkingLevel("high");
974
+ ```
975
+
976
+ ### indusagi.events
977
+
978
+ Shared event bus for communication between extensions:
979
+
980
+ ```typescript
981
+ indusagi.events.on("my:event", (data) => { ... });
982
+ indusagi.events.emit("my:event", { ... });
983
+ ```
984
+
985
+ ### indusagi.registerProvider(name, config)
986
+
987
+ Register or override a model provider dynamically. Useful for proxies, custom endpoints, or team-wide model configurations.
988
+
989
+ ```typescript
990
+ // Register a new provider with custom models
991
+ indusagi.registerProvider("my-proxy", {
992
+ baseUrl: "https://proxy.example.com",
993
+ apiKey: "PROXY_AINDUSAGI_KEY", // env var name or literal
994
+ api: "anthropic-messages",
995
+ models: [
996
+ {
997
+ id: "claude-sonnet-4-20250514",
998
+ name: "Claude 4 Sonnet (proxy)",
999
+ reasoning: false,
1000
+ input: ["text", "image"],
1001
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1002
+ contextWindow: 200000,
1003
+ maxTokens: 16384
1004
+ }
1005
+ ]
1006
+ });
1007
+
1008
+ // Override baseUrl for an existing provider (keeps all models)
1009
+ indusagi.registerProvider("anthropic", {
1010
+ baseUrl: "https://proxy.example.com"
1011
+ });
1012
+
1013
+ // Register provider with OAuth support for /login
1014
+ indusagi.registerProvider("corporate-ai", {
1015
+ baseUrl: "https://ai.corp.com",
1016
+ api: "openai-responses",
1017
+ models: [...],
1018
+ oauth: {
1019
+ name: "Corporate AI (SSO)",
1020
+ async login(callbacks) {
1021
+ // Custom OAuth flow
1022
+ callbacks.onAuth({ url: "https://sso.corp.com/..." });
1023
+ const code = await callbacks.onPrompt({ message: "Enter code:" });
1024
+ return { refresh: code, access: code, expires: Date.now() + 3600000 };
1025
+ },
1026
+ async refreshToken(credentials) {
1027
+ // Refresh logic
1028
+ return credentials;
1029
+ },
1030
+ getApiKey(credentials) {
1031
+ return credentials.access;
1032
+ }
1033
+ }
1034
+ });
1035
+ ```
1036
+
1037
+ **Config options:**
1038
+ - `baseUrl` - API endpoint URL. Required when defining models.
1039
+ - `apiKey` - API key or environment variable name. Required when defining models (unless `oauth` provided).
1040
+ - `api` - API type: `"anthropic-messages"`, `"openai-completions"`, `"openai-responses"`, etc.
1041
+ - `headers` - Custom headers to include in requests.
1042
+ - `authHeader` - If true, adds `Authorization: Bearer` header automatically.
1043
+ - `models` - Array of model definitions. If provided, replaces all existing models for this provider.
1044
+ - `oauth` - OAuth provider config for `/login` support. When provided, the provider appears in the login menu.
1045
+ - `streamSimple` - Custom streaming implementation for non-standard APIs.
1046
+
1047
+ See [custom-provider.md](custom-provider.md) for advanced topics: custom streaming APIs, OAuth details, model definition reference.
1048
+
1049
+ ## State Management
1050
+
1051
+ Extensions with state should store it in tool result `details` for proper branching support:
1052
+
1053
+ ```typescript
1054
+ export default function (indusagi: ExtensionAPI) {
1055
+ let items: string[] = [];
1056
+
1057
+ // Reconstruct state from session
1058
+ indusagi.on("session_start", async (_event, ctx) => {
1059
+ items = [];
1060
+ for (const entry of ctx.sessionManager.getBranch()) {
1061
+ if (entry.type === "message" && entry.message.role === "toolResult") {
1062
+ if (entry.message.toolName === "my_tool") {
1063
+ items = entry.message.details?.items ?? [];
1064
+ }
1065
+ }
1066
+ }
1067
+ });
1068
+
1069
+ indusagi.registerTool({
1070
+ name: "my_tool",
1071
+ // ...
1072
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
1073
+ items.push("new item");
1074
+ return {
1075
+ content: [{ type: "text", text: "Added" }],
1076
+ details: { items: [...items] }, // Store for reconstruction
1077
+ };
1078
+ },
1079
+ });
1080
+ }
1081
+ ```
1082
+
1083
+ ## Custom Tools
1084
+
1085
+ Register tools the LLM can call via `indusagi.registerTool()`. Tools appear in the system prompt and can have custom rendering.
1086
+
1087
+ ### Tool Definition
1088
+
1089
+ ```typescript
1090
+ import { Type } from "@sinclair/typebox";
1091
+ import { StringEnum } from "indusagi-ai";
1092
+ import { Text } from "indusagi-tui";
1093
+
1094
+ indusagi.registerTool({
1095
+ name: "my_tool",
1096
+ label: "My Tool",
1097
+ description: "What this tool does (shown to LLM)",
1098
+ parameters: Type.Object({
1099
+ action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
1100
+ text: Type.Optional(Type.String()),
1101
+ }),
1102
+
1103
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
1104
+ // Check for cancellation
1105
+ if (signal?.aborted) {
1106
+ return { content: [{ type: "text", text: "Cancelled" }] };
1107
+ }
1108
+
1109
+ // Stream progress updates
1110
+ onUpdate?.({
1111
+ content: [{ type: "text", text: "Working..." }],
1112
+ details: { progress: 50 },
1113
+ });
1114
+
1115
+ // Run commands via indusagi.exec (captured from extension closure)
1116
+ const result = await indusagi.exec("some-command", [], { signal });
1117
+
1118
+ // Return result
1119
+ return {
1120
+ content: [{ type: "text", text: "Done" }], // Sent to LLM
1121
+ details: { data: result }, // For rendering & state
1122
+ };
1123
+ },
1124
+
1125
+ // Optional: Custom rendering
1126
+ renderCall(args, theme) { ... },
1127
+ renderResult(result, options, theme) { ... },
1128
+ });
1129
+ ```
1130
+
1131
+ **Important:** Use `StringEnum` from `indusagi-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.
1132
+
1133
+ ### Overriding Built-in Tools
1134
+
1135
+ Extensions can override built-in tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) by registering a tool with the same name. Interactive mode displays a warning when this happens.
1136
+
1137
+ ```bash
1138
+ # Extension's read tool replaces built-in read
1139
+ indusagi -e ./tool-override.ts
1140
+ ```
1141
+
1142
+ Alternatively, use `--no-tools` to start without any built-in tools:
1143
+ ```bash
1144
+ # No built-in tools, only extension tools
1145
+ indusagi --no-tools -e ./my-extension.ts
1146
+ ```
1147
+
1148
+ See [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) for a complete example that overrides `read` with logging and access control.
1149
+
1150
+ **Rendering:** If your override doesn't provide custom `renderCall`/`renderResult` functions, the built-in renderer is used automatically (syntax highlighting, diffs, etc.). This lets you wrap built-in tools for logging or access control without reimplementing the UI.
1151
+
1152
+ **Your implementation must match the exact result shape**, including the `details` type. The UI and session logic depend on these shapes for rendering and state tracking.
1153
+
1154
+ Built-in tool implementations:
1155
+ - [read.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`
1156
+ - [bash.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`
1157
+ - [edit.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)
1158
+ - [write.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)
1159
+ - [grep.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`
1160
+ - [find.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`
1161
+ - [ls.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`
1162
+
1163
+ ### Remote Execution
1164
+
1165
+ Built-in tools support pluggable operations for delegating to remote systems (SSH, containers, etc.):
1166
+
1167
+ ```typescript
1168
+ import { createReadTool, createBashTool, type ReadOperations } from "indusagi-coding-agent";
1169
+
1170
+ // Create tool with custom operations
1171
+ const remoteRead = createReadTool(cwd, {
1172
+ operations: {
1173
+ readFile: (path) => sshExec(remote, `cat ${path}`),
1174
+ access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
1175
+ }
1176
+ });
1177
+
1178
+ // Register, checking flag at execution time
1179
+ indusagi.registerTool({
1180
+ ...remoteRead,
1181
+ async execute(id, params, onUpdate, _ctx, signal) {
1182
+ const ssh = getSshConfig();
1183
+ if (ssh) {
1184
+ const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
1185
+ return tool.execute(id, params, signal, onUpdate);
1186
+ }
1187
+ return localRead.execute(id, params, signal, onUpdate);
1188
+ },
1189
+ });
1190
+ ```
1191
+
1192
+ **Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`
1193
+
1194
+ See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
1195
+
1196
+ ### Output Truncation
1197
+
1198
+ **Tools MUST truncate their output** to avoid overwhelming the LLM context. Large outputs can cause:
1199
+ - Context overflow errors (prompt too long)
1200
+ - Compaction failures
1201
+ - Degraded model performance
1202
+
1203
+ The built-in limit is **50KB** (~10k tokens) and **2000 lines**, whichever is hit first. Use the exported truncation utilities:
1204
+
1205
+ ```typescript
1206
+ import {
1207
+ truncateHead, // Keep first N lines/bytes (good for file reads, search results)
1208
+ truncateTail, // Keep last N lines/bytes (good for logs, command output)
1209
+ truncateLine, // Truncate a single line to maxBytes with ellipsis
1210
+ formatSize, // Human-readable size (e.g., "50KB", "1.5MB")
1211
+ DEFAULT_MAX_BYTES, // 50KB
1212
+ DEFAULT_MAX_LINES, // 2000
1213
+ } from "indusagi-coding-agent";
1214
+
1215
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
1216
+ const output = await runCommand();
1217
+
1218
+ // Apply truncation
1219
+ const truncation = truncateHead(output, {
1220
+ maxLines: DEFAULT_MAX_LINES,
1221
+ maxBytes: DEFAULT_MAX_BYTES,
1222
+ });
1223
+
1224
+ let result = truncation.content;
1225
+
1226
+ if (truncation.truncated) {
1227
+ // Write full output to temp file
1228
+ const tempFile = writeTempFile(output);
1229
+
1230
+ // Inform the LLM where to find complete output
1231
+ result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
1232
+ result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
1233
+ result += ` Full output saved to: ${tempFile}]`;
1234
+ }
1235
+
1236
+ return { content: [{ type: "text", text: result }] };
1237
+ }
1238
+ ```
1239
+
1240
+ **Key points:**
1241
+ - Use `truncateHead` for content where the beginning matters (search results, file reads)
1242
+ - Use `truncateTail` for content where the end matters (logs, command output)
1243
+ - Always inform the LLM when output is truncated and where to find the full version
1244
+ - Document the truncation limits in your tool's description
1245
+
1246
+ See [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) for a complete example wrapping `rg` (ripgrep) with proper truncation.
1247
+
1248
+ ### Multiple Tools
1249
+
1250
+ One extension can register multiple tools with shared state:
1251
+
1252
+ ```typescript
1253
+ export default function (indusagi: ExtensionAPI) {
1254
+ let connection = null;
1255
+
1256
+ indusagi.registerTool({ name: "db_connect", ... });
1257
+ indusagi.registerTool({ name: "db_query", ... });
1258
+ indusagi.registerTool({ name: "db_close", ... });
1259
+
1260
+ indusagi.on("session_shutdown", async () => {
1261
+ connection?.close();
1262
+ });
1263
+ }
1264
+ ```
1265
+
1266
+ ### Custom Rendering
1267
+
1268
+ Tools can provide `renderCall` and `renderResult` for custom TUI display. See [tui.md](tui.md) for the full component API and [tool-execution.ts](https://github.com/badlogic/indusagi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) for how built-in tools render.
1269
+
1270
+ Tool output is wrapped in a `Box` that handles padding and background. Your render methods return `Component` instances (typically `Text`).
1271
+
1272
+ #### renderCall
1273
+
1274
+ Renders the tool call (before/during execution):
1275
+
1276
+ ```typescript
1277
+ import { Text } from "indusagi-tui";
1278
+
1279
+ renderCall(args, theme) {
1280
+ let text = theme.fg("toolTitle", theme.bold("my_tool "));
1281
+ text += theme.fg("muted", args.action);
1282
+ if (args.text) {
1283
+ text += " " + theme.fg("dim", `"${args.text}"`);
1284
+ }
1285
+ return new Text(text, 0, 0); // 0,0 padding - Box handles it
1286
+ }
1287
+ ```
1288
+
1289
+ #### renderResult
1290
+
1291
+ Renders the tool result:
1292
+
1293
+ ```typescript
1294
+ renderResult(result, { expanded, isPartial }, theme) {
1295
+ // Handle streaming
1296
+ if (isPartial) {
1297
+ return new Text(theme.fg("warning", "Processing..."), 0, 0);
1298
+ }
1299
+
1300
+ // Handle errors
1301
+ if (result.details?.error) {
1302
+ return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
1303
+ }
1304
+
1305
+ // Normal result - support expanded view (Ctrl+O)
1306
+ let text = theme.fg("success", "✓ Done");
1307
+ if (expanded && result.details?.items) {
1308
+ for (const item of result.details.items) {
1309
+ text += "\n " + theme.fg("dim", item);
1310
+ }
1311
+ }
1312
+ return new Text(text, 0, 0);
1313
+ }
1314
+ ```
1315
+
1316
+ #### Keybinding Hints
1317
+
1318
+ Use `keyHint()` to display keybinding hints that respect user's keybinding configuration:
1319
+
1320
+ ```typescript
1321
+ import { keyHint } from "indusagi-coding-agent";
1322
+
1323
+ renderResult(result, { expanded }, theme) {
1324
+ let text = theme.fg("success", "✓ Done");
1325
+ if (!expanded) {
1326
+ text += ` (${keyHint("expandTools", "to expand")})`;
1327
+ }
1328
+ return new Text(text, 0, 0);
1329
+ }
1330
+ ```
1331
+
1332
+ Available functions:
1333
+ - `keyHint(action, description)` - Editor actions (e.g., `"expandTools"`, `"selectConfirm"`)
1334
+ - `appKeyHint(keybindings, action, description)` - App actions (requires `KeybindingsManager`)
1335
+ - `editorKey(action)` - Get raw key string for editor action
1336
+ - `rawKeyHint(key, description)` - Format a raw key string
1337
+
1338
+ #### Best Practices
1339
+
1340
+ - Use `Text` with padding `(0, 0)` - the Box handles padding
1341
+ - Use `\n` for multi-line content
1342
+ - Handle `isPartial` for streaming progress
1343
+ - Support `expanded` for detail on demand
1344
+ - Keep default view compact
1345
+
1346
+ #### Fallback
1347
+
1348
+ If `renderCall`/`renderResult` is not defined or throws:
1349
+ - `renderCall`: Shows tool name
1350
+ - `renderResult`: Shows raw text from `content`
1351
+
1352
+ ## Custom UI
1353
+
1354
+ Extensions can interact with users via `ctx.ui` methods and customize how messages/tools render.
1355
+
1356
+ **For custom components, see [tui.md](tui.md)** which has copy-paste patterns for:
1357
+ - Selection dialogs (SelectList)
1358
+ - Async operations with cancel (BorderedLoader)
1359
+ - Settings toggles (SettingsList)
1360
+ - Status indicators (setStatus)
1361
+ - Working message during streaming (setWorkingMessage)
1362
+ - Widgets above/below editor (setWidget)
1363
+ - Custom footers (setFooter)
1364
+
1365
+ ### Dialogs
1366
+
1367
+ ```typescript
1368
+ // Select from options
1369
+ const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
1370
+
1371
+ // Confirm dialog
1372
+ const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
1373
+
1374
+ // Text input
1375
+ const name = await ctx.ui.input("Name:", "placeholder");
1376
+
1377
+ // Multi-line editor
1378
+ const text = await ctx.ui.editor("Edit:", "prefilled text");
1379
+
1380
+ // Notification (non-blocking)
1381
+ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
1382
+ ```
1383
+
1384
+ #### Timed Dialogs with Countdown
1385
+
1386
+ Dialogs support a `timeout` option that auto-dismisses with a live countdown display:
1387
+
1388
+ ```typescript
1389
+ // Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
1390
+ const confirmed = await ctx.ui.confirm(
1391
+ "Timed Confirmation",
1392
+ "This dialog will auto-cancel in 5 seconds. Confirm?",
1393
+ { timeout: 5000 }
1394
+ );
1395
+
1396
+ if (confirmed) {
1397
+ // User confirmed
1398
+ } else {
1399
+ // User cancelled or timed out
1400
+ }
1401
+ ```
1402
+
1403
+ **Return values on timeout:**
1404
+ - `select()` returns `undefined`
1405
+ - `confirm()` returns `false`
1406
+ - `input()` returns `undefined`
1407
+
1408
+ #### Manual Dismissal with AbortSignal
1409
+
1410
+ For more control (e.g., to distinguish timeout from user cancel), use `AbortSignal`:
1411
+
1412
+ ```typescript
1413
+ const controller = new AbortController();
1414
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
1415
+
1416
+ const confirmed = await ctx.ui.confirm(
1417
+ "Timed Confirmation",
1418
+ "This dialog will auto-cancel in 5 seconds. Confirm?",
1419
+ { signal: controller.signal }
1420
+ );
1421
+
1422
+ clearTimeout(timeoutId);
1423
+
1424
+ if (confirmed) {
1425
+ // User confirmed
1426
+ } else if (controller.signal.aborted) {
1427
+ // Dialog timed out
1428
+ } else {
1429
+ // User cancelled (pressed Escape or selected "No")
1430
+ }
1431
+ ```
1432
+
1433
+ See [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) for complete examples.
1434
+
1435
+ ### Widgets, Status, and Footer
1436
+
1437
+ ```typescript
1438
+ // Status in footer (persistent until cleared)
1439
+ ctx.ui.setStatus("my-ext", "Processing...");
1440
+ ctx.ui.setStatus("my-ext", undefined); // Clear
1441
+
1442
+ // Working message (shown during streaming)
1443
+ ctx.ui.setWorkingMessage("Thinking deeply...");
1444
+ ctx.ui.setWorkingMessage(); // Restore default
1445
+
1446
+ // Widget above editor (default)
1447
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
1448
+ // Widget below editor
1449
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
1450
+ ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
1451
+ ctx.ui.setWidget("my-widget", undefined); // Clear
1452
+
1453
+ // Custom footer (replaces built-in footer entirely)
1454
+ ctx.ui.setFooter((tui, theme) => ({
1455
+ render(width) { return [theme.fg("dim", "Custom footer")]; },
1456
+ invalidate() {},
1457
+ }));
1458
+ ctx.ui.setFooter(undefined); // Restore built-in footer
1459
+
1460
+ // Terminal title
1461
+ ctx.ui.setTitle("indusagi - my-project");
1462
+
1463
+ // Editor text
1464
+ ctx.ui.setEditorText("Prefill text");
1465
+ const current = ctx.ui.getEditorText();
1466
+
1467
+ // Custom editor (vim mode, emacs mode, etc.)
1468
+ ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
1469
+ ctx.ui.setEditorComponent(undefined); // Restore default editor
1470
+
1471
+ // Theme management (see themes.md for creating themes)
1472
+ const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
1473
+ const lightTheme = ctx.ui.getTheme("light"); // Load without switching
1474
+ const result = ctx.ui.setTheme("light"); // Switch by name
1475
+ if (!result.success) {
1476
+ ctx.ui.notify(`Failed: ${result.error}`, "error");
1477
+ }
1478
+ ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
1479
+ ctx.ui.theme.fg("accent", "styled text"); // Access current theme
1480
+ ```
1481
+
1482
+ ### Custom Components
1483
+
1484
+ For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called:
1485
+
1486
+ ```typescript
1487
+ import { Text, Component } from "indusagi-tui";
1488
+
1489
+ const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
1490
+ const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
1491
+
1492
+ text.onKey = (key) => {
1493
+ if (key === "return") done(true);
1494
+ if (key === "escape") done(false);
1495
+ return true;
1496
+ };
1497
+
1498
+ return text;
1499
+ });
1500
+
1501
+ if (result) {
1502
+ // User pressed Enter
1503
+ }
1504
+ ```
1505
+
1506
+ The callback receives:
1507
+ - `tui` - TUI instance (for screen dimensions, focus management)
1508
+ - `theme` - Current theme for styling
1509
+ - `keybindings` - App keybinding manager (for checking shortcuts)
1510
+ - `done(value)` - Call to close component and return value
1511
+
1512
+ See [tui.md](tui.md) for the full component API.
1513
+
1514
+ #### Overlay Mode (Experimental)
1515
+
1516
+ Pass `{ overlay: true }` to render the component as a floating modal on top of existing content, without clearing the screen:
1517
+
1518
+ ```typescript
1519
+ const result = await ctx.ui.custom<string | null>(
1520
+ (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
1521
+ { overlay: true }
1522
+ );
1523
+ ```
1524
+
1525
+ For advanced positioning (anchors, margins, percentages, responsive visibility), pass `overlayOptions`. Use `onHandle` to control visibility programmatically:
1526
+
1527
+ ```typescript
1528
+ const result = await ctx.ui.custom<string | null>(
1529
+ (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
1530
+ {
1531
+ overlay: true,
1532
+ overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
1533
+ onHandle: (handle) => { /* handle.setHidden(true/false) */ }
1534
+ }
1535
+ );
1536
+ ```
1537
+
1538
+ See [tui.md](tui.md) for the full `OverlayOptions` API and [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for examples.
1539
+
1540
+ ### Custom Editor
1541
+
1542
+ Replace the main input editor with a custom implementation (vim mode, emacs mode, etc.):
1543
+
1544
+ ```typescript
1545
+ import { CustomEditor, type ExtensionAPI } from "indusagi-coding-agent";
1546
+ import { matchesKey } from "indusagi-tui";
1547
+
1548
+ class VimEditor extends CustomEditor {
1549
+ private mode: "normal" | "insert" = "insert";
1550
+
1551
+ handleInput(data: string): void {
1552
+ if (matchesKey(data, "escape") && this.mode === "insert") {
1553
+ this.mode = "normal";
1554
+ return;
1555
+ }
1556
+ if (this.mode === "normal" && data === "i") {
1557
+ this.mode = "insert";
1558
+ return;
1559
+ }
1560
+ super.handleInput(data); // App keybindings + text editing
1561
+ }
1562
+ }
1563
+
1564
+ export default function (indusagi: ExtensionAPI) {
1565
+ indusagi.on("session_start", (_event, ctx) => {
1566
+ ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
1567
+ new VimEditor(theme, keybindings)
1568
+ );
1569
+ });
1570
+ }
1571
+ ```
1572
+
1573
+ **Key points:**
1574
+ - Extend `CustomEditor` (not base `Editor`) to get app keybindings (escape to abort, ctrl+d, model switching)
1575
+ - Call `super.handleInput(data)` for keys you don't handle
1576
+ - Factory receives `theme` and `keybindings` from the app
1577
+ - Pass `undefined` to restore default: `ctx.ui.setEditorComponent(undefined)`
1578
+
1579
+ See [tui.md](tui.md) Pattern 7 for a complete example with mode indicator.
1580
+
1581
+ ### Message Rendering
1582
+
1583
+ Register a custom renderer for messages with your `customType`:
1584
+
1585
+ ```typescript
1586
+ import { Text } from "indusagi-tui";
1587
+
1588
+ indusagi.registerMessageRenderer("my-extension", (message, options, theme) => {
1589
+ const { expanded } = options;
1590
+ let text = theme.fg("accent", `[${message.customType}] `);
1591
+ text += message.content;
1592
+
1593
+ if (expanded && message.details) {
1594
+ text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
1595
+ }
1596
+
1597
+ return new Text(text, 0, 0);
1598
+ });
1599
+ ```
1600
+
1601
+ Messages are sent via `indusagi.sendMessage()`:
1602
+
1603
+ ```typescript
1604
+ indusagi.sendMessage({
1605
+ customType: "my-extension", // Matches registerMessageRenderer
1606
+ content: "Status update",
1607
+ display: true, // Show in TUI
1608
+ details: { ... }, // Available in renderer
1609
+ });
1610
+ ```
1611
+
1612
+ ### Theme Colors
1613
+
1614
+ All render functions receive a `theme` object. See [themes.md](themes.md) for creating custom themes and the full color palette.
1615
+
1616
+ ```typescript
1617
+ // Foreground colors
1618
+ theme.fg("toolTitle", text) // Tool names
1619
+ theme.fg("accent", text) // Highlights
1620
+ theme.fg("success", text) // Success (green)
1621
+ theme.fg("error", text) // Errors (red)
1622
+ theme.fg("warning", text) // Warnings (yellow)
1623
+ theme.fg("muted", text) // Secondary text
1624
+ theme.fg("dim", text) // Tertiary text
1625
+
1626
+ // Text styles
1627
+ theme.bold(text)
1628
+ theme.italic(text)
1629
+ theme.strikethrough(text)
1630
+ ```
1631
+
1632
+ For syntax highlighting in custom tool renderers:
1633
+
1634
+ ```typescript
1635
+ import { highlightCode, getLanguageFromPath } from "indusagi-coding-agent";
1636
+
1637
+ // Highlight code with explicit language
1638
+ const highlighted = highlightCode("const x = 1;", "typescript", theme);
1639
+
1640
+ // Auto-detect language from file path
1641
+ const lang = getLanguageFromPath("/path/to/file.rs"); // "rust"
1642
+ const highlighted = highlightCode(code, lang, theme);
1643
+ ```
1644
+
1645
+ ## Error Handling
1646
+
1647
+ - Extension errors are logged, agent continues
1648
+ - `tool_call` errors block the tool (fail-safe)
1649
+ - Tool `execute` errors are reported to the LLM with `isError: true`
1650
+
1651
+ ## Mode Behavior
1652
+
1653
+ | Mode | UI Methods | Notes |
1654
+ |------|-----------|-------|
1655
+ | Interactive | Full TUI | Normal operation |
1656
+ | RPC (`--mode rpc`) | JSON protocol | Host handles UI, see [rpc.md](rpc.md) |
1657
+ | JSON (`--mode json`) | No-op | Event stream to stdout, see [json.md](json.md) |
1658
+ | Print (`-p`) | No-op | Extensions run but can't prompt |
1659
+
1660
+ In non-interactive modes, check `ctx.hasUI` before using UI methods.
1661
+
1662
+ ## Examples Reference
1663
+
1664
+ All examples in [examples/extensions/](../examples/extensions/).
1665
+
1666
+ | Example | Description | Key APIs |
1667
+ |---------|-------------|----------|
1668
+ | **Tools** |||
1669
+ | `hello.ts` | Minimal tool registration | `registerTool` |
1670
+ | `question.ts` | Tool with user interaction | `registerTool`, `ui.select` |
1671
+ | `questionnaire.ts` | Multi-step wizard tool | `registerTool`, `ui.custom` |
1672
+ | `todo.ts` | Stateful tool with persistence | `registerTool`, `appendEntry`, `renderResult`, session events |
1673
+ | `truncated-tool.ts` | Output truncation example | `registerTool`, `truncateHead` |
1674
+ | `tool-override.ts` | Override built-in read tool | `registerTool` (same name as built-in) |
1675
+ | **Commands** |||
1676
+ | `pirate.ts` | Modify system prompt per-turn | `registerCommand`, `before_agent_start` |
1677
+ | `summarize.ts` | Conversation summary command | `registerCommand`, `ui.custom` |
1678
+ | `handoff.ts` | Cross-provider model handoff | `registerCommand`, `ui.editor`, `ui.custom` |
1679
+ | `qna.ts` | Q&A with custom UI | `registerCommand`, `ui.custom`, `setEditorText` |
1680
+ | `send-user-message.ts` | Inject user messages | `registerCommand`, `sendUserMessage` |
1681
+ | `shutdown-command.ts` | Graceful shutdown command | `registerCommand`, `shutdown()` |
1682
+ | **Events & Gates** |||
1683
+ | `permission-gate.ts` | Block dangerous commands | `on("tool_call")`, `ui.confirm` |
1684
+ | `protected-paths.ts` | Block writes to specific paths | `on("tool_call")` |
1685
+ | `confirm-destructive.ts` | Confirm session changes | `on("session_before_switch")`, `on("session_before_fork")` |
1686
+ | `dirty-repo-guard.ts` | Warn on dirty git repo | `on("session_before_*")`, `exec` |
1687
+ | `input-transform.ts` | Transform user input | `on("input")` |
1688
+ | `model-status.ts` | React to model changes | `on("model_select")`, `setStatus` |
1689
+ | `claude-rules.ts` | Load rules from files | `on("session_start")`, `on("before_agent_start")` |
1690
+ | `file-trigger.ts` | File watcher triggers messages | `sendMessage` |
1691
+ | **Compaction & Sessions** |||
1692
+ | `custom-compaction.ts` | Custom compaction summary | `on("session_before_compact")` |
1693
+ | `trigger-compact.ts` | Trigger compaction manually | `compact()` |
1694
+ | `git-checkpoint.ts` | Git stash on turns | `on("turn_end")`, `on("session_fork")`, `exec` |
1695
+ | `auto-commit-on-exit.ts` | Commit on shutdown | `on("session_shutdown")`, `exec` |
1696
+ | **UI Components** |||
1697
+ | `status-line.ts` | Footer status indicator | `setStatus`, session events |
1698
+ | `custom-footer.ts` | Replace footer entirely | `registerCommand`, `setFooter` |
1699
+ | `custom-header.ts` | Replace startup header | `on("session_start")`, `setHeader` |
1700
+ | `modal-editor.ts` | Vim-style modal editor | `setEditorComponent`, `CustomEditor` |
1701
+ | `rainbow-editor.ts` | Custom editor styling | `setEditorComponent` |
1702
+ | `widget-placement.ts` | Widget above/below editor | `setWidget` |
1703
+ | `overlay-test.ts` | Overlay components | `ui.custom` with overlay options |
1704
+ | `overlay-qa-tests.ts` | Comprehensive overlay tests | `ui.custom`, all overlay options |
1705
+ | `notify.ts` | Simple notifications | `ui.notify` |
1706
+ | `timed-confirm.ts` | Dialogs with timeout | `ui.confirm` with timeout/signal |
1707
+ | `mac-system-theme.ts` | Auto-switch theme | `setTheme`, `exec` |
1708
+ | **Complex Extensions** |||
1709
+ | `plan-mode/` | Full plan mode implementation | All event types, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |
1710
+ | `preset.ts` | Saveable presets (model, tools, thinking) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |
1711
+ | `tools.ts` | Toggle tools on/off UI | `registerCommand`, `setActiveTools`, `SettingsList`, session events |
1712
+ | **Remote & Sandbox** |||
1713
+ | `ssh.ts` | SSH remote execution | `registerFlag`, `on("user_bash")`, `on("before_agent_start")`, tool operations |
1714
+ | `interactive-shell.ts` | Persistent shell session | `on("user_bash")` |
1715
+ | `sandbox/` | Sandboxed tool execution | Tool operations |
1716
+ | `subagent/` | Spawn sub-agents | `registerTool`, `exec` |
1717
+ | **Games** |||
1718
+ | `snake.ts` | Snake game | `registerCommand`, `ui.custom`, keyboard handling |
1719
+ | `space-invaders.ts` | Space Invaders game | `registerCommand`, `ui.custom` |
1720
+ | `doom-overlay/` | Doom in overlay | `ui.custom` with overlay |
1721
+ | **Providers** |||
1722
+ | `custom-provider-anthropic/` | Custom Anthropic proxy | `registerProvider` |
1723
+ | `custom-provider-gitlab-duo/` | GitLab Duo integration | `registerProvider` with OAuth |
1724
+ | **Messages & Communication** |||
1725
+ | `message-renderer.ts` | Custom message rendering | `registerMessageRenderer`, `sendMessage` |
1726
+ | `event-bus.ts` | Inter-extension events | `indusagi.events` |
1727
+ | **Session Metadata** |||
1728
+ | `session-name.ts` | Name sessions for selector | `setSessionName`, `getSessionName` |
1729
+ | `bookmark.ts` | Bookmark entries for /tree | `setLabel` |
1730
+ | **Misc** |||
1731
+ | `antigravity-image-gen.ts` | Image generation tool | `registerTool`, Google Antigravity |
1732
+ | `inline-bash.ts` | Inline bash in tool calls | `on("tool_call")` |
1733
+ | `with-deps/` | Extension with npm dependencies | Package structure with `package.json` |