@oh-my-pi/pi-coding-agent 3.37.0 → 4.0.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 (70) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +44 -3
  3. package/docs/extensions.md +29 -4
  4. package/docs/sdk.md +3 -3
  5. package/package.json +5 -5
  6. package/src/cli/args.ts +8 -0
  7. package/src/config.ts +5 -15
  8. package/src/core/agent-session.ts +193 -47
  9. package/src/core/auth-storage.ts +16 -3
  10. package/src/core/bash-executor.ts +79 -14
  11. package/src/core/custom-commands/types.ts +1 -1
  12. package/src/core/custom-tools/types.ts +1 -1
  13. package/src/core/export-html/index.ts +33 -1
  14. package/src/core/export-html/template.css +99 -0
  15. package/src/core/export-html/template.generated.ts +1 -1
  16. package/src/core/export-html/template.js +133 -8
  17. package/src/core/extensions/index.ts +22 -4
  18. package/src/core/extensions/loader.ts +152 -214
  19. package/src/core/extensions/runner.ts +139 -79
  20. package/src/core/extensions/types.ts +143 -19
  21. package/src/core/extensions/wrapper.ts +5 -8
  22. package/src/core/hooks/types.ts +1 -1
  23. package/src/core/index.ts +2 -1
  24. package/src/core/keybindings.ts +4 -1
  25. package/src/core/model-registry.ts +1 -1
  26. package/src/core/model-resolver.ts +35 -26
  27. package/src/core/sdk.ts +96 -76
  28. package/src/core/settings-manager.ts +45 -14
  29. package/src/core/system-prompt.ts +5 -15
  30. package/src/core/tools/bash.ts +115 -54
  31. package/src/core/tools/find.ts +86 -7
  32. package/src/core/tools/grep.ts +27 -6
  33. package/src/core/tools/index.ts +15 -6
  34. package/src/core/tools/ls.ts +49 -18
  35. package/src/core/tools/render-utils.ts +2 -1
  36. package/src/core/tools/task/worker.ts +35 -12
  37. package/src/core/tools/web-search/auth.ts +37 -32
  38. package/src/core/tools/web-search/providers/anthropic.ts +35 -22
  39. package/src/index.ts +101 -9
  40. package/src/main.ts +60 -20
  41. package/src/migrations.ts +47 -2
  42. package/src/modes/index.ts +2 -2
  43. package/src/modes/interactive/components/assistant-message.ts +25 -7
  44. package/src/modes/interactive/components/bash-execution.ts +5 -0
  45. package/src/modes/interactive/components/branch-summary-message.ts +5 -0
  46. package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
  47. package/src/modes/interactive/components/countdown-timer.ts +38 -0
  48. package/src/modes/interactive/components/custom-editor.ts +8 -0
  49. package/src/modes/interactive/components/custom-message.ts +5 -0
  50. package/src/modes/interactive/components/footer.ts +2 -5
  51. package/src/modes/interactive/components/hook-input.ts +29 -20
  52. package/src/modes/interactive/components/hook-selector.ts +52 -38
  53. package/src/modes/interactive/components/index.ts +39 -0
  54. package/src/modes/interactive/components/login-dialog.ts +160 -0
  55. package/src/modes/interactive/components/model-selector.ts +10 -2
  56. package/src/modes/interactive/components/session-selector.ts +5 -1
  57. package/src/modes/interactive/components/settings-defs.ts +9 -0
  58. package/src/modes/interactive/components/status-line/segments.ts +3 -3
  59. package/src/modes/interactive/components/tool-execution.ts +9 -16
  60. package/src/modes/interactive/components/tree-selector.ts +1 -6
  61. package/src/modes/interactive/interactive-mode.ts +466 -215
  62. package/src/modes/interactive/theme/theme.ts +50 -2
  63. package/src/modes/print-mode.ts +78 -31
  64. package/src/modes/rpc/rpc-mode.ts +186 -78
  65. package/src/modes/rpc/rpc-types.ts +10 -3
  66. package/src/prompts/system-prompt.md +36 -28
  67. package/src/utils/clipboard.ts +90 -50
  68. package/src/utils/image-convert.ts +1 -1
  69. package/src/utils/image-resize.ts +1 -1
  70. package/src/utils/tools-manager.ts +2 -2
