feat: add thinking indicator while model is generating

- src/types.ts: Add isThinking flag to ChatMessage to track transient
  'model is working' state.

- src/chat-view.ts: Set isThinking: true on the assistant placeholder
  message when user sends input. Clear it when the first stream chunk
  arrives or on error. Update render() to show a spinner + 'Thinking…'
  text while isThinking is active.

- styles.css: Add ollama-thinking-indicator class with a CSS spinner
  animation and muted italic text styling.
This commit is contained in:
2026-05-19 21:21:47 +02:00
parent 810676ff21
commit 70bf963f28
4 changed files with 56 additions and 7 deletions
+16 -5
View File
@@ -9059,7 +9059,11 @@ var ChatView = class extends import_obsidian3.ItemView {
if (existingEl) { if (existingEl) {
const contentEl = existingEl.querySelector(".ollama-message-content"); const contentEl = existingEl.querySelector(".ollama-message-content");
if (contentEl) { if (contentEl) {
contentEl.textContent = msg.content; if (msg.isThinking) {
contentEl.innerHTML = '<span class="ollama-thinking-indicator">Thinking\u2026</span>';
} else {
contentEl.textContent = msg.content;
}
} }
} else { } else {
const messageEl = container.createEl("div", { const messageEl = container.createEl("div", {
@@ -9069,7 +9073,11 @@ var ChatView = class extends import_obsidian3.ItemView {
const headerEl = messageEl.createEl("div", { cls: "ollama-message-header" }); const headerEl = messageEl.createEl("div", { cls: "ollama-message-header" });
headerEl.createEl("span", { cls: "ollama-message-role", text: msg.role }); headerEl.createEl("span", { cls: "ollama-message-role", text: msg.role });
const contentEl = messageEl.createEl("div", { cls: "ollama-message-content" }); const contentEl = messageEl.createEl("div", { cls: "ollama-message-content" });
contentEl.textContent = msg.content; if (msg.isThinking) {
contentEl.innerHTML = '<span class="ollama-thinking-indicator">Thinking\u2026</span>';
} else {
contentEl.textContent = msg.content;
}
} }
} }
const streamingMessage = messagesSnapshot.find((msg) => msg.isStreaming); const streamingMessage = messagesSnapshot.find((msg) => msg.isStreaming);
@@ -9297,7 +9305,8 @@ var ChatView = class extends import_obsidian3.ItemView {
role: "assistant", role: "assistant",
content: "", content: "",
timestamp: Date.now(), timestamp: Date.now(),
isStreaming: true isStreaming: true,
isThinking: true
}; };
const previousStreamingEl = this.lastMessageEl; const previousStreamingEl = this.lastMessageEl;
this.messages = [...this.messages, userChatMessage, assistantMessage]; this.messages = [...this.messages, userChatMessage, assistantMessage];
@@ -9334,7 +9343,8 @@ ${userMessage}` : userMessage;
fullResponse += chunk.content; fullResponse += chunk.content;
this.updateLastMessage({ this.updateLastMessage({
content: fullResponse, content: fullResponse,
isStreaming: true isStreaming: true,
isThinking: false
}); });
} }
if (chunk.tool_calls) { if (chunk.tool_calls) {
@@ -9373,7 +9383,8 @@ ${userMessage}` : userMessage;
ErrorHandler.handleError(error, "ChatView.handleUserInput"); ErrorHandler.handleError(error, "ChatView.handleUserInput");
this.updateMessageById(assistantMessageId, { this.updateMessageById(assistantMessageId, {
content: "An error occurred while processing your request.", content: "An error occurred while processing your request.",
isStreaming: false isStreaming: false,
isThinking: false
}); });
} finally { } finally {
this.cleanupStreamingResources(); this.cleanupStreamingResources();
+13 -2
View File
@@ -146,7 +146,11 @@ export class ChatView extends ItemView {
if (existingEl) { if (existingEl) {
const contentEl = existingEl.querySelector('.ollama-message-content'); const contentEl = existingEl.querySelector('.ollama-message-content');
if (contentEl) { if (contentEl) {
contentEl.textContent = msg.content; if (msg.isThinking) {
contentEl.innerHTML = '<span class="ollama-thinking-indicator">Thinking…</span>';
} else {
contentEl.textContent = msg.content;
}
} }
} else { } else {
const messageEl = container.createEl('div', { const messageEl = container.createEl('div', {
@@ -158,7 +162,11 @@ export class ChatView extends ItemView {
headerEl.createEl('span', { cls: 'ollama-message-role', text: msg.role }); headerEl.createEl('span', { cls: 'ollama-message-role', text: msg.role });
const contentEl = messageEl.createEl('div', { cls: 'ollama-message-content' }); const contentEl = messageEl.createEl('div', { cls: 'ollama-message-content' });
contentEl.textContent = msg.content; if (msg.isThinking) {
contentEl.innerHTML = '<span class="ollama-thinking-indicator">Thinking…</span>';
} else {
contentEl.textContent = msg.content;
}
} }
} }
@@ -437,6 +445,7 @@ export class ChatView extends ItemView {
content: '', content: '',
timestamp: Date.now(), timestamp: Date.now(),
isStreaming: true, isStreaming: true,
isThinking: true,
}; };
const previousStreamingEl = this.lastMessageEl; const previousStreamingEl = this.lastMessageEl;
@@ -486,6 +495,7 @@ export class ChatView extends ItemView {
this.updateLastMessage({ this.updateLastMessage({
content: fullResponse, content: fullResponse,
isStreaming: true, isStreaming: true,
isThinking: false,
}); });
} }
@@ -536,6 +546,7 @@ export class ChatView extends ItemView {
this.updateMessageById(assistantMessageId, { this.updateMessageById(assistantMessageId, {
content: 'An error occurred while processing your request.', content: 'An error occurred while processing your request.',
isStreaming: false, isStreaming: false,
isThinking: false,
}); });
} finally { } finally {
// Clean up streaming resources regardless of outcome // Clean up streaming resources regardless of outcome
+1
View File
@@ -141,6 +141,7 @@ export interface ChatMessage {
content: string; content: string;
timestamp: number; timestamp: number;
isStreaming?: boolean; isStreaming?: boolean;
isThinking?: boolean;
tool_calls?: OllamaToolCall[]; tool_calls?: OllamaToolCall[];
// Refinement tracking // Refinement tracking
originalQuery?: string; originalQuery?: string;
+26
View File
@@ -149,3 +149,29 @@
.ollama-send-button:active { .ollama-send-button:active {
filter: brightness(0.95); filter: brightness(0.95);
} }
/* Thinking indicator */
.ollama-thinking-indicator {
display: inline-flex;
align-items: center;
gap: var(--size-4-1);
font-style: italic;
color: var(--text-muted);
}
.ollama-thinking-indicator::before {
content: '';
display: inline-block;
width: 0.9em;
height: 0.9em;
border: 2px solid var(--text-muted);
border-top-color: transparent;
border-radius: 50%;
animation: ollama-spin 1s linear infinite;
}
@keyframes ollama-spin {
to {
transform: rotate(360deg);
}
}