@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
@@ -4,7 +4,9 @@ import {
4
4
  type AgentSideConnection,
5
5
  type AuthenticateRequest,
6
6
  type AuthenticateResponse,
7
+ type AuthMethod,
7
8
  type AvailableCommand,
9
+ type ClientCapabilities,
8
10
  type CloseSessionRequest,
9
11
  type CloseSessionResponse,
10
12
  type ForkSessionRequest,
@@ -37,32 +39,49 @@ import {
37
39
  type SetSessionModeResponse,
38
40
  type Usage,
39
41
  } from "@agentclientprotocol/sdk";
40
- import type { Model } from "@oh-my-pi/pi-ai";
42
+ import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
41
43
  import { logger, VERSION } from "@oh-my-pi/pi-utils";
42
- import { disableProvider, enableProvider } from "../../capability";
44
+ import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
43
45
  import { Settings } from "../../config/settings";
46
+ import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
44
47
  import type { ExtensionUIContext } from "../../extensibility/extensions";
45
48
  import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
49
+ import { buildSkillPromptMessage, getSkillSlashCommandName } from "../../extensibility/skills";
46
50
  import { loadSlashCommands } from "../../extensibility/slash-commands";
47
51
  import { MCPManager } from "../../mcp/manager";
48
52
  import type { MCPServerConfig } from "../../mcp/types";
49
53
  import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
50
54
  import { theme } from "../../modes/theme/theme";
51
55
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
56
+ import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
52
57
  import {
53
58
  SessionManager,
54
59
  type SessionInfo as StoredSessionInfo,
55
60
  type UsageStatistics,
56
61
  } from "../../session/session-manager";
62
+ import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
57
63
  import { parseThinkingLevel } from "../../thinking";
64
+ import { createAcpClientBridge } from "./acp-client-bridge";
58
65
  import { mapAgentSessionEventToAcpSessionUpdates, mapToolKind } from "./acp-event-mapper";
66
+ import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
59
67
 
60
- const ACP_MODE_ID = "default";
68
+ const ACP_DEFAULT_MODE_ID = "default";
69
+ const ACP_PLAN_MODE_ID = "plan";
70
+ const DEFAULT_PLAN_FILE_URL = "local://PLAN.md";
61
71
  const MODE_CONFIG_ID = "mode";
62
72
  const MODEL_CONFIG_ID = "model";
63
73
  const THINKING_CONFIG_ID = "thinking";
64
74
  const THINKING_OFF = "off";
65
75
  const SESSION_PAGE_SIZE = 50;
76
+ /**
77
+ * Delay between `session/new` (or `session/load` / `session/resume` /
78
+ * `unstable_session/fork`) returning and the agent firing the first
79
+ * notifications against the new session id. Mitigates Zed's
80
+ * `Received session notification for unknown session` race — see
81
+ * `#scheduleBootstrapUpdates`. Exported so the ACP test harness can
82
+ * wait past this guard without hard-coding the literal.
83
+ */
84
+ export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
66
85
 
67
86
  type AgentImageContent = {
68
87
  type: "image";
@@ -84,8 +103,12 @@ type ManagedSessionRecord = {
84
103
  session: AgentSession;
85
104
  mcpManager: MCPManager | undefined;
86
105
  promptTurn: PromptTurnState | undefined;
87
- liveMessageIds: WeakMap<object, string>;
106
+ liveMessageId: string | undefined;
107
+ liveMessageProgress: { textEmitted: boolean; thoughtEmitted: boolean } | undefined;
88
108
  extensionsConfigured: boolean;
109
+ // Installed inside `#scheduleBootstrapUpdates` (post-race-guard); released
110
+ // in `#disposeSessionRecord`. Lives independent of any prompt turn.
111
+ lifetimeUnsubscribe: (() => void) | undefined;
89
112
  };
90
113
 
91
114
  type ReplayableMessage = {
@@ -152,6 +175,7 @@ export class AcpAgent implements Agent {
152
175
  #sessions = new Map<string, ManagedSessionRecord>();
153
176
  #disposePromise: Promise<void> | undefined;
154
177
  #cleanupRegistered = false;
178
+ #clientCapabilities: ClientCapabilities | undefined;
155
179
 
156
180
  constructor(connection: AgentSideConnection, initialSession: AgentSession, createSession: CreateAcpSession) {
157
181
  this.#connection = connection;
@@ -159,8 +183,25 @@ export class AcpAgent implements Agent {
159
183
  this.#createSession = createSession;
160
184
  }
161
185
 
162
- async initialize(_params: InitializeRequest): Promise<InitializeResponse> {
186
+ async initialize(params: InitializeRequest): Promise<InitializeResponse> {
163
187
  this.#registerConnectionCleanup();
188
+ this.#clientCapabilities = params.clientCapabilities;
189
+ const authMethods: AuthMethod[] = [
190
+ {
191
+ id: "agent",
192
+ name: "Use existing local credentials",
193
+ description: "Authenticate via the provider keys/OAuth state already configured under ~/.omp.",
194
+ },
195
+ ];
196
+ if (params.clientCapabilities?.auth?.terminal === true) {
197
+ authMethods.push({
198
+ type: "terminal",
199
+ id: "terminal",
200
+ name: "Set up Oh My Pi in terminal",
201
+ description: "Launch the omp TUI to add provider keys and select models.",
202
+ args: [ACP_TERMINAL_AUTH_FLAG],
203
+ });
204
+ }
164
205
  return {
165
206
  protocolVersion: PROTOCOL_VERSION,
166
207
  agentInfo: {
@@ -168,13 +209,7 @@ export class AcpAgent implements Agent {
168
209
  title: "Oh My Pi",
169
210
  version: VERSION,
170
211
  },
171
- authMethods: [
172
- {
173
- id: "agent",
174
- name: "Agent-managed authentication",
175
- description: "Oh My Pi uses its existing local authentication and provider configuration.",
176
- },
177
- ],
212
+ authMethods,
178
213
  agentCapabilities: {
179
214
  loadSession: true,
180
215
  mcpCapabilities: {
@@ -195,7 +230,15 @@ export class AcpAgent implements Agent {
195
230
  };
196
231
  }
197
232
 
198
- async authenticate(_params: AuthenticateRequest): Promise<AuthenticateResponse> {
233
+ async authenticate(params: AuthenticateRequest): Promise<AuthenticateResponse> {
234
+ // ACP spec: `methodId` must be one of the methods advertised by `initialize`.
235
+ // Reject anything else so malformed clients fail fast rather than appearing
236
+ // authenticated and surfacing a downstream model failure later.
237
+ const supportsTerminalAuth = this.#clientCapabilities?.auth?.terminal === true;
238
+ const validMethods = supportsTerminalAuth ? ["agent", "terminal"] : ["agent"];
239
+ if (!validMethods.includes(params.methodId)) {
240
+ throw new Error(`Unknown ACP auth method: ${params.methodId}`);
241
+ }
199
242
  return {};
200
243
  }
201
244
 
@@ -206,7 +249,7 @@ export class AcpAgent implements Agent {
206
249
  sessionId: record.session.sessionId,
207
250
  configOptions: this.#buildConfigOptions(record.session),
208
251
  models: this.#buildModelState(record.session),
209
- modes: this.#buildModeState(),
252
+ modes: this.#buildModeState(record.session),
210
253
  };
211
254
  this.#scheduleBootstrapUpdates(record.session.sessionId);
212
255
  return response;
@@ -219,7 +262,7 @@ export class AcpAgent implements Agent {
219
262
  const response: LoadSessionResponse = {
220
263
  configOptions: this.#buildConfigOptions(record.session),
221
264
  models: this.#buildModelState(record.session),
222
- modes: this.#buildModeState(),
265
+ modes: this.#buildModeState(record.session),
223
266
  };
224
267
  this.#scheduleBootstrapUpdates(record.session.sessionId);
225
268
  return response;
@@ -242,13 +285,13 @@ export class AcpAgent implements Agent {
242
285
  };
243
286
  }
244
287
 
245
- async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
288
+ async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
246
289
  this.#assertAbsoluteCwd(params.cwd);
247
290
  const record = await this.#resumeManagedSession(params.sessionId, params.cwd, params.mcpServers ?? []);
248
291
  const response: ResumeSessionResponse = {
249
292
  configOptions: this.#buildConfigOptions(record.session),
250
293
  models: this.#buildModelState(record.session),
251
- modes: this.#buildModeState(),
294
+ modes: this.#buildModeState(record.session),
252
295
  };
253
296
  this.#scheduleBootstrapUpdates(record.session.sessionId);
254
297
  return response;
@@ -261,13 +304,13 @@ export class AcpAgent implements Agent {
261
304
  sessionId: record.session.sessionId,
262
305
  configOptions: this.#buildConfigOptions(record.session),
263
306
  models: this.#buildModelState(record.session),
264
- modes: this.#buildModeState(),
307
+ modes: this.#buildModeState(record.session),
265
308
  };
266
309
  this.#scheduleBootstrapUpdates(record.session.sessionId);
267
310
  return response;
268
311
  }
269
312
 
270
- async unstable_closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
313
+ async closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
271
314
  const record = this.#sessions.get(params.sessionId);
272
315
  if (!record) {
273
316
  return {};
@@ -278,13 +321,12 @@ export class AcpAgent implements Agent {
278
321
 
279
322
  async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse> {
280
323
  const record = this.#getSessionRecord(params.sessionId);
281
- if (params.modeId !== ACP_MODE_ID) {
282
- throw new Error(`Unsupported ACP mode: ${params.modeId}`);
283
- }
324
+ this.#applyModeChange(record.session, params.modeId);
284
325
  await this.#connection.sessionUpdate({
285
326
  sessionId: record.session.sessionId,
286
- update: this.#buildCurrentModeUpdate(),
327
+ update: this.#buildCurrentModeUpdate(record.session),
287
328
  });
329
+ await this.#pushConfigOptionUpdate(record);
288
330
  return {};
289
331
  }
290
332
 
@@ -296,9 +338,7 @@ export class AcpAgent implements Agent {
296
338
 
297
339
  switch (params.configId) {
298
340
  case MODE_CONFIG_ID:
299
- if (params.value !== ACP_MODE_ID) {
300
- throw new Error(`Unsupported ACP mode config value: ${params.value}`);
301
- }
341
+ this.#applyModeChange(record.session, params.value);
302
342
  break;
303
343
  case MODEL_CONFIG_ID:
304
344
  await this.#setModelById(record.session, params.value);
@@ -310,27 +350,31 @@ export class AcpAgent implements Agent {
310
350
  throw new Error(`Unknown ACP config option: ${params.configId}`);
311
351
  }
312
352
 
313
- const configOptions = this.#buildConfigOptions(record.session);
314
- await this.#connection.sessionUpdate({
315
- sessionId: record.session.sessionId,
316
- update: {
317
- sessionUpdate: "config_option_update",
318
- configOptions,
319
- },
320
- });
321
- return { configOptions };
353
+ // When mode is changed via the generic config-option API, mirror the
354
+ // `current_mode_update` notification that `setSessionMode` emits so
355
+ // ACP clients tracking session-mode state see a consistent transition.
356
+ if (params.configId === MODE_CONFIG_ID) {
357
+ await this.#connection.sessionUpdate({
358
+ sessionId: record.session.sessionId,
359
+ update: this.#buildCurrentModeUpdate(record.session),
360
+ });
361
+ }
362
+
363
+ // For `thinking` the lifetime subscription pushes post-bootstrap; only
364
+ // push here when it's not yet installed so pre-bootstrap callers still
365
+ // see the change without a post-bootstrap duplicate.
366
+ const thinkingHandledBySubscription =
367
+ params.configId === THINKING_CONFIG_ID && record.lifetimeUnsubscribe !== undefined;
368
+ if (!thinkingHandledBySubscription) {
369
+ await this.#pushConfigOptionUpdate(record);
370
+ }
371
+ return { configOptions: this.#buildConfigOptions(record.session) };
322
372
  }
323
373
 
324
374
  async unstable_setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse> {
325
375
  const record = this.#getSessionRecord(params.sessionId);
326
376
  await this.#setModelById(record.session, params.modelId);
327
- await this.#connection.sessionUpdate({
328
- sessionId: record.session.sessionId,
329
- update: {
330
- sessionUpdate: "config_option_update",
331
- configOptions: this.#buildConfigOptions(record.session),
332
- },
333
- });
377
+ await this.#pushConfigOptionUpdate(record);
334
378
  return {};
335
379
  }
336
380
 
@@ -356,13 +400,88 @@ export class AcpAgent implements Agent {
356
400
  void this.#handlePromptEvent(record, event);
357
401
  });
358
402
 
359
- record.session.prompt(converted.text, { images: converted.images }).catch((error: unknown) => {
403
+ this.#runPromptOrCommand(record, converted.text, converted.images).catch((error: unknown) => {
360
404
  this.#finishPrompt(record, undefined, error);
361
405
  });
362
406
 
363
407
  return await pendingPrompt.promise;
364
408
  }
365
409
 
410
+ async #runPromptOrCommand(record: ManagedSessionRecord, text: string, images: AgentImageContent[]): Promise<void> {
411
+ const skillResult = await this.#tryRunSkillCommand(record, text);
412
+ if (skillResult) {
413
+ return;
414
+ }
415
+
416
+ const builtinResult = await executeAcpBuiltinSlashCommand(text, {
417
+ session: record.session,
418
+ sessionManager: record.session.sessionManager,
419
+ settings: Settings.instance,
420
+ cwd: record.session.sessionManager.getCwd(),
421
+ output: output => this.#emitCommandOutput(record, output),
422
+ refreshCommands: () => this.#emitAvailableCommandsUpdate(record),
423
+ reloadPlugins: () => this.#reloadPluginState(record),
424
+ notifyTitleChanged: async () => {
425
+ await this.#connection.sessionUpdate({
426
+ sessionId: record.session.sessionId,
427
+ update: {
428
+ sessionUpdate: "session_info_update",
429
+ title: record.session.sessionName,
430
+ updatedAt: new Date().toISOString(),
431
+ },
432
+ });
433
+ },
434
+ notifyConfigChanged: async () => {
435
+ await this.#pushConfigOptionUpdate(record);
436
+ },
437
+ });
438
+ if (builtinResult !== false) {
439
+ if ("prompt" in builtinResult) {
440
+ await record.session.prompt(builtinResult.prompt, { images });
441
+ return;
442
+ }
443
+ const promptTurn = record.promptTurn;
444
+ this.#finishPrompt(record, {
445
+ stopReason: "end_turn",
446
+ usage: this.#buildTurnUsage(
447
+ promptTurn?.usageBaseline ??
448
+ this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
449
+ record.session.sessionManager.getUsageStatistics(),
450
+ ),
451
+ userMessageId: promptTurn?.userMessageId,
452
+ });
453
+ return;
454
+ }
455
+
456
+ await record.session.prompt(text, { images });
457
+ }
458
+
459
+ async #tryRunSkillCommand(record: ManagedSessionRecord, text: string): Promise<boolean> {
460
+ if (!text.startsWith("/skill:")) {
461
+ return false;
462
+ }
463
+ if (!record.session.skillsSettings?.enableSkillCommands) {
464
+ return false;
465
+ }
466
+ const spaceIndex = text.indexOf(" ");
467
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
468
+ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
469
+ const skillName = commandName.slice("skill:".length);
470
+ const skill = record.session.skills.find(candidate => candidate.name === skillName);
471
+ if (!skill) {
472
+ return false;
473
+ }
474
+ const built = await buildSkillPromptMessage(skill, args);
475
+ await record.session.promptCustomMessage({
476
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
477
+ content: built.message,
478
+ display: true,
479
+ details: built.details,
480
+ attribution: "user",
481
+ });
482
+ return true;
483
+ }
484
+
366
485
  async cancel(params: { sessionId: string }): Promise<void> {
367
486
  const record = this.#getSessionRecord(params.sessionId);
368
487
  const promptTurn = record.promptTurn;
@@ -384,7 +503,7 @@ export class AcpAgent implements Agent {
384
503
 
385
504
  async extMethod(method: string, params: { [key: string]: unknown }): Promise<{ [key: string]: unknown }> {
386
505
  switch (method) {
387
- case "omp/sessions/listAll": {
506
+ case "_omp/sessions/listAll": {
388
507
  const limit = typeof params.limit === "number" ? Math.max(1, Math.min(5000, params.limit as number)) : 1000;
389
508
  const sessions = await SessionManager.listAll();
390
509
  const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
@@ -393,7 +512,7 @@ export class AcpAgent implements Agent {
393
512
  total: sessions.length,
394
513
  };
395
514
  }
396
- case "omp/projects/list": {
515
+ case "_omp/projects/list": {
397
516
  const sessions = await SessionManager.listAll();
398
517
  const buckets = new Map<
399
518
  string,
@@ -421,7 +540,7 @@ export class AcpAgent implements Agent {
421
540
  const projects = Array.from(buckets.values()).sort((a, b) => b.lastActivityAt - a.lastActivityAt);
422
541
  return { projects, totalSessions: sessions.length };
423
542
  }
424
- case "omp/chats/byCwd": {
543
+ case "_omp/chats/byCwd": {
425
544
  const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
426
545
  if (!cwd) throw new Error("cwd required");
427
546
  const limit = typeof params.limit === "number" ? Math.max(1, Math.min(500, params.limit as number)) : 100;
@@ -429,20 +548,20 @@ export class AcpAgent implements Agent {
429
548
  const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
430
549
  return { sessions: sorted.map(s => this.#toSessionInfo(s)) };
431
550
  }
432
- case "omp/usage": {
551
+ case "_omp/usage": {
433
552
  const [firstRecord] = this.#sessions.values();
434
553
  const target = firstRecord?.session ?? this.#initialSession;
435
554
  const reports = await target.fetchUsageReports();
436
555
  return { reports: reports ?? [] };
437
556
  }
438
- case "omp/extensions": {
557
+ case "_omp/extensions": {
439
558
  const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
440
559
  const sm = await Settings.init();
441
560
  const disabledIds = (sm.get("disabledExtensions") as string[] | undefined) ?? [];
442
561
  const extensions = await loadAllExtensions(cwd, disabledIds);
443
562
  return { extensions: extensions as unknown as Array<{ [key: string]: unknown }> };
444
563
  }
445
- case "omp/extensions/toggle": {
564
+ case "_omp/extensions/toggle": {
446
565
  const providerId = params.providerId;
447
566
  if (typeof providerId !== "string") throw new Error("providerId required");
448
567
  if (params.enabled === false) {
@@ -562,6 +681,9 @@ export class AcpAgent implements Agent {
562
681
 
563
682
  async #registerPreparedSession(session: AgentSession, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
564
683
  const record = this.#createManagedSessionRecord(session);
684
+ session.setClientBridge(createAcpClientBridge(this.#connection, session.sessionId, this.#clientCapabilities));
685
+ // `record.lifetimeUnsubscribe` is installed in `#scheduleBootstrapUpdates`
686
+ // so it shares the bootstrap race guard — see that comment for why.
565
687
  try {
566
688
  await this.#configureExtensions(record);
567
689
  await this.#configureMcpServers(record, mcpServers);
@@ -578,11 +700,27 @@ export class AcpAgent implements Agent {
578
700
  session,
579
701
  mcpManager: undefined,
580
702
  promptTurn: undefined,
581
- liveMessageIds: new WeakMap<object, string>(),
703
+ liveMessageId: undefined,
704
+ liveMessageProgress: undefined,
582
705
  extensionsConfigured: false,
706
+ lifetimeUnsubscribe: undefined,
583
707
  };
584
708
  }
585
709
 
710
+ async #handleLifetimeEvent(record: ManagedSessionRecord, event: AgentSessionEvent): Promise<void> {
711
+ if (event.type !== "thinking_level_changed") {
712
+ return;
713
+ }
714
+ try {
715
+ await this.#pushConfigOptionUpdate(record);
716
+ } catch (error) {
717
+ logger.warn("Failed to push thinking-level config_option_update", {
718
+ sessionId: record.session.sessionId,
719
+ error,
720
+ });
721
+ }
722
+ }
723
+
586
724
  #getSessionRecord(sessionId: string): ManagedSessionRecord {
587
725
  const record = this.#sessions.get(sessionId);
588
726
  if (!record) {
@@ -627,33 +765,61 @@ export class AcpAgent implements Agent {
627
765
  return;
628
766
  }
629
767
 
768
+ this.#prepareLiveAssistantMessage(record, event);
630
769
  for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
631
770
  getMessageId: message => this.#getLiveMessageId(record, message),
771
+ getMessageProgress: message => this.#getLiveMessageProgress(record, message),
772
+ cwd: record.session.sessionManager.getCwd(),
632
773
  })) {
633
774
  await this.#connection.sessionUpdate(notification);
634
775
  }
776
+ this.#clearLiveAssistantMessageAfterEvent(record, event);
635
777
 
636
778
  if (event.type === "agent_end") {
637
779
  await this.#emitEndOfTurnUpdates(record);
638
780
  this.#finishPrompt(record, {
639
- stopReason: promptTurn.cancelRequested ? "cancelled" : "end_turn",
781
+ stopReason: this.#resolveStopReason(event, promptTurn.cancelRequested),
640
782
  usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
641
783
  userMessageId: promptTurn.userMessageId,
642
784
  });
643
785
  }
644
786
  }
645
787
 
788
+ #prepareLiveAssistantMessage(record: ManagedSessionRecord, event: AgentSessionEvent): void {
789
+ if (
790
+ (event.type === "message_start" || event.type === "message_update" || event.type === "message_end") &&
791
+ event.message.role === "assistant" &&
792
+ (event.type === "message_start" || !record.liveMessageId || !record.liveMessageProgress)
793
+ ) {
794
+ record.liveMessageId = crypto.randomUUID();
795
+ record.liveMessageProgress = { textEmitted: false, thoughtEmitted: false };
796
+ }
797
+ }
798
+
799
+ #clearLiveAssistantMessageAfterEvent(record: ManagedSessionRecord, event: AgentSessionEvent): void {
800
+ if ((event.type === "message_end" && event.message.role === "assistant") || event.type === "agent_end") {
801
+ record.liveMessageId = undefined;
802
+ record.liveMessageProgress = undefined;
803
+ }
804
+ }
805
+
646
806
  #getLiveMessageId(record: ManagedSessionRecord, message: unknown): string | undefined {
647
807
  if (typeof message !== "object" || message === null) {
648
808
  return undefined;
649
809
  }
650
- const existing = record.liveMessageIds.get(message);
651
- if (existing) {
652
- return existing;
810
+ record.liveMessageId ??= crypto.randomUUID();
811
+ return record.liveMessageId;
812
+ }
813
+
814
+ #getLiveMessageProgress(
815
+ record: ManagedSessionRecord,
816
+ message: unknown,
817
+ ): { textEmitted: boolean; thoughtEmitted: boolean } | undefined {
818
+ if (typeof message !== "object" || message === null) {
819
+ return undefined;
653
820
  }
654
- const nextMessageId = crypto.randomUUID();
655
- record.liveMessageIds.set(message, nextMessageId);
656
- return nextMessageId;
821
+ record.liveMessageProgress ??= { textEmitted: false, thoughtEmitted: false };
822
+ return record.liveMessageProgress;
657
823
  }
658
824
 
659
825
  #finishPrompt(record: ManagedSessionRecord, response?: PromptResponse, error?: unknown): void {
@@ -671,6 +837,48 @@ export class AcpAgent implements Agent {
671
837
  promptTurn.resolve(response ?? { stopReason: "end_turn" });
672
838
  }
673
839
 
840
+ #resolveStopReason(
841
+ event: Extract<AgentSessionEvent, { type: "agent_end" }>,
842
+ cancelRequested: boolean,
843
+ ): PromptResponse["stopReason"] {
844
+ if (cancelRequested) {
845
+ return "cancelled";
846
+ }
847
+ const lastAssistant = [...event.messages]
848
+ .reverse()
849
+ .find((message): message is AssistantMessage => message.role === "assistant");
850
+ const reason = lastAssistant?.stopReason;
851
+ switch (reason) {
852
+ case "aborted":
853
+ return "cancelled";
854
+ case "length":
855
+ return "max_tokens";
856
+ case "error": {
857
+ const errorMessage = lastAssistant?.errorMessage ?? "";
858
+ if (/content[_ ]?filter|refus(al|ed)/i.test(errorMessage)) {
859
+ return "refusal";
860
+ }
861
+ return "end_turn";
862
+ }
863
+ default:
864
+ return "end_turn";
865
+ }
866
+ }
867
+
868
+ async #emitCommandOutput(record: ManagedSessionRecord, text: string): Promise<void> {
869
+ if (!text) {
870
+ return;
871
+ }
872
+ await this.#connection.sessionUpdate({
873
+ sessionId: record.session.sessionId,
874
+ update: {
875
+ sessionUpdate: "agent_message_chunk",
876
+ content: { type: "text", text },
877
+ messageId: crypto.randomUUID(),
878
+ },
879
+ });
880
+ }
881
+
674
882
  #assertAbsoluteCwd(cwd: string): void {
675
883
  if (!path.isAbsolute(cwd)) {
676
884
  throw new Error(`ACP cwd must be absolute: ${cwd}`);
@@ -691,6 +899,12 @@ export class AcpAgent implements Agent {
691
899
  case "resource":
692
900
  if ("text" in block.resource) {
693
901
  textParts.push(block.resource.text);
902
+ } else if (typeof block.resource.mimeType === "string" && block.resource.mimeType.startsWith("image/")) {
903
+ // `embeddedContext: true` covers both text and blob resources, but
904
+ // blobs aren't directly consumable by the LLM. Route image blobs
905
+ // to the images array so the user's intent survives; everything
906
+ // else falls back to the URI placeholder below.
907
+ images.push({ type: "image", data: block.resource.blob, mimeType: block.resource.mimeType });
694
908
  } else {
695
909
  textParts.push(`[embedded resource: ${block.resource.uri}]`);
696
910
  }
@@ -709,15 +923,31 @@ export class AcpAgent implements Agent {
709
923
  };
710
924
  }
711
925
 
926
+ async #pushConfigOptionUpdate(record: ManagedSessionRecord): Promise<void> {
927
+ await this.#connection.sessionUpdate({
928
+ sessionId: record.session.sessionId,
929
+ update: {
930
+ sessionUpdate: "config_option_update",
931
+ configOptions: this.#buildConfigOptions(record.session),
932
+ },
933
+ });
934
+ }
935
+
712
936
  #buildConfigOptions(session: AgentSession): SessionConfigOption[] {
937
+ const currentModeId = this.#getCurrentModeId(session);
938
+ const modeOptions = this.#getAvailableModes(session).map(mode => ({
939
+ value: mode.id,
940
+ name: mode.name,
941
+ description: mode.description,
942
+ }));
713
943
  const configOptions: SessionConfigOption[] = [
714
944
  {
715
945
  id: MODE_CONFIG_ID,
716
946
  name: "Mode",
717
947
  category: "mode",
718
948
  type: "select",
719
- currentValue: ACP_MODE_ID,
720
- options: [{ value: ACP_MODE_ID, name: "Default", description: "Standard ACP headless mode" }],
949
+ currentValue: currentModeId,
950
+ options: modeOptions,
721
951
  },
722
952
  ];
723
953
 
@@ -805,17 +1035,52 @@ export class AcpAgent implements Agent {
805
1035
  return `${model.provider}/${model.id}`;
806
1036
  }
807
1037
 
808
- #buildModeState(): SessionModeState {
1038
+ #getAvailableModes(session: AgentSession): Array<{ id: string; name: string; description: string }> {
1039
+ const modes = [{ id: ACP_DEFAULT_MODE_ID, name: "Default", description: "Standard ACP headless mode" }];
1040
+ if (Settings.instance.get("plan.enabled")) {
1041
+ modes.push({
1042
+ id: ACP_PLAN_MODE_ID,
1043
+ name: "Plan",
1044
+ description: "Read-only planning mode that drafts a plan to a markdown file before any code changes",
1045
+ });
1046
+ }
1047
+ void session;
1048
+ return modes;
1049
+ }
1050
+
1051
+ #getCurrentModeId(session: AgentSession): string {
1052
+ return session.getPlanModeState()?.enabled ? ACP_PLAN_MODE_ID : ACP_DEFAULT_MODE_ID;
1053
+ }
1054
+
1055
+ #applyModeChange(session: AgentSession, modeId: string): void {
1056
+ const availableModes = this.#getAvailableModes(session);
1057
+ if (!availableModes.some(mode => mode.id === modeId)) {
1058
+ throw new Error(`Unsupported ACP mode: ${modeId}`);
1059
+ }
1060
+ if (modeId === ACP_PLAN_MODE_ID) {
1061
+ const previous = session.getPlanModeState();
1062
+ session.setPlanModeState({
1063
+ enabled: true,
1064
+ planFilePath: previous?.planFilePath ?? DEFAULT_PLAN_FILE_URL,
1065
+ workflow: previous?.workflow ?? "parallel",
1066
+ reentry: previous !== undefined,
1067
+ });
1068
+ } else {
1069
+ session.setPlanModeState(undefined);
1070
+ }
1071
+ }
1072
+
1073
+ #buildModeState(session: AgentSession): SessionModeState {
809
1074
  return {
810
- availableModes: [{ id: ACP_MODE_ID, name: "Default", description: "Standard ACP headless mode" }],
811
- currentModeId: ACP_MODE_ID,
1075
+ availableModes: this.#getAvailableModes(session),
1076
+ currentModeId: this.#getCurrentModeId(session),
812
1077
  };
