@oh-my-pi/pi-coding-agent 14.9.9 → 15.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 (128) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +1 -1
  4. package/src/cli/args.ts +2 -2
  5. package/src/cli.ts +1 -0
  6. package/src/commands/acp.ts +24 -0
  7. package/src/commands/launch.ts +6 -4
  8. package/src/commit/agentic/prompts/system.md +1 -1
  9. package/src/config/model-resolver.ts +30 -0
  10. package/src/config/settings-schema.ts +31 -0
  11. package/src/edit/index.ts +22 -1
  12. package/src/edit/modes/patch.ts +10 -0
  13. package/src/edit/modes/replace.ts +3 -0
  14. package/src/edit/renderer.ts +10 -0
  15. package/src/eval/js/context-manager.ts +1 -1
  16. package/src/eval/js/shared/rewrite-imports.ts +120 -48
  17. package/src/eval/js/shared/runtime.ts +31 -4
  18. package/src/eval/js/tool-bridge.ts +43 -21
  19. package/src/extensibility/extensions/runner.ts +54 -1
  20. package/src/extensibility/extensions/types.ts +11 -0
  21. package/src/extensibility/skills.ts +33 -1
  22. package/src/internal-urls/docs-index.generated.ts +6 -6
  23. package/src/internal-urls/index.ts +1 -0
  24. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  25. package/src/internal-urls/router.ts +6 -3
  26. package/src/internal-urls/types.ts +22 -1
  27. package/src/main.ts +13 -9
  28. package/src/modes/acp/acp-agent.ts +361 -54
  29. package/src/modes/acp/acp-client-bridge.ts +152 -0
  30. package/src/modes/acp/acp-event-mapper.ts +180 -15
  31. package/src/modes/acp/terminal-auth.ts +37 -0
  32. package/src/modes/components/read-tool-group.ts +29 -1
  33. package/src/modes/controllers/command-controller.ts +14 -6
  34. package/src/modes/controllers/event-controller.ts +24 -11
  35. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  36. package/src/modes/controllers/input-controller.ts +72 -39
  37. package/src/modes/interactive-mode.ts +71 -7
  38. package/src/modes/rpc/rpc-mode.ts +17 -2
  39. package/src/modes/types.ts +6 -2
  40. package/src/modes/utils/ui-helpers.ts +15 -3
  41. package/src/prompts/agents/designer.md +5 -5
  42. package/src/prompts/agents/explore.md +7 -7
  43. package/src/prompts/agents/init.md +9 -9
  44. package/src/prompts/agents/librarian.md +14 -14
  45. package/src/prompts/agents/plan.md +4 -4
  46. package/src/prompts/agents/reviewer.md +5 -5
  47. package/src/prompts/agents/task.md +10 -10
  48. package/src/prompts/commands/orchestrate.md +2 -2
  49. package/src/prompts/compaction/branch-summary.md +3 -3
  50. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  51. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  52. package/src/prompts/compaction/compaction-summary.md +5 -5
  53. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  54. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  55. package/src/prompts/memories/consolidation.md +2 -2
  56. package/src/prompts/memories/read-path.md +1 -1
  57. package/src/prompts/memories/stage_one_input.md +1 -1
  58. package/src/prompts/memories/stage_one_system.md +5 -5
  59. package/src/prompts/review-request.md +4 -4
  60. package/src/prompts/system/agent-creation-architect.md +17 -17
  61. package/src/prompts/system/agent-creation-user.md +2 -2
  62. package/src/prompts/system/commit-message-system.md +2 -2
  63. package/src/prompts/system/custom-system-prompt.md +2 -2
  64. package/src/prompts/system/eager-todo.md +6 -6
  65. package/src/prompts/system/handoff-document.md +1 -1
  66. package/src/prompts/system/plan-mode-active.md +22 -21
  67. package/src/prompts/system/plan-mode-approved.md +4 -4
  68. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  69. package/src/prompts/system/plan-mode-reference.md +2 -2
  70. package/src/prompts/system/plan-mode-subagent.md +8 -8
  71. package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
  72. package/src/prompts/system/project-prompt.md +4 -4
  73. package/src/prompts/system/subagent-system-prompt.md +7 -7
  74. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  75. package/src/prompts/system/system-prompt.md +72 -71
  76. package/src/prompts/system/ttsr-interrupt.md +1 -1
  77. package/src/prompts/tools/apply-patch.md +1 -1
  78. package/src/prompts/tools/ast-edit.md +3 -3
  79. package/src/prompts/tools/ast-grep.md +3 -3
  80. package/src/prompts/tools/browser.md +3 -3
  81. package/src/prompts/tools/checkpoint.md +3 -3
  82. package/src/prompts/tools/exit-plan-mode.md +2 -2
  83. package/src/prompts/tools/find.md +3 -3
  84. package/src/prompts/tools/github.md +2 -5
  85. package/src/prompts/tools/hashline.md +6 -6
  86. package/src/prompts/tools/image-gen.md +3 -3
  87. package/src/prompts/tools/irc.md +1 -1
  88. package/src/prompts/tools/lsp.md +2 -2
  89. package/src/prompts/tools/patch.md +6 -6
  90. package/src/prompts/tools/read.md +7 -7
  91. package/src/prompts/tools/replace.md +5 -5
  92. package/src/prompts/tools/retain.md +1 -1
  93. package/src/prompts/tools/rewind.md +2 -2
  94. package/src/prompts/tools/search.md +2 -2
  95. package/src/prompts/tools/ssh.md +2 -2
  96. package/src/prompts/tools/task.md +12 -6
  97. package/src/prompts/tools/web-search.md +2 -2
  98. package/src/prompts/tools/write.md +3 -3
  99. package/src/sdk.ts +69 -12
  100. package/src/session/agent-session.ts +231 -22
  101. package/src/session/client-bridge.ts +81 -0
  102. package/src/session/compaction/errors.ts +31 -0
  103. package/src/session/compaction/index.ts +1 -0
  104. package/src/slash-commands/acp-builtins.ts +46 -0
  105. package/src/slash-commands/builtin-registry.ts +699 -116
  106. package/src/slash-commands/helpers/context-report.ts +39 -0
  107. package/src/slash-commands/helpers/format.ts +23 -0
  108. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  109. package/src/slash-commands/helpers/mcp.ts +532 -0
  110. package/src/slash-commands/helpers/parse.ts +85 -0
  111. package/src/slash-commands/helpers/ssh.ts +193 -0
  112. package/src/slash-commands/helpers/todo.ts +279 -0
  113. package/src/slash-commands/helpers/usage-report.ts +91 -0
  114. package/src/slash-commands/types.ts +126 -0
  115. package/src/task/executor.ts +10 -3
  116. package/src/task/index.ts +17 -1
  117. package/src/task/render.ts +6 -3
  118. package/src/tools/bash.ts +176 -2
  119. package/src/tools/conflict-detect.ts +6 -6
  120. package/src/tools/fetch.ts +15 -4
  121. package/src/tools/find.ts +19 -1
  122. package/src/tools/gh-renderer.ts +0 -12
  123. package/src/tools/gh.ts +682 -176
  124. package/src/tools/github-cache.ts +548 -0
  125. package/src/tools/index.ts +3 -0
  126. package/src/tools/read.ts +110 -27
  127. package/src/tools/write.ts +23 -1
  128. package/src/tui/code-cell.ts +70 -2
