@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,755 @@
1
+ /**
2
+ * Message Service
3
+ *
4
+ * Pure functions for prompt preparation and sending.
5
+ * Extracted from SendMessageUseCase for better separation of concerns.
6
+ *
7
+ * Responsibilities:
8
+ * - Process mentions (@[[note]] syntax)
9
+ * - Add auto-mention for active note
10
+ * - Convert mentions to file paths
11
+ * - Send prompt to agent via AcpClient
12
+ * - Handle authentication errors with retry logic
13
+ */
14
+
15
+ import type { AcpClient } from "../acp/acp-client";
16
+ import type {
17
+ IVaultAccess,
18
+ NoteMetadata,
19
+ EditorPosition,
20
+ } from "../services/vault-service";
21
+ import { AcpErrorCode, type AcpError } from "../types/errors";
22
+ import {
23
+ extractErrorCode,
24
+ toAcpError,
25
+ isEmptyResponseError,
26
+ } from "../utils/error-utils";
27
+ import type { AuthenticationMethod } from "../types/session";
28
+ import type {
29
+ PromptContent,
30
+ ImagePromptContent,
31
+ ResourcePromptContent,
32
+ ResourceLinkPromptContent,
33
+ } from "../types/chat";
34
+ import {
35
+ extractMentionedNotes,
36
+ type IMentionService,
37
+ } from "../utils/mention-parser";
38
+ import { convertWindowsPathToWsl } from "../utils/platform";
39
+ import { buildFileUri } from "../utils/paths";
40
+
41
+ // ============================================================================
42
+ // Types
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Input for preparing a prompt
47
+ */
48
+ export interface PreparePromptInput {
49
+ /** User's message text (may contain @mentions) */
50
+ message: string;
51
+
52
+ /** Attached images */
53
+ images?: ImagePromptContent[];
54
+
55
+ /** Attached file references (resource links) */
56
+ resourceLinks?: ResourceLinkPromptContent[];
57
+
58
+ /** Currently active note (for auto-mention feature) */
59
+ activeNote?: NoteMetadata | null;
60
+
61
+ /** Vault base path for converting mentions to absolute paths */
62
+ vaultBasePath: string;
63
+
64
+ /** Whether auto-mention is temporarily disabled */
65
+ isAutoMentionDisabled?: boolean;
66
+
67
+ /** Whether to convert paths to WSL format (Windows + WSL mode) */
68
+ convertToWsl?: boolean;
69
+
70
+ /** Whether agent supports embeddedContext capability */
71
+ supportsEmbeddedContext?: boolean;
72
+
73
+ /** Maximum characters per mentioned note (default: 10000) */
74
+ maxNoteLength?: number;
75
+
76
+ /** Maximum characters for selection (default: 10000) */
77
+ maxSelectionLength?: number;
78
+ }
79
+
80
+ /**
81
+ * Result of preparing a prompt
82
+ */
83
+ export interface PreparePromptResult {
84
+ /** Content for UI display (original text + images) */
85
+ displayContent: PromptContent[];
86
+
87
+ /** Content to send to agent (processed text + images) */
88
+ agentContent: PromptContent[];
89
+
90
+ /** Auto-mention context metadata (if auto-mention is active) */
91
+ autoMentionContext?: {
92
+ noteName: string;
93
+ notePath: string;
94
+ selection?: {
95
+ fromLine: number;
96
+ toLine: number;
97
+ };
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Input for sending a prepared prompt
103
+ */
104
+ export interface SendPreparedPromptInput {
105
+ /** Current session ID */
106
+ sessionId: string;
107
+
108
+ /** The prepared agent content (from preparePrompt) */
109
+ agentContent: PromptContent[];
110
+
111
+ /** The display content (for error reporting) */
112
+ displayContent: PromptContent[];
113
+
114
+ /** Available authentication methods */
115
+ authMethods: AuthenticationMethod[];
116
+ }
117
+
118
+ /**
119
+ * Result of sending a prompt
120
+ */
121
+ export interface SendPromptResult {
122
+ /** Whether the prompt was sent successfully */
123
+ success: boolean;
124
+
125
+ /** The display content */
126
+ displayContent: PromptContent[];
127
+
128
+ /** The agent content sent */
129
+ agentContent: PromptContent[];
130
+
131
+ /** Error information if sending failed */
132
+ error?: AcpError;
133
+
134
+ /** Whether authentication is required */
135
+ requiresAuth?: boolean;
136
+
137
+ /** Whether the prompt was successfully sent after retry */
138
+ retriedSuccessfully?: boolean;
139
+ }
140
+
141
+ // ============================================================================
142
+ // Constants
143
+ // ============================================================================
144
+
145
+ const DEFAULT_MAX_NOTE_LENGTH = 10000; // Default maximum characters per note
146
+ const DEFAULT_MAX_SELECTION_LENGTH = 10000; // Default maximum characters for selection
147
+
148
+ // ============================================================================
149
+ // Shared Helper Functions
150
+ // ============================================================================
151
+
152
+ /**
153
+ * Processed note data ready for formatting.
154
+ */
155
+ interface ProcessedNote {
156
+ content: string;
157
+ absolutePath: string;
158
+ uri: string;
159
+ lastModified: string;
160
+ wasTruncated: boolean;
161
+ originalLength: number;
162
+ }
163
+
164
+ /**
165
+ * Read a note, truncate if needed, and resolve its absolute path.
166
+ */
167
+ async function processNote(
168
+ file: { path: string; stat: { mtime: number } },
169
+ vaultBasePath: string,
170
+ vaultAccess: IVaultAccess,
171
+ convertToWsl: boolean,
172
+ maxNoteLength: number,
173
+ ): Promise<ProcessedNote | null> {
174
+ try {
175
+ const content = await vaultAccess.readNote(file.path);
176
+
177
+ let absolutePath = vaultBasePath
178
+ ? `${vaultBasePath}/${file.path}`
179
+ : file.path;
180
+
181
+ if (convertToWsl) {
182
+ absolutePath = convertWindowsPathToWsl(absolutePath);
183
+ }
184
+
185
+ const wasTruncated = content.length > maxNoteLength;
186
+ const processedContent = wasTruncated
187
+ ? content.substring(0, maxNoteLength)
188
+ : content;
189
+
190
+ return {
191
+ content: processedContent,
192
+ absolutePath,
193
+ uri: buildFileUri(absolutePath),
194
+ lastModified: new Date(file.stat.mtime).toISOString(),
195
+ wasTruncated,
196
+ originalLength: content.length,
197
+ };
198
+ } catch (error) {
199
+ console.error(`Failed to read note ${file.path}:`, error);
200
+ return null;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Read selected text from a note and truncate if needed.
206
+ */
207
+ async function readSelection(
208
+ notePath: string,
209
+ selection: { from: EditorPosition; to: EditorPosition },
210
+ vaultAccess: IVaultAccess,
211
+ maxSelectionLength: number,
212
+ ): Promise<{
213
+ text: string;
214
+ wasTruncated: boolean;
215
+ originalLength: number;
216
+ } | null> {
217
+ try {
218
+ const content = await vaultAccess.readNote(notePath);
219
+ const lines = content.split("\n");
220
+ const selectedLines = lines.slice(
221
+ selection.from.line,
222
+ selection.to.line + 1,
223
+ );
224
+ const fullText = selectedLines.join("\n");
225
+ const wasTruncated = fullText.length > maxSelectionLength;
226
+
227
+ return {
228
+ text: wasTruncated
229
+ ? fullText.substring(0, maxSelectionLength)
230
+ : fullText,
231
+ wasTruncated,
232
+ originalLength: fullText.length,
233
+ };
234
+ } catch (error) {
235
+ console.error(`Failed to read selection from ${notePath}:`, error);
236
+ return null;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Build auto-mention prefix string for session/load recovery.
242
+ * Format: "@[[note name]]:from-to\n" or "@[[note name]]\n"
243
+ */
244
+ function buildAutoMentionPrefix(
245
+ activeNote: NoteMetadata | null | undefined,
246
+ isDisabled: boolean | undefined,
247
+ ): string {
248
+ if (!activeNote || isDisabled) return "";
249
+ if (activeNote.selection) {
250
+ return `@[[${activeNote.name}]]:${activeNote.selection.from.line + 1}-${activeNote.selection.to.line + 1}\n`;
251
+ }
252
+ return `@[[${activeNote.name}]]\n`;
253
+ }
254
+
255
+ /**
256
+ * Build display content array (message + images + resource links).
257
+ */
258
+ function buildDisplayContent(input: PreparePromptInput): PromptContent[] {
259
+ return [
260
+ ...(input.message
261
+ ? [{ type: "text" as const, text: input.message }]
262
+ : []),
263
+ ...(input.images || []),
264
+ ...(input.resourceLinks || []),
265
+ ];
266
+ }
267
+
268
+ /**
269
+ * Build auto-mention context metadata for UI.
270
+ */
271
+ function buildAutoMentionContext(
272
+ activeNote: NoteMetadata | null | undefined,
273
+ isDisabled: boolean | undefined,
274
+ ): PreparePromptResult["autoMentionContext"] {
275
+ if (!activeNote || isDisabled) return undefined;
276
+ return {
277
+ noteName: activeNote.name,
278
+ notePath: activeNote.path,
279
+ selection: activeNote.selection
280
+ ? {
281
+ fromLine: activeNote.selection.from.line + 1,
282
+ toLine: activeNote.selection.to.line + 1,
283
+ }
284
+ : undefined,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Resolve absolute path with optional WSL conversion.
290
+ */
291
+ function resolveAbsolutePath(
292
+ relativePath: string,
293
+ vaultBasePath: string,
294
+ convertToWsl: boolean,
295
+ ): string {
296
+ let absolutePath = vaultBasePath
297
+ ? `${vaultBasePath}/${relativePath}`
298
+ : relativePath;
299
+ if (convertToWsl) {
300
+ absolutePath = convertWindowsPathToWsl(absolutePath);
301
+ }
302
+ return absolutePath;
303
+ }
304
+
305
+ // ============================================================================
306
+ // Prompt Preparation Functions
307
+ // ============================================================================
308
+
309
+ /**
310
+ * Prepare a prompt for sending to the agent.
311
+ *
312
+ * Processes the message by:
313
+ * - Building context blocks for mentioned notes
314
+ * - Adding auto-mention context for active note
315
+ * - Creating agent content with context + user message + images + resource links
316
+ *
317
+ * When agent supports embeddedContext capability, mentioned notes are sent
318
+ * as Resource content blocks. Otherwise, they are embedded as XML text.
319
+ */
320
+ export async function preparePrompt(
321
+ input: PreparePromptInput,
322
+ vaultAccess: IVaultAccess,
323
+ mentionService: IMentionService,
324
+ ): Promise<PreparePromptResult> {
325
+ // Step 1: Extract all mentioned notes from the message
326
+ const mentionedNotes = extractMentionedNotes(input.message, mentionService);
327
+
328
+ // Step 2: Build context based on agent capabilities
329
+ if (input.supportsEmbeddedContext) {
330
+ return preparePromptWithEmbeddedContext(
331
+ input,
332
+ vaultAccess,
333
+ mentionedNotes,
334
+ );
335
+ } else {
336
+ return preparePromptWithTextContext(input, vaultAccess, mentionedNotes);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Prepare prompt using embedded Resource format (for embeddedContext-capable agents).
342
+ */
343
+ async function preparePromptWithEmbeddedContext(
344
+ input: PreparePromptInput,
345
+ vaultAccess: IVaultAccess,
346
+ mentionedNotes: Array<{
347
+ noteTitle: string;
348
+ file: { path: string; stat: { mtime: number } } | undefined;
349
+ }>,
350
+ ): Promise<PreparePromptResult> {
351
+ const maxNoteLen = input.maxNoteLength ?? DEFAULT_MAX_NOTE_LENGTH;
352
+ const resourceBlocks: ResourcePromptContent[] = [];
353
+
354
+ // Build Resource blocks for each mentioned note
355
+ for (const { file } of mentionedNotes) {
356
+ if (!file) continue;
357
+
358
+ const note = await processNote(
359
+ file,
360
+ input.vaultBasePath,
361
+ vaultAccess,
362
+ input.convertToWsl ?? false,
363
+ maxNoteLen,
364
+ );
365
+ if (!note) continue;
366
+
367
+ const text = note.wasTruncated
368
+ ? note.content +
369
+ `\n\n[Note: Truncated from ${note.originalLength} to ${maxNoteLen} characters]`
370
+ : note.content;
371
+
372
+ resourceBlocks.push({
373
+ type: "resource",
374
+ resource: { uri: note.uri, mimeType: "text/markdown", text },
375
+ annotations: {
376
+ audience: ["assistant"],
377
+ priority: 1.0,
378
+ lastModified: note.lastModified,
379
+ },
380
+ });
381
+ }
382
+
383
+ // Build auto-mention Resource block
384
+ const autoMentionBlocks: PromptContent[] = [];
385
+ if (input.activeNote && !input.isAutoMentionDisabled) {
386
+ const autoMentionResource = await buildAutoMentionResource(
387
+ input.activeNote,
388
+ input.vaultBasePath,
389
+ vaultAccess,
390
+ input.convertToWsl ?? false,
391
+ input.maxSelectionLength ?? DEFAULT_MAX_SELECTION_LENGTH,
392
+ );
393
+ autoMentionBlocks.push(...autoMentionResource);
394
+ }
395
+
396
+ const autoMentionPrefix = buildAutoMentionPrefix(
397
+ input.activeNote,
398
+ input.isAutoMentionDisabled,
399
+ );
400
+
401
+ const agentContent: PromptContent[] = [
402
+ ...resourceBlocks,
403
+ ...autoMentionBlocks,
404
+ ...(input.message || autoMentionPrefix
405
+ ? [
406
+ {
407
+ type: "text" as const,
408
+ text: autoMentionPrefix + input.message,
409
+ },
410
+ ]
411
+ : []),
412
+ ...(input.images || []),
413
+ ...(input.resourceLinks || []),
414
+ ];
415
+
416
+ return {
417
+ displayContent: buildDisplayContent(input),
418
+ agentContent,
419
+ autoMentionContext: buildAutoMentionContext(
420
+ input.activeNote,
421
+ input.isAutoMentionDisabled,
422
+ ),
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Prepare prompt using XML text format (fallback for agents without embeddedContext).
428
+ */
429
+ async function preparePromptWithTextContext(
430
+ input: PreparePromptInput,
431
+ vaultAccess: IVaultAccess,
432
+ mentionedNotes: Array<{
433
+ noteTitle: string;
434
+ file: { path: string; stat: { mtime: number } } | undefined;
435
+ }>,
436
+ ): Promise<PreparePromptResult> {
437
+ const maxNoteLen = input.maxNoteLength ?? DEFAULT_MAX_NOTE_LENGTH;
438
+ const contextBlocks: string[] = [];
439
+
440
+ // Build XML context blocks for each mentioned note
441
+ for (const { file } of mentionedNotes) {
442
+ if (!file) continue;
443
+
444
+ const note = await processNote(
445
+ file,
446
+ input.vaultBasePath,
447
+ vaultAccess,
448
+ input.convertToWsl ?? false,
449
+ maxNoteLen,
450
+ );
451
+ if (!note) continue;
452
+
453
+ const truncationNote = note.wasTruncated
454
+ ? `\n\n[Note: This note was truncated. Original length: ${note.originalLength} characters, showing first ${maxNoteLen} characters]`
455
+ : "";
456
+
457
+ contextBlocks.push(
458
+ `<obsidian_mentioned_note ref="${note.absolutePath}">\n${note.content}${truncationNote}\n</obsidian_mentioned_note>`,
459
+ );
460
+ }
461
+
462
+ // Build auto-mention XML context
463
+ if (input.activeNote && !input.isAutoMentionDisabled) {
464
+ const autoMentionContextBlock = await buildAutoMentionTextContext(
465
+ input.activeNote.path,
466
+ input.vaultBasePath,
467
+ vaultAccess,
468
+ input.convertToWsl ?? false,
469
+ input.activeNote.selection,
470
+ input.maxSelectionLength ?? DEFAULT_MAX_SELECTION_LENGTH,
471
+ );
472
+ contextBlocks.push(autoMentionContextBlock);
473
+ }
474
+
475
+ const autoMentionPrefix = buildAutoMentionPrefix(
476
+ input.activeNote,
477
+ input.isAutoMentionDisabled,
478
+ );
479
+
480
+ // Build agent message text (context blocks + auto-mention prefix + original message)
481
+ const agentMessageText =
482
+ contextBlocks.length > 0
483
+ ? contextBlocks.join("\n") +
484
+ "\n\n" +
485
+ autoMentionPrefix +
486
+ input.message
487
+ : autoMentionPrefix + input.message;
488
+
489
+ const agentContent: PromptContent[] = [
490
+ ...(agentMessageText
491
+ ? [{ type: "text" as const, text: agentMessageText }]
492
+ : []),
493
+ ...(input.images || []),
494
+ ...(input.resourceLinks || []),
495
+ ];
496
+
497
+ return {
498
+ displayContent: buildDisplayContent(input),
499
+ agentContent,
500
+ autoMentionContext: buildAutoMentionContext(
501
+ input.activeNote,
502
+ input.isAutoMentionDisabled,
503
+ ),
504
+ };
505
+ }
506
+
507
+ /**
508
+ * Build Resource content blocks for auto-mentioned note.
509
+ */
510
+ async function buildAutoMentionResource(
511
+ activeNote: NoteMetadata,
512
+ vaultPath: string,
513
+ vaultAccess: IVaultAccess,
514
+ convertToWsl: boolean,
515
+ maxSelectionLength: number,
516
+ ): Promise<PromptContent[]> {
517
+ const absolutePath = resolveAbsolutePath(
518
+ activeNote.path,
519
+ vaultPath,
520
+ convertToWsl,
521
+ );
522
+ const uri = buildFileUri(absolutePath);
523
+
524
+ if (activeNote.selection) {
525
+ const fromLine = activeNote.selection.from.line + 1;
526
+ const toLine = activeNote.selection.to.line + 1;
527
+
528
+ const sel = await readSelection(
529
+ activeNote.path,
530
+ activeNote.selection,
531
+ vaultAccess,
532
+ maxSelectionLength,
533
+ );
534
+ if (!sel) {
535
+ return [
536
+ {
537
+ type: "text",
538
+ text: `The user has selected lines ${fromLine}-${toLine} in ${uri}. If relevant, use the Read tool to examine the specific lines.`,
539
+ },
540
+ ];
541
+ }
542
+
543
+ const text = sel.wasTruncated
544
+ ? sel.text +
545
+ `\n\n[Note: Truncated from ${sel.originalLength} to ${maxSelectionLength} characters]`
546
+ : sel.text;
547
+
548
+ return [
549
+ {
550
+ type: "resource",
551
+ resource: { uri, mimeType: "text/markdown", text },
552
+ annotations: {
553
+ audience: ["assistant"],
554
+ priority: 0.8,
555
+ lastModified: new Date(activeNote.modified).toISOString(),
556
+ },
557
+ },
558
+ {
559
+ type: "text",
560
+ text: `The user has selected lines ${fromLine}-${toLine} in the above note. This is what they are currently focusing on.`,
561
+ },
562
+ ];
563
+ }
564
+
565
+ return [
566
+ {
567
+ type: "text",
568
+ text: `The user has opened the note ${uri} in Obsidian. This may or may not be related to the current conversation. If it seems relevant, consider using the Read tool to examine its content.`,
569
+ },
570
+ ];
571
+ }
572
+
573
+ /**
574
+ * Build XML text context from auto-mentioned note (fallback format).
575
+ */
576
+ async function buildAutoMentionTextContext(
577
+ notePath: string,
578
+ vaultPath: string,
579
+ vaultAccess: IVaultAccess,
580
+ convertToWsl: boolean,
581
+ selection: { from: EditorPosition; to: EditorPosition } | undefined,
582
+ maxSelectionLength: number,
583
+ ): Promise<string> {
584
+ const absolutePath = resolveAbsolutePath(notePath, vaultPath, convertToWsl);
585
+
586
+ if (selection) {
587
+ const fromLine = selection.from.line + 1;
588
+ const toLine = selection.to.line + 1;
589
+
590
+ const sel = await readSelection(
591
+ notePath,
592
+ selection,
593
+ vaultAccess,
594
+ maxSelectionLength,
595
+ );
596
+ if (!sel) {
597
+ return `<obsidian_opened_note selection="lines ${fromLine}-${toLine}">The user opened the note ${absolutePath} in Obsidian and is focusing on lines ${fromLine}-${toLine}. This may or may not be related to the current conversation. If it seems relevant, consider using the Read tool to examine the specific lines.</obsidian_opened_note>`;
598
+ }
599
+
600
+ const truncationNote = sel.wasTruncated
601
+ ? `\n\n[Note: The selection was truncated. Original length: ${sel.originalLength} characters, showing first ${maxSelectionLength} characters]`
602
+ : "";
603
+
604
+ return `<obsidian_opened_note selection="lines ${fromLine}-${toLine}">
605
+ The user opened the note ${absolutePath} in Obsidian and selected the following text (lines ${fromLine}-${toLine}):
606
+
607
+ ${sel.text}${truncationNote}
608
+
609
+ This is what the user is currently focusing on.
610
+ </obsidian_opened_note>`;
611
+ }
612
+
613
+ return `<obsidian_opened_note>The user opened the note ${absolutePath} in Obsidian. This may or may not be related to the current conversation. If it seems relevant, consider using the Read tool to examine the content.</obsidian_opened_note>`;
614
+ }
615
+
616
+ // ============================================================================
617
+ // Prompt Sending Functions
618
+ // ============================================================================
619
+
620
+ /**
621
+ * Send a prepared prompt to the agent.
622
+ */
623
+ export async function sendPreparedPrompt(
624
+ input: SendPreparedPromptInput,
625
+ agentClient: AcpClient,
626
+ ): Promise<SendPromptResult> {
627
+ try {
628
+ await agentClient.sendPrompt(input.sessionId, input.agentContent);
629
+
630
+ return {
631
+ success: true,
632
+ displayContent: input.displayContent,
633
+ agentContent: input.agentContent,
634
+ };
635
+ } catch (error) {
636
+ return await handleSendError(
637
+ error,
638
+ input.sessionId,
639
+ input.agentContent,
640
+ input.displayContent,
641
+ input.authMethods,
642
+ agentClient,
643
+ );
644
+ }
645
+ }
646
+
647
+ // ============================================================================
648
+ // Error Handling Functions
649
+ // ============================================================================
650
+
651
+ /**
652
+ * Handle errors that occur during prompt sending.
653
+ *
654
+ * Error handling strategy:
655
+ * 1. "empty response text" errors are ignored (not real errors)
656
+ * 2. -32000 (Authentication Required) triggers authentication retry
657
+ * 3. All other errors are converted to AcpError and displayed directly
658
+ */
659
+ async function handleSendError(
660
+ error: unknown,
661
+ sessionId: string,
662
+ agentContent: PromptContent[],
663
+ displayContent: PromptContent[],
664
+ authMethods: AuthenticationMethod[],
665
+ agentClient: AcpClient,
666
+ ): Promise<SendPromptResult> {
667
+ // Check for "empty response text" error - ignore silently
668
+ if (isEmptyResponseError(error)) {
669
+ return {
670
+ success: true,
671
+ displayContent,
672
+ agentContent,
673
+ };
674
+ }
675
+
676
+ const errorCode = extractErrorCode(error);
677
+
678
+ // Only attempt authentication retry for -32000 (Authentication Required)
679
+ if (errorCode === AcpErrorCode.AUTHENTICATION_REQUIRED) {
680
+ // Check if authentication methods are available
681
+ if (authMethods && authMethods.length > 0) {
682
+ // Try automatic authentication retry if only one method available
683
+ if (authMethods.length === 1) {
684
+ const retryResult = await retryWithAuthentication(
685
+ sessionId,
686
+ agentContent,
687
+ displayContent,
688
+ authMethods[0].id,
689
+ agentClient,
690
+ );
691
+
692
+ if (retryResult) {
693
+ return retryResult;
694
+ }
695
+ }
696
+
697
+ // Multiple auth methods or retry failed - let user choose
698
+ return {
699
+ success: false,
700
+ displayContent,
701
+ agentContent,
702
+ requiresAuth: true,
703
+ error: toAcpError(error, sessionId),
704
+ };
705
+ }
706
+
707
+ // No auth methods available - still show the error
708
+ // This is not an error condition, agent just doesn't support auth
709
+ }
710
+
711
+ // For all other errors, convert to AcpError and display directly
712
+ // The agent's error message is preserved and shown to the user
713
+ return {
714
+ success: false,
715
+ displayContent,
716
+ agentContent,
717
+ error: toAcpError(error, sessionId),
718
+ };
719
+ }
720
+
721
+ /**
722
+ * Retry sending prompt after authentication.
723
+ */
724
+ async function retryWithAuthentication(
725
+ sessionId: string,
726
+ agentContent: PromptContent[],
727
+ displayContent: PromptContent[],
728
+ authMethodId: string,
729
+ agentClient: AcpClient,
730
+ ): Promise<SendPromptResult | null> {
731
+ try {
732
+ const authSuccess = await agentClient.authenticate(authMethodId);
733
+
734
+ if (!authSuccess) {
735
+ return null;
736
+ }
737
+
738
+ await agentClient.sendPrompt(sessionId, agentContent);
739
+
740
+ return {
741
+ success: true,
742
+ displayContent,
743
+ agentContent,
744
+ retriedSuccessfully: true,
745
+ };
746
+ } catch (retryError) {
747
+ // Convert retry error to AcpError
748
+ return {
749
+ success: false,
750
+ displayContent,
751
+ agentContent,
752
+ error: toAcpError(retryError, sessionId),
753
+ };
754
+ }
755
+ }