@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.
Files changed (146) hide show
  1. package/.claude/hooks/gh-setup.sh +49 -0
  2. package/.claude/settings.json +15 -0
  3. package/.claude/skills/release-notes/SKILL.md +331 -0
  4. package/.editorconfig +10 -0
  5. package/.github/FUNDING.yml +2 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
  9. package/.github/copilot-instructions.md +45 -0
  10. package/.github/pull_request_template.md +32 -0
  11. package/.github/workflows/ci.yaml +25 -0
  12. package/.github/workflows/docs.yml +58 -0
  13. package/.github/workflows/relay_to_openclaw.yml +59 -0
  14. package/.github/workflows/release.yaml +45 -0
  15. package/.prettierignore +10 -0
  16. package/.prettierrc +13 -0
  17. package/.vscode/extensions.json +7 -0
  18. package/.vscode/settings.json +37 -0
  19. package/.zed/settings.json +42 -0
  20. package/AGENTS.md +330 -0
  21. package/ARCHITECTURE.md +390 -0
  22. package/CONTRIBUTING.md +216 -0
  23. package/LICENSE +202 -0
  24. package/NOTICE +2 -0
  25. package/README.ja.md +121 -0
  26. package/README.md +125 -0
  27. package/docs/.vitepress/config.mts +124 -0
  28. package/docs/.vitepress/theme/custom.css +111 -0
  29. package/docs/.vitepress/theme/index.ts +4 -0
  30. package/docs/agent-setup/claude-code.md +84 -0
  31. package/docs/agent-setup/codex.md +76 -0
  32. package/docs/agent-setup/custom-agents.md +67 -0
  33. package/docs/agent-setup/gemini-cli.md +99 -0
  34. package/docs/agent-setup/index.md +34 -0
  35. package/docs/announcements/gemini-cli-deprecation.md +73 -0
  36. package/docs/getting-started/index.md +78 -0
  37. package/docs/getting-started/quick-start.md +38 -0
  38. package/docs/help/faq.md +181 -0
  39. package/docs/help/troubleshooting.md +221 -0
  40. package/docs/index.md +63 -0
  41. package/docs/public/apple-touch-icon.png +0 -0
  42. package/docs/public/demo.mp4 +0 -0
  43. package/docs/public/favicon-16x16.png +0 -0
  44. package/docs/public/favicon-32x32.png +0 -0
  45. package/docs/public/favicon.ico +0 -0
  46. package/docs/public/images/editing.webp +0 -0
  47. package/docs/public/images/export.webp +0 -0
  48. package/docs/public/images/floating-chat-button.webp +0 -0
  49. package/docs/public/images/floating-chat-instance-menu.webp +0 -0
  50. package/docs/public/images/floating-chat-view.webp +0 -0
  51. package/docs/public/images/mode-selection.webp +0 -0
  52. package/docs/public/images/model-selection.webp +0 -0
  53. package/docs/public/images/multi-session.webp +0 -0
  54. package/docs/public/images/remove-image.webp +0 -0
  55. package/docs/public/images/ribbon-icon.webp +0 -0
  56. package/docs/public/images/selection-context.gif +0 -0
  57. package/docs/public/images/sending-images.webp +0 -0
  58. package/docs/public/images/sending-messages.webp +0 -0
  59. package/docs/public/images/session-history-button.webp +0 -0
  60. package/docs/public/images/slash-commands-1.webp +0 -0
  61. package/docs/public/images/slash-commands-2.webp +0 -0
  62. package/docs/public/images/switch-agent.webp +0 -0
  63. package/docs/public/images/switch-default-agent.webp +0 -0
  64. package/docs/public/images/temporary-disable.gif +0 -0
  65. package/docs/reference/acp-support.md +110 -0
  66. package/docs/usage/chat-export.md +80 -0
  67. package/docs/usage/commands.md +51 -0
  68. package/docs/usage/context-files.md +57 -0
  69. package/docs/usage/editing.md +69 -0
  70. package/docs/usage/floating-chat.md +84 -0
  71. package/docs/usage/index.md +97 -0
  72. package/docs/usage/mcp-tools.md +33 -0
  73. package/docs/usage/mentions.md +70 -0
  74. package/docs/usage/mode-selection.md +28 -0
  75. package/docs/usage/model-selection.md +32 -0
  76. package/docs/usage/multi-session.md +68 -0
  77. package/docs/usage/sending-images.md +64 -0
  78. package/docs/usage/session-history.md +91 -0
  79. package/docs/usage/slash-commands.md +44 -0
  80. package/esbuild.config.mjs +49 -0
  81. package/eslint.config.mjs +25 -0
  82. package/main.js +228 -0
  83. package/manifest.json +11 -0
  84. package/package.json +52 -0
  85. package/src/acp/acp-client.ts +921 -0
  86. package/src/acp/acp-handler.ts +252 -0
  87. package/src/acp/permission-handler.ts +282 -0
  88. package/src/acp/terminal-handler.ts +264 -0
  89. package/src/acp/type-converter.ts +272 -0
  90. package/src/hooks/useAgent.ts +250 -0
  91. package/src/hooks/useAgentMessages.ts +470 -0
  92. package/src/hooks/useAgentSession.ts +544 -0
  93. package/src/hooks/useChatActions.ts +400 -0
  94. package/src/hooks/useHistoryModal.ts +219 -0
  95. package/src/hooks/useSessionHistory.ts +863 -0
  96. package/src/hooks/useSettings.ts +19 -0
  97. package/src/hooks/useSuggestions.ts +342 -0
  98. package/src/main.ts +9 -0
  99. package/src/plugin.ts +1126 -0
  100. package/src/services/chat-exporter.ts +552 -0
  101. package/src/services/message-sender.ts +755 -0
  102. package/src/services/message-state.ts +375 -0
  103. package/src/services/session-helpers.ts +211 -0
  104. package/src/services/session-state.ts +130 -0
  105. package/src/services/session-storage.ts +267 -0
  106. package/src/services/settings-normalizer.ts +255 -0
  107. package/src/services/settings-service.ts +285 -0
  108. package/src/services/update-checker.ts +128 -0
  109. package/src/services/vault-service.ts +558 -0
  110. package/src/services/view-registry.ts +345 -0
  111. package/src/types/agent.ts +92 -0
  112. package/src/types/chat.ts +351 -0
  113. package/src/types/errors.ts +136 -0
  114. package/src/types/obsidian-internals.d.ts +14 -0
  115. package/src/types/session.ts +731 -0
  116. package/src/ui/ChangeDirectoryModal.ts +137 -0
  117. package/src/ui/ChatContext.ts +25 -0
  118. package/src/ui/ChatHeader.tsx +295 -0
  119. package/src/ui/ChatPanel.tsx +1162 -0
  120. package/src/ui/ChatView.tsx +348 -0
  121. package/src/ui/ErrorBanner.tsx +104 -0
  122. package/src/ui/FloatingButton.tsx +351 -0
  123. package/src/ui/FloatingChatView.tsx +531 -0
  124. package/src/ui/InputArea.tsx +1107 -0
  125. package/src/ui/InputToolbar.tsx +371 -0
  126. package/src/ui/MessageBubble.tsx +442 -0
  127. package/src/ui/MessageList.tsx +265 -0
  128. package/src/ui/PermissionBanner.tsx +61 -0
  129. package/src/ui/SessionHistoryModal.tsx +821 -0
  130. package/src/ui/SettingsTab.ts +1337 -0
  131. package/src/ui/SuggestionPopup.tsx +138 -0
  132. package/src/ui/TerminalBlock.tsx +107 -0
  133. package/src/ui/ToolCallBlock.tsx +456 -0
  134. package/src/ui/shared/AttachmentStrip.tsx +57 -0
  135. package/src/ui/shared/IconButton.tsx +55 -0
  136. package/src/ui/shared/MarkdownRenderer.tsx +103 -0
  137. package/src/ui/view-host.ts +56 -0
  138. package/src/utils/error-utils.ts +274 -0
  139. package/src/utils/logger.ts +44 -0
  140. package/src/utils/mention-parser.ts +129 -0
  141. package/src/utils/paths.ts +246 -0
  142. package/src/utils/platform.ts +425 -0
  143. package/styles.css +2322 -0
  144. package/tsconfig.json +18 -0
  145. package/version-bump.mjs +18 -0
  146. 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
+ }