@mseep/obsidian-agent-client 0.10.6
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/.claude/hooks/gh-setup.sh +49 -0
- package/.claude/settings.json +15 -0
- package/.claude/skills/release-notes/SKILL.md +331 -0
- package/.editorconfig +10 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
- package/.github/copilot-instructions.md +45 -0
- package/.github/pull_request_template.md +32 -0
- package/.github/workflows/ci.yaml +25 -0
- package/.github/workflows/docs.yml +58 -0
- package/.github/workflows/relay_to_openclaw.yml +59 -0
- package/.github/workflows/release.yaml +45 -0
- package/.prettierignore +10 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +37 -0
- package/.zed/settings.json +42 -0
- package/AGENTS.md +330 -0
- package/ARCHITECTURE.md +390 -0
- package/CONTRIBUTING.md +216 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.ja.md +121 -0
- package/README.md +125 -0
- package/docs/.vitepress/config.mts +124 -0
- package/docs/.vitepress/theme/custom.css +111 -0
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/agent-setup/claude-code.md +84 -0
- package/docs/agent-setup/codex.md +76 -0
- package/docs/agent-setup/custom-agents.md +67 -0
- package/docs/agent-setup/gemini-cli.md +99 -0
- package/docs/agent-setup/index.md +34 -0
- package/docs/announcements/gemini-cli-deprecation.md +73 -0
- package/docs/getting-started/index.md +78 -0
- package/docs/getting-started/quick-start.md +38 -0
- package/docs/help/faq.md +181 -0
- package/docs/help/troubleshooting.md +221 -0
- package/docs/index.md +63 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/demo.mp4 +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/public/images/editing.webp +0 -0
- package/docs/public/images/export.webp +0 -0
- package/docs/public/images/floating-chat-button.webp +0 -0
- package/docs/public/images/floating-chat-instance-menu.webp +0 -0
- package/docs/public/images/floating-chat-view.webp +0 -0
- package/docs/public/images/mode-selection.webp +0 -0
- package/docs/public/images/model-selection.webp +0 -0
- package/docs/public/images/multi-session.webp +0 -0
- package/docs/public/images/remove-image.webp +0 -0
- package/docs/public/images/ribbon-icon.webp +0 -0
- package/docs/public/images/selection-context.gif +0 -0
- package/docs/public/images/sending-images.webp +0 -0
- package/docs/public/images/sending-messages.webp +0 -0
- package/docs/public/images/session-history-button.webp +0 -0
- package/docs/public/images/slash-commands-1.webp +0 -0
- package/docs/public/images/slash-commands-2.webp +0 -0
- package/docs/public/images/switch-agent.webp +0 -0
- package/docs/public/images/switch-default-agent.webp +0 -0
- package/docs/public/images/temporary-disable.gif +0 -0
- package/docs/reference/acp-support.md +110 -0
- package/docs/usage/chat-export.md +80 -0
- package/docs/usage/commands.md +51 -0
- package/docs/usage/context-files.md +57 -0
- package/docs/usage/editing.md +69 -0
- package/docs/usage/floating-chat.md +84 -0
- package/docs/usage/index.md +97 -0
- package/docs/usage/mcp-tools.md +33 -0
- package/docs/usage/mentions.md +70 -0
- package/docs/usage/mode-selection.md +28 -0
- package/docs/usage/model-selection.md +32 -0
- package/docs/usage/multi-session.md +68 -0
- package/docs/usage/sending-images.md +64 -0
- package/docs/usage/session-history.md +91 -0
- package/docs/usage/slash-commands.md +44 -0
- package/esbuild.config.mjs +49 -0
- package/eslint.config.mjs +25 -0
- package/main.js +228 -0
- package/manifest.json +11 -0
- package/package.json +52 -0
- package/src/acp/acp-client.ts +921 -0
- package/src/acp/acp-handler.ts +252 -0
- package/src/acp/permission-handler.ts +282 -0
- package/src/acp/terminal-handler.ts +264 -0
- package/src/acp/type-converter.ts +272 -0
- package/src/hooks/useAgent.ts +250 -0
- package/src/hooks/useAgentMessages.ts +470 -0
- package/src/hooks/useAgentSession.ts +544 -0
- package/src/hooks/useChatActions.ts +400 -0
- package/src/hooks/useHistoryModal.ts +219 -0
- package/src/hooks/useSessionHistory.ts +863 -0
- package/src/hooks/useSettings.ts +19 -0
- package/src/hooks/useSuggestions.ts +342 -0
- package/src/main.ts +9 -0
- package/src/plugin.ts +1126 -0
- package/src/services/chat-exporter.ts +552 -0
- package/src/services/message-sender.ts +755 -0
- package/src/services/message-state.ts +375 -0
- package/src/services/session-helpers.ts +211 -0
- package/src/services/session-state.ts +130 -0
- package/src/services/session-storage.ts +267 -0
- package/src/services/settings-normalizer.ts +255 -0
- package/src/services/settings-service.ts +285 -0
- package/src/services/update-checker.ts +128 -0
- package/src/services/vault-service.ts +558 -0
- package/src/services/view-registry.ts +345 -0
- package/src/types/agent.ts +92 -0
- package/src/types/chat.ts +351 -0
- package/src/types/errors.ts +136 -0
- package/src/types/obsidian-internals.d.ts +14 -0
- package/src/types/session.ts +731 -0
- package/src/ui/ChangeDirectoryModal.ts +137 -0
- package/src/ui/ChatContext.ts +25 -0
- package/src/ui/ChatHeader.tsx +295 -0
- package/src/ui/ChatPanel.tsx +1162 -0
- package/src/ui/ChatView.tsx +348 -0
- package/src/ui/ErrorBanner.tsx +104 -0
- package/src/ui/FloatingButton.tsx +351 -0
- package/src/ui/FloatingChatView.tsx +531 -0
- package/src/ui/InputArea.tsx +1107 -0
- package/src/ui/InputToolbar.tsx +371 -0
- package/src/ui/MessageBubble.tsx +442 -0
- package/src/ui/MessageList.tsx +265 -0
- package/src/ui/PermissionBanner.tsx +61 -0
- package/src/ui/SessionHistoryModal.tsx +821 -0
- package/src/ui/SettingsTab.ts +1337 -0
- package/src/ui/SuggestionPopup.tsx +138 -0
- package/src/ui/TerminalBlock.tsx +107 -0
- package/src/ui/ToolCallBlock.tsx +456 -0
- package/src/ui/shared/AttachmentStrip.tsx +57 -0
- package/src/ui/shared/IconButton.tsx +55 -0
- package/src/ui/shared/MarkdownRenderer.tsx +103 -0
- package/src/ui/view-host.ts +56 -0
- package/src/utils/error-utils.ts +274 -0
- package/src/utils/logger.ts +44 -0
- package/src/utils/mention-parser.ts +129 -0
- package/src/utils/paths.ts +246 -0
- package/src/utils/platform.ts +425 -0
- package/styles.css +2322 -0
- package/tsconfig.json +18 -0
- package/version-bump.mjs +18 -0
- package/versions.json +3 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from "child_process";
|
|
2
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
3
|
+
import { Platform } from "obsidian";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
InitializeResult,
|
|
7
|
+
SessionConfigOption,
|
|
8
|
+
SessionUpdate,
|
|
9
|
+
ListSessionsResult,
|
|
10
|
+
SessionResult,
|
|
11
|
+
} from "../types/session";
|
|
12
|
+
import type { PromptContent } from "../types/chat";
|
|
13
|
+
import type { ProcessError } from "../types/errors";
|
|
14
|
+
import { AcpTypeConverter } from "./type-converter";
|
|
15
|
+
import { TerminalManager } from "./terminal-handler";
|
|
16
|
+
import { PermissionManager } from "./permission-handler";
|
|
17
|
+
import { AcpHandler } from "./acp-handler";
|
|
18
|
+
import { getLogger, Logger } from "../utils/logger";
|
|
19
|
+
import type AgentClientPlugin from "../plugin";
|
|
20
|
+
import {
|
|
21
|
+
convertWindowsPathToWsl,
|
|
22
|
+
getEnhancedWindowsEnv,
|
|
23
|
+
prepareShellCommand,
|
|
24
|
+
} from "../utils/platform";
|
|
25
|
+
import { resolveNodeDirectory } from "../utils/paths";
|
|
26
|
+
import {
|
|
27
|
+
extractStderrErrorHint,
|
|
28
|
+
getSpawnErrorInfo,
|
|
29
|
+
getCommandNotFoundSuggestion,
|
|
30
|
+
isEmptyResponseError,
|
|
31
|
+
isUserAbortedError,
|
|
32
|
+
} from "../utils/error-utils";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Runtime configuration for launching an AI agent process.
|
|
36
|
+
* Converted from BaseAgentSettings by toAgentConfig() in settings-service.
|
|
37
|
+
*/
|
|
38
|
+
export interface AgentConfig {
|
|
39
|
+
id: string;
|
|
40
|
+
displayName: string;
|
|
41
|
+
command: string;
|
|
42
|
+
args: string[];
|
|
43
|
+
env?: Record<string, string>;
|
|
44
|
+
workingDirectory: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result of polling terminal output.
|
|
49
|
+
*/
|
|
50
|
+
export interface TerminalOutputResult {
|
|
51
|
+
output: string;
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
exitStatus: {
|
|
54
|
+
exitCode: number | null;
|
|
55
|
+
signal: string | null;
|
|
56
|
+
} | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* ACP client for agent communication and process lifecycle management.
|
|
61
|
+
*/
|
|
62
|
+
export class AcpClient {
|
|
63
|
+
// Connection & process
|
|
64
|
+
private connection: acp.ClientSideConnection | null = null;
|
|
65
|
+
private agentProcess: ChildProcess | null = null;
|
|
66
|
+
private currentConfig: AgentConfig | null = null;
|
|
67
|
+
private isInitializedFlag = false;
|
|
68
|
+
private currentAgentId: string | null = null;
|
|
69
|
+
private currentSessionId: string | null = null;
|
|
70
|
+
|
|
71
|
+
// Callbacks (none — all events flow through onSessionUpdate via AcpHandler)
|
|
72
|
+
|
|
73
|
+
// Delegates
|
|
74
|
+
private terminalManager: TerminalManager;
|
|
75
|
+
private permissionManager: PermissionManager;
|
|
76
|
+
private handler: AcpHandler;
|
|
77
|
+
|
|
78
|
+
// Prompt state (reset per sendPrompt)
|
|
79
|
+
private recentStderr = "";
|
|
80
|
+
|
|
81
|
+
private logger: Logger;
|
|
82
|
+
|
|
83
|
+
constructor(private plugin: AgentClientPlugin) {
|
|
84
|
+
this.logger = getLogger();
|
|
85
|
+
|
|
86
|
+
// Initialize managers
|
|
87
|
+
this.terminalManager = new TerminalManager(plugin);
|
|
88
|
+
this.permissionManager = new PermissionManager(
|
|
89
|
+
{
|
|
90
|
+
onSessionUpdate: (update) =>
|
|
91
|
+
this.handler.emitSessionUpdate(update),
|
|
92
|
+
},
|
|
93
|
+
false, // autoAllow — updated in initialize()
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Initialize protocol handler
|
|
97
|
+
this.handler = new AcpHandler(
|
|
98
|
+
this.permissionManager,
|
|
99
|
+
this.terminalManager,
|
|
100
|
+
() => this.currentConfig?.workingDirectory ?? "",
|
|
101
|
+
() => this.currentSessionId,
|
|
102
|
+
this.logger,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Initialize connection to an AI agent.
|
|
108
|
+
* Spawns the agent process and establishes ACP connection.
|
|
109
|
+
*/
|
|
110
|
+
async initialize(config: AgentConfig): Promise<InitializeResult> {
|
|
111
|
+
this.logger.log(
|
|
112
|
+
"[AcpClient] Starting initialization with config:",
|
|
113
|
+
config,
|
|
114
|
+
);
|
|
115
|
+
this.logger.log(
|
|
116
|
+
`[AcpClient] Current state - process: ${!!this.agentProcess}, PID: ${this.agentProcess?.pid}`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Clean up existing process if any (e.g., when switching agents)
|
|
120
|
+
if (this.agentProcess) {
|
|
121
|
+
this.killProcessTree();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Clean up existing connection
|
|
125
|
+
if (this.connection) {
|
|
126
|
+
this.logger.log("[AcpClient] Cleaning up existing connection");
|
|
127
|
+
this.connection = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.currentConfig = config;
|
|
131
|
+
|
|
132
|
+
// Update auto-allow permissions from plugin settings
|
|
133
|
+
this.permissionManager.setAutoAllow(
|
|
134
|
+
this.plugin.settings.autoAllowPermissions,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Validate command
|
|
138
|
+
if (!config.command || config.command.trim().length === 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Command not configured for agent "${config.displayName}" (${config.id}). Please configure the agent command in settings.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const command = config.command.trim();
|
|
145
|
+
const args = config.args.length > 0 ? [...config.args] : [];
|
|
146
|
+
|
|
147
|
+
this.logger.log(
|
|
148
|
+
`[AcpClient] Active agent: ${config.displayName} (${config.id})`,
|
|
149
|
+
);
|
|
150
|
+
this.logger.log("[AcpClient] Command:", command);
|
|
151
|
+
this.logger.log(
|
|
152
|
+
"[AcpClient] Args:",
|
|
153
|
+
args.length > 0 ? args.join(" ") : "(none)",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Prepare environment variables
|
|
157
|
+
let baseEnv: NodeJS.ProcessEnv = {
|
|
158
|
+
...process.env,
|
|
159
|
+
...(config.env || {}),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// On Windows, enhance PATH with full system/user PATH from registry.
|
|
163
|
+
// Electron apps launched from shortcuts don't inherit the full PATH,
|
|
164
|
+
// which causes executables like python, node, etc. to not be found.
|
|
165
|
+
if (Platform.isWin && !this.plugin.settings.windowsWslMode) {
|
|
166
|
+
baseEnv = getEnhancedWindowsEnv(baseEnv);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Add Node.js directory to PATH only when nodePath is an explicit absolute path.
|
|
170
|
+
// When nodePath is empty or a bare command name, the login shell handles it.
|
|
171
|
+
const nodeDir = resolveNodeDirectory(this.plugin.settings.nodePath);
|
|
172
|
+
if (nodeDir) {
|
|
173
|
+
const separator = Platform.isWin ? ";" : ":";
|
|
174
|
+
baseEnv.PATH = baseEnv.PATH
|
|
175
|
+
? `${nodeDir}${separator}${baseEnv.PATH}`
|
|
176
|
+
: nodeDir;
|
|
177
|
+
this.logger.log(
|
|
178
|
+
"[AcpClient] Node.js directory added to PATH:",
|
|
179
|
+
nodeDir,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.logger.log(
|
|
184
|
+
"[AcpClient] Starting agent process in directory:",
|
|
185
|
+
config.workingDirectory,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Prepare command and args for spawning (platform-specific shell wrapping)
|
|
189
|
+
const prepared = prepareShellCommand(
|
|
190
|
+
command,
|
|
191
|
+
args,
|
|
192
|
+
config.workingDirectory,
|
|
193
|
+
{
|
|
194
|
+
wslMode: this.plugin.settings.windowsWslMode,
|
|
195
|
+
wslDistribution: this.plugin.settings.windowsWslDistribution,
|
|
196
|
+
nodeDir,
|
|
197
|
+
alwaysEscape: true,
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
const spawnCommand = prepared.command;
|
|
201
|
+
const spawnArgs = prepared.args;
|
|
202
|
+
const needsShell = prepared.needsShell;
|
|
203
|
+
|
|
204
|
+
this.logger.log(
|
|
205
|
+
"[AcpClient] Prepared spawn command:",
|
|
206
|
+
spawnCommand,
|
|
207
|
+
spawnArgs,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Spawn the agent process
|
|
211
|
+
// detached: true (Unix only) creates a new process group, allowing us to kill
|
|
212
|
+
// the entire process tree (agent + child processes) with process.kill(-pid).
|
|
213
|
+
// On Windows, detached: true opens a new console window, so we skip it
|
|
214
|
+
// and use taskkill /T instead for tree kill.
|
|
215
|
+
const agentProcess = spawn(spawnCommand, spawnArgs, {
|
|
216
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
217
|
+
env: baseEnv,
|
|
218
|
+
cwd: config.workingDirectory,
|
|
219
|
+
shell: needsShell,
|
|
220
|
+
detached: !Platform.isWin,
|
|
221
|
+
});
|
|
222
|
+
this.agentProcess = agentProcess;
|
|
223
|
+
|
|
224
|
+
const agentLabel = `${config.displayName} (${config.id})`;
|
|
225
|
+
|
|
226
|
+
// Set up process event handlers
|
|
227
|
+
agentProcess.on("spawn", () => {
|
|
228
|
+
this.logger.log(
|
|
229
|
+
`[AcpClient] ${agentLabel} process spawned successfully, PID:`,
|
|
230
|
+
agentProcess.pid,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
agentProcess.on("error", (error) => {
|
|
235
|
+
this.logger.error(
|
|
236
|
+
`[AcpClient] ${agentLabel} process error:`,
|
|
237
|
+
error,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const processError: ProcessError = {
|
|
241
|
+
type: "spawn_failed",
|
|
242
|
+
agentId: config.id,
|
|
243
|
+
errorCode: (error as NodeJS.ErrnoException).code,
|
|
244
|
+
originalError: error,
|
|
245
|
+
...getSpawnErrorInfo(
|
|
246
|
+
error,
|
|
247
|
+
command,
|
|
248
|
+
agentLabel,
|
|
249
|
+
this.plugin.settings.windowsWslMode,
|
|
250
|
+
),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
this.handler.emitSessionUpdate({
|
|
254
|
+
type: "process_error",
|
|
255
|
+
sessionId: this.currentSessionId ?? "",
|
|
256
|
+
error: processError,
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
agentProcess.on("exit", (code, signal) => {
|
|
261
|
+
this.logger.log(
|
|
262
|
+
`[AcpClient] ${agentLabel} process exited with code:`,
|
|
263
|
+
code,
|
|
264
|
+
"signal:",
|
|
265
|
+
signal,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (code === 127) {
|
|
269
|
+
this.logger.error(`[AcpClient] Command not found: ${command}`);
|
|
270
|
+
|
|
271
|
+
const processError: ProcessError = {
|
|
272
|
+
type: "command_not_found",
|
|
273
|
+
agentId: config.id,
|
|
274
|
+
exitCode: code,
|
|
275
|
+
title: "Command Not Found",
|
|
276
|
+
message: `The command "${command}" could not be found. Please check the path configuration for ${agentLabel}.`,
|
|
277
|
+
suggestion: getCommandNotFoundSuggestion(
|
|
278
|
+
command,
|
|
279
|
+
this.plugin.settings.windowsWslMode,
|
|
280
|
+
),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
this.handler.emitSessionUpdate({
|
|
284
|
+
type: "process_error",
|
|
285
|
+
sessionId: this.currentSessionId ?? "",
|
|
286
|
+
error: processError,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
agentProcess.on("close", (code, signal) => {
|
|
292
|
+
this.logger.log(
|
|
293
|
+
`[AcpClient] ${agentLabel} process closed with code:`,
|
|
294
|
+
code,
|
|
295
|
+
"signal:",
|
|
296
|
+
signal,
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
agentProcess.stderr?.setEncoding("utf8");
|
|
301
|
+
agentProcess.stderr?.on("data", (data) => {
|
|
302
|
+
this.logger.log(`[AcpClient] ${agentLabel} stderr:`, data);
|
|
303
|
+
// Keep a rolling window of recent stderr for error diagnostics
|
|
304
|
+
this.recentStderr += data;
|
|
305
|
+
if (this.recentStderr.length > 8192) {
|
|
306
|
+
this.recentStderr = this.recentStderr.slice(-4096);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Create stream for ACP communication
|
|
311
|
+
// stdio is configured as ["pipe", "pipe", "pipe"] so stdin/stdout are guaranteed to exist
|
|
312
|
+
if (!agentProcess.stdin || !agentProcess.stdout) {
|
|
313
|
+
throw new Error("Agent process stdin/stdout not available");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const stdin = agentProcess.stdin;
|
|
317
|
+
const stdout = agentProcess.stdout;
|
|
318
|
+
|
|
319
|
+
const input = new WritableStream<Uint8Array>({
|
|
320
|
+
write(chunk: Uint8Array) {
|
|
321
|
+
stdin.write(chunk);
|
|
322
|
+
},
|
|
323
|
+
close() {
|
|
324
|
+
stdin.end();
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
const output = new ReadableStream<Uint8Array>({
|
|
328
|
+
start(controller) {
|
|
329
|
+
stdout.on("data", (chunk: Uint8Array) => {
|
|
330
|
+
controller.enqueue(chunk);
|
|
331
|
+
});
|
|
332
|
+
stdout.on("end", () => {
|
|
333
|
+
controller.close();
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
this.logger.log(
|
|
339
|
+
"[AcpClient] Using working directory:",
|
|
340
|
+
config.workingDirectory,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const stream = acp.ndJsonStream(input, output);
|
|
344
|
+
this.connection = new acp.ClientSideConnection(
|
|
345
|
+
() => this.handler,
|
|
346
|
+
stream,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
this.logger.log("[AcpClient] Starting ACP initialization...");
|
|
351
|
+
|
|
352
|
+
const initResult = await this.connection.initialize({
|
|
353
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
354
|
+
clientCapabilities: {
|
|
355
|
+
fs: {
|
|
356
|
+
readTextFile: false,
|
|
357
|
+
writeTextFile: false,
|
|
358
|
+
},
|
|
359
|
+
terminal: true,
|
|
360
|
+
},
|
|
361
|
+
clientInfo: {
|
|
362
|
+
name: "obsidian-agent-client",
|
|
363
|
+
title: "Agent Client for Obsidian",
|
|
364
|
+
version: this.plugin.manifest.version,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
this.logger.log(
|
|
369
|
+
`[AcpClient] ✅ Connected to agent (protocol v${initResult.protocolVersion})`,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
this.isInitializedFlag = true;
|
|
373
|
+
this.currentAgentId = config.id;
|
|
374
|
+
|
|
375
|
+
return AcpTypeConverter.toInitializeResult(initResult);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
this.logger.error("[AcpClient] Initialization Error:", error);
|
|
378
|
+
|
|
379
|
+
// Reset flags on failure
|
|
380
|
+
this.isInitializedFlag = false;
|
|
381
|
+
this.currentAgentId = null;
|
|
382
|
+
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Create a new chat session with the agent.
|
|
389
|
+
*/
|
|
390
|
+
async newSession(workingDirectory: string): Promise<SessionResult> {
|
|
391
|
+
const connection = this.requireConnection();
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
this.logger.log("[AcpClient] Creating new session...");
|
|
395
|
+
|
|
396
|
+
const response = await connection.newSession({
|
|
397
|
+
cwd: this.toSessionCwd(workingDirectory),
|
|
398
|
+
mcpServers: [],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
this.logger.log(
|
|
402
|
+
`[AcpClient] Created session: ${response.sessionId}`,
|
|
403
|
+
);
|
|
404
|
+
const result = AcpTypeConverter.toSessionResult(
|
|
405
|
+
response.sessionId,
|
|
406
|
+
response,
|
|
407
|
+
);
|
|
408
|
+
this.currentSessionId = result.sessionId;
|
|
409
|
+
return result;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.logger.error("[AcpClient] New Session Error:", error);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Authenticate with the agent using a specific method.
|
|
418
|
+
*/
|
|
419
|
+
async authenticate(methodId: string): Promise<boolean> {
|
|
420
|
+
const connection = this.requireConnection();
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
await connection.authenticate({ methodId });
|
|
424
|
+
this.logger.log("[AcpClient] ✅ authenticate ok:", methodId);
|
|
425
|
+
return true;
|
|
426
|
+
} catch (error: unknown) {
|
|
427
|
+
this.logger.error("[AcpClient] Authentication Error:", error);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Send a message to the agent in a specific session.
|
|
434
|
+
*/
|
|
435
|
+
async sendPrompt(
|
|
436
|
+
sessionId: string,
|
|
437
|
+
content: PromptContent[],
|
|
438
|
+
): Promise<void> {
|
|
439
|
+
const connection = this.requireConnection();
|
|
440
|
+
|
|
441
|
+
this.handler.resetUpdateCount();
|
|
442
|
+
this.recentStderr = "";
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
// Convert domain PromptContent to ACP ContentBlock
|
|
446
|
+
const acpContent = content.map((c) =>
|
|
447
|
+
AcpTypeConverter.toAcpContentBlock(c),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
this.logger.log(
|
|
451
|
+
`[AcpClient] Sending prompt with ${content.length} content blocks`,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const promptResult = await connection.prompt({
|
|
455
|
+
sessionId: sessionId,
|
|
456
|
+
prompt: acpContent,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
this.logger.log(
|
|
460
|
+
`[AcpClient] Agent completed with: ${promptResult.stopReason}`,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Detect silent failures: agent returned end_turn but sent no content.
|
|
464
|
+
// Only surface an error when stderr contains a recognized error pattern
|
|
465
|
+
// (e.g., missing API key). Some commands like /compact legitimately
|
|
466
|
+
// return no session updates, so we avoid false positives.
|
|
467
|
+
if (
|
|
468
|
+
!this.handler.hasReceivedUpdates() &&
|
|
469
|
+
promptResult.stopReason === "end_turn"
|
|
470
|
+
) {
|
|
471
|
+
// Allow pending stderr data events to flush before checking
|
|
472
|
+
await new Promise((r) => window.setTimeout(r, 100));
|
|
473
|
+
|
|
474
|
+
const stderrHint = extractStderrErrorHint(this.recentStderr);
|
|
475
|
+
if (stderrHint) {
|
|
476
|
+
this.logger.warn(
|
|
477
|
+
"[AcpClient] Agent returned end_turn with no session updates — detected error in stderr",
|
|
478
|
+
);
|
|
479
|
+
throw new Error(
|
|
480
|
+
`The agent returned an empty response. ${stderrHint}`,
|
|
481
|
+
);
|
|
482
|
+
} else {
|
|
483
|
+
this.logger.log(
|
|
484
|
+
"[AcpClient] Agent returned end_turn with no session updates (may be expected for some commands)",
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} catch (error: unknown) {
|
|
489
|
+
if (isEmptyResponseError(error) || isUserAbortedError(error)) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
this.logger.error("[AcpClient] Prompt Error:", error);
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Cancel the current operation in a session.
|
|
499
|
+
*/
|
|
500
|
+
async cancel(sessionId: string): Promise<void> {
|
|
501
|
+
if (!this.connection) {
|
|
502
|
+
this.cancelAllOperations();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
this.logger.log(
|
|
507
|
+
"[AcpClient] Sending session/cancel notification...",
|
|
508
|
+
);
|
|
509
|
+
await this.connection.cancel({ sessionId });
|
|
510
|
+
this.logger.log(
|
|
511
|
+
"[AcpClient] Cancellation request sent successfully",
|
|
512
|
+
);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
this.logger.warn("[AcpClient] Failed to send cancellation:", error);
|
|
515
|
+
} finally {
|
|
516
|
+
this.cancelAllOperations();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Kill the agent process and its entire process tree.
|
|
522
|
+
* Uses process.kill(-pid) to send SIGTERM to the process group
|
|
523
|
+
* (requires detached: true on spawn). Falls back to regular kill.
|
|
524
|
+
* On Windows, uses taskkill /T for tree kill.
|
|
525
|
+
*/
|
|
526
|
+
private killProcessTree(): void {
|
|
527
|
+
if (!this.agentProcess) return;
|
|
528
|
+
|
|
529
|
+
const pid = this.agentProcess.pid;
|
|
530
|
+
this.logger.log(`[AcpClient] Killing process tree (PID: ${pid})`);
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
if (Platform.isWin && pid) {
|
|
534
|
+
// Windows: taskkill /T kills the entire process tree
|
|
535
|
+
spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
536
|
+
stdio: "ignore",
|
|
537
|
+
});
|
|
538
|
+
} else if (pid) {
|
|
539
|
+
// Unix: kill the entire process group (negative PID)
|
|
540
|
+
// Requires detached: true on spawn to create a process group
|
|
541
|
+
process.kill(-pid, "SIGTERM");
|
|
542
|
+
} else {
|
|
543
|
+
this.agentProcess.kill();
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// Fallback: kill just the direct process
|
|
547
|
+
try {
|
|
548
|
+
this.agentProcess.kill();
|
|
549
|
+
} catch {
|
|
550
|
+
// Process may already be dead
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.agentProcess = null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Disconnect from the agent and clean up resources.
|
|
559
|
+
*/
|
|
560
|
+
disconnect(): Promise<void> {
|
|
561
|
+
this.logger.log("[AcpClient] Disconnecting...");
|
|
562
|
+
|
|
563
|
+
// Cancel all pending operations
|
|
564
|
+
this.cancelAllOperations();
|
|
565
|
+
|
|
566
|
+
// Kill the agent process tree
|
|
567
|
+
this.killProcessTree();
|
|
568
|
+
|
|
569
|
+
// Clear connection and config references
|
|
570
|
+
this.connection = null;
|
|
571
|
+
this.currentConfig = null;
|
|
572
|
+
|
|
573
|
+
// Reset initialization state
|
|
574
|
+
this.isInitializedFlag = false;
|
|
575
|
+
this.currentAgentId = null;
|
|
576
|
+
this.currentSessionId = null;
|
|
577
|
+
|
|
578
|
+
this.logger.log("[AcpClient] Disconnected");
|
|
579
|
+
return Promise.resolve();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Check if the agent connection is initialized and ready.
|
|
584
|
+
*/
|
|
585
|
+
isInitialized(): boolean {
|
|
586
|
+
return (
|
|
587
|
+
this.isInitializedFlag &&
|
|
588
|
+
this.connection !== null &&
|
|
589
|
+
this.agentProcess !== null
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get the ID of the currently connected agent.
|
|
595
|
+
*/
|
|
596
|
+
getCurrentAgentId(): string | null {
|
|
597
|
+
return this.currentAgentId;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* DEPRECATED: Use setSessionConfigOption instead.
|
|
602
|
+
*/
|
|
603
|
+
async setSessionMode(sessionId: string, modeId: string): Promise<void> {
|
|
604
|
+
const connection = this.requireConnection();
|
|
605
|
+
|
|
606
|
+
this.logger.log(
|
|
607
|
+
`[AcpClient] Setting session mode to: ${modeId} for session: ${sessionId}`,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
await connection.setSessionMode({
|
|
612
|
+
sessionId,
|
|
613
|
+
modeId,
|
|
614
|
+
});
|
|
615
|
+
this.logger.log(`[AcpClient] Session mode set to: ${modeId}`);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
this.logger.error("[AcpClient] Failed to set session mode:", error);
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* DEPRECATED: Use setSessionConfigOption instead.
|
|
624
|
+
*/
|
|
625
|
+
async setSessionModel(sessionId: string, modelId: string): Promise<void> {
|
|
626
|
+
const connection = this.requireConnection();
|
|
627
|
+
|
|
628
|
+
this.logger.log(
|
|
629
|
+
`[AcpClient] Setting session model to: ${modelId} for session: ${sessionId}`,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
await connection.unstable_setSessionModel({
|
|
634
|
+
sessionId,
|
|
635
|
+
modelId,
|
|
636
|
+
});
|
|
637
|
+
this.logger.log(`[AcpClient] Session model set to: ${modelId}`);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.logger.error(
|
|
640
|
+
"[AcpClient] Failed to set session model:",
|
|
641
|
+
error,
|
|
642
|
+
);
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Set a session configuration option.
|
|
649
|
+
*
|
|
650
|
+
* Sends a config option change to the agent. The response contains the
|
|
651
|
+
* complete set of all config options with their current values, as changing
|
|
652
|
+
* one option may affect others.
|
|
653
|
+
*/
|
|
654
|
+
async setSessionConfigOption(
|
|
655
|
+
sessionId: string,
|
|
656
|
+
configId: string,
|
|
657
|
+
value: string,
|
|
658
|
+
): Promise<SessionConfigOption[]> {
|
|
659
|
+
const connection = this.requireConnection();
|
|
660
|
+
|
|
661
|
+
this.logger.log(
|
|
662
|
+
`[AcpClient] Setting config option: ${configId}=${value} for session: ${sessionId}`,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
const response = await connection.setSessionConfigOption({
|
|
667
|
+
sessionId,
|
|
668
|
+
configId,
|
|
669
|
+
value,
|
|
670
|
+
});
|
|
671
|
+
this.logger.log(
|
|
672
|
+
`[AcpClient] Config option set. Updated options:`,
|
|
673
|
+
response.configOptions,
|
|
674
|
+
);
|
|
675
|
+
return AcpTypeConverter.toSessionConfigOptions(
|
|
676
|
+
response.configOptions,
|
|
677
|
+
);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
this.logger.error(
|
|
680
|
+
"[AcpClient] Failed to set config option:",
|
|
681
|
+
error,
|
|
682
|
+
);
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Register a callback to receive session updates from the agent.
|
|
689
|
+
*
|
|
690
|
+
* This unified callback receives all session update events:
|
|
691
|
+
* - agent_message_chunk: Text chunk from agent's response
|
|
692
|
+
* - agent_thought_chunk: Text chunk from agent's reasoning
|
|
693
|
+
* - tool_call: New tool call event
|
|
694
|
+
* - tool_call_update: Update to existing tool call
|
|
695
|
+
* - plan: Agent's task plan
|
|
696
|
+
* - available_commands_update: Slash commands changed
|
|
697
|
+
* - current_mode_update: Mode changed
|
|
698
|
+
*/
|
|
699
|
+
onSessionUpdate(callback: (update: SessionUpdate) => void): () => void {
|
|
700
|
+
return this.handler.onSessionUpdate(callback);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Respond to a permission request from the agent.
|
|
705
|
+
*/
|
|
706
|
+
respondToPermission(requestId: string, optionId: string): Promise<void> {
|
|
707
|
+
this.requireConnection();
|
|
708
|
+
|
|
709
|
+
this.logger.log(
|
|
710
|
+
"[AcpClient] Responding to permission request:",
|
|
711
|
+
requestId,
|
|
712
|
+
"with option:",
|
|
713
|
+
optionId,
|
|
714
|
+
);
|
|
715
|
+
this.permissionManager.respond(requestId, optionId);
|
|
716
|
+
return Promise.resolve();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Helper methods
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Assert that the ACP connection is initialized and return it.
|
|
723
|
+
* @throws Error if connection is not available
|
|
724
|
+
*/
|
|
725
|
+
private requireConnection(): acp.ClientSideConnection {
|
|
726
|
+
if (!this.connection) {
|
|
727
|
+
throw new Error(
|
|
728
|
+
"Connection not initialized. Call initialize() first.",
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
return this.connection;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Convert working directory to WSL path if in WSL mode on Windows.
|
|
736
|
+
*/
|
|
737
|
+
private toSessionCwd(cwd: string): string {
|
|
738
|
+
if (Platform.isWin && this.plugin.settings.windowsWslMode) {
|
|
739
|
+
return convertWindowsPathToWsl(cwd);
|
|
740
|
+
}
|
|
741
|
+
return cwd;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private cancelAllOperations(): void {
|
|
745
|
+
this.permissionManager.cancelAll();
|
|
746
|
+
this.terminalManager.killAllTerminals();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get terminal output for UI rendering.
|
|
751
|
+
*/
|
|
752
|
+
getTerminalOutput(terminalId: string): Promise<TerminalOutputResult> {
|
|
753
|
+
const result = this.terminalManager.getOutput(terminalId);
|
|
754
|
+
if (!result) {
|
|
755
|
+
throw new Error(`Terminal ${terminalId} not found`);
|
|
756
|
+
}
|
|
757
|
+
return Promise.resolve(result);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ========================================================================
|
|
761
|
+
// Session Management Methods
|
|
762
|
+
// ========================================================================
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* List available sessions (unstable).
|
|
766
|
+
*
|
|
767
|
+
* Only available if session.agentCapabilities.sessionCapabilities?.list is defined.
|
|
768
|
+
*
|
|
769
|
+
* @param cwd - Optional filter by working directory
|
|
770
|
+
* @param cursor - Pagination cursor from previous call
|
|
771
|
+
* @returns Promise resolving to sessions array and optional next cursor
|
|
772
|
+
*/
|
|
773
|
+
async listSessions(
|
|
774
|
+
cwd?: string,
|
|
775
|
+
cursor?: string,
|
|
776
|
+
): Promise<ListSessionsResult> {
|
|
777
|
+
const connection = this.requireConnection();
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
this.logger.log("[AcpClient] Listing sessions...");
|
|
781
|
+
|
|
782
|
+
const filterCwd = cwd ? this.toSessionCwd(cwd) : undefined;
|
|
783
|
+
|
|
784
|
+
const response = await connection.unstable_listSessions({
|
|
785
|
+
cwd: filterCwd ?? null,
|
|
786
|
+
cursor: cursor ?? null,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
this.logger.log(
|
|
790
|
+
`[AcpClient] Found ${response.sessions.length} sessions`,
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
sessions: response.sessions.map((s) => ({
|
|
795
|
+
sessionId: s.sessionId,
|
|
796
|
+
cwd: s.cwd,
|
|
797
|
+
title: s.title ?? undefined,
|
|
798
|
+
updatedAt: s.updatedAt ?? undefined,
|
|
799
|
+
})),
|
|
800
|
+
nextCursor: response.nextCursor ?? undefined,
|
|
801
|
+
};
|
|
802
|
+
} catch (error) {
|
|
803
|
+
this.logger.error("[AcpClient] List Sessions Error:", error);
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Load a previous session with history replay (stable).
|
|
810
|
+
*
|
|
811
|
+
* Conversation history is received via onSessionUpdate callback
|
|
812
|
+
* as user_message_chunk, agent_message_chunk, tool_call, etc.
|
|
813
|
+
*
|
|
814
|
+
* @param sessionId - Session to load
|
|
815
|
+
* @param cwd - Working directory
|
|
816
|
+
* @returns Promise resolving to session result with modes and models
|
|
817
|
+
*/
|
|
818
|
+
async loadSession(sessionId: string, cwd: string): Promise<SessionResult> {
|
|
819
|
+
const connection = this.requireConnection();
|
|
820
|
+
|
|
821
|
+
// Set sessionId before await so replay updates pass the sessionId filter
|
|
822
|
+
this.currentSessionId = sessionId;
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
this.logger.log(`[AcpClient] Loading session: ${sessionId}...`);
|
|
826
|
+
|
|
827
|
+
const response = await connection.loadSession({
|
|
828
|
+
sessionId,
|
|
829
|
+
cwd: this.toSessionCwd(cwd),
|
|
830
|
+
mcpServers: [],
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
this.logger.log(`[AcpClient] Session loaded: ${sessionId}`);
|
|
834
|
+
const result = AcpTypeConverter.toSessionResult(
|
|
835
|
+
sessionId,
|
|
836
|
+
response,
|
|
837
|
+
);
|
|
838
|
+
this.currentSessionId = result.sessionId;
|
|
839
|
+
return result;
|
|
840
|
+
} catch (error) {
|
|
841
|
+
this.logger.error("[AcpClient] Load Session Error:", error);
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Resume a session without history replay (unstable).
|
|
848
|
+
*
|
|
849
|
+
* Use when client manages its own history storage.
|
|
850
|
+
*
|
|
851
|
+
* @param sessionId - Session to resume
|
|
852
|
+
* @param cwd - Working directory
|
|
853
|
+
* @returns Promise resolving to session result with modes and models
|
|
854
|
+
*/
|
|
855
|
+
async resumeSession(
|
|
856
|
+
sessionId: string,
|
|
857
|
+
cwd: string,
|
|
858
|
+
): Promise<SessionResult> {
|
|
859
|
+
const connection = this.requireConnection();
|
|
860
|
+
|
|
861
|
+
// Set sessionId before await so any updates pass the sessionId filter
|
|
862
|
+
this.currentSessionId = sessionId;
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
this.logger.log(`[AcpClient] Resuming session: ${sessionId}...`);
|
|
866
|
+
|
|
867
|
+
const response = await connection.unstable_resumeSession({
|
|
868
|
+
sessionId,
|
|
869
|
+
cwd: this.toSessionCwd(cwd),
|
|
870
|
+
mcpServers: [],
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
this.logger.log(`[AcpClient] Session resumed: ${sessionId}`);
|
|
874
|
+
const result = AcpTypeConverter.toSessionResult(
|
|
875
|
+
sessionId,
|
|
876
|
+
response,
|
|
877
|
+
);
|
|
878
|
+
this.currentSessionId = result.sessionId;
|
|
879
|
+
return result;
|
|
880
|
+
} catch (error) {
|
|
881
|
+
this.logger.error("[AcpClient] Resume Session Error:", error);
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Fork a session to create a new branch (unstable).
|
|
888
|
+
*
|
|
889
|
+
* Creates a new session with inherited context from the original.
|
|
890
|
+
*
|
|
891
|
+
* @param sessionId - Session to fork from
|
|
892
|
+
* @param cwd - Working directory
|
|
893
|
+
* @returns Promise resolving to session result with new sessionId
|
|
894
|
+
*/
|
|
895
|
+
async forkSession(sessionId: string, cwd: string): Promise<SessionResult> {
|
|
896
|
+
const connection = this.requireConnection();
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
this.logger.log(`[AcpClient] Forking session: ${sessionId}...`);
|
|
900
|
+
|
|
901
|
+
const response = await connection.unstable_forkSession({
|
|
902
|
+
sessionId,
|
|
903
|
+
cwd: this.toSessionCwd(cwd),
|
|
904
|
+
mcpServers: [],
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
this.logger.log(
|
|
908
|
+
`[AcpClient] Session forked: ${sessionId} -> ${response.sessionId}`,
|
|
909
|
+
);
|
|
910
|
+
const result = AcpTypeConverter.toSessionResult(
|
|
911
|
+
response.sessionId,
|
|
912
|
+
response,
|
|
913
|
+
);
|
|
914
|
+
this.currentSessionId = result.sessionId;
|
|
915
|
+
return result;
|
|
916
|
+
} catch (error) {
|
|
917
|
+
this.logger.error("[AcpClient] Fork Session Error:", error);
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|