@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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 (193) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-editor.ts +1 -0
  110. package/src/modes/interactive/components/hook-selector.ts +3 -3
  111. package/src/modes/interactive/components/model-selector.ts +7 -6
  112. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  113. package/src/modes/interactive/components/settings-defs.ts +55 -6
  114. package/src/modes/interactive/components/status-line/separators.ts +4 -4
  115. package/src/modes/interactive/components/status-line.ts +45 -35
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +644 -113
  118. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  119. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  120. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  121. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  122. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  123. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  124. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  125. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  126. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  127. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  128. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  129. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  130. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  131. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  132. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  133. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  134. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  135. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  136. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  137. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  138. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  139. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  140. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  141. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  142. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  143. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  144. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  145. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  146. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  147. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  148. package/src/modes/interactive/theme/defaults/index.ts +128 -0
  149. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  150. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  151. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  152. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  154. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  155. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  156. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  157. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  158. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  159. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  160. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  161. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  162. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  163. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  166. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  167. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  168. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  169. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  170. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  171. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  172. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  173. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  174. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  175. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  176. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  177. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  178. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  179. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  180. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  181. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  182. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  183. package/src/modes/print-mode.ts +14 -72
  184. package/src/modes/rpc/rpc-client.ts +23 -9
  185. package/src/modes/rpc/rpc-mode.ts +137 -125
  186. package/src/modes/rpc/rpc-types.ts +46 -24
  187. package/src/prompts/task.md +1 -0
  188. package/src/prompts/tools/gemini-image.md +4 -0
  189. package/src/prompts/tools/git.md +9 -0
  190. package/src/prompts/voice-summary.md +12 -0
  191. package/src/utils/image-convert.ts +26 -0
  192. package/src/utils/image-resize.ts +215 -0
  193. package/src/utils/shell-snapshot.ts +22 -20
@@ -8,25 +8,35 @@
8
8
  * - Commands: JSON objects with `type` field, optional `id` for correlation
9
9
  * - Responses: JSON objects with `type: "response"`, `command`, `success`, and optional `data`/`error`
10
10
  * - Events: AgentSessionEvent objects streamed as they occur
11
- * - Hook UI: Hook UI requests are emitted, client responds with hook_ui_response
11
+ * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
 
14
- import { nanoid } from "nanoid";
15
14
  import type { AgentSession } from "../../core/agent-session";
16
- import type { HookUIContext } from "../../core/hooks/index";
17
- import { logger } from "../../core/logger";
15
+ import type { ExtensionUIContext } from "../../core/extensions/index";
18
16
  import { theme } from "../interactive/theme/theme";
19
- import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
17
+ import type {
18
+ RpcCommand,
19
+ RpcExtensionUIRequest,
20
+ RpcExtensionUIResponse,
21
+ RpcResponse,
22
+ RpcSessionState,
23
+ } from "./rpc-types";
20
24
 
21
25
  // Re-export types for consumers
22
- export type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
26
+ export type {
27
+ RpcCommand,
28
+ RpcExtensionUIRequest,
29
+ RpcExtensionUIResponse,
30
+ RpcResponse,
31
+ RpcSessionState,
32
+ } from "./rpc-types";
23
33
 
24
34
  /**
25
35
  * Run in RPC mode.
26
36
  * Listens for JSON commands on stdin, outputs events and responses on stdout.
27
37
  */
28
38
  export async function runRpcMode(session: AgentSession): Promise<never> {
29
- const output = (obj: RpcResponse | RpcHookUIRequest | object) => {
39
+ const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
30
40
  console.log(JSON.stringify(obj));
31
41
  };
32
42
 
@@ -45,18 +55,21 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
45
55
  return { id, type: "response", command, success: false, error: message };
46
56
  };
47
57
 
48
- // Pending hook UI requests waiting for response
49
- const pendingHookRequests = new Map<string, { resolve: (value: any) => void; reject: (error: Error) => void }>();
58
+ // Pending extension UI requests waiting for response
59
+ const pendingExtensionRequests = new Map<
60
+ string,
61
+ { resolve: (value: any) => void; reject: (error: Error) => void }
62
+ >();
50
63
 