@@ -1012,10 +1012,12 @@ function detectColorMode(): ColorMode {
1012
1012
  return "truecolor";
1013
1013
  }
1014
1014
  const term = process.env.TERM || "";
1015
- if (term.includes("256color")) {
1015
+ // Only fall back to 256color for truly limited terminals
1016
+ if (term === "dumb" || term === "" || term === "linux") {
1016
1017
  return "256color";
1017
1018
  }
1018
- return "256color";
1019
+ // Assume truecolor for everything else - virtually all modern terminals support it
1020
+ return "truecolor";
1019
1021
  }
1020
1022
 
1021
1023
  function hexToRgb(hex: string): { r: number; g: number; b: number } {
@@ -1599,6 +1601,35 @@ export function getAvailableThemes(): string[] {
1599
1601
  return Array.from(themes).sort();
1600
1602
  }
1601
1603
 
1604
+ export interface ThemeInfo {
1605
+ name: string;
1606
+ path: string | undefined;
1607
+ }
1608
+
1609
+ export function getAvailableThemesWithPaths(): ThemeInfo[] {
1610
+ const result: ThemeInfo[] = [];
1611
+
1612
+ // Built-in themes (embedded, no file path)
1613
+ for (const name of Object.keys(getBuiltinThemes())) {
1614
+ result.push({ name, path: undefined });
1615
+ }
1616
+
1617
+ // Custom themes
1618
+ const customThemesDir = getCustomThemesDir();
1619
+ if (fs.existsSync(customThemesDir)) {
1620
+ for (const file of fs.readdirSync(customThemesDir)) {
1621
+ if (file.endsWith(".json")) {
1622
+ const name = file.slice(0, -5);
1623
+ if (!result.some((themeInfo) => themeInfo.name === name)) {
1624
+ result.push({ name, path: path.join(customThemesDir, file) });
1625
+ }
1626
+ }
1627
+ }
1628
+ }
1629
+
1630
+ return result.sort((a, b) => a.name.localeCompare(b.name));
1631
+ }
1632
+
1602
1633
  function loadThemeJson(name: string): ThemeJson {
1603
1634
  const builtinThemes = getBuiltinThemes();
1604
1635
  if (name in builtinThemes) {
@@ -1679,6 +1710,14 @@ function loadTheme(name: string, mode?: ColorMode, symbolPresetOverride?: Symbol
1679
1710
  return createTheme(themeJson, mode, symbolPresetOverride);
1680
1711
  }
1681
1712
 
1713
+ export function getThemeByName(name: string): Theme | undefined {
1714
+ try {
1715
+ return loadTheme(name);
1716
+ } catch {
1717
+ return undefined;
1718
+ }
1719
+ }
1720
+
1682
1721
  function detectTerminalBackground(): "dark" | "light" {
1683
1722
  const colorfgbg = process.env.COLORFGBG || "";
1684
1723
  if (colorfgbg) {
@@ -1748,6 +1787,15 @@ export function setTheme(name: string, enableWatcher: boolean = false): { succes
1748
1787
  }
1749
1788
  }
1750
1789
 
1790
+ export function setThemeInstance(themeInstance: Theme): void {
1791
+ theme = themeInstance;
1792
+ currentThemeName = "<in-memory>";
1793
+ stopThemeWatcher();
1794
+ if (onThemeChangeCallback) {
1795
+ onThemeChangeCallback();
1796
+ }
1797
+ }
1798
+
1751
1799
  /**
1752
1800
  * Set the symbol preset override, recreating the theme with the new preset.
1753
1801
  */
@@ -9,43 +9,90 @@
9
9
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
10
10
  import type { AgentSession } from "../core/agent-session";
11
11
 
12
+ /**
13
+ * Options for print mode.
14
+ */
15
+ export interface PrintModeOptions {
16
+ /** Output mode: "text" for final response only, "json" for all events */
17
+ mode: "text" | "json";
18
+ /** Array of additional prompts to send after initialMessage */
19
+ messages?: string[];
20
+ /** First message to send (may contain @file content) */
21
+ initialMessage?: string;
22
+ /** Images to attach to the initial message */
23
+ initialImages?: ImageContent[];
24
+ }
25
+
12
26
  /**
13
27
  * Run in print (single-shot) mode.
14
28
  * Sends prompts to the agent and outputs the result.
15
- *
16
- * @param session The agent session
17
- * @param mode Output mode: "text" for final response only, "json" for all events
18
- * @param messages Array of prompts to send
19
- * @param initialMessage Optional first message (may contain @file content)
20
- * @param initialImages Optional images for the initial message
21
29
  */
22
- export async function runPrintMode(
23
- session: AgentSession,
24
- mode: "text" | "json",
25
- messages: string[],
26
- initialMessage?: string,
27
- initialImages?: ImageContent[],
28
- ): Promise<void> {
29
- // Extension runner already has no-op UI context by default (set in loader)
30
- // Set up extensions for print mode (no UI)
30
+ export async function runPrintMode(session: AgentSession, options: PrintModeOptions): Promise<void> {
31
+ const { mode, messages = [], initialMessage, initialImages } = options;
32
+ // Set up extensions for print mode (no UI, no command context)
31
33
  const extensionRunner = session.extensionRunner;
32
34
  if (extensionRunner) {
33
- extensionRunner.initialize({
34
- getModel: () => session.model,
35
- sendMessageHandler: (message, options) => {
36
- session.sendCustomMessage(message, options).catch((e) => {
37
- console.error(`Extension sendMessage failed: ${e instanceof Error ? e.message : String(e)}`);
38
- });
35
+ extensionRunner.initialize(
36
+ // ExtensionActions
37
+ {
38
+ sendMessage: (message, options) => {
39
+ session.sendCustomMessage(message, options).catch((e) => {
40
+ process.stderr.write(`Extension sendMessage failed: ${e instanceof Error ? e.message : String(e)}\n`);
41
+ });
42
+ },
43
+ sendUserMessage: (content, options) => {
44
+ session.sendUserMessage(content, options).catch((e) => {
45
+ process.stderr.write(
46
+ `Extension sendUserMessage failed: ${e instanceof Error ? e.message : String(e)}\n`,
47
+ );
48
+ });
49
+ },
50
+ appendEntry: (customType, data) => {
51
+ session.sessionManager.appendCustomEntry(customType, data);
52
+ },
53
+ getActiveTools: () => session.getActiveToolNames(),
54
+ getAllTools: () => session.getAllToolNames(),
55
+ setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
56
+ setModel: async (model) => {
57
+ const key = await session.modelRegistry.getApiKey(model);
58
+ if (!key) return false;
59
+ await session.setModel(model);
60
+ return true;
61
+ },
62
+ getThinkingLevel: () => session.thinkingLevel,
63
+ setThinkingLevel: (level) => session.setThinkingLevel(level),
39
64
  },
40
- appendEntryHandler: (customType, data) => {
41
- session.sessionManager.appendCustomEntry(customType, data);
65
+ // ExtensionContextActions
66
+ {
67
+ getModel: () => session.model,
68
+ isIdle: () => !session.isStreaming,
69
+ abort: () => session.abort(),
70
+ hasPendingMessages: () => session.queuedMessageCount > 0,
71
+ shutdown: () => {},
42
72
  },
43
- getActiveToolsHandler: () => session.getActiveToolNames(),
44
- getAllToolsHandler: () => session.getAllToolNames(),
45
- setActiveToolsHandler: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
46
- });
73
+ // ExtensionCommandContextActions - commands invokable via prompt("/command")
74
+ {
75
+ waitForIdle: () => session.agent.waitForIdle(),
76
+ newSession: async (options) => {
77
+ const success = await session.newSession({ parentSession: options?.parentSession });
78
+ if (success && options?.setup) {
79
+ await options.setup(session.sessionManager);
80
+ }
81
+ return { cancelled: !success };
82
+ },
83
+ branch: async (entryId) => {
84
+ const result = await session.branch(entryId);
85
+ return { cancelled: result.cancelled };
86
+ },
87
+ navigateTree: async (targetId, options) => {
88
+ const result = await session.navigateTree(targetId, { summarize: options?.summarize });
89
+ return { cancelled: result.cancelled };
90
+ },
91
+ },
92
+ // No UI context
93
+ );
47
94
  extensionRunner.onError((err) => {
48
- console.error(`Extension error (${err.extensionPath}): ${err.error}`);
95
+ process.stderr.write(`Extension error (${err.extensionPath}): ${err.error}\n`);
49
96
  });
50
97
  // Emit session_start event
51
98
  await extensionRunner.emit({
@@ -57,7 +104,7 @@ export async function runPrintMode(
57
104
  session.subscribe((event) => {
58
105
  // In JSON mode, output all events
59
106
  if (mode === "json") {
60
- console.log(JSON.stringify(event));
107
+ process.stdout.write(`${JSON.stringify(event)}\n`);
61
108
  }
62
109
  });
63
110
 
@@ -81,14 +128,14 @@ export async function runPrintMode(
81
128
 
82
129
  // Check for error/aborted
83
130
  if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
84
- console.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
131
+ process.stderr.write(`${assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`}\n`);
85
132
  process.exit(1);
86
133
  }
87
134
 
88
135
  // Output text content
89
136
  for (const content of assistantMsg.content) {
90
137
  if (content.type === "text") {
91
- console.log(content.text);
138
+ process.stdout.write(`${content.text}\n`);
92
139
  }
93
140
  }
94
141
  }
@@ -13,8 +13,8 @@
13
13
 
14
14
  import { nanoid } from "nanoid";
15
15
  import type { AgentSession } from "../../core/agent-session";
16
- import type { ExtensionUIContext } from "../../core/extensions/index";
17
- import { theme } from "../interactive/theme/theme";
16
+ import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../core/extensions/index";
17
+ import { type Theme, theme } from "../interactive/theme/theme";
18
18
  import type {
19
19
  RpcCommand,
20
20
  RpcExtensionUIRequest,
@@ -38,7 +38,7 @@ export type {
38
38
  */
39
39
  export async function runRpcMode(session: AgentSession): Promise<never> {
40
40
  const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
41
- console.log(JSON.stringify(obj));
41
+ process.stdout.write(`${JSON.stringify(obj)}\n`);
42
42
  };
43
43
 
44
44
  const success = <T extends RpcCommand["type"]>(
@@ -57,71 +57,101 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
57
57
  };
58
58
 
59
59
  // Pending extension UI requests waiting for response
60
- const pendingExtensionRequests = new Map<
61
- string,
62
- { resolve: (value: any) => void; reject: (error: Error) => void }
63
- >();
60
+ type PendingExtensionRequest = {
61
+ resolve: (response: RpcExtensionUIResponse) => void;
62
+ reject: (error: Error) => void;
63
+ };
64
+
65
+ const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
66
+
67
+ // Shutdown request flag (wrapped in object to allow mutation with const)
68
+ const shutdownState = { requested: false };
69
+
70
+ /** Helper for dialog methods with signal/timeout support */
71
+ function createDialogPromise<T>(
72
+ opts: ExtensionUIDialogOptions | undefined,
73
+ defaultValue: T,
74
+ request: Record<string, unknown>,
75
+ parseResponse: (response: RpcExtensionUIResponse) => T,
76
+ ): Promise<T> {
77
+ if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
78
+
79
+ const id = nanoid();
80
+ return new Promise((resolve, reject) => {
81
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
82
+
83
+ const cleanup = () => {
84
+ if (timeoutId) clearTimeout(timeoutId);
85
+ opts?.signal?.removeEventListener("abort", onAbort);
86
+ pendingExtensionRequests.delete(id);
87
+ };
88
+
89
+ const onAbort = () => {
90
+ cleanup();
91
+ resolve(defaultValue);
92
+ };
93
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
94
+
95
+ if (opts?.timeout !== undefined) {
96
+ timeoutId = setTimeout(() => {
97
+ cleanup();
98
+ resolve(defaultValue);
99
+ }, opts.timeout);
100
+ }
101
+
102
+ pendingExtensionRequests.set(id, {
103
+ resolve: (response: RpcExtensionUIResponse) => {
104
+ cleanup();
105
+ resolve(parseResponse(response));
106
+ },
107
+ reject,
108
+ });
109
+ output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
110
+ });
111
+ }
64
112
 
65
113
  /**
66
114
  * Create an extension UI context that uses the RPC protocol.
67
115
  */
68
116
  const createExtensionUIContext = (): ExtensionUIContext => ({
69
- async select(title: string, options: string[]): Promise<string | undefined> {
70
- const id = nanoid();
71
- return new Promise((resolve, reject) => {
72
- pendingExtensionRequests.set(id, {
73
- resolve: (response: RpcExtensionUIResponse) => {
74
- if ("cancelled" in response && response.cancelled) {
75
- resolve(undefined);
76
- } else if ("value" in response) {
77
- resolve(response.value);
78
- } else {
79
- resolve(undefined);
80
- }
81
- },
82
- reject,
83
- });
84
- output({ type: "extension_ui_request", id, method: "select", title, options } as RpcExtensionUIRequest);
85
- });
86
- },
87
-
88
- async confirm(title: string, message: string): Promise<boolean> {
89
- const id = nanoid();
90
- return new Promise((resolve, reject) => {
91
- pendingExtensionRequests.set(id, {
92
- resolve: (response: RpcExtensionUIResponse) => {
93
- if ("cancelled" in response && response.cancelled) {
94
- resolve(false);
95
- } else if ("confirmed" in response) {
96
- resolve(response.confirmed);
97
- } else {
98
- resolve(false);
99
- }
100
- },
101
- reject,
102
- });
103
- output({ type: "extension_ui_request", id, method: "confirm", title, message } as RpcExtensionUIRequest);
104
- });
105
- },
106
-
107
- async input(title: string, placeholder?: string): Promise<string | undefined> {
108
- const id = nanoid();
109
- return new Promise((resolve, reject) => {
110
- pendingExtensionRequests.set(id, {
111
- resolve: (response: RpcExtensionUIResponse) => {
112
- if ("cancelled" in response && response.cancelled) {
113
- resolve(undefined);
114
- } else if ("value" in response) {
115
- resolve(response.value);
116
- } else {
117
- resolve(undefined);
118
- }
119
- },
120
- reject,
121
- });
122
- output({ type: "extension_ui_request", id, method: "input", title, placeholder } as RpcExtensionUIRequest);
123
- });
124
- },
117
+ select: (title, options, dialogOptions) =>
118
+ createDialogPromise(
119
+ dialogOptions,
120
+ undefined,
121
+ { method: "select", title, options, timeout: dialogOptions?.timeout },
122
+ (response) =>
123
+ "cancelled" in response && response.cancelled
124
+ ? undefined
125
+ : "value" in response
126
+ ? response.value
127
+ : undefined,
128
+ ),
129
+
130
+ confirm: (title, message, dialogOptions) =>
131
+ createDialogPromise(
132
+ dialogOptions,
133
+ false,
134
+ { method: "confirm", title, message, timeout: dialogOptions?.timeout },
135
+ (response) =>
136
+ "cancelled" in response && response.cancelled
137
+ ? false
138
+ : "confirmed" in response
139
+ ? response.confirmed
140
+ : false,
141
+ ),
142
+
143
+ input: (title, placeholder, dialogOptions) =>
144
+ createDialogPromise(
145
+ dialogOptions,
146
+ undefined,
147
+ { method: "input", title, placeholder, timeout: dialogOptions?.timeout },
148
+ (response) =>
149
+ "cancelled" in response && response.cancelled
150
+ ? undefined
151
+ : "value" in response
152
+ ? response.value
153
+ : undefined,
154
+ ),
125
155
 
126
156
  notify(message: string, type?: "info" | "warning" | "error"): void {
127
157
  // Fire and forget - no response needed
@@ -195,6 +225,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
195
225
  return new Promise((resolve, reject) => {
196
226
  pendingExtensionRequests.set(id, {
197
227
  resolve: (response: RpcExtensionUIResponse) => {
228
+ pendingExtensionRequests.delete(id);
198
229
  if ("cancelled" in response && response.cancelled) {
199
230
  resolve(undefined);
200
231
  } else if ("value" in response) {
@@ -212,27 +243,88 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
212
243
  get theme() {
213
244
  return theme;
214
245
  },
246
+
247
+ getAllThemes() {
248
+ return [];
249
+ },
250
+
251
+ getTheme(_name: string) {
252
+ return undefined;
253
+ },
254
+
255
+ setTheme(_theme: string | Theme) {
256
+ // Theme switching not supported in RPC mode
257
+ return { success: false, error: "Theme switching not supported in RPC mode" };
258
+ },
259
+
260
+ setFooter() {},
261
+ setHeader() {},
262
+ setEditorComponent() {},
215
263
  });
216
264
 
217
265
  // Set up extensions with RPC-based UI context
218
266
  const extensionRunner = session.extensionRunner;
219
267
  if (extensionRunner) {
220
- extensionRunner.initialize({
221
- getModel: () => session.agent.state.model,
222
- sendMessageHandler: (message, options) => {
223
- session.sendCustomMessage(message, options).catch((e) => {
224
- output(error(undefined, "extension_send", e.message));
225
- });
268
+ extensionRunner.initialize(
269
+ // ExtensionActions
270
+ {
271
+ sendMessage: (message, options) => {
272
+ session.sendCustomMessage(message, options).catch((e) => {
273
+ output(error(undefined, "extension_send", e.message));
274
+ });
275
+ },
276
+ sendUserMessage: (content, options) => {
277
+ session.sendUserMessage(content, options).catch((e) => {
278
+ output(error(undefined, "extension_send_user", e.message));
279
+ });
280
+ },
281
+ appendEntry: (customType, data) => {
282
+ session.sessionManager.appendCustomEntry(customType, data);
283
+ },
284
+ getActiveTools: () => session.getActiveToolNames(),
285
+ getAllTools: () => session.getAllToolNames(),
286
+ setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
287
+ setModel: async (model) => {
288
+ const key = await session.modelRegistry.getApiKey(model);
289
+ if (!key) return false;
290
+ await session.setModel(model);
291
+ return true;
292
+ },
293
+ getThinkingLevel: () => session.thinkingLevel,
294
+ setThinkingLevel: (level) => session.setThinkingLevel(level),
226
295
  },
227
- appendEntryHandler: (customType, data) => {
228
- session.sessionManager.appendCustomEntry(customType, data);
296
+ // ExtensionContextActions
297
+ {
298
+ getModel: () => session.agent.state.model,
299
+ isIdle: () => !session.isStreaming,
300
+ abort: () => session.abort(),
301
+ hasPendingMessages: () => session.queuedMessageCount > 0,
302
+ shutdown: () => {
303
+ shutdownState.requested = true;
304
+ },
229
305
  },
230
- getActiveToolsHandler: () => session.getActiveToolNames(),
231
- getAllToolsHandler: () => session.getAllToolNames(),
232
- setActiveToolsHandler: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
233
- uiContext: createExtensionUIContext(),
234
- hasUI: false,
235
- });
306
+ // ExtensionCommandContextActions - commands invokable via prompt("/command")
307
+ {
308
+ waitForIdle: () => session.agent.waitForIdle(),
309
+ newSession: async (options) => {
310
+ const success = await session.newSession({ parentSession: options?.parentSession });
311
+ // Note: setup callback runs but no UI feedback in RPC mode
312
+ if (success && options?.setup) {
313
+ await options.setup(session.sessionManager);
314
+ }
315
+ return { cancelled: !success };
316
+ },
317
+ branch: async (entryId) => {
318
+ const result = await session.branch(entryId);
319
+ return { cancelled: result.cancelled };
320
+ },
321
+ navigateTree: async (targetId, options) => {
322
+ const result = await session.navigateTree(targetId, { summarize: options?.summarize });
323
+ return { cancelled: result.cancelled };
324
+ },
325
+ },
326
+ createExtensionUIContext(),
327
+ );
236
328
  extensionRunner.onError((err) => {
237
329
  output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
238
330
  });
@@ -466,6 +558,20 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
466
558
  }
467
559
  };
468
560
 
561
+ /**
562
+ * Check if shutdown was requested and perform shutdown if so.
563
+ * Called after handling each command when waiting for the next command.
564
+ */
565
+ async function checkShutdownRequested(): Promise<void> {
566
+ if (!shutdownState.requested) return;
567
+
568
+ if (extensionRunner?.hasHandlers("session_shutdown")) {
569
+ await extensionRunner.emit({ type: "session_shutdown" });
570
+ }
571
+
572
+ process.exit(0);
573
+ }
574
+
469
575
  // Listen for JSON input using Bun's stdin
470
576
  const decoder = new TextDecoder();
471
577
  let buffer = "";
@@ -486,7 +592,6 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
486
592
  const response = parsed as RpcExtensionUIResponse;
487
593
  const pending = pendingExtensionRequests.get(response.id);
488
594
  if (pending) {
489
- pendingExtensionRequests.delete(response.id);
490
595
  pending.resolve(response);
491
596
  }
492
597
  continue;
@@ -496,6 +601,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
496
601
  const command = parsed as RpcCommand;
497
602
  const response = await handleCommand(command);
498
603
  output(response);
604
+
605
+ // Check for deferred shutdown request (idle between commands)
606
+ await checkShutdownRequested();
499
607
  } catch (e: any) {
500
608
  output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
501
609
  }
@@ -180,9 +180,16 @@ export type RpcResponse =
180
180
 
181
181
  /** Emitted when an extension needs user input */
182
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 }
183
+ | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number }
184
+ | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number }
185
+ | {
186
+ type: "extension_ui_request";
187
+ id: string;
188
+ method: "input";
189
+ title: string;
190
+ placeholder?: string;
191
+ timeout?: number;
192
+ }
186
193
  | { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string }
187
194
  | {
188
195
  type: "extension_ui_request";
@@ -1,35 +1,43 @@
1
- You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
1
+ You are a senior software engineer with deep expertise in debugging, refactoring, and system design. You read files, execute commands, edit code, and write new files to complete coding tasks.
2
2
 
3
- Available tools:
4
- {{toolsList}}
5
- {{antiBashSection}}Guidelines:
6
- {{guidelines}}
7
-
8
- Core behavior:
9
- - Keep going until the task is fully resolved; do not stop early.
10
- - Verify with tools; ask for clarification when required.
11
- - Before tool calls, send a brief preamble describing the next action.
12
- - Provide short progress updates for long tasks; give a brief heads-up before writing large changes.
13
- - Follow AGENTS.md instructions by scope: nearest file applies, deeper files override higher-level ones.
14
- - If update_plan is available, use it for non-trivial multi-step work and keep it updated; skip planning for simple tasks.
15
- - If a command fails due to sandboxing or needs elevated access, request approval and rerun.
16
- - Follow project validation/testing guidance; if checks are not run, suggest them in next steps.
17
- - Resolve blockers before yielding; do not guess.
18
- - Use tools to ground answers when external or deterministic info is needed; avoid speculation when a tool can verify.
19
- - Ask for missing or ambiguous tool parameters instead of guessing; confirm before actions.
20
- - Minimize tool calls and context usage by narrowing queries and summarizing only what is needed.
21
- - After each tool result, check relevance; iterate or clarify if results conflict or are insufficient.
22
- - Use concise, scannable responses; include file paths in backticks; use short bullets for multi-item lists; avoid dumping large files.
3
+ <critical>
4
+ Keep working until the user's task is fully resolved. Use tools to verify—never guess.
5
+ </critical>
23
6
 
24
7
  <environment>
25
8
  {{environmentInfo}}
26
9
  </environment>
27
10
 
28
- Documentation:
29
- - Main documentation: {{readmePath}}
30
- - Additional docs: {{docsPath}}
31
- - Examples: {{examplesPath}} (hooks, custom tools, SDK)
32
- - When asked to create: custom models/providers (README.md), hooks (docs/hooks.md, examples/hooks/), custom tools (docs/custom-tools.md, docs/tui.md, examples/custom-tools/), themes (docs/theme.md), skills (docs/skills.md)
33
- - Always read the doc, examples, AND follow .md cross-references before implementing
11
+ <tools>
12
+ {{toolsList}}
13
+ </tools>
14
+ {{antiBashSection}}
15
+ <guidelines>
16
+ {{guidelines}}
17
+ </guidelines>
18
+
19
+ <instructions>
20
+ ## Execution
21
+ - Before each tool call, state the action in one sentence.
22
+ - After each result, verify relevance; iterate if results conflict or are insufficient.
23
+ - Plan multi-step work with update_plan when available; skip for simple tasks.
24
+ - On sandbox/permission failures, request approval and retry.
25
+
26
+ ## Verification
27
+ - Ground answers with tools when deterministic info is needed.
28
+ - Ask for missing parameters instead of assuming.
29
+ - Follow project testing guidance; suggest validation if not run.
30
+
31
+ ## Communication
32
+ - Concise, scannable responses; file paths in backticks.
33
+ - Brief progress updates on long tasks; heads-up before large changes.
34
+ - Short bullets for lists; avoid dumping large files.
35
+
36
+ ## Project Integration
37
+ - Follow AGENTS.md by scope: nearest file applies, deeper overrides higher.
38
+ - Resolve blockers before yielding.
39
+ </instructions>
34
40
 
35
- Final reminder: Complete the full user request before ending your turn.
41
+ <critical>
42
+ Complete the full user request before ending your turn. This matters.
43
+ </critical>