@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,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
|
+
}
|