summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-24 14:18:11 +0900
committerAdam Malczewski <[email protected]>2026-03-24 14:18:11 +0900
commitc34eca01c8fc8bc8ff6a14d0c48a9c2323daf915 (patch)
treeaa76c09e9ed6e83c184a4d6d19b0fbb8d8d2e842
parent7f9b25a1479f9897aea7f85c3fb58a568b0bd642 (diff)
downloadai-pulse-obsidian-plugin-c34eca01c8fc8bc8ff6a14d0c48a9c2323daf915.tar.gz
ai-pulse-obsidian-plugin-c34eca01c8fc8bc8ff6a14d0c48a9c2323daf915.zip
styling fixes
-rw-r--r--.rules/changelog/2026-03/24/08.md34
-rw-r--r--src/chat-view.ts60
-rw-r--r--src/ollama-client.ts32
-rw-r--r--src/tools.ts4
-rw-r--r--styles.css18
5 files changed, 136 insertions, 12 deletions
diff --git a/.rules/changelog/2026-03/24/08.md b/.rules/changelog/2026-03/24/08.md
new file mode 100644
index 0000000..af75364
--- /dev/null
+++ b/.rules/changelog/2026-03/24/08.md
@@ -0,0 +1,34 @@
+# Chat UI Bug Fixes and Improvements
+
+## Bug Fixes
+
+### Invisible assistant chat bubbles (`styles.css`)
+- Assistant message bubbles used `--background-secondary` which is the same as the sidebar panel background, making them invisible.
+- Changed to `--background-primary` with a `1px solid` border for visible contrast.
+
+### Tool calls appearing below final AI message (`chat-view.ts`, `ollama-client.ts`)
+- The streaming bubble was created once before the agent loop, so tool call elements were always appended after it in the DOM.
+- Added `onCreateBubble` callback to `StreamingChatOptions` so a fresh bubble is created for each iteration of the agent loop.
+- Tool call elements now appear between rounds in correct chronological order.
+
+### Empty bubble from tool-only rounds (`chat-view.ts`)
+- When the AI calls tools without sending text, an empty assistant bubble remained visible.
+- Empty bubbles are now automatically removed from the DOM after each round and at finalization.
+
+## New Features
+
+### Loading indicator (`chat-view.ts`, `styles.css`)
+- New streaming bubbles display a `more-horizontal` Lucide icon while waiting for AI response.
+- Icon is removed as soon as the first text chunk arrives.
+- Added `.ai-organizer-streaming` and `.ai-organizer-loading-icon` CSS styles.
+
+### System prompt for tool guidance (`ollama-client.ts`, `tools.ts`)
+- Added a system prompt (injected when tools are enabled) instructing the model to use exact file paths from search results.
+- Improved `search_files` and `read_file` tool descriptions to emphasize paths are exact and must be used verbatim.
+- Addresses issue where the model would hallucinate file paths instead of using actual search results.
+
+## Files Changed
+- `src/chat-view.ts`
+- `src/ollama-client.ts`
+- `src/tools.ts`
+- `styles.css`
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"],
diff --git a/styles.css b/styles.css
index cdf5b54..0a4e53f 100644
--- a/styles.css
+++ b/styles.css
@@ -37,7 +37,23 @@
.ai-organizer-message.assistant {
align-self: flex-start;
- background-color: var(--background-secondary);
+ background-color: var(--background-primary);
+ border: 1px solid var(--background-modifier-border);
+}
+
+.ai-organizer-streaming {
+ opacity: 0.85;
+}
+
+.ai-organizer-loading-icon {
+ display: flex;
+ align-items: center;
+ color: var(--text-muted);
+}
+
+.ai-organizer-loading-icon svg {
+ width: 20px;
+ height: 20px;
}
.ai-organizer-message.error {