51
64
  /**
52
- * Create a hook UI context that uses the RPC protocol.
65
+ * Create an extension UI context that uses the RPC protocol.
53
66
  */
54
- const createHookUIContext = (): HookUIContext => ({
67
+ const createExtensionUIContext = (): ExtensionUIContext => ({
55
68
  async select(title: string, options: string[]): Promise<string | undefined> {
56
- const id = nanoid();
69
+ const id = globalThis.crypto.randomUUID();
57
70
  return new Promise((resolve, reject) => {
58
- pendingHookRequests.set(id, {
59
- resolve: (response: RpcHookUIResponse) => {
71
+ pendingExtensionRequests.set(id, {
72
+ resolve: (response: RpcExtensionUIResponse) => {
60
73
  if ("cancelled" in response && response.cancelled) {
61
74
  resolve(undefined);
62
75
  } else if ("value" in response) {
@@ -67,15 +80,15 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
67
80
  },
68
81
  reject,
69
82
  });
70
- output({ type: "hook_ui_request", id, method: "select", title, options } as RpcHookUIRequest);
83
+ output({ type: "extension_ui_request", id, method: "select", title, options } as RpcExtensionUIRequest);
71
84
  });
72
85
  },
73
86
 
74
87
  async confirm(title: string, message: string): Promise<boolean> {
75
- const id = nanoid();
88
+ const id = globalThis.crypto.randomUUID();
76
89
  return new Promise((resolve, reject) => {
77
- pendingHookRequests.set(id, {
78
- resolve: (response: RpcHookUIResponse) => {
90
+ pendingExtensionRequests.set(id, {
91
+ resolve: (response: RpcExtensionUIResponse) => {
79
92
  if ("cancelled" in response && response.cancelled) {
80
93
  resolve(false);
81
94
  } else if ("confirmed" in response) {
@@ -86,15 +99,15 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
86
99
  },
87
100
  reject,
88
101
  });
89
- output({ type: "hook_ui_request", id, method: "confirm", title, message } as RpcHookUIRequest);
102
+ output({ type: "extension_ui_request", id, method: "confirm", title, message } as RpcExtensionUIRequest);
90
103
  });
91
104
  },
92
105
 
93
106
  async input(title: string, placeholder?: string): Promise<string | undefined> {
94
- const id = nanoid();
107
+ const id = globalThis.crypto.randomUUID();
95
108
  return new Promise((resolve, reject) => {
96
- pendingHookRequests.set(id, {
97
- resolve: (response: RpcHookUIResponse) => {
109
+ pendingExtensionRequests.set(id, {
110
+ resolve: (response: RpcExtensionUIResponse) => {
98
111
  if ("cancelled" in response && response.cancelled) {
99
112
  resolve(undefined);
100
113
  } else if ("value" in response) {
@@ -105,30 +118,54 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
105
118
  },
106
119
  reject,
107
120
  });
108
- output({ type: "hook_ui_request", id, method: "input", title, placeholder } as RpcHookUIRequest);
121
+ output({ type: "extension_ui_request", id, method: "input", title, placeholder } as RpcExtensionUIRequest);
109
122
  });
110
123
  },
111
124
 
112
125
  notify(message: string, type?: "info" | "warning" | "error"): void {
113
126
  // Fire and forget - no response needed
114
127
  output({
115
- type: "hook_ui_request",
116
- id: nanoid(),
128
+ type: "extension_ui_request",
129
+ id: globalThis.crypto.randomUUID(),
117
130
  method: "notify",
118
131
  message,
119
132
  notifyType: type,
120
- } as RpcHookUIRequest);
133
+ } as RpcExtensionUIRequest);
121
134
  },
122
135
 
123
136
  setStatus(key: string, text: string | undefined): void {
124
137
  // Fire and forget - no response needed
125
138
  output({
126
- type: "hook_ui_request",
127
- id: nanoid(),
139
+ type: "extension_ui_request",
140
+ id: globalThis.crypto.randomUUID(),
128
141
  method: "setStatus",
129
142
  statusKey: key,
130
143
  statusText: text,
131
- } as RpcHookUIRequest);
144
+ } as RpcExtensionUIRequest);
145
+ },
146
+
147
+ setWidget(key: string, content: unknown): void {
148
+ // Only support string arrays in RPC mode - factory functions are ignored
149
+ if (content === undefined || Array.isArray(content)) {
150
+ output({
151
+ type: "extension_ui_request",
152
+ id: globalThis.crypto.randomUUID(),
153
+ method: "setWidget",
154
+ widgetKey: key,
155
+ widgetLines: content as string[] | undefined,
156
+ } as RpcExtensionUIRequest);
157
+ }
158
+ // Component factories are not supported in RPC mode - would need TUI access
159
+ },
160
+
161
+ setTitle(title: string): void {
162
+ // Fire and forget - host can implement terminal title control
163
+ output({
164
+ type: "extension_ui_request",
165
+ id: globalThis.crypto.randomUUID(),
166
+ method: "setTitle",
167
+ title,
168
+ } as RpcExtensionUIRequest);
132
169
  },
133
170
 
134
171
  async custom() {
@@ -139,11 +176,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
139
176
  setEditorText(text: string): void {
140
177
  // Fire and forget - host can implement editor control
141
178
  output({
142
- type: "hook_ui_request",
143
- id: nanoid(),
179
+ type: "extension_ui_request",
180
+ id: globalThis.crypto.randomUUID(),
144
181
  method: "set_editor_text",
145
182
  text,
146
- } as RpcHookUIRequest);
183
+ } as RpcExtensionUIRequest);
147
184
  },
148
185
 
149
186
  getEditorText(): string {
@@ -153,10 +190,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
153
190
  },
154
191
 
155
192
  async editor(title: string, prefill?: string): Promise<string | undefined> {
156
- const id = nanoid();
193
+ const id = globalThis.crypto.randomUUID();
157
194
  return new Promise((resolve, reject) => {
158
- pendingHookRequests.set(id, {
159
- resolve: (response: RpcHookUIResponse) => {
195
+ pendingExtensionRequests.set(id, {
196
+ resolve: (response: RpcExtensionUIResponse) => {
160
197
  if ("cancelled" in response && response.cancelled) {
161
198
  resolve(undefined);
162
199
  } else if ("value" in response) {
@@ -167,7 +204,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
167
204
  },
168
205
  reject,
169
206
  });
170
- output({ type: "hook_ui_request", id, method: "editor", title, prefill } as RpcHookUIRequest);
207
+ output({ type: "extension_ui_request", id, method: "editor", title, prefill } as RpcExtensionUIRequest);
171
208
  });
172
209
  },
173
210
 
@@ -176,66 +213,39 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
176
213
  },
177
214
  });
178
215
 
179
- // Set up hooks with RPC-based UI context
180
- const hookRunner = session.hookRunner;
181
- if (hookRunner) {
182
- hookRunner.initialize({
216
+ // Set up extensions with RPC-based UI context
217
+ const extensionRunner = session.extensionRunner;
218
+ if (extensionRunner) {
219
+ extensionRunner.initialize({
183
220
  getModel: () => session.agent.state.model,
184
- sendMessageHandler: (message, triggerTurn) => {
185
- session.sendHookMessage(message, triggerTurn).catch((e) => {
186
- output(error(undefined, "hook_send", e.message));
221
+ sendMessageHandler: (message, options) => {
222
+ session.sendCustomMessage(message, options).catch((e) => {
223
+ output(error(undefined, "extension_send", e.message));
187
224
  });
188
225
  },
189
226
  appendEntryHandler: (customType, data) => {
190
227
  session.sessionManager.appendCustomEntry(customType, data);
191
228
  },
192
- uiContext: createHookUIContext(),
229
+ getActiveToolsHandler: () => session.getActiveToolNames(),
230
+ getAllToolsHandler: () => session.getAllToolNames(),
231
+ setActiveToolsHandler: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
232
+ uiContext: createExtensionUIContext(),
193
233
  hasUI: false,
194
234
  });
195
- hookRunner.onError((err) => {
196
- output({ type: "hook_error", hookPath: err.hookPath, event: err.event, error: err.error });
235
+ extensionRunner.onError((err) => {
236
+ output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
197
237
  });
198
238
  // Emit session_start event
199
- await hookRunner.emit({
239
+ await extensionRunner.emit({
200
240
  type: "session_start",
201
241
  });
202
242
  }
203
243
 
204
- // Emit session start event to custom tools
205
- // Note: Tools get no-op UI context in RPC mode (host handles UI via protocol)
206
- for (const { tool } of session.customTools) {
207
- if (tool.onSession) {
208
- try {
209
- await tool.onSession(
210
- {
211
- previousSessionFile: undefined,
212
- reason: "start",
213
- },
214
- {
215
- sessionManager: session.sessionManager,
216
- modelRegistry: session.modelRegistry,
217
- model: session.model,
218
- isIdle: () => !session.isStreaming,
219
- hasQueuedMessages: () => session.queuedMessageCount > 0,
220
- abort: () => {
221
- session.abort();
222
- },
223
- },
224
- );
225
- } catch (err) {
226
- logger.warn("Tool onSession error", { error: String(err) });
227
- }
228
- }
229
- }
230
-
231
244
  // Output all agent events as JSON
232
245
  session.subscribe((event) => {
233
246
  output(event);
234
247
  });
235
248
 
236
- // Serialize prompt commands to prevent concurrent execution
237
- let activePrompt: Promise<void> | null = null;
238
-
239
249
  // Handle a single command
240
250
  const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
241
251
  const id = command.id;
@@ -246,24 +256,26 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
246
256
  // =================================================================
247
257
 
248
258
  case "prompt": {
249
- // Serialize prompts to prevent concurrent execution
250
- if (activePrompt) {
251
- await activePrompt;
252
- }
253
- activePrompt = session
259
+ // Don't await - events will stream
260
+ // Extension commands are executed immediately, file prompt templates are expanded
261
+ // If streaming and streamingBehavior specified, queues via steer/followUp
262
+ session
254
263
  .prompt(command.message, {
255
264
  images: command.images,
265
+ streamingBehavior: command.streamingBehavior,
256
266
  })
257
- .catch((e) => output(error(id, "prompt", e.message)))
258
- .finally(() => {
259
- activePrompt = null;
260
- });
267
+ .catch((e) => output(error(id, "prompt", e.message)));
261
268
  return success(id, "prompt");
262
269
  }
263
270
 
264
- case "queue_message": {
265
- await session.queueMessage(command.message);
266
- return success(id, "queue_message");
271
+ case "steer": {
272
+ await session.steer(command.message);
273
+ return success(id, "steer");
274
+ }
275
+
276
+ case "follow_up": {
277
+ await session.followUp(command.message);
278
+ return success(id, "follow_up");
267
279
  }
268
280
 
269
281
  case "abort": {
@@ -287,7 +299,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
287
299
  thinkingLevel: session.thinkingLevel,
288
300
  isStreaming: session.isStreaming,
289
301
  isCompacting: session.isCompacting,
290
- queueMode: session.queueMode,
302
+ steeringMode: session.steeringMode,
303
+ followUpMode: session.followUpMode,
304
+ interruptMode: session.interruptMode,
291
305
  sessionFile: session.sessionFile,
292
306
  sessionId: session.sessionId,
293
307
  autoCompactionEnabled: session.autoCompactionEnabled,
@@ -302,7 +316,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
302
316
  // =================================================================
303
317
 
304
318
  case "set_model": {
305
- const models = await session.getAvailableModels();
319
+ const models = session.getAvailableModels();
306
320
  const model = models.find((m) => m.provider === command.provider && m.id === command.modelId);
307
321
  if (!model) {
308
322
  return error(id, "set_model", `Model not found: ${command.provider}/${command.modelId}`);
@@ -320,7 +334,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
320
334
  }
321
335
 
322
336
  case "get_available_models": {
323
- const models = await session.getAvailableModels();
337
+ const models = session.getAvailableModels();
324
338
  return success(id, "get_available_models", { models });
325
339
  }
326
340
 
@@ -342,12 +356,22 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
342
356
  }
343
357
 
344
358
  // =================================================================
345
- // Queue Mode
359
+ // Queue Modes
346
360
  // =================================================================
347
361
 
348
- case "set_queue_mode": {
349
- session.setQueueMode(command.mode);
350
- return success(id, "set_queue_mode");
362
+ case "set_steering_mode": {
363
+ session.setSteeringMode(command.mode);
364
+ return success(id, "set_steering_mode");
365
+ }
366
+
367
+ case "set_follow_up_mode": {
368
+ session.setFollowUpMode(command.mode);
369
+ return success(id, "set_follow_up_mode");
370
+ }
371
+
372
+ case "set_interrupt_mode": {
373
+ session.setInterruptMode(command.mode);
374
+ return success(id, "set_interrupt_mode");
351
375
  }
352
376
 
353
377
  // =================================================================
@@ -441,42 +465,30 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
441
465
  }
442
466
  };
443
467
 
444
- // Listen for JSON input - use Bun's ReadableStream
445
- const stdinReader = (Bun.stdin.stream() as ReadableStream<Uint8Array>)
446
- .pipeThrough(new TextDecoderStream())
447
- .pipeThrough(
448
- new TransformStream({
449
- transform(chunk, controller) {
450
- const lines = chunk.split("\n");
451
- for (const line of lines) {
452
- if (line.trim()) {
453
- controller.enqueue(line);
454
- }
455
- }
456
- },
457
- }),
458
- )
459
- .getReader();
468
+ // Listen for JSON input using Bun's stdin
469
+ const decoder = new TextDecoder();
470
+ let buffer = "";
471
+
472
+ for await (const chunk of Bun.stdin.stream()) {
473
+ buffer += decoder.decode(chunk, { stream: true });
474
+ const lines = buffer.split("\n");
475
+ buffer = lines.pop() || "";
460
476
 
461
- // Process lines in background
462
- (async () => {
463
- while (true) {
464
- const { done, value: line } = await stdinReader.read();
465
- if (done) break;
477
+ for (const line of lines) {
478
+ if (!line.trim()) continue;
466
479
 
467
480
  try {
468
481
  const parsed = JSON.parse(line);
469
482
 
470
- // Handle hook UI responses
471
- if (parsed.type === "hook_ui_response") {
472
- const response = parsed as RpcHookUIResponse;
473
- const pending = pendingHookRequests.get(response.id);
483
+ // Handle extension UI responses
484
+ if (parsed.type === "extension_ui_response") {
485
+ const response = parsed as RpcExtensionUIResponse;
486
+ const pending = pendingExtensionRequests.get(response.id);
474
487
  if (pending) {
475
- // Atomic delete: remove before resolve to prevent double-resolution
476
- pendingHookRequests.delete(response.id);
488
+ pendingExtensionRequests.delete(response.id);
477
489
  pending.resolve(response);
478
490
  }
479
- return;
491
+ continue;
480
492
  }
481
493
 
482
494
  // Handle regular commands
@@ -487,7 +499,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
487
499
  output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
488
500
  }
489
501
  }
490
- })();
502
+ }
491
503
 
492
504
  // Keep process alive forever
493
505
  return new Promise(() => {});
@@ -17,8 +17,9 @@ import type { CompactionResult } from "../../core/compaction/index";
17
17
 
18
18
  export type RpcCommand =
19
19
  // Prompting
20
- | { id?: string; type: "prompt"; message: string; images?: ImageContent[] }
21
- | { id?: string; type: "queue_message"; message: string }
20
+ | { id?: string; type: "prompt"; message: string; images?: ImageContent[]; streamingBehavior?: "steer" | "followUp" }
21
+ | { id?: string; type: "steer"; message: string }
22
+ | { id?: string; type: "follow_up"; message: string }
22
23
  | { id?: string; type: "abort" }
23
24
  | { id?: string; type: "new_session"; parentSession?: string }
24
25
 
@@ -34,8 +35,10 @@ export type RpcCommand =
34
35
  | { id?: string; type: "set_thinking_level"; level: ThinkingLevel }
35
36
  | { id?: string; type: "cycle_thinking_level" }
36
37
 
37
- // Queue mode
38
- | { id?: string; type: "set_queue_mode"; mode: "all" | "one-at-a-time" }
38
+ // Queue modes
39
+ | { id?: string; type: "set_steering_mode"; mode: "all" | "one-at-a-time" }
40
+ | { id?: string; type: "set_follow_up_mode"; mode: "all" | "one-at-a-time" }
41
+ | { id?: string; type: "set_interrupt_mode"; mode: "immediate" | "wait" }
39
42
 
40
43
  // Compaction
41
44
  | { id?: string; type: "compact"; customInstructions?: string }
@@ -69,7 +72,9 @@ export interface RpcSessionState {
69
72
  thinkingLevel: ThinkingLevel;
70
73
  isStreaming: boolean;
71
74
  isCompacting: boolean;
72
- queueMode: "all" | "one-at-a-time";
75
+ steeringMode: "all" | "one-at-a-time";
76
+ followUpMode: "all" | "one-at-a-time";
77
+ interruptMode: "immediate" | "wait";
73
78
  sessionFile?: string;
74
79
  sessionId: string;
75
80
  autoCompactionEnabled: boolean;
@@ -85,7 +90,8 @@ export interface RpcSessionState {
85
90
  export type RpcResponse =
86
91
  // Prompting (async - events follow)
87
92
  | { id?: string; type: "response"; command: "prompt"; success: true }
88
- | { id?: string; type: "response"; command: "queue_message"; success: true }
93
+ | { id?: string; type: "response"; command: "steer"; success: true }
94
+ | { id?: string; type: "response"; command: "follow_up"; success: true }
89
95
  | { id?: string; type: "response"; command: "abort"; success: true }
90
96
  | { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }
91
97
 
@@ -125,8 +131,10 @@ export type RpcResponse =
125
131
  data: { level: ThinkingLevel } | null;
126
132
  }
127
133
 
128
- // Queue mode
129
- | { id?: string; type: "response"; command: "set_queue_mode"; success: true }
134
+ // Queue modes
135
+ | { id?: string; type: "response"; command: "set_steering_mode"; success: true }
136
+ | { id?: string; type: "response"; command: "set_follow_up_mode"; success: true }
137
+ | { id?: string; type: "response"; command: "set_interrupt_mode"; success: true }
130
138
 
131
139
  // Compaction
132
140
  | { id?: string; type: "response"; command: "compact"; success: true; data: CompactionResult }
@@ -167,34 +175,48 @@ export type RpcResponse =
167
175
  | { id?: string; type: "response"; command: string; success: false; error: string };
168
176
 
169
177
  // ============================================================================
170
- // Hook UI Events (stdout)
178
+ // Extension UI Events (stdout)
171
179
  // ============================================================================
172
180
 
173
- /** Emitted when a hook needs user input */
174
- export type RpcHookUIRequest =
175
- | { type: "hook_ui_request"; id: string; method: "select"; title: string; options: string[] }
176
- | { type: "hook_ui_request"; id: string; method: "confirm"; title: string; message: string }
177
- | { type: "hook_ui_request"; id: string; method: "input"; title: string; placeholder?: string }
178
- | { type: "hook_ui_request"; id: string; method: "editor"; title: string; prefill?: string }
181
+ /** Emitted when an extension needs user input */
182
+ export type RpcExtensionUIRequest =
183
+ | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[] }
184
+ | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string }
185
+ | { type: "extension_ui_request"; id: string; method: "input"; title: string; placeholder?: string }
186
+ | { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string }
179
187
  | {
180
- type: "hook_ui_request";
188
+ type: "extension_ui_request";
181
189
  id: string;
182
190
  method: "notify";
183
191
  message: string;
184
192
  notifyType?: "info" | "warning" | "error";
185
193
  }
186
- | { type: "hook_ui_request"; id: string; method: "setStatus"; statusKey: string; statusText: string | undefined }
187
- | { type: "hook_ui_request"; id: string; method: "set_editor_text"; text: string };
194
+ | {
195
+ type: "extension_ui_request";
196
+ id: string;
197
+ method: "setStatus";
198
+ statusKey: string;
199
+ statusText: string | undefined;
200
+ }
201
+ | {
202
+ type: "extension_ui_request";
203
+ id: string;
204
+ method: "setWidget";
205
+ widgetKey: string;
206
+ widgetLines: string[] | undefined;
207
+ }
208
+ | { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
209
+ | { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string };
188
210
 
189
211
  // ============================================================================
190
- // Hook UI Commands (stdin)
212
+ // Extension UI Commands (stdin)
191
213
  // ============================================================================
192
214
 
193
- /** Response to a hook UI request */
194
- export type RpcHookUIResponse =
195
- | { type: "hook_ui_response"; id: string; value: string }
196
- | { type: "hook_ui_response"; id: string; confirmed: boolean }
197
- | { type: "hook_ui_response"; id: string; cancelled: true };
215
+ /** Response to an extension UI request */
216
+ export type RpcExtensionUIResponse =
217
+ | { type: "extension_ui_response"; id: string; value: string }
218
+ | { type: "extension_ui_response"; id: string; confirmed: boolean }
219
+ | { type: "extension_ui_response"; id: string; cancelled: true };
198
220
 
199
221
  // ============================================================================
200
222
  // Helper type for extracting command types
@@ -23,6 +23,7 @@ Guidelines:
23
23
  - For file searches: Use grep/glob when you need to search broadly. Use read when you know the specific file path.
24
24
  - For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
25
25
  - Be thorough: Check multiple locations, consider different naming conventions, look for related files.
26
+ - When spawning subagents with the Task tool, include a short, user-facing `description` for each task (5-8 words) that summarizes the approach.
26
27
  - NEVER create files unless absolutely necessary. ALWAYS prefer editing existing files.
27
28
  - NEVER proactively create documentation files (\*.md) or README files unless explicitly requested.
28
29
  - Any file paths in your response MUST be absolute. Do NOT use relative paths.
@@ -0,0 +1,4 @@
1
+ Generate or edit images using Google Gemini image models ("Nano Banana").
2
+
3
+ Provide a text prompt and optional input images. Use response modalities to request image-only output,
4
+ set aspect ratio or image size, and choose the model explicitly when needed.
@@ -0,0 +1,9 @@
1
+ Structured Git operations with safety guards and typed output. Use this tool instead of raw git commands.
2
+
3
+ Operations:
4
+ - READ: status, diff, log, show, blame, branch
5
+ - WRITE: add, restore, commit, checkout, merge, rebase, stash, cherry-pick
6
+ - REMOTE: fetch, pull, push, tag
7
+ - GITHUB: pr, issue, ci, release
8
+
9
+ Returns structured data plus a rendered summary for display. Safety checks may block or require confirmation for destructive actions.
@@ -0,0 +1,12 @@
1
+ You are a voice summarizer for a coding agent.
2
+
3
+ Summarize the assistant response for spoken playback.
4
+
5
+ Rules:
6
+ - Output 1 to 3 sentences, maximum 50 words.
7
+ - No markdown, no code blocks, no inline code, no URLs.
8
+ - If the response already fits, return it unchanged.
9
+ - Preserve the most important question if one is asked.
10
+ - If a decision or missing info is required, ask one short question.
11
+
12
+ Return only the spoken text.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Convert image to PNG format for terminal display.
3
+ * Kitty graphics protocol requires PNG format (f=100).
4
+ */
5
+ export async function convertToPng(
6
+ base64Data: string,
7
+ mimeType: string,
8
+ ): Promise<{ data: string; mimeType: string } | null> {
9
+ // Already PNG, no conversion needed
10
+ if (mimeType === "image/png") {
11
+ return { data: base64Data, mimeType };
12
+ }
13
+
14
+ try {
15
+ const sharp = (await import("sharp")).default;
16
+ const buffer = Buffer.from(base64Data, "base64");
17
+ const pngBuffer = await sharp(buffer).png().toBuffer();
18
+ return {
19
+ data: pngBuffer.toString("base64"),
20
+ mimeType: "image/png",
21
+ };
22
+ } catch {
23
+ // Sharp not available or conversion failed
24
+ return null;
25
+ }
26
+ }