@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,264 @@
|
|
|
1
|
+
import { spawn, ChildProcess, SpawnOptions } from "child_process";
|
|
2
|
+
import type AgentClientPlugin from "../plugin";
|
|
3
|
+
import { getLogger, Logger } from "../utils/logger";
|
|
4
|
+
import { Platform } from "obsidian";
|
|
5
|
+
import { resolveNodeDirectory } from "../utils/paths";
|
|
6
|
+
import { getEnhancedWindowsEnv, prepareShellCommand } from "../utils/platform";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parameters for creating a terminal process.
|
|
10
|
+
*
|
|
11
|
+
* This is the TerminalManager's own parameter type, independent of the ACP SDK.
|
|
12
|
+
* The AcpClient is responsible for converting ACP protocol types to this format.
|
|
13
|
+
*/
|
|
14
|
+
interface CreateTerminalParams {
|
|
15
|
+
/** The command to execute */
|
|
16
|
+
command: string;
|
|
17
|
+
/** Command arguments */
|
|
18
|
+
args?: string[];
|
|
19
|
+
/** Working directory for the command (absolute path) */
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** Environment variables as name-value pairs */
|
|
22
|
+
env?: Array<{ name: string; value: string }>;
|
|
23
|
+
/** Maximum number of output bytes to retain */
|
|
24
|
+
outputByteLimit?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TerminalProcess {
|
|
28
|
+
id: string;
|
|
29
|
+
process: ChildProcess;
|
|
30
|
+
output: string;
|
|
31
|
+
exitStatus: { exitCode: number | null; signal: string | null } | null;
|
|
32
|
+
outputByteLimit?: number;
|
|
33
|
+
waitPromises: Array<
|
|
34
|
+
(exitStatus: { exitCode: number | null; signal: string | null }) => void
|
|
35
|
+
>;
|
|
36
|
+
cleanupTimeout?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class TerminalManager {
|
|
40
|
+
private terminals = new Map<string, TerminalProcess>();
|
|
41
|
+
private logger: Logger;
|
|
42
|
+
private plugin: AgentClientPlugin;
|
|
43
|
+
|
|
44
|
+
constructor(plugin: AgentClientPlugin) {
|
|
45
|
+
this.logger = getLogger();
|
|
46
|
+
this.plugin = plugin;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
createTerminal(params: CreateTerminalParams): string {
|
|
50
|
+
const terminalId = crypto.randomUUID();
|
|
51
|
+
|
|
52
|
+
// Check current platform
|
|
53
|
+
if (!Platform.isDesktopApp) {
|
|
54
|
+
throw new Error("Agent Client is only available on desktop");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Set up environment variables
|
|
58
|
+
// Desktop-only: Node.js process environment for terminal operations
|
|
59
|
+
let env: NodeJS.ProcessEnv = { ...process.env };
|
|
60
|
+
|
|
61
|
+
// On Windows (non-WSL mode), enhance PATH with full system/user PATH from registry.
|
|
62
|
+
// Electron apps launched from shortcuts don't inherit the full PATH.
|
|
63
|
+
if (Platform.isWin && !this.plugin.settings.windowsWslMode) {
|
|
64
|
+
env = getEnhancedWindowsEnv(env);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (params.env) {
|
|
68
|
+
for (const envVar of params.env) {
|
|
69
|
+
env[envVar.name] = envVar.value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle command parsing
|
|
74
|
+
let command = params.command;
|
|
75
|
+
let args = params.args || [];
|
|
76
|
+
|
|
77
|
+
// Platform-specific shell wrapping
|
|
78
|
+
const nodeDir = resolveNodeDirectory(this.plugin.settings.nodePath);
|
|
79
|
+
const prepared = prepareShellCommand(
|
|
80
|
+
command,
|
|
81
|
+
args,
|
|
82
|
+
params.cwd || process.cwd(),
|
|
83
|
+
{
|
|
84
|
+
wslMode: this.plugin.settings.windowsWslMode,
|
|
85
|
+
wslDistribution: this.plugin.settings.windowsWslDistribution,
|
|
86
|
+
nodeDir,
|
|
87
|
+
alwaysEscape: false,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
command = prepared.command;
|
|
91
|
+
args = prepared.args;
|
|
92
|
+
const needsShell = prepared.needsShell;
|
|
93
|
+
|
|
94
|
+
this.logger.log(`[Terminal ${terminalId}] Creating terminal:`, {
|
|
95
|
+
command,
|
|
96
|
+
args,
|
|
97
|
+
cwd: params.cwd,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Spawn the process
|
|
101
|
+
const spawnOptions: SpawnOptions = {
|
|
102
|
+
cwd: params.cwd || undefined,
|
|
103
|
+
env,
|
|
104
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
105
|
+
shell: needsShell,
|
|
106
|
+
};
|
|
107
|
+
const childProcess = spawn(command, args, spawnOptions);
|
|
108
|
+
|
|
109
|
+
const terminal: TerminalProcess = {
|
|
110
|
+
id: terminalId,
|
|
111
|
+
process: childProcess,
|
|
112
|
+
output: "",
|
|
113
|
+
exitStatus: null,
|
|
114
|
+
outputByteLimit:
|
|
115
|
+
params.outputByteLimit !== undefined
|
|
116
|
+
? Number(params.outputByteLimit)
|
|
117
|
+
: undefined,
|
|
118
|
+
waitPromises: [],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Handle spawn errors
|
|
122
|
+
childProcess.on("error", (error) => {
|
|
123
|
+
this.logger.log(
|
|
124
|
+
`[Terminal ${terminalId}] Process error:`,
|
|
125
|
+
error.message,
|
|
126
|
+
);
|
|
127
|
+
// Set exit status to indicate failure
|
|
128
|
+
const exitStatus = { exitCode: 127, signal: null }; // 127 = command not found
|
|
129
|
+
terminal.exitStatus = exitStatus;
|
|
130
|
+
// Resolve all waiting promises
|
|
131
|
+
terminal.waitPromises.forEach((resolve) => resolve(exitStatus));
|
|
132
|
+
terminal.waitPromises = [];
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Capture stdout and stderr
|
|
136
|
+
childProcess.stdout?.on("data", (data: Buffer) => {
|
|
137
|
+
const output = data.toString();
|
|
138
|
+
this.logger.log(`[Terminal ${terminalId}] stdout:`, output);
|
|
139
|
+
this.appendOutput(terminal, output);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
childProcess.stderr?.on("data", (data: Buffer) => {
|
|
143
|
+
const output = data.toString();
|
|
144
|
+
this.logger.log(`[Terminal ${terminalId}] stderr:`, output);
|
|
145
|
+
this.appendOutput(terminal, output);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Handle process exit
|
|
149
|
+
childProcess.on("exit", (code, signal) => {
|
|
150
|
+
this.logger.log(
|
|
151
|
+
`[Terminal ${terminalId}] Process exited with code: ${code}, signal: ${signal}`,
|
|
152
|
+
);
|
|
153
|
+
const exitStatus = { exitCode: code, signal };
|
|
154
|
+
terminal.exitStatus = exitStatus;
|
|
155
|
+
// Resolve all waiting promises
|
|
156
|
+
terminal.waitPromises.forEach((resolve) => resolve(exitStatus));
|
|
157
|
+
terminal.waitPromises = [];
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
this.terminals.set(terminalId, terminal);
|
|
161
|
+
return terminalId;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private appendOutput(terminal: TerminalProcess, data: string): void {
|
|
165
|
+
terminal.output += data;
|
|
166
|
+
|
|
167
|
+
// Apply output byte limit if specified
|
|
168
|
+
if (
|
|
169
|
+
terminal.outputByteLimit &&
|
|
170
|
+
Buffer.byteLength(terminal.output, "utf8") >
|
|
171
|
+
terminal.outputByteLimit
|
|
172
|
+
) {
|
|
173
|
+
// Truncate from the beginning, ensuring we stay at character boundaries
|
|
174
|
+
const bytes = Buffer.from(terminal.output, "utf8");
|
|
175
|
+
const truncatedBytes = bytes.subarray(
|
|
176
|
+
bytes.length - terminal.outputByteLimit,
|
|
177
|
+
);
|
|
178
|
+
terminal.output = truncatedBytes.toString("utf8");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getOutput(terminalId: string): {
|
|
183
|
+
output: string;
|
|
184
|
+
truncated: boolean;
|
|
185
|
+
exitStatus: { exitCode: number | null; signal: string | null } | null;
|
|
186
|
+
} | null {
|
|
187
|
+
const terminal = this.terminals.get(terminalId);
|
|
188
|
+
if (!terminal) return null;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
output: terminal.output,
|
|
192
|
+
truncated: terminal.outputByteLimit
|
|
193
|
+
? Buffer.byteLength(terminal.output, "utf8") >=
|
|
194
|
+
terminal.outputByteLimit
|
|
195
|
+
: false,
|
|
196
|
+
exitStatus: terminal.exitStatus,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
waitForExit(
|
|
201
|
+
terminalId: string,
|
|
202
|
+
): Promise<{ exitCode: number | null; signal: string | null }> {
|
|
203
|
+
const terminal = this.terminals.get(terminalId);
|
|
204
|
+
if (!terminal) {
|
|
205
|
+
return Promise.reject(
|
|
206
|
+
new Error(`Terminal ${terminalId} not found`),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (terminal.exitStatus) {
|
|
211
|
+
return Promise.resolve(terminal.exitStatus);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
terminal.waitPromises.push(resolve);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
killTerminal(terminalId: string): boolean {
|
|
220
|
+
const terminal = this.terminals.get(terminalId);
|
|
221
|
+
if (!terminal) return false;
|
|
222
|
+
|
|
223
|
+
if (!terminal.exitStatus) {
|
|
224
|
+
terminal.process.kill("SIGTERM");
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
releaseTerminal(terminalId: string): boolean {
|
|
230
|
+
const terminal = this.terminals.get(terminalId);
|
|
231
|
+
if (!terminal) return false;
|
|
232
|
+
|
|
233
|
+
this.logger.log(`[Terminal ${terminalId}] Releasing terminal`);
|
|
234
|
+
if (!terminal.exitStatus) {
|
|
235
|
+
terminal.process.kill("SIGTERM");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Schedule cleanup after 30 seconds to allow UI to poll final output
|
|
239
|
+
terminal.cleanupTimeout = window.setTimeout(() => {
|
|
240
|
+
this.logger.log(
|
|
241
|
+
`[Terminal ${terminalId}] Cleaning up terminal after grace period`,
|
|
242
|
+
);
|
|
243
|
+
this.terminals.delete(terminalId);
|
|
244
|
+
}, 30000);
|
|
245
|
+
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
killAllTerminals(): void {
|
|
250
|
+
this.logger.log(`Killing ${this.terminals.size} running terminals...`);
|
|
251
|
+
this.terminals.forEach((terminal, terminalId) => {
|
|
252
|
+
// Clear cleanup timeout if scheduled
|
|
253
|
+
if (terminal.cleanupTimeout) {
|
|
254
|
+
window.clearTimeout(terminal.cleanupTimeout);
|
|
255
|
+
}
|
|
256
|
+
if (!terminal.exitStatus) {
|
|
257
|
+
this.logger.log(`Killing terminal ${terminalId}`);
|
|
258
|
+
this.killTerminal(terminalId);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
// Clear all terminals
|
|
262
|
+
this.terminals.clear();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { ToolCallContent, PromptContent } from "../types/chat";
|
|
3
|
+
import type {
|
|
4
|
+
InitializeResult,
|
|
5
|
+
SessionConfigOption,
|
|
6
|
+
SessionConfigSelectGroup,
|
|
7
|
+
SessionConfigSelectOption,
|
|
8
|
+
SessionResult,
|
|
9
|
+
SlashCommand,
|
|
10
|
+
} from "../types/session";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Common shape of ACP session responses.
|
|
14
|
+
*
|
|
15
|
+
* NewSessionResponse and ForkSessionResponse include sessionId.
|
|
16
|
+
* LoadSessionResponse and ResumeSessionResponse do not (the sessionId
|
|
17
|
+
* is the same as the request parameter). This interface captures the
|
|
18
|
+
* shared fields for type-safe conversion; sessionId is optional here
|
|
19
|
+
* and supplied explicitly when missing from the response.
|
|
20
|
+
*/
|
|
21
|
+
interface AcpSessionResponse {
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
modes?: acp.SessionModeState | null;
|
|
24
|
+
models?: acp.SessionModelState | null;
|
|
25
|
+
configOptions?: acp.SessionConfigOption[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Type converter between ACP Protocol types and Domain types.
|
|
30
|
+
*
|
|
31
|
+
* This adapter ensures the domain layer remains independent of the ACP library.
|
|
32
|
+
* When the ACP protocol changes, only this converter needs to be updated.
|
|
33
|
+
*/
|
|
34
|
+
export class AcpTypeConverter {
|
|
35
|
+
/**
|
|
36
|
+
* Convert ACP ToolCallContent to domain ToolCallContent.
|
|
37
|
+
*
|
|
38
|
+
* Filters out content types that are not supported by the domain model:
|
|
39
|
+
* - Supports: "diff", "terminal"
|
|
40
|
+
* - Ignores: "content" (not implemented in UI)
|
|
41
|
+
*
|
|
42
|
+
* @param acpContent - Tool call content from ACP protocol
|
|
43
|
+
* @returns Domain model tool call content, or undefined if input is null/empty
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Convert ACP AvailableCommand[] to domain SlashCommand[].
|
|
47
|
+
*/
|
|
48
|
+
static toSlashCommands(
|
|
49
|
+
acpCommands: acp.AvailableCommand[] | undefined | null,
|
|
50
|
+
): SlashCommand[] {
|
|
51
|
+
return (acpCommands || []).map((cmd) => ({
|
|
52
|
+
name: cmd.name,
|
|
53
|
+
description: cmd.description,
|
|
54
|
+
hint: cmd.input?.hint ?? null,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static toToolCallContent(
|
|
59
|
+
acpContent: acp.ToolCallContent[] | undefined | null,
|
|
60
|
+
): ToolCallContent[] | undefined {
|
|
61
|
+
if (!acpContent) return undefined;
|
|
62
|
+
|
|
63
|
+
const converted: ToolCallContent[] = [];
|
|
64
|
+
|
|
65
|
+
for (const item of acpContent) {
|
|
66
|
+
if (item.type === "diff") {
|
|
67
|
+
converted.push({
|
|
68
|
+
type: "diff",
|
|
69
|
+
path: item.path,
|
|
70
|
+
newText: item.newText,
|
|
71
|
+
oldText: item.oldText,
|
|
72
|
+
});
|
|
73
|
+
} else if (item.type === "terminal") {
|
|
74
|
+
converted.push({
|
|
75
|
+
type: "terminal",
|
|
76
|
+
terminalId: item.terminalId,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// "content" type is intentionally ignored (not implemented in UI)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return converted.length > 0 ? converted : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Convert domain PromptContent to ACP ContentBlock.
|
|
87
|
+
*
|
|
88
|
+
* This converts our domain-layer prompt content to the ACP protocol format
|
|
89
|
+
* for sending to the agent.
|
|
90
|
+
*
|
|
91
|
+
* @param content - Domain prompt content (text, image, resource, or resource_link)
|
|
92
|
+
* @returns ACP ContentBlock for use with the prompt API
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Convert ACP SessionConfigOption[] to domain SessionConfigOption[].
|
|
96
|
+
*
|
|
97
|
+
* @param acpOptions - Config options from ACP protocol
|
|
98
|
+
* @returns Domain model config options
|
|
99
|
+
*/
|
|
100
|
+
static toSessionConfigOptions(
|
|
101
|
+
acpOptions: acp.SessionConfigOption[],
|
|
102
|
+
): SessionConfigOption[] {
|
|
103
|
+
return acpOptions.map((opt) => ({
|
|
104
|
+
id: opt.id,
|
|
105
|
+
name: opt.name,
|
|
106
|
+
description: opt.description ?? undefined,
|
|
107
|
+
category: opt.category ?? undefined,
|
|
108
|
+
type: opt.type,
|
|
109
|
+
currentValue: opt.currentValue,
|
|
110
|
+
options: this.toSessionConfigSelectOptions(opt.options),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private static toSessionConfigSelectOptions(
|
|
115
|
+
acpOptions: acp.SessionConfigSelectOptions,
|
|
116
|
+
): SessionConfigSelectOption[] | SessionConfigSelectGroup[] {
|
|
117
|
+
if (acpOptions.length === 0) return [];
|
|
118
|
+
|
|
119
|
+
// Determine if grouped or flat by checking first element
|
|
120
|
+
const first = acpOptions[0];
|
|
121
|
+
if ("group" in first) {
|
|
122
|
+
return (acpOptions as acp.SessionConfigSelectGroup[]).map((g) => ({
|
|
123
|
+
group: g.group,
|
|
124
|
+
name: g.name,
|
|
125
|
+
options: g.options.map((o) => ({
|
|
126
|
+
value: o.value,
|
|
127
|
+
name: o.name,
|
|
128
|
+
description: o.description ?? undefined,
|
|
129
|
+
})),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (acpOptions as acp.SessionConfigSelectOption[]).map((o) => ({
|
|
134
|
+
value: o.value,
|
|
135
|
+
name: o.name,
|
|
136
|
+
description: o.description ?? undefined,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convert ACP session response to domain SessionResult.
|
|
142
|
+
*
|
|
143
|
+
* Handles the modes/models/configOptions conversion that is common
|
|
144
|
+
* to newSession, loadSession, resumeSession, and forkSession responses.
|
|
145
|
+
*
|
|
146
|
+
* ACP uses `null` for absent optional fields, while domain uses `undefined`.
|
|
147
|
+
* This method normalizes that difference.
|
|
148
|
+
*
|
|
149
|
+
* @param sessionId - The session ID (from response or from request params)
|
|
150
|
+
* @param response - ACP session response (new/load/resume/fork)
|
|
151
|
+
* @returns Domain SessionResult
|
|
152
|
+
*/
|
|
153
|
+
static toSessionResult(
|
|
154
|
+
sessionId: string,
|
|
155
|
+
response: AcpSessionResponse,
|
|
156
|
+
): SessionResult {
|
|
157
|
+
let modes: SessionResult["modes"];
|
|
158
|
+
if (response.modes) {
|
|
159
|
+
modes = {
|
|
160
|
+
availableModes: response.modes.availableModes.map((m) => ({
|
|
161
|
+
id: m.id,
|
|
162
|
+
name: m.name,
|
|
163
|
+
description: m.description ?? undefined,
|
|
164
|
+
})),
|
|
165
|
+
currentModeId: response.modes.currentModeId,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let models: SessionResult["models"];
|
|
170
|
+
if (response.models) {
|
|
171
|
+
models = {
|
|
172
|
+
availableModels: response.models.availableModels.map((m) => ({
|
|
173
|
+
modelId: m.modelId,
|
|
174
|
+
name: m.name,
|
|
175
|
+
description: m.description ?? undefined,
|
|
176
|
+
})),
|
|
177
|
+
currentModelId: response.models.currentModelId,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const configOptions = response.configOptions
|
|
182
|
+
? this.toSessionConfigOptions(response.configOptions)
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
sessionId,
|
|
187
|
+
modes,
|
|
188
|
+
models,
|
|
189
|
+
configOptions,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static toAcpContentBlock(content: PromptContent): acp.ContentBlock {
|
|
194
|
+
switch (content.type) {
|
|
195
|
+
case "text":
|
|
196
|
+
return { type: "text", text: content.text };
|
|
197
|
+
case "image":
|
|
198
|
+
return {
|
|
199
|
+
type: "image",
|
|
200
|
+
data: content.data,
|
|
201
|
+
mimeType: content.mimeType,
|
|
202
|
+
};
|
|
203
|
+
case "resource":
|
|
204
|
+
return {
|
|
205
|
+
type: "resource",
|
|
206
|
+
resource: {
|
|
207
|
+
uri: content.resource.uri,
|
|
208
|
+
mimeType: content.resource.mimeType,
|
|
209
|
+
text: content.resource.text,
|
|
210
|
+
},
|
|
211
|
+
annotations: content.annotations,
|
|
212
|
+
};
|
|
213
|
+
case "resource_link":
|
|
214
|
+
return {
|
|
215
|
+
type: "resource_link",
|
|
216
|
+
uri: content.uri,
|
|
217
|
+
name: content.name,
|
|
218
|
+
mimeType: content.mimeType,
|
|
219
|
+
size: content.size,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Convert ACP InitializeResponse to domain InitializeResult.
|
|
226
|
+
*/
|
|
227
|
+
static toInitializeResult(
|
|
228
|
+
initResult: acp.InitializeResponse,
|
|
229
|
+
): InitializeResult {
|
|
230
|
+
const promptCaps = initResult.agentCapabilities?.promptCapabilities;
|
|
231
|
+
const mcpCaps = initResult.agentCapabilities?.mcpCapabilities;
|
|
232
|
+
const sessionCaps = initResult.agentCapabilities?.sessionCapabilities;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
protocolVersion: initResult.protocolVersion,
|
|
236
|
+
authMethods: initResult.authMethods || [],
|
|
237
|
+
promptCapabilities: {
|
|
238
|
+
image: promptCaps?.image ?? false,
|
|
239
|
+
audio: promptCaps?.audio ?? false,
|
|
240
|
+
embeddedContext: promptCaps?.embeddedContext ?? false,
|
|
241
|
+
},
|
|
242
|
+
agentCapabilities: {
|
|
243
|
+
loadSession: initResult.agentCapabilities?.loadSession ?? false,
|
|
244
|
+
sessionCapabilities: sessionCaps
|
|
245
|
+
? {
|
|
246
|
+
resume: sessionCaps.resume ?? undefined,
|
|
247
|
+
fork: sessionCaps.fork ?? undefined,
|
|
248
|
+
list: sessionCaps.list ?? undefined,
|
|
249
|
+
}
|
|
250
|
+
: undefined,
|
|
251
|
+
mcpCapabilities: mcpCaps
|
|
252
|
+
? {
|
|
253
|
+
http: mcpCaps.http ?? false,
|
|
254
|
+
sse: mcpCaps.sse ?? false,
|
|
255
|
+
}
|
|
256
|
+
: undefined,
|
|
257
|
+
promptCapabilities: {
|
|
258
|
+
image: promptCaps?.image ?? false,
|
|
259
|
+
audio: promptCaps?.audio ?? false,
|
|
260
|
+
embeddedContext: promptCaps?.embeddedContext ?? false,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
agentInfo: initResult.agentInfo
|
|
264
|
+
? {
|
|
265
|
+
name: initResult.agentInfo.name,
|
|
266
|
+
title: initResult.agentInfo.title ?? undefined,
|
|
267
|
+
version: initResult.agentInfo.version ?? undefined,
|
|
268
|
+
}
|
|
269
|
+
: undefined,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|