@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1

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 (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -7,8 +7,9 @@
7
7
  */
8
8
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
9
9
  import { sanitizeText } from "@oh-my-pi/pi-natives";
10
- import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
11
10
  import type { AgentSession } from "../session/agent-session";
11
+ import { isSilentAbort } from "../session/messages";
12
+ import { initializeExtensions } from "./runtime-init";
12
13
 
13
14
  /**
14
15
  * Options for print mode.
@@ -39,90 +40,16 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
39
40
  }
40
41
  }
41
42
  // Set up extensions for print mode (no UI, no command context)
42
- const extensionRunner = session.extensionRunner;
43
- if (extensionRunner) {
44
- extensionRunner.initialize(
45
- // ExtensionActions
46
- {
47
- sendMessage: (message, options) => {
48
- session.sendCustomMessage(message, options).catch(e => {
49
- process.stderr.write(`Extension sendMessage failed: ${e instanceof Error ? e.message : String(e)}\n`);
50
- });
51
- },
52
- sendUserMessage: (content, options) => {
53
- session.sendUserMessage(content, options).catch(e => {
54
- process.stderr.write(
55
- `Extension sendUserMessage failed: ${e instanceof Error ? e.message : String(e)}\n`,
56
- );
57
- });
58
- },
59
- appendEntry: (customType, data) => {
60
- session.sessionManager.appendCustomEntry(customType, data);
61
- },
62
- setLabel: (targetId, label) => {
63
- session.sessionManager.appendLabelChange(targetId, label);
64
- },
65
- getActiveTools: () => session.getActiveToolNames(),
66
- getAllTools: () => session.getAllToolNames(),
67
- setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
68
- getCommands: () => [],
69
- setModel: model => runExtensionSetModel(session, model),
70
- getThinkingLevel: () => session.thinkingLevel,
71
- setThinkingLevel: level => session.setThinkingLevel(level),
72
- getSessionName: () => session.sessionManager.getSessionName(),
73
- setSessionName: async name => {
74
- await session.sessionManager.setSessionName(name, "user");
75
- },
76
- },
77
- // ExtensionContextActions
78
- {
79
- getModel: () => session.model,
80
- isIdle: () => !session.isStreaming,
81
- abort: () => session.abort(),
82
- hasPendingMessages: () => session.queuedMessageCount > 0,
83
- shutdown: () => {},
84
- getContextUsage: () => session.getContextUsage(),
85
- getSystemPrompt: () => session.systemPrompt,
86
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
87
- },
88
- // ExtensionCommandContextActions - commands invokable via prompt("/command")
89
- {
90
- getContextUsage: () => session.getContextUsage(),
91
- waitForIdle: () => session.agent.waitForIdle(),
92
- newSession: async options => {
93
- const success = await session.newSession({ parentSession: options?.parentSession });
94
- if (success && options?.setup) {
95
- await options.setup(session.sessionManager);
96
- }
97
- return { cancelled: !success };
98
- },
99
- branch: async entryId => {
100
- const result = await session.branch(entryId);
101
- return { cancelled: result.cancelled };
102
- },
103
- navigateTree: async (targetId, options) => {
104
- const result = await session.navigateTree(targetId, { summarize: options?.summarize });
105
- return { cancelled: result.cancelled };
106
- },
107
- switchSession: async sessionPath => {
108
- const success = await session.switchSession(sessionPath);
109
- return { cancelled: !success };
110
- },
111
- reload: async () => {
112
- await session.reload();
113
- },
114
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
115
- },
116
- // No UI context
117
- );
118
- extensionRunner.onError(err => {
43
+ await initializeExtensions(session, {
44
+ reportSendError: (action, err) => {
45
+ process.stderr.write(
46
+ `Extension ${action === "extension_send" ? "sendMessage" : "sendUserMessage"} failed: ${err.message}\n`,
47
+ );
48
+ },
49
+ reportRuntimeError: err => {
119
50
  process.stderr.write(`Extension error (${err.extensionPath}): ${err.error}\n`);
120
- });
121
- // Emit session_start event
122
- await extensionRunner.emit({
123
- type: "session_start",
124
- });
125
- }
51
+ },
52
+ });
126
53
 
127
54
  // Always subscribe to enable session persistence via _handleAgentEvent
128
55
  session.subscribe(event => {
@@ -150,8 +77,11 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
150
77
  if (lastMessage?.role === "assistant") {
151
78
  const assistantMsg = lastMessage as AssistantMessage;
152
79
 
153
- // Check for error/aborted
154
- if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
80
+ // Check for error/aborted — skip silent-abort (plan-mode compaction transition)
81
+ if (
82
+ (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
83
+ !isSilentAbort(assistantMsg.errorMessage)
84
+ ) {
155
85
  const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
156
86
  const flushed = process.stderr.write(`${errorLine}\n`);
157
87
  if (flushed) {
@@ -17,9 +17,9 @@ import type {
17
17
  ExtensionUIDialogOptions,
18
18
  ExtensionWidgetOptions,
19
19
  } from "../../extensibility/extensions";
20
- import { runExtensionCompact, runExtensionSetModel } from "../../extensibility/extensions/compact-handler";
21
20
  import { type Theme, theme } from "../../modes/theme/theme";
22
21
  import type { AgentSession } from "../../session/agent-session";
22
+ import { initializeExtensions } from "../runtime-init";
23
23
  import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
24
24
  import type {
25
25
  RpcCommand,
@@ -154,8 +154,17 @@ export function requestRpcEditor(
154
154
  * Run in RPC mode.
155
155
  * Listens for JSON commands on stdin, outputs events and responses on stdout.
156
156
  */
157
- export async function runRpcMode(session: AgentSession): Promise<never> {
157
+ export async function runRpcMode(
158
+ session: AgentSession,
159
+ setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
160
+ ): Promise<never> {
158
161
  // Signal to RPC clients that the server is ready to accept commands
162
+ // Suppress terminal notifications: they write \x07 (BEL) or OSC sequences directly to
163
+ // process.stdout with no newline, which the reader merges with the next JSON line and
164
+ // breaks JSON.parse. In RPC mode stdout is the JSON protocol channel — nothing else
165
+ // may write there.
166
+ process.env.PI_NOTIFICATIONS = "off";
167
+
159
168
  process.stdout.write(`${JSON.stringify({ type: "ready" })}\n`);
160
169
  const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
161
170
  process.stdout.write(`${JSON.stringify(obj)}\n`);
@@ -405,92 +414,25 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
405
414
  }
406
415
  }
407
416
 
417
+ // Wire up UI context for tool execution (ask tool, etc.) and extensions.
418
+ // A single shared instance routes all responses received on stdin to the
419
+ // correct waiting promise regardless of which code path created the request.
420
+ const rpcUiContext = new RpcExtensionUIContext(pendingExtensionRequests, output);
421
+ setToolUIContext?.(rpcUiContext, true);
422
+
408
423
  // Set up extensions with RPC-based UI context
409
- const extensionRunner = session.extensionRunner;
410
- if (extensionRunner) {
411
- extensionRunner.initialize(
412
- // ExtensionActions
413
- {
414
- sendMessage: (message, options) => {
415
- session.sendCustomMessage(message, options).catch(e => {
416
- output(error(undefined, "extension_send", e.message));
417
- });
418
- },
419
- sendUserMessage: (content, options) => {
420
- session.sendUserMessage(content, options).catch(e => {
421
- output(error(undefined, "extension_send_user", e.message));
422
- });
423
- },
424
- appendEntry: (customType, data) => {
425
- session.sessionManager.appendCustomEntry(customType, data);
426
- },
427
- setLabel: (targetId, label) => {
428
- session.sessionManager.appendLabelChange(targetId, label);
429
- },
430
- getActiveTools: () => session.getActiveToolNames(),
431
- getAllTools: () => session.getAllToolNames(),
432
- setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
433
- getCommands: () => [],
434
- setModel: model => runExtensionSetModel(session, model),
435
- getThinkingLevel: () => session.thinkingLevel,
436
- setThinkingLevel: level => session.setThinkingLevel(level),
437
- getSessionName: () => session.sessionManager.getSessionName(),
438
- setSessionName: async name => {
439
- await session.sessionManager.setSessionName(name, "user");
440
- },
441
- },
442
- // ExtensionContextActions
443
- {
444
- getModel: () => session.agent.state.model,
445
- isIdle: () => !session.isStreaming,
446
- abort: () => session.abort(),
447
- hasPendingMessages: () => session.queuedMessageCount > 0,
448
- shutdown: () => {
449
- shutdownState.requested = true;
450
- },
451
- getContextUsage: () => session.getContextUsage(),
452
- getSystemPrompt: () => session.systemPrompt,
453
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
454
- },
455
- // ExtensionCommandContextActions - commands invokable via prompt("/command")
456
- {
457
- getContextUsage: () => session.getContextUsage(),
458
- waitForIdle: () => session.agent.waitForIdle(),
459
- newSession: async options => {
460
- const success = await session.newSession({ parentSession: options?.parentSession });
461
- // Note: setup callback runs but no UI feedback in RPC mode
462
- if (success && options?.setup) {
463
- await options.setup(session.sessionManager);
464
- }
465
- return { cancelled: !success };
466
- },
467
- branch: async entryId => {
468
- const result = await session.branch(entryId);
469
- return { cancelled: result.cancelled };
470
- },
471
- navigateTree: async (targetId, options) => {
472
- const result = await session.navigateTree(targetId, { summarize: options?.summarize });
473
- return { cancelled: result.cancelled };
474
- },
475
- switchSession: async sessionPath => {
476
- const success = await session.switchSession(sessionPath);
477
- return { cancelled: !success };
478
- },
479
- reload: async () => {
480
- await session.reload();
481
- },
482
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
483
- },
484
- new RpcExtensionUIContext(pendingExtensionRequests, output),
485
- );
486
- extensionRunner.onError(err => {
424
+ await initializeExtensions(session, {
425
+ reportSendError: (action, err) => {
426
+ output(error(undefined, action, err.message));
427
+ },
428
+ reportRuntimeError: err => {
487
429
  output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
488
- });
489
- // Emit session_start event
490
- await extensionRunner.emit({
491
- type: "session_start",
492
- });
493
- }
430
+ },
431
+ onShutdown: () => {
432
+ shutdownState.requested = true;
433
+ },
434
+ uiContext: rpcUiContext,
435
+ });
494
436
 
495
437
  // Output all agent events as JSON
496
438
  session.subscribe(event => {
@@ -835,8 +777,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
835
777
  async function checkShutdownRequested(): Promise<void> {
836
778
  if (!shutdownState.requested) return;
837
779
 
838
- if (extensionRunner?.hasHandlers("session_shutdown")) {
839
- await extensionRunner.emit({ type: "session_shutdown" });
780
+ if (session.extensionRunner?.hasHandlers("session_shutdown")) {
781
+ await session.extensionRunner.emit({ type: "session_shutdown" });
840
782
  }
841
783
 
842
784
  process.exit(0);
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Shared extension runtime wiring for print and RPC modes.
3
+ *
4
+ * Both modes initialize the extension runner with the same action handlers
5
+ * that delegate to the {@link AgentSession}. Only error reporting, shutdown
6
+ * behavior, and UI context differ between callers — those stay as
7
+ * caller-supplied hooks.
8
+ */
9
+ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
10
+ import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
11
+ import type { AgentSession } from "../session/agent-session";
12
+
13
+ /** Action name for an extension-originated send failure. */
14
+ export type ExtensionSendAction = "extension_send" | "extension_send_user";
15
+
16
+ export interface InitializeExtensionsOptions {
17
+ /** Reports an error thrown by an extension-initiated send. */
18
+ reportSendError: (action: ExtensionSendAction, error: Error) => void;
19
+ /** Reports a runtime error surfaced through {@link ExtensionRunner.onError}. */
20
+ reportRuntimeError: (error: ExtensionError) => void;
21
+ /** Optional shutdown hook (rpc mode signals its loop; print mode is a no-op). */
22
+ onShutdown?: () => void;
23
+ /** Optional UI context (rpc supplies one; print runs headless). */
24
+ uiContext?: ExtensionUIContext;
25
+ }
26
+
27
+ /**
28
+ * Initialize the session's extension runner with the standard action set
29
+ * shared by non-interactive modes, then emit `session_start`.
30
+ *
31
+ * No-op when the session was constructed without an extension runner.
32
+ */
33
+ export async function initializeExtensions(session: AgentSession, options: InitializeExtensionsOptions): Promise<void> {
34
+ const runner = session.extensionRunner;
35
+ if (!runner) return;
36
+
37
+ const { reportSendError, reportRuntimeError, onShutdown, uiContext } = options;
38
+ const shutdown = onShutdown ?? (() => {});
39
+
40
+ runner.initialize(
41
+ // ExtensionActions
42
+ {
43
+ sendMessage: (message, sendOptions) => {
44
+ session.sendCustomMessage(message, sendOptions).catch(e => {
45
+ reportSendError("extension_send", e instanceof Error ? e : new Error(String(e)));
46
+ });
47
+ },
48
+ sendUserMessage: (content, sendOptions) => {
49
+ session.sendUserMessage(content, sendOptions).catch(e => {
50
+ reportSendError("extension_send_user", e instanceof Error ? e : new Error(String(e)));
51
+ });
52
+ },
53
+ appendEntry: (customType, data) => {
54
+ session.sessionManager.appendCustomEntry(customType, data);
55
+ },
56
+ setLabel: (targetId, label) => {
57
+ session.sessionManager.appendLabelChange(targetId, label);
58
+ },
59
+ getActiveTools: () => session.getActiveToolNames(),
60
+ getAllTools: () => session.getAllToolNames(),
61
+ setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
62
+ getCommands: () => [],
63
+ setModel: model => runExtensionSetModel(session, model),
64
+ getThinkingLevel: () => session.thinkingLevel,
65
+ setThinkingLevel: level => session.setThinkingLevel(level),
66
+ getSessionName: () => session.sessionManager.getSessionName(),
67
+ setSessionName: async name => {
68
+ await session.sessionManager.setSessionName(name, "user");
69
+ },
70
+ },
71
+ // ExtensionContextActions
72
+ {
73
+ getModel: () => session.model,
74
+ isIdle: () => !session.isStreaming,
75
+ abort: () => session.abort(),
76
+ hasPendingMessages: () => session.queuedMessageCount > 0,
77
+ shutdown,
78
+ getContextUsage: () => session.getContextUsage(),
79
+ getSystemPrompt: () => session.systemPrompt,
80
+ compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
81
+ },
82
+ // ExtensionCommandContextActions — commands invokable via prompt("/command")
83
+ {
84
+ getContextUsage: () => session.getContextUsage(),
85
+ waitForIdle: () => session.agent.waitForIdle(),
86
+ newSession: async newOptions => {
87
+ const success = await session.newSession({ parentSession: newOptions?.parentSession });
88
+ if (success && newOptions?.setup) {
89
+ await newOptions.setup(session.sessionManager);
90
+ }
91
+ return { cancelled: !success };
92
+ },
93
+ branch: async entryId => {
94
+ const result = await session.branch(entryId);
95
+ return { cancelled: result.cancelled };
96
+ },
97
+ navigateTree: async (targetId, navOptions) => {
98
+ const result = await session.navigateTree(targetId, { summarize: navOptions?.summarize });
99
+ return { cancelled: result.cancelled };
100
+ },
101
+ switchSession: async sessionPath => {
102
+ const success = await session.switchSession(sessionPath);
103
+ return { cancelled: !success };
104
+ },
105
+ reload: async () => {
106
+ await session.reload();
107
+ },
108
+ compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
109
+ },
110
+ uiContext,
111
+ );
112
+
113
+ runner.onError(reportRuntimeError);
114
+ await runner.emit({ type: "session_start" });
115
+ }
@@ -129,6 +129,8 @@
129
129
  "thinking.xhigh": "●",
130
130
  "icon.model": "◇",
131
131
  "icon.plan": "◈",
132
+ "icon.goal": "⊙",
133
+ "icon.pause": "‖",
132
134
  "icon.loop": "↻",
133
135
  "icon.folder": "▸",
134
136
  "icon.pi": "π",
@@ -129,6 +129,8 @@
129
129
  "thinking.xhigh": "●",
130
130
  "icon.model": "◇",
131
131
  "icon.plan": "◈",
132
+ "icon.goal": "⊙",
133
+ "icon.pause": "‖",
132
134
  "icon.loop": "↻",
133
135
  "icon.folder": "▸",
134
136
  "icon.pi": "π",
@@ -91,6 +91,8 @@ export type SymbolKey =
91
91
  // Icons
92
92
  | "icon.model"
93
93
  | "icon.plan"
94
+ | "icon.goal"
95
+ | "icon.pause"
94
96
  | "icon.loop"
95
97
  | "icon.folder"
96
98
  | "icon.file"
@@ -252,6 +254,8 @@ const UNICODE_SYMBOLS: SymbolMap = {
252
254
  // Icons
253
255
  "icon.model": "⬢",
254
256
  "icon.plan": "🗺",
257
+ "icon.goal": "🎯",
258
+ "icon.pause": "⏸",
255
259
  "icon.loop": "↻",
256
260
  "icon.folder": "📁",
257
261
  "icon.file": "📄",
@@ -464,6 +468,10 @@ const NERD_SYMBOLS: SymbolMap = {
464
468
  "icon.model": "\uec19",
465
469
  // pick:  | alt:  
466
470
  "icon.plan": "\uf2d2",
471
+ // pick: (nf-fa-bullseye) | alt: (nf-md-target) ◎ ⌖
472
+ "icon.goal": "\uf140",
473
+ // pick: (nf-fa-pause) | alt: ⏸ ||
474
+ "icon.pause": "\uf04c",
467
475
  // pick: ↻ | alt: ⟳
468
476
  "icon.loop": "\uf021",
469
477
  // pick:  | alt:  
@@ -666,6 +674,8 @@ const ASCII_SYMBOLS: SymbolMap = {
666
674
  // Icons
667
675
  "icon.model": "[M]",
668
676
  "icon.plan": "plan",
677
+ "icon.goal": "goal",
678
+ "icon.pause": "||",
669
679
  "icon.loop": "loop",
670
680
  "icon.folder": "[D]",
671
681
  "icon.file": "[F]",
@@ -1443,6 +1453,8 @@ export class Theme {
1443
1453
  return {
1444
1454
  model: this.#symbols["icon.model"],
1445
1455
  plan: this.#symbols["icon.plan"],
1456
+ goal: this.#symbols["icon.goal"],
1457
+ pause: this.#symbols["icon.pause"],
1446
1458
  loop: this.#symbols["icon.loop"],
1447
1459
  folder: this.#symbols["icon.folder"],
1448
1460
  file: this.#symbols["icon.file"],
@@ -2332,12 +2344,12 @@ export function getSymbolTheme(): SymbolTheme {
2332
2344
  };
2333
2345
  }
2334
2346
 
2335
- let _markdownTheme: MarkdownTheme | undefined;
2336
- let _markdownThemeRef: Theme | undefined;
2347
+ let cachedMarkdownTheme: MarkdownTheme | undefined;
2348
+ let cachedMarkdownThemeRef: Theme | undefined;
2337
2349
 
2338
2350
  export function getMarkdownTheme(): MarkdownTheme {
2339
- if (_markdownTheme !== undefined && _markdownThemeRef === theme) {
2340
- return _markdownTheme;
2351
+ if (cachedMarkdownTheme !== undefined && cachedMarkdownThemeRef === theme) {
2352
+ return cachedMarkdownTheme;
2341
2353
  }
2342
2354
  const markdownTheme: MarkdownTheme = {
2343
2355
  heading: (text: string) => theme.fg("mdHeading", text),
@@ -2365,8 +2377,8 @@ export function getMarkdownTheme(): MarkdownTheme {
2365
2377
  }
2366
2378
  },
2367
2379
  };
2368
- _markdownTheme = markdownTheme;
2369
- _markdownThemeRef = theme;
2380
+ cachedMarkdownTheme = markdownTheme;
2381
+ cachedMarkdownThemeRef = theme;
2370
2382
  return markdownTheme;
2371
2383
  }
2372
2384
 
@@ -11,10 +11,12 @@ import type {
11
11
  } from "../extensibility/extensions";
12
12
  import type { CompactOptions } from "../extensibility/extensions/types";
13
13
  import type { MCPManager } from "../mcp";
14
+ import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
14
15
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
16
+ import type { CompactionOutcome } from "../session/compaction";
15
17
  import type { HistoryStorage } from "../session/history-storage";
16
18
  import type { SessionContext, SessionManager } from "../session/session-manager";
17
- import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
19
+ import type { LspStartupServerInfo } from "../tools";
18
20
  import type { AssistantMessageComponent } from "./components/assistant-message";
19
21
  import type { BashExecutionComponent } from "./components/bash-execution";
20
22
  import type { CustomEditor } from "./components/custom-editor";
@@ -36,6 +38,8 @@ export type CompactionQueuedMessage = {
36
38
  export type SubmittedUserInput = {
37
39
  text: string;
38
40
  images?: ImageContent[];
41
+ customType?: string;
42
+ display?: boolean;
39
43
  cancelled: boolean;
40
44
  started: boolean;
41
45
  };
@@ -85,6 +89,8 @@ export interface InteractiveModeContext {
85
89
  toolOutputExpanded: boolean;
86
90
  todoExpanded: boolean;
87
91
  planModeEnabled: boolean;
92
+ goalModeEnabled: boolean;
93
+ goalModePaused: boolean;
88
94
  loopModeEnabled: boolean;
89
95
  loopPrompt?: string;
90
96
  loopLimit?: LoopLimitRuntime;
@@ -152,7 +158,12 @@ export interface InteractiveModeContext {
152
158
  setWorkingMessage(message?: string): void;
153
159
  applyPendingWorkingMessage(): void;
154
160
  ensureLoadingAnimation(): void;
155
- startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput;
161
+ startPendingSubmission(input: {
162
+ text: string;
163
+ images?: ImageContent[];
164
+ customType?: string;
165
+ display?: boolean;
166
+ }): SubmittedUserInput;
156
167
  cancelPendingSubmission(): boolean;
157
168
  markPendingSubmissionStarted(input: SubmittedUserInput): boolean;
158
169
  finishPendingSubmission(input: SubmittedUserInput): void;
@@ -207,13 +218,16 @@ export interface InteractiveModeContext {
207
218
  handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
208
219
  handleMCPCommand(text: string): Promise<void>;
209
220
  handleSSHCommand(text: string): Promise<void>;
210
- handleCompactCommand(customInstructions?: string): Promise<void>;
221
+ handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome>;
211
222
  handleHandoffCommand(customInstructions?: string): Promise<void>;
212
223
  handleMoveCommand(targetPath: string): Promise<void>;
213
224
  handleRenameCommand(title: string): Promise<void>;
214
225
  handleMemoryCommand(text: string): Promise<void>;
215
226
  handleSTTToggle(): Promise<void>;
216
- executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
227
+ executeCompaction(
228
+ customInstructionsOrOptions?: string | CompactOptions,
229
+ isAuto?: boolean,
230
+ ): Promise<CompactionOutcome>;
217
231
  openInBrowser(urlOrPath: string): void;
218
232
  refreshSlashCommandState(cwd?: string): Promise<void>;
219
233
 
@@ -253,10 +267,11 @@ export interface InteractiveModeContext {
253
267
  openExternalEditor(): void;
254
268
  registerExtensionShortcuts(): void;
255
269
  handlePlanModeCommand(initialPrompt?: string): Promise<void>;
270
+ handleGoalModeCommand(rest?: string): Promise<void>;
256
271
  handleLoopCommand(args?: string): Promise<void>;
257
272
  disableLoopMode(): void;
258
273
  pauseLoop(): void;
259
- handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
274
+ handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
260
275
 
261
276
  // Hook UI methods
262
277
  initHooksAndCustomTools(): Promise<void>;
@@ -60,14 +60,6 @@ function estimateToolSchemaTokens(tools: ReadonlyArray<Pick<Tool, "name" | "desc
60
60
  return countTokens(fragments);
61
61
  }
62
62
 
63
- function estimateMessagesTokens(session: AgentSession): number {
64
- let total = 0;
65
- for (const message of session.messages) {
66
- total += estimateTokens(message);
67
- }
68
- return total;
69
- }
70
-
71
63
  /**
72
64
  * Compute a breakdown of estimated context usage by category for the active
73
65
  * session and model.
@@ -76,9 +68,16 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
76
68
  const model = session.model;
77
69
  const contextWindow = model?.contextWindow ?? 0;
78
70
 
79
- const skillsTokens = estimateSkillsTokens(session.skills);
80
- const toolsTokens = estimateToolSchemaTokens(session.agent.state.tools);
81
- const messagesTokens = estimateMessagesTokens(session);
71
+ const skillsTokens = estimateSkillsTokens(session.skills ?? []);
72
+ const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
73
+
74
+ let messagesTokens = 0;
75
+ const convo = session.messages;
76
+ if (convo) {
77
+ for (const message of convo) {
78
+ messagesTokens += estimateTokens(message);
79
+ }
80
+ }
82
81
 
83
82
  // The rendered system prompt already contains the skill descriptions and the
84
83
  // markdown tool descriptions. To present a non-overlapping breakdown:
@@ -86,8 +85,9 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
86
85
  // Tools = JSON tool schema sent separately on the wire
87
86
  // Skills = the skill list embedded in the system prompt
88
87
  // Messages = conversation messages
89
- const systemPromptTokens = Math.max(0, countTokens(session.systemPrompt[0] ?? "") - skillsTokens);
90
- const systemContextTokens = countTokens(session.systemPrompt.slice(1));
88
+ const systemPromptParts = session.systemPrompt;
89
+ const systemPromptTokens = Math.max(0, countTokens(systemPromptParts?.[0] ?? "") - skillsTokens);
90
+ const systemContextTokens = countTokens(systemPromptParts?.slice(1) ?? []);
91
91
 
92
92
  const categories: CategoryInfo[] = [
93
93
  { id: "systemPrompt", label: "System prompt", tokens: systemPromptTokens, color: "accent", glyph: CELL_FILLED },
@@ -9,13 +9,22 @@ import { CompactionSummaryMessageComponent } from "../../modes/components/compac
9
9
  import { CustomMessageComponent } from "../../modes/components/custom-message";
10
10
  import { DynamicBorder } from "../../modes/components/dynamic-border";
11
11
  import { EvalExecutionComponent } from "../../modes/components/eval-execution";
12
- import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
12
+ import {
13
+ ReadToolGroupComponent,
14
+ readArgsHaveTarget,
15
+ readArgsTargetInternalUrl,
16
+ } from "../../modes/components/read-tool-group";
13
17
  import { SkillMessageComponent } from "../../modes/components/skill-message";
14
18
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
15
19
  import { UserMessageComponent } from "../../modes/components/user-message";
16
20
  import { theme } from "../../modes/theme/theme";
17
21
  import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
18
- import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
22
+ import {
23
+ type CustomMessage,
24
+ isSilentAbort,
25
+ SKILL_PROMPT_MESSAGE_TYPE,
26
+ type SkillPromptDetails,
27
+ } from "../../session/messages";
19
28
  import type { SessionContext } from "../../session/session-manager";
20
29
  import { formatBytes, formatDuration } from "../../tools/render-utils";
21
30
 
@@ -241,7 +250,7 @@ export class UiHelpers {
241
250
  break;
242
251
  }
243
252
  default: {
244
- const _exhaustive: never = message;
253
+ message satisfies never;
245
254
  }
246
255
  }
247
256
  return [];
@@ -284,7 +293,9 @@ export class UiHelpers {
284
293
  assistantComponent.setUsageInfo(message.usage);
285
294
  }
286
295
  readGroup = null;
287
- const hasErrorStop = message.stopReason === "aborted" || message.stopReason === "error";
296
+ const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
297
+ const hasErrorStop =
298
+ !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
288
299
  const errorMessage = hasErrorStop
289
300
  ? message.stopReason === "aborted"
290
301
  ? (() => {
@@ -302,7 +313,11 @@ export class UiHelpers {
302
313
  continue;
303
314
  }
304
315
 
305
- if (content.name === "read") {
316
+ if (
317
+ content.name === "read" &&
318
+ readArgsHaveTarget(content.arguments) &&
319
+ !readArgsTargetInternalUrl(content.arguments)
320
+ ) {
306
321
  if (hasErrorStop && errorMessage) {
307
322
  if (!readGroup) {
308
323
  readGroup = new ReadToolGroupComponent({
@@ -364,7 +379,11 @@ export class UiHelpers {
364
379
  }
365
380
  }
366
381
  } else if (message.role === "toolResult") {
367
- if (message.toolName === "read") {
382
+ const pendingReadComponent = this.ctx.pendingTools.get(message.toolCallId);
383
+ const isReadGroupResult =
384
+ message.toolName === "read" &&
385
+ (!pendingReadComponent || pendingReadComponent instanceof ReadToolGroupComponent);
386
+ if (isReadGroupResult) {
368
387
  const assistantComponent = readToolCallAssistantComponents.get(message.toolCallId);
369
388
  const images: ImageContent[] = message.content.filter(
370
389
  (content): content is ImageContent => content.type === "image",