@@ -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,27 +39,35 @@ 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 { 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";
@@ -84,7 +94,8 @@ type ManagedSessionRecord = {
84
94
  session: AgentSession;
85
95
  mcpManager: MCPManager | undefined;
86
96
  promptTurn: PromptTurnState | undefined;
87
- liveMessageIds: WeakMap<object, string>;
97
+ liveMessageId: string | undefined;
98
+ liveMessageProgress: { textEmitted: boolean; thoughtEmitted: boolean } | undefined;
88
99
  extensionsConfigured: boolean;
89
100
  };
90
101
 
@@ -152,6 +163,7 @@ export class AcpAgent implements Agent {
152
163
  #sessions = new Map<string, ManagedSessionRecord>();
153
164
  #disposePromise: Promise<void> | undefined;
154
165
  #cleanupRegistered = false;
166
+ #clientCapabilities: ClientCapabilities | undefined;
155
167
 
156
168
  constructor(connection: AgentSideConnection, initialSession: AgentSession, createSession: CreateAcpSession) {
157
169
  this.#connection = connection;
@@ -159,8 +171,25 @@ export class AcpAgent implements Agent {
159
171
  this.#createSession = createSession;
160
172
  }
161
173
 
162
- async initialize(_params: InitializeRequest): Promise<InitializeResponse> {
174
+ async initialize(params: InitializeRequest): Promise<InitializeResponse> {
163
175
  this.#registerConnectionCleanup();
176
+ this.#clientCapabilities = params.clientCapabilities;
177
+ const authMethods: AuthMethod[] = [
178
+ {
179
+ id: "agent",
180
+ name: "Use existing local credentials",
181
+ description: "Authenticate via the provider keys/OAuth state already configured under ~/.omp.",
182
+ },
183
+ ];
184
+ if (params.clientCapabilities?.auth?.terminal === true) {
185
+ authMethods.push({
186
+ type: "terminal",
187
+ id: "terminal",
188
+ name: "Set up Oh My Pi in terminal",
189
+ description: "Launch the omp TUI to add provider keys and select models.",
190
+ args: [ACP_TERMINAL_AUTH_FLAG],
191
+ });
192
+ }
164
193
  return {
165
194
  protocolVersion: PROTOCOL_VERSION,
166
195
  agentInfo: {
@@ -168,13 +197,7 @@ export class AcpAgent implements Agent {
168
197
  title: "Oh My Pi",
169
198
  version: VERSION,
170
199
  },
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
- ],
200
+ authMethods,
178
201
  agentCapabilities: {
179
202
  loadSession: true,
180
203
  mcpCapabilities: {
@@ -195,7 +218,15 @@ export class AcpAgent implements Agent {
195
218
  };
196
219
  }
197
220
 
198
- async authenticate(_params: AuthenticateRequest): Promise<AuthenticateResponse> {
221
+ async authenticate(params: AuthenticateRequest): Promise<AuthenticateResponse> {
222
+ // ACP spec: `methodId` must be one of the methods advertised by `initialize`.
223
+ // Reject anything else so malformed clients fail fast rather than appearing
224
+ // authenticated and surfacing a downstream model failure later.
225
+ const supportsTerminalAuth = this.#clientCapabilities?.auth?.terminal === true;
226
+ const validMethods = supportsTerminalAuth ? ["agent", "terminal"] : ["agent"];
227
+ if (!validMethods.includes(params.methodId)) {
228
+ throw new Error(`Unknown ACP auth method: ${params.methodId}`);
229
+ }
199
230
  return {};
200
231
  }
201
232
 
@@ -206,7 +237,7 @@ export class AcpAgent implements Agent {
206
237
  sessionId: record.session.sessionId,
207
238
  configOptions: this.#buildConfigOptions(record.session),
208
239
  models: this.#buildModelState(record.session),
209
- modes: this.#buildModeState(),
240
+ modes: this.#buildModeState(record.session),
210
241
  };
211
242
  this.#scheduleBootstrapUpdates(record.session.sessionId);
212
243
  return response;
@@ -219,7 +250,7 @@ export class AcpAgent implements Agent {
219
250
  const response: LoadSessionResponse = {
220
251
  configOptions: this.#buildConfigOptions(record.session),
221
252
  models: this.#buildModelState(record.session),
222
- modes: this.#buildModeState(),
253
+ modes: this.#buildModeState(record.session),
223
254
  };
224
255
  this.#scheduleBootstrapUpdates(record.session.sessionId);
225
256
  return response;
@@ -242,13 +273,13 @@ export class AcpAgent implements Agent {
242
273
  };
243
274
  }
244
275
 
245
- async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
276
+ async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
246
277
  this.#assertAbsoluteCwd(params.cwd);
247
278
  const record = await this.#resumeManagedSession(params.sessionId, params.cwd, params.mcpServers ?? []);
248
279
  const response: ResumeSessionResponse = {
249
280
  configOptions: this.#buildConfigOptions(record.session),
250
281
  models: this.#buildModelState(record.session),
251
- modes: this.#buildModeState(),
282
+ modes: this.#buildModeState(record.session),
252
283
  };
253
284
  this.#scheduleBootstrapUpdates(record.session.sessionId);
254
285
  return response;
@@ -261,13 +292,13 @@ export class AcpAgent implements Agent {
261
292
  sessionId: record.session.sessionId,
262
293
  configOptions: this.#buildConfigOptions(record.session),
263
294
  models: this.#buildModelState(record.session),
264
- modes: this.#buildModeState(),
295
+ modes: this.#buildModeState(record.session),
265
296
  };
266
297
  this.#scheduleBootstrapUpdates(record.session.sessionId);
267
298
  return response;
268
299
  }
269
300
 
270
- async unstable_closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
301
+ async closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
271
302
  const record = this.#sessions.get(params.sessionId);
272
303
  if (!record) {
273
304
  return {};
@@ -278,12 +309,17 @@ export class AcpAgent implements Agent {
278
309
 
279
310
  async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse> {
280
311
  const record = this.#getSessionRecord(params.sessionId);
281
- if (params.modeId !== ACP_MODE_ID) {
282
- throw new Error(`Unsupported ACP mode: ${params.modeId}`);
283
- }
312
+ this.#applyModeChange(record.session, params.modeId);
284
313
  await this.#connection.sessionUpdate({
285
314
  sessionId: record.session.sessionId,
286
- update: this.#buildCurrentModeUpdate(),
315
+ update: this.#buildCurrentModeUpdate(record.session),
316
+ });
317
+ await this.#connection.sessionUpdate({
318
+ sessionId: record.session.sessionId,
319
+ update: {
320
+ sessionUpdate: "config_option_update",
321
+ configOptions: this.#buildConfigOptions(record.session),
322
+ },
287
323
  });
288
324
  return {};
289
325
  }
@@ -296,9 +332,7 @@ export class AcpAgent implements Agent {
296
332
 
297
333
  switch (params.configId) {
298
334
  case MODE_CONFIG_ID:
299
- if (params.value !== ACP_MODE_ID) {
300
- throw new Error(`Unsupported ACP mode config value: ${params.value}`);
301
- }
335
+ this.#applyModeChange(record.session, params.value);
302
336
  break;
303
337
  case MODEL_CONFIG_ID:
304
338
  await this.#setModelById(record.session, params.value);
@@ -310,6 +344,16 @@ export class AcpAgent implements Agent {
310
344
  throw new Error(`Unknown ACP config option: ${params.configId}`);
311
345
  }
312
346
 
347
+ // When mode is changed via the generic config-option API, mirror the
348
+ // `current_mode_update` notification that `setSessionMode` emits so
349
+ // ACP clients tracking session-mode state see a consistent transition.
350
+ if (params.configId === MODE_CONFIG_ID) {
351
+ await this.#connection.sessionUpdate({
352
+ sessionId: record.session.sessionId,
353
+ update: this.#buildCurrentModeUpdate(record.session),
354
+ });
355
+ }
356
+
313
357
  const configOptions = this.#buildConfigOptions(record.session);
314
358
  await this.#connection.sessionUpdate({
315
359
  sessionId: record.session.sessionId,
@@ -356,13 +400,94 @@ 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.#connection.sessionUpdate({
436
+ sessionId: record.session.sessionId,
437
+ update: {
438
+ sessionUpdate: "config_option_update",
439
+ configOptions: this.#buildConfigOptions(record.session),
440
+ },
441
+ });
442
+ },
443
+ });
444
+ if (builtinResult !== false) {
445
+ if ("prompt" in builtinResult) {
446
+ await record.session.prompt(builtinResult.prompt, { images });
447
+ return;
448
+ }
449
+ const promptTurn = record.promptTurn;
450
+ this.#finishPrompt(record, {
451
+ stopReason: "end_turn",
452
+ usage: this.#buildTurnUsage(
453
+ promptTurn?.usageBaseline ??
454
+ this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
455
+ record.session.sessionManager.getUsageStatistics(),
456
+ ),
457
+ userMessageId: promptTurn?.userMessageId,
458
+ });
459
+ return;
460
+ }
461
+
462
+ await record.session.prompt(text, { images });
463
+ }
464
+
465
+ async #tryRunSkillCommand(record: ManagedSessionRecord, text: string): Promise<boolean> {
466
+ if (!text.startsWith("/skill:")) {
467
+ return false;
468
+ }
469
+ if (!record.session.skillsSettings?.enableSkillCommands) {
470
+ return false;
471
+ }
472
+ const spaceIndex = text.indexOf(" ");
473
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
474
+ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
475
+ const skillName = commandName.slice("skill:".length);
476
+ const skill = record.session.skills.find(candidate => candidate.name === skillName);
477
+ if (!skill) {
478
+ return false;
479
+ }
480
+ const built = await buildSkillPromptMessage(skill, args);
481
+ await record.session.promptCustomMessage({
482
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
483
+ content: built.message,
484
+ display: true,
485
+ details: built.details,
486
+ attribution: "user",
487
+ });
488
+ return true;
489
+ }
490
+
366
491
  async cancel(params: { sessionId: string }): Promise<void> {
367
492
  const record = this.#getSessionRecord(params.sessionId);
368
493
  const promptTurn = record.promptTurn;
@@ -384,7 +509,7 @@ export class AcpAgent implements Agent {
384
509
 
385
510
  async extMethod(method: string, params: { [key: string]: unknown }): Promise<{ [key: string]: unknown }> {
386
511
  switch (method) {
387
- case "omp/sessions/listAll": {
512
+ case "_omp/sessions/listAll": {
388
513
  const limit = typeof params.limit === "number" ? Math.max(1, Math.min(5000, params.limit as number)) : 1000;
389
514
  const sessions = await SessionManager.listAll();
390
515
  const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
@@ -393,7 +518,7 @@ export class AcpAgent implements Agent {
393
518
  total: sessions.length,
394
519
  };
395
520
  }
396
- case "omp/projects/list": {
521
+ case "_omp/projects/list": {
397
522
  const sessions = await SessionManager.listAll();
398
523
  const buckets = new Map<
399
524
  string,
@@ -421,7 +546,7 @@ export class AcpAgent implements Agent {
421
546
  const projects = Array.from(buckets.values()).sort((a, b) => b.lastActivityAt - a.lastActivityAt);
422
547
  return { projects, totalSessions: sessions.length };
423
548
  }
424
- case "omp/chats/byCwd": {
549
+ case "_omp/chats/byCwd": {
425
550
  const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
426
551
  if (!cwd) throw new Error("cwd required");
427
552
  const limit = typeof params.limit === "number" ? Math.max(1, Math.min(500, params.limit as number)) : 100;
@@ -429,20 +554,20 @@ export class AcpAgent implements Agent {
429
554
  const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
430
555
  return { sessions: sorted.map(s => this.#toSessionInfo(s)) };
431
556
  }
432
- case "omp/usage": {
557
+ case "_omp/usage": {
433
558
  const [firstRecord] = this.#sessions.values();
434
559
  const target = firstRecord?.session ?? this.#initialSession;
435
560
  const reports = await target.fetchUsageReports();
436
561
  return { reports: reports ?? [] };
437
562
  }
438
- case "omp/extensions": {
563
+ case "_omp/extensions": {
439
564
  const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
440
565
  const sm = await Settings.init();
441
566
  const disabledIds = (sm.get("disabledExtensions") as string[] | undefined) ?? [];
442
567
  const extensions = await loadAllExtensions(cwd, disabledIds);
443
568
  return { extensions: extensions as unknown as Array<{ [key: string]: unknown }> };
444
569
  }
445
- case "omp/extensions/toggle": {
570
+ case "_omp/extensions/toggle": {
446
571
  const providerId = params.providerId;
447
572
  if (typeof providerId !== "string") throw new Error("providerId required");
448
573
  if (params.enabled === false) {
@@ -562,6 +687,7 @@ export class AcpAgent implements Agent {
562
687
 
563
688
  async #registerPreparedSession(session: AgentSession, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
564
689
  const record = this.#createManagedSessionRecord(session);
690
+ session.setClientBridge(createAcpClientBridge(this.#connection, session.sessionId, this.#clientCapabilities));
565
691
  try {
566
692
  await this.#configureExtensions(record);
567
693
  await this.#configureMcpServers(record, mcpServers);
@@ -578,7 +704,8 @@ export class AcpAgent implements Agent {
578
704
  session,
579
705
  mcpManager: undefined,
580
706
  promptTurn: undefined,
581
- liveMessageIds: new WeakMap<object, string>(),
707
+ liveMessageId: undefined,
708
+ liveMessageProgress: undefined,
582
709
  extensionsConfigured: false,
583
710
  };
584
711
  }
@@ -627,33 +754,61 @@ export class AcpAgent implements Agent {
627
754
  return;
628
755
  }
629
756
 
757
+ this.#prepareLiveAssistantMessage(record, event);
630
758
  for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
631
759
  getMessageId: message => this.#getLiveMessageId(record, message),
760
+ getMessageProgress: message => this.#getLiveMessageProgress(record, message),
761
+ cwd: record.session.sessionManager.getCwd(),
632
762
  })) {
633
763
  await this.#connection.sessionUpdate(notification);
634
764
  }
765
+ this.#clearLiveAssistantMessageAfterEvent(record, event);
635
766
 
636
767
  if (event.type === "agent_end") {
637
768
  await this.#emitEndOfTurnUpdates(record);
638
769
  this.#finishPrompt(record, {
639
- stopReason: promptTurn.cancelRequested ? "cancelled" : "end_turn",
770
+ stopReason: this.#resolveStopReason(event, promptTurn.cancelRequested),
640
771
  usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
641
772
  userMessageId: promptTurn.userMessageId,
642
773
  });
643
774
  }
644
775
  }
645
776
 
777
+ #prepareLiveAssistantMessage(record: ManagedSessionRecord, event: AgentSessionEvent): void {
778
+ if (
779
+ (event.type === "message_start" || event.type === "message_update" || event.type === "message_end") &&
780
+ event.message.role === "assistant" &&
781
+ (event.type === "message_start" || !record.liveMessageId || !record.liveMessageProgress)
782
+ ) {
783
+ record.liveMessageId = crypto.randomUUID();
784
+ record.liveMessageProgress = { textEmitted: false, thoughtEmitted: false };
785
+ }
786
+ }
787
+
788
+ #clearLiveAssistantMessageAfterEvent(record: ManagedSessionRecord, event: AgentSessionEvent): void {
789
+ if ((event.type === "message_end" && event.message.role === "assistant") || event.type === "agent_end") {
790
+ record.liveMessageId = undefined;
791
+ record.liveMessageProgress = undefined;
792
+ }
793
+ }
794
+
646
795
  #getLiveMessageId(record: ManagedSessionRecord, message: unknown): string | undefined {
647
796
  if (typeof message !== "object" || message === null) {
648
797
  return undefined;
649
798
  }
650
- const existing = record.liveMessageIds.get(message);
651
- if (existing) {
652
- return existing;
799
+ record.liveMessageId ??= crypto.randomUUID();
800
+ return record.liveMessageId;
801
+ }
802
+
803
+ #getLiveMessageProgress(
804
+ record: ManagedSessionRecord,
805
+ message: unknown,
806
+ ): { textEmitted: boolean; thoughtEmitted: boolean } | undefined {
807
+ if (typeof message !== "object" || message === null) {
808
+ return undefined;
653
809
  }
654
- const nextMessageId = crypto.randomUUID();
655
- record.liveMessageIds.set(message, nextMessageId);
656
- return nextMessageId;
810
+ record.liveMessageProgress ??= { textEmitted: false, thoughtEmitted: false };
811
+ return record.liveMessageProgress;
657
812
  }
658
813
 
659
814
  #finishPrompt(record: ManagedSessionRecord, response?: PromptResponse, error?: unknown): void {
@@ -671,6 +826,48 @@ export class AcpAgent implements Agent {
671
826
  promptTurn.resolve(response ?? { stopReason: "end_turn" });
672
827
  }
673
828
 
829
+ #resolveStopReason(
830
+ event: Extract<AgentSessionEvent, { type: "agent_end" }>,
831
+ cancelRequested: boolean,
832
+ ): PromptResponse["stopReason"] {
833
+ if (cancelRequested) {
834
+ return "cancelled";
835
+ }
836
+ const lastAssistant = [...event.messages]
837
+ .reverse()
838
+ .find((message): message is AssistantMessage => message.role === "assistant");
839
+ const reason = lastAssistant?.stopReason;
840
+ switch (reason) {
841
+ case "aborted":
842
+ return "cancelled";
843
+ case "length":
844
+ return "max_tokens";
845
+ case "error": {
846
+ const errorMessage = lastAssistant?.errorMessage ?? "";
847
+ if (/content[_ ]?filter|refus(al|ed)/i.test(errorMessage)) {
848
+ return "refusal";
849
+ }
850
+ return "end_turn";
851
+ }
852
+ default:
853
+ return "end_turn";
854
+ }
855
+ }
856
+
857
+ async #emitCommandOutput(record: ManagedSessionRecord, text: string): Promise<void> {
858
+ if (!text) {
859
+ return;
860
+ }
861
+ await this.#connection.sessionUpdate({
862
+ sessionId: record.session.sessionId,
863
+ update: {
864
+ sessionUpdate: "agent_message_chunk",
865
+ content: { type: "text", text },
866
+ messageId: crypto.randomUUID(),
867
+ },
868
+ });
869
+ }
870
+
674
871
  #assertAbsoluteCwd(cwd: string): void {
675
872
  if (!path.isAbsolute(cwd)) {
676
873
  throw new Error(`ACP cwd must be absolute: ${cwd}`);
@@ -691,6 +888,12 @@ export class AcpAgent implements Agent {
691
888
  case "resource":
692
889
  if ("text" in block.resource) {
693
890
  textParts.push(block.resource.text);
891
+ } else if (typeof block.resource.mimeType === "string" && block.resource.mimeType.startsWith("image/")) {
892
+ // `embeddedContext: true` covers both text and blob resources, but
893
+ // blobs aren't directly consumable by the LLM. Route image blobs
894
+ // to the images array so the user's intent survives; everything
895
+ // else falls back to the URI placeholder below.
896
+ images.push({ type: "image", data: block.resource.blob, mimeType: block.resource.mimeType });
694
897
  } else {
695
898
  textParts.push(`[embedded resource: ${block.resource.uri}]`);
696
899
  }
@@ -710,14 +913,20 @@ export class AcpAgent implements Agent {
710
913
  }
711
914
 
712
915
  #buildConfigOptions(session: AgentSession): SessionConfigOption[] {
916
+ const currentModeId = this.#getCurrentModeId(session);
917
+ const modeOptions = this.#getAvailableModes(session).map(mode => ({
918
+ value: mode.id,
919
+ name: mode.name,
920
+ description: mode.description,
921
+ }));
713
922
  const configOptions: SessionConfigOption[] = [
714
923
  {
715
924
  id: MODE_CONFIG_ID,
716
925
  name: "Mode",
717
926
  category: "mode",
718
927
  type: "select",
719
- currentValue: ACP_MODE_ID,
720
- options: [{ value: ACP_MODE_ID, name: "Default", description: "Standard ACP headless mode" }],
928
+ currentValue: currentModeId,
929
+ options: modeOptions,
721
930
  },
722
931
  ];
723
932
 
@@ -805,17 +1014,52 @@ export class AcpAgent implements Agent {
805
1014
  return `${model.provider}/${model.id}`;
806
1015
  }
807
1016
 
808
- #buildModeState(): SessionModeState {
1017
+ #getAvailableModes(session: AgentSession): Array<{ id: string; name: string; description: string }> {
1018
+ const modes = [{ id: ACP_DEFAULT_MODE_ID, name: "Default", description: "Standard ACP headless mode" }];
1019
+ if (Settings.instance.get("plan.enabled")) {
1020
+ modes.push({
1021
+ id: ACP_PLAN_MODE_ID,
1022
+ name: "Plan",
1023
+ description: "Read-only planning mode that drafts a plan to a markdown file before any code changes",
1024
+ });
1025
+ }
1026
+ void session;
1027
+ return modes;
1028
+ }
1029
+
1030
+ #getCurrentModeId(session: AgentSession): string {
1031
+ return session.getPlanModeState()?.enabled ? ACP_PLAN_MODE_ID : ACP_DEFAULT_MODE_ID;
1032
+ }
1033
+
1034
+ #applyModeChange(session: AgentSession, modeId: string): void {
1035
+ const availableModes = this.#getAvailableModes(session);
1036
+ if (!availableModes.some(mode => mode.id === modeId)) {
1037
+ throw new Error(`Unsupported ACP mode: ${modeId}`);
1038
+ }
1039
+ if (modeId === ACP_PLAN_MODE_ID) {
1040
+ const previous = session.getPlanModeState();
1041
+ session.setPlanModeState({
1042
+ enabled: true,
1043
+ planFilePath: previous?.planFilePath ?? DEFAULT_PLAN_FILE_URL,
1044
+ workflow: previous?.workflow ?? "parallel",
1045
+ reentry: previous !== undefined,
1046
+ });
1047
+ } else {
1048
+ session.setPlanModeState(undefined);
1049
+ }
1050
+ }
1051
+
1052
+ #buildModeState(session: AgentSession): SessionModeState {
809
1053
  return {
810
- availableModes: [{ id: ACP_MODE_ID, name: "Default", description: "Standard ACP headless mode" }],
811
- currentModeId: ACP_MODE_ID,
1054
+ availableModes: this.#getAvailableModes(session),
1055
+ currentModeId: this.#getCurrentModeId(session),
812
1056
  };
813
1057
  }
814
1058
 
815
- #buildCurrentModeUpdate(): SessionUpdate {
1059
+ #buildCurrentModeUpdate(session: AgentSession): SessionUpdate {
816
1060
  return {
817
1061
  sessionUpdate: "current_mode_update",
818
- currentModeId: ACP_MODE_ID,
1062
+ currentModeId: this.#getCurrentModeId(session),
819
1063
  };
820
1064
  }
