@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.
- package/CHANGELOG.md +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- 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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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.#
|
|
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
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
655
|
-
record.
|
|
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:
|
|
720
|
-
options:
|
|
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
|
-
#
|
|
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:
|
|
811
|
-
currentModeId:
|
|
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:
|
|
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
|
-
},
|
|
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();
|