813
1078
  }
814
1079
 
815
- #buildCurrentModeUpdate(): SessionUpdate {
1080
+ #buildCurrentModeUpdate(session: AgentSession): SessionUpdate {
816
1081
  return {
817
1082
  sessionUpdate: "current_mode_update",
818
- currentModeId: ACP_MODE_ID,
1083
+ currentModeId: this.#getCurrentModeId(session),
819
1084
  };
820
1085
  }
821
1086
 
@@ -830,6 +1095,24 @@ export class AcpAgent implements Agent {
830
1095
  commands.push(command);
831
1096
  };
832
1097
 
1098
+ // Advertise in the order dispatch resolves them: ACP builtins first
1099
+ // (so core commands like `/model`, `/mcp`, `/todo` cannot be shadowed),
1100
+ // then skills, then custom/user commands, then file-based slash
1101
+ // commands. `appendCommand` dedupes by name so earlier entries win.
1102
+ for (const command of ACP_BUILTIN_SLASH_COMMANDS) {
1103
+ appendCommand(command);
1104
+ }
1105
+
1106
+ if (session.skillsSettings?.enableSkillCommands) {
1107
+ for (const skill of session.skills) {
1108
+ appendCommand({
1109
+ name: getSkillSlashCommandName(skill),
1110
+ description: skill.description || `Run ${skill.name} skill`,
1111
+ input: { hint: "arguments" },
1112
+ });
1113
+ }
1114
+ }
1115
+
833
1116
  for (const command of session.customCommands) {
834
1117
  appendCommand({
835
1118
  name: command.command.name,
@@ -854,10 +1137,33 @@ export class AcpAgent implements Agent {
854
1137
  cwd: session.cwd,
855
1138
  title: session.title,
856
1139
  updatedAt: session.modified.toISOString(),
1140
+ _meta: {
1141
+ messageCount: session.messageCount,
1142
+ size: session.size,
1143
+ },
857
1144
  };
858
1145
  }
859
1146
 
860
1147
  #scheduleBootstrapUpdates(sessionId: string): void {
1148
+ // Defer first notifications until the response has reached the client.
1149
+ // Zed's agent-client-protocol reader dispatches responses and
1150
+ // notifications to different async tasks; sending the first
1151
+ // `available_commands_update` from `setTimeout(0)` reliably loses the
1152
+ // race against the response handler and Zed logs `Received session
1153
+ // notification for unknown session` then drops the update — leaving
1154
+ // the slash-command palette empty (#1015 follow-up; see
1155
+ // zed-industries/zed#55965 for the same race biting other ACP agents).
1156
+ // `ACP_BOOTSTRAP_RACE_GUARD_MS` is invisible to the operator and large
1157
+ // enough that the response future has scheduled before our timer fires
1158
+ // on stdio-only transports.
1159
+ //
1160
+ // The session-lifetime subscription is installed inside the same timer
1161
+ // so it shares this guard — without it, an extension's `session_start`
1162
+ // handler (or any async work it schedules) calling `setThinkingLevel`
1163
+ // would push a `config_option_update` for a session id the client
1164
+ // hasn't been told about yet. The pre-bootstrap thinking level is
1165
+ // reported in the response's `configOptions`, so deferring the
1166
+ // notification loses no state.
861
1167
  setTimeout(() => {
862
1168
  if (this.#connection.signal.aborted) {
863
1169
  return;
@@ -866,8 +1172,13 @@ export class AcpAgent implements Agent {
866
1172
  if (!record) {
867
1173
  return;
868
1174
  }
1175
+ if (!record.lifetimeUnsubscribe) {
1176
+ record.lifetimeUnsubscribe = record.session.subscribe(event => {
1177
+ void this.#handleLifetimeEvent(record, event);
1178
+ });
1179
+ }
869
1180
  void this.#emitBootstrapUpdates(sessionId, record);
870
- }, 0);
1181
+ }, ACP_BOOTSTRAP_RACE_GUARD_MS);
871
1182
  }
872
1183
 
873
1184
  async #emitBootstrapUpdates(sessionId: string, record: ManagedSessionRecord): Promise<void> {
@@ -891,6 +1202,33 @@ export class AcpAgent implements Agent {
891
1202
  });