821
1065
 
@@ -830,6 +1074,24 @@ export class AcpAgent implements Agent {
830
1074
  commands.push(command);
831
1075
  };
832
1076
 
1077
+ // Advertise in the order dispatch resolves them: ACP builtins first
1078
+ // (so core commands like `/model`, `/mcp`, `/todo` cannot be shadowed),
1079
+ // then skills, then custom/user commands, then file-based slash
1080
+ // commands. `appendCommand` dedupes by name so earlier entries win.
1081
+ for (const command of ACP_BUILTIN_SLASH_COMMANDS) {
1082
+ appendCommand(command);
1083
+ }
1084
+
1085
+ if (session.skillsSettings?.enableSkillCommands) {
1086
+ for (const skill of session.skills) {
1087
+ appendCommand({
1088
+ name: getSkillSlashCommandName(skill),
1089
+ description: skill.description || `Run ${skill.name} skill`,
1090
+ input: { hint: "arguments" },
1091
+ });
1092
+ }
1093
+ }
1094
+
833
1095
  for (const command of session.customCommands) {
834
1096
  appendCommand({
835
1097
  name: command.command.name,
@@ -854,10 +1116,26 @@ export class AcpAgent implements Agent {
854
1116
  cwd: session.cwd,
855
1117
  title: session.title,
856
1118
  updatedAt: session.modified.toISOString(),
1119
+ _meta: {
1120
+ messageCount: session.messageCount,
1121
+ size: session.size,
1122
+ },
857
1123
  };
858
1124
  }
859
1125
 
860
1126
  #scheduleBootstrapUpdates(sessionId: string): void {
1127
+ // Delay the bootstrap so the client has time to handle the `session/new`
1128
+ // (or `session/load` / `session/resume`) RPC response and register the
1129
+ // new sessionId before we start firing notifications against it. Zed's
1130
+ // agent-client-protocol reader dispatches responses and notifications
1131
+ // to different async tasks; sending the first `available_commands_update`
1132
+ // from `setTimeout(0)` reliably loses the race against the response
1133
+ // handler and Zed logs `Received session notification for unknown
1134
+ // session` then drops the update — leaving the slash-command palette
1135
+ // empty (#1015 follow-up; see zed-industries/zed#55965 for the same
1136
+ // race biting other ACP agents). 50ms is invisible to the operator and
1137
+ // large enough that the response future has scheduled before our timer
1138
+ // fires on stdio-only transports.
861
1139
  setTimeout(() => {
862
1140
  if (this.#connection.signal.aborted) {
863
1141
  return;
@@ -867,7 +1145,7 @@ export class AcpAgent implements Agent {
867
1145
  return;
868
1146
  }
869
1147
  void this.#emitBootstrapUpdates(sessionId, record);
870
- }, 0);
1148
+ }, 50);
871
1149
  }
