diff options
| author | Adam Malczewski <[email protected]> | 2026-03-24 14:18:11 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-24 14:18:11 +0900 |
| commit | c34eca01c8fc8bc8ff6a14d0c48a9c2323daf915 (patch) | |
| tree | aa76c09e9ed6e83c184a4d6d19b0fbb8d8d2e842 /src | |
| parent | 7f9b25a1479f9897aea7f85c3fb58a568b0bd642 (diff) | |
| download | ai-pulse-obsidian-plugin-c34eca01c8fc8bc8ff6a14d0c48a9c2323daf915.tar.gz ai-pulse-obsidian-plugin-c34eca01c8fc8bc8ff6a14d0c48a9c2323daf915.zip | |
styling fixes
Diffstat (limited to 'src')
| -rw-r--r-- | src/chat-view.ts | 60 | ||||
| -rw-r--r-- | src/ollama-client.ts | 32 | ||||
| -rw-r--r-- | src/tools.ts | 4 |
3 files changed, 85 insertions, 11 deletions
diff --git a/src/chat-view.ts b/src/chat-view.ts index 9263c57..4ba89f9 100644 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -161,8 +161,7 @@ export class ChatView extends ItemView { this.abortController = new AbortController(); this.setStreamingState(true); - // Create the assistant bubble for streaming into - const streamingBubble = this.createStreamingBubble(); + let currentBubble: HTMLDivElement | null = null; try { const enabledTools = this.getEnabledTools(); @@ -173,9 +172,28 @@ export class ChatView extends ItemView { this.scrollToBottom(); }; + const onCreateBubble = (): void => { + // Finalize any previous bubble before creating a new one + if (currentBubble !== null) { + currentBubble.removeClass("ai-organizer-streaming"); + // Remove empty bubbles from tool-only rounds + if (currentBubble.textContent?.trim() === "") { + currentBubble.remove(); + } + } + currentBubble = this.createStreamingBubble(); + }; + const onChunk = (chunk: string): void => { - streamingBubble.textContent += chunk; - this.debouncedScrollToBottom(); + if (currentBubble !== null) { + // Remove the loading indicator on first chunk + const loadingIcon = currentBubble.querySelector(".ai-organizer-loading-icon"); + if (loadingIcon !== null) { + loadingIcon.remove(); + } + currentBubble.appendText(chunk); + this.debouncedScrollToBottom(); + } }; const response = await sendChatMessageStreaming({ @@ -186,16 +204,38 @@ export class ChatView extends ItemView { app: hasTools ? this.plugin.app : undefined, onChunk, onToolCall: hasTools ? onToolCall : undefined, + onCreateBubble, abortSignal: this.abortController.signal, }); - // Finalize the streaming bubble - streamingBubble.removeClass("ai-organizer-streaming"); + // Finalize the last streaming bubble + if (currentBubble !== null) { + (currentBubble as HTMLDivElement).removeClass("ai-organizer-streaming"); + // Remove loading icon if still present + const remainingIcon = (currentBubble as HTMLDivElement).querySelector(".ai-organizer-loading-icon"); + if (remainingIcon !== null) { + remainingIcon.remove(); + } + // Remove empty assistant bubbles (e.g., tool-only rounds with no content) + if ((currentBubble as HTMLDivElement).textContent?.trim() === "") { + (currentBubble as HTMLDivElement).remove(); + } + } this.messages.push({ role: "assistant", content: response }); this.scrollToBottom(); } catch (err: unknown) { // Finalize bubble even on error - streamingBubble.removeClass("ai-organizer-streaming"); + if (currentBubble !== null) { + (currentBubble as HTMLDivElement).removeClass("ai-organizer-streaming"); + const errorIcon = (currentBubble as HTMLDivElement).querySelector(".ai-organizer-loading-icon"); + if (errorIcon !== null) { + errorIcon.remove(); + } + // Remove empty bubble on error + if ((currentBubble as HTMLDivElement).textContent?.trim() === "") { + (currentBubble as HTMLDivElement).remove(); + } + } const errMsg = err instanceof Error ? err.message : "Unknown error."; new Notice(errMsg); @@ -214,9 +254,13 @@ export class ChatView extends ItemView { // Should not happen, but satisfy TS throw new Error("Message container not initialized."); } - return this.messageContainer.createDiv({ + const bubble = this.messageContainer.createDiv({ cls: "ai-organizer-message assistant ai-organizer-streaming", }); + // Add a loading indicator icon + const iconSpan = bubble.createSpan({ cls: "ai-organizer-loading-icon" }); + setIcon(iconSpan, "more-horizontal"); + return bubble; } private appendMessage(role: "user" | "assistant" | "error", content: string): void { diff --git a/src/ollama-client.ts b/src/ollama-client.ts index 66badc6..c9e4042 100644 --- a/src/ollama-client.ts +++ b/src/ollama-client.ts @@ -105,6 +105,19 @@ export async function sendChatMessage( const workingMessages = messages.map((m) => ({ ...m })); + // Inject a system prompt when tools are available to guide the model + if (tools !== undefined && tools.length > 0) { + const systemPrompt: ChatMessage = { + role: "system", + content: + "You are a helpful assistant with access to tools for interacting with an Obsidian vault. " + + "When you use the search_files tool, the results contain exact file paths. " + + "You MUST use these exact paths when calling read_file or referencing files. " + + "NEVER guess or modify file paths — always use the paths returned by search_files verbatim.", + }; + workingMessages.unshift(systemPrompt); + } + while (iterations < maxIterations) { iterations++; @@ -202,6 +215,7 @@ export interface StreamingChatOptions { app?: App; onChunk: (text: string) => void; onToolCall?: (event: ToolCallEvent) => void; + onCreateBubble: () => void; abortSignal?: AbortSignal; } @@ -247,15 +261,31 @@ async function* readNdjsonStream( export async function sendChatMessageStreaming( opts: StreamingChatOptions, ): Promise<string> { - const { ollamaUrl, model, messages, tools, app, onChunk, onToolCall, abortSignal } = opts; + const { ollamaUrl, model, messages, tools, app, onChunk, onToolCall, onCreateBubble, abortSignal } = opts; const maxIterations = 10; let iterations = 0; const workingMessages = messages.map((m) => ({ ...m })); + // Inject a system prompt when tools are available to guide the model + if (tools !== undefined && tools.length > 0) { + const systemPrompt: ChatMessage = { + role: "system", + content: + "You are a helpful assistant with access to tools for interacting with an Obsidian vault. " + + "When you use the search_files tool, the results contain exact file paths. " + + "You MUST use these exact paths when calling read_file or referencing files. " + + "NEVER guess or modify file paths — always use the paths returned by search_files verbatim.", + }; + workingMessages.unshift(systemPrompt); + } + while (iterations < maxIterations) { iterations++; + // Signal the UI to create a new bubble for this round + onCreateBubble(); + const body: Record<string, unknown> = { model, messages: workingMessages, diff --git a/src/tools.ts b/src/tools.ts index a702b18..7e31fb1 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -115,7 +115,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [ type: "function", function: { name: "search_files", - description: "Search for files in the Obsidian vault by name or path. Returns a list of matching file paths.", + description: "Search for files in the Obsidian vault by name or path. Returns a list of exact file paths. Use these exact paths for any subsequent file operations.", parameters: { type: "object", required: ["query"], @@ -150,7 +150,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [ type: "function", function: { name: "read_file", - description: "Read the full text content of a file in the Obsidian vault given its path.", + description: "Read the full text content of a file in the Obsidian vault. The file_path must be an exact path as returned by search_files.", parameters: { type: "object", required: ["file_path"], |