892
1203
  }
893
1204
 
1205
+ async #emitAvailableCommandsUpdate(record: ManagedSessionRecord): Promise<void> {
1206
+ await this.#connection.sessionUpdate({
1207
+ sessionId: record.session.sessionId,
1208
+ update: {
1209
+ sessionUpdate: "available_commands_update",
1210
+ availableCommands: await this.#buildAvailableCommands(record.session),
1211
+ },
1212
+ });
1213
+ }
1214
+
1215
+ /**
1216
+ * Reload plugin/registry state for an ACP session. Mirrors the interactive
1217
+ * `/reload-plugins` and `/move` flows: invalidates the plugin-roots cache,
1218
+ * resets the capability cache, refreshes the session's slash-command state,
1219
+ * then re-advertises commands so the client sees newly installed/disabled
1220
+ * plugins.
1221
+ */
1222
+ async #reloadPluginState(record: ManagedSessionRecord): Promise<void> {
1223
+ const cwd = record.session.sessionManager.getCwd();
1224
+ const projectPath = await resolveActiveProjectRegistryPath(cwd);
1225
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
1226
+ resetCapabilities();
1227
+ const fileCommands = await loadSlashCommands({ cwd });
1228
+ record.session.setSlashCommands(fileCommands);
1229
+ await this.#emitAvailableCommandsUpdate(record);
1230
+ }
1231
+
894
1232
  async #emitEndOfTurnUpdates(record: ManagedSessionRecord): Promise<void> {