872
1150
 
873
1151
  async #emitBootstrapUpdates(sessionId: string, record: ManagedSessionRecord): Promise<void> {
@@ -891,6 +1169,33 @@ export class AcpAgent implements Agent {
891
1169
  });
892
1170
  }
893
1171
 
1172
+ async #emitAvailableCommandsUpdate(record: ManagedSessionRecord): Promise<void> {
1173
+ await this.#connection.sessionUpdate({
1174
+ sessionId: record.session.sessionId,
1175
+ update: {
1176
+ sessionUpdate: "available_commands_update",
1177
+ availableCommands: await this.#buildAvailableCommands(record.session),
1178
+ },
1179
+ });
1180
+ }
1181
+
1182
+ /**
1183
+ * Reload plugin/registry state for an ACP session. Mirrors the interactive
1184
+ * `/reload-plugins` and `/move` flows: invalidates the plugin-roots cache,
1185
+ * resets the capability cache, refreshes the session's slash-command state,
1186
+ * then re-advertises commands so the client sees newly installed/disabled
1187
+ * plugins.
1188
+ */
1189
+ async #reloadPluginState(record: ManagedSessionRecord): Promise<void> {
1190
+ const cwd = record.session.sessionManager.getCwd();
1191
+ const projectPath = await resolveActiveProjectRegistryPath(cwd);
1192
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
1193
+ resetCapabilities();
1194
+ const fileCommands = await loadSlashCommands({ cwd });
1195
+ record.session.setSlashCommands(fileCommands);
1196
+ await this.#emitAvailableCommandsUpdate(record);
1197
+ }
1198
+
894
1199
  async #emitEndOfTurnUpdates(record: ManagedSessionRecord): Promise<void> {
895
1200
  const sessionId = record.session.sessionId;
896
1201
 
@@ -981,14 +1286,15 @@ export class AcpAgent implements Agent {
981
1286
  }
982
1287
 
983
1288
  async #replaySessionHistory(record: ManagedSessionRecord): Promise<void> {
1289
+ const cwd = record.session.sessionManager.getCwd();
984
1290
  for (const message of record.session.sessionManager.buildSessionContext().messages as ReplayableMessage[]) {
985
- for (const notification of this.#messageToReplayNotifications(record.session.sessionId, message)) {
1291
+ for (const notification of this.#messageToReplayNotifications(record.session.sessionId, message, cwd)) {
986
1292
  await this.#connection.sessionUpdate(notification);
987
1293
  }
988
1294
  }
989
1295
  }
990
1296
 
991
- #messageToReplayNotifications(sessionId: string, message: ReplayableMessage): SessionNotification[] {
1297
+ #messageToReplayNotifications(sessionId: string, message: ReplayableMessage, cwd: string): SessionNotification[] {
992
1298
  if (message.role === "assistant") {
993
1299
  return this.#replayAssistantMessage(sessionId, message);
994
1300
  }
@@ -1010,7 +1316,7 @@ export class AcpAgent implements Agent {
1010
1316
  typeof message.toolCallId === "string" &&
1011
1317
  typeof message.toolName === "string"
1012
1318
  ) {
1013
- return this.#replayToolResult(sessionId, {
1319
+ return this.#replayToolResult(sessionId, cwd, {
1014
1320
  ...message,
1015
1321
  toolCallId: message.toolCallId,
1016
1322
  toolName: message.toolName,
@@ -1102,6 +1408,7 @@ export class AcpAgent implements Agent {
1102
1408
 
1103
1409
  #replayToolResult(
1104
1410
  sessionId: string,
1411
+ cwd: string,
1105
1412
  message: Required<Pick<ReplayableMessage, "toolCallId" | "toolName">> & ReplayableMessage,
1106
1413
  ): SessionNotification[] {
1107
1414
  const args = this.#buildReplayToolArgs(message.details);
@@ -1123,8 +1430,8 @@ export class AcpAgent implements Agent {
1123
1430
  },
1124
1431
  };
1125
1432
  return [
1126
- ...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId),
1127
- ...mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId),
1433
+ ...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId, { cwd }),
1434
+ ...mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, { cwd }),
1128
1435
  ];
1129
1436
  }
1130
1437