895
1233
  const sessionId = record.session.sessionId;
896
1234
 
@@ -981,14 +1319,15 @@ export class AcpAgent implements Agent {
981
1319
  }
982
1320
 
983
1321
  async #replaySessionHistory(record: ManagedSessionRecord): Promise<void> {
1322
+ const cwd = record.session.sessionManager.getCwd();
984
1323
  for (const message of record.session.sessionManager.buildSessionContext().messages as ReplayableMessage[]) {
985
- for (const notification of this.#messageToReplayNotifications(record.session.sessionId, message)) {
1324
+ for (const notification of this.#messageToReplayNotifications(record.session.sessionId, message, cwd)) {
986
1325
  await this.#connection.sessionUpdate(notification);
987
1326
  }
988
1327
  }
989
1328
  }
990
1329
 
991
- #messageToReplayNotifications(sessionId: string, message: ReplayableMessage): SessionNotification[] {
1330
+ #messageToReplayNotifications(sessionId: string, message: ReplayableMessage, cwd: string): SessionNotification[] {
992
1331
  if (message.role === "assistant") {
993
1332
  return this.#replayAssistantMessage(sessionId, message);
994
1333
  }
@@ -1010,7 +1349,7 @@ export class AcpAgent implements Agent {
1010
1349
  typeof message.toolCallId === "string" &&
1011
1350
  typeof message.toolName === "string"
1012
1351
  ) {
1013
- return this.#replayToolResult(sessionId, {
1352
+ return this.#replayToolResult(sessionId, cwd, {
1014
1353
  ...message,
1015
1354
  toolCallId: message.toolCallId,
1016
1355
  toolName: message.toolName,
@@ -1087,7 +1426,7 @@ export class AcpAgent implements Agent {
1087
1426
  }
1088
1427
  }
1089
1428
  }
1090
- if (notifications.length === 0 && message.errorMessage) {
1429
+ if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message.errorMessage)) {
1091
1430
  notifications.push({
1092
1431
  sessionId,
1093
1432
  update: {
@@ -1102,6 +1441,7 @@ export class AcpAgent implements Agent {
1102
1441
 
1103
1442
  #replayToolResult(
1104
1443
  sessionId: string,
1444
+ cwd: string,
1105
1445
  message: Required<Pick<ReplayableMessage, "toolCallId" | "toolName">> & ReplayableMessage,
1106
1446
  ): SessionNotification[] {
1107
1447
  const args = this.#buildReplayToolArgs(message.details);
@@ -1123,8 +1463,8 @@ export class AcpAgent implements Agent {
1123
1463
  },
1124
1464
  };
1125
1465
  return [
1126
- ...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId),
1127
- ...mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId),
1466
+ ...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId, { cwd }),
1467
+ ...mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, { cwd }),
1128
1468
  ];
1129
1469
  }
1130
1470
 
@@ -1367,6 +1707,7 @@ export class AcpAgent implements Agent {
1367
1707
  }
1368
1708
 
1369
1709
  async #disposeSessionRecord(record: ManagedSessionRecord): Promise<void> {
1710
+ record.lifetimeUnsubscribe?.();
1370
1711
  if (record.mcpManager) {
1371
1712
  try {
1372
1713
  await record.mcpManager.disconnectAll();