From 949e7d77ff5cb935557bcfcf109b177cd8f961c0 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Thu, 21 May 2026 10:48:07 +0200 Subject: [PATCH] Add activity indicator for chat operations Shows a pulsing dot with status text during workflow execution, LLM thinking, and tool use. Disables the input and send button while active to prevent duplicate submissions. Also restructures the chat layout to use flexbox with the input container locked at the bottom. --- main.js | 56 ++++++++++++++++++++++++++++++++- src/chat-view.ts | 61 +++++++++++++++++++++++++++++++++++- styles.css | 81 ++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 187 insertions(+), 11 deletions(-) diff --git a/main.js b/main.js index 38a4f6d..aebbf34 100644 --- a/main.js +++ b/main.js @@ -10885,6 +10885,8 @@ var ChatView = class extends import_obsidian5.ItemView { this.removeLogListener = null; this.showLogsClickHandler = null; this.chatScrollHandler = null; + // Activity indicator + this.activityIndicatorEl = null; this.messages = []; this.lastMessageEl = null; this.newChatButton = null; @@ -11182,9 +11184,27 @@ var ChatView = class extends import_obsidian5.ItemView { } else { inputContainer.appendChild(this.sendButton); } + if (!this.activityIndicatorEl) { + this.activityIndicatorEl = inputContainer.createEl("div", { + cls: "ollama-activity-indicator" + }); + this.activityIndicatorEl.createEl("span", { cls: "ollama-activity-dot" }); + this.activityIndicatorEl.createEl("span", { + cls: "ollama-activity-text", + text: "" + }); + this.hideActivityIndicator(); + } else { + inputContainer.appendChild(this.activityIndicatorEl); + } + if (this.contentEl.addClass) { + this.contentEl.addClass("ollama-chat-view-content"); + } else { + this.contentEl.classList?.add("ollama-chat-view-content"); + } this.contentEl.appendChild(newChatContainer); - this.contentEl.appendChild(inputContainer); this.contentEl.appendChild(container); + this.contentEl.appendChild(inputContainer); if (this.shouldAutoScroll && container) { container.scrollTop = container.scrollHeight; } @@ -11940,11 +11960,14 @@ ${action.preview.after.slice(0, 500)}` this.lastMessageEl = previousStreamingEl; } if (isWorkflowCommand) { + this.showActivityIndicator("Running workflow\u2026"); await this.handleWorkflowRequest(actualMessage, assistantMessageId); + this.hideActivityIndicator(); this.cleanupStreamingResources(); return; } try { + this.showActivityIndicator("Thinking\u2026"); const noteContext = await this.noteContextBuilder.buildContext( actualMessage, this.settings.vaultSearchLimit, @@ -12003,6 +12026,7 @@ ${actualMessage}` : actualMessage; durationMs: llmDurationMs }); if (toolCalls.length > 0) { + this.showActivityIndicator("Using tools\u2026"); await this.processToolCalls( toolCalls, messagesWithMemory, @@ -12016,6 +12040,7 @@ ${actualMessage}` : actualMessage; if (toolCalls.length === 0 && toolCapableModes.includes(this.currentAgentMode) && fullResponse.trim().length > 0) { shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse); if (shouldFallbackToReadTools) { + this.showActivityIndicator("Thinking\u2026"); const nudgeMessages = [ ...messagesWithMemory, { role: "assistant", content: fullResponse }, @@ -12041,6 +12066,7 @@ ${actualMessage}` : actualMessage; } } if (toolCalls.length > 0) { + this.showActivityIndicator("Using tools\u2026"); await this.processToolCalls( toolCalls, nudgeMessages, @@ -12057,6 +12083,7 @@ ${actualMessage}` : actualMessage; if (toolCalls.length === 0 && shouldFallbackToReadTools) { const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools); if (autoToolCalls.length > 0) { + this.showActivityIndicator("Using tools\u2026"); toolCalls = autoToolCalls; fullResponse = ""; await this.processToolCalls( @@ -12120,6 +12147,7 @@ ${actualMessage}` : actualMessage; }); this.syncMessagesToSession(); } finally { + this.hideActivityIndicator(); this.cleanupStreamingResources(); } } @@ -12182,6 +12210,32 @@ ${actualMessage}` : actualMessage; } return message.replace(/\bplease\b/gi, "").replace(/\bcontinue\b/gi, "").replace(/\bimplement\b/gi, "").replace(/\bcreate\b/gi, "").replace(/\bmove\b/gi, "").replace(/\bnotes?\b/gi, "").replace(/\bfolders?\b/gi, "").replace(/\bstructure\b/gi, "").replace(/\s+/g, " ").trim(); } + showActivityIndicator(text) { + if (!this.activityIndicatorEl) return; + const textEl = this.activityIndicatorEl.querySelector(".ollama-activity-text"); + if (textEl) { + textEl.textContent = text; + } + this.activityIndicatorEl.style.display = "flex"; + if (this.sendButton && this.sendButton instanceof HTMLButtonElement) { + this.sendButton.disabled = true; + this.sendButton.textContent = "Working\u2026"; + } + if (this.inputEl) { + this.inputEl.disabled = true; + } + } + hideActivityIndicator() { + if (!this.activityIndicatorEl) return; + this.activityIndicatorEl.style.display = "none"; + if (this.sendButton && this.sendButton instanceof HTMLButtonElement) { + this.sendButton.disabled = false; + this.sendButton.textContent = "Send"; + } + if (this.inputEl) { + this.inputEl.disabled = false; + } + } createOllamaClient(model, settings) { return new OllamaClient(settings.ollamaUrl, model, void 0, settings.cacheConfig); } diff --git a/src/chat-view.ts b/src/chat-view.ts index 10f9870..585d620 100755 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -398,10 +398,30 @@ export class ChatView extends ItemView { inputContainer.appendChild(this.sendButton); } + // Setup activity indicator + if (!this.activityIndicatorEl) { + this.activityIndicatorEl = inputContainer.createEl('div', { + cls: 'ollama-activity-indicator', + }); + this.activityIndicatorEl.createEl('span', { cls: 'ollama-activity-dot' }); + this.activityIndicatorEl.createEl('span', { + cls: 'ollama-activity-text', + text: '', + }); + this.hideActivityIndicator(); + } else { + inputContainer.appendChild(this.activityIndicatorEl); + } + // Append containers to contentEl + if (this.contentEl.addClass) { + this.contentEl.addClass('ollama-chat-view-content'); + } else { + this.contentEl.classList?.add('ollama-chat-view-content'); + } this.contentEl.appendChild(newChatContainer); - this.contentEl.appendChild(inputContainer); this.contentEl.appendChild(container); + this.contentEl.appendChild(inputContainer); // Auto-scroll chat to bottom if enabled if (this.shouldAutoScroll && container) { @@ -1287,12 +1307,15 @@ export class ChatView extends ItemView { } if (isWorkflowCommand) { + this.showActivityIndicator('Running workflow…'); await this.handleWorkflowRequest(actualMessage, assistantMessageId); + this.hideActivityIndicator(); this.cleanupStreamingResources(); return; } try { + this.showActivityIndicator('Thinking…'); const noteContext = await this.noteContextBuilder.buildContext( actualMessage, this.settings.vaultSearchLimit, @@ -1369,6 +1392,7 @@ export class ChatView extends ItemView { // Process tool calls if any if (toolCalls.length > 0) { + this.showActivityIndicator('Using tools…'); await this.processToolCalls( toolCalls, messagesWithMemory, @@ -1388,6 +1412,7 @@ export class ChatView extends ItemView { ) { shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse); if (shouldFallbackToReadTools) { + this.showActivityIndicator('Thinking…'); const nudgeMessages: OllamaMessage[] = [ ...messagesWithMemory, { role: 'assistant', content: fullResponse }, @@ -1414,6 +1439,7 @@ export class ChatView extends ItemView { } } if (toolCalls.length > 0) { + this.showActivityIndicator('Using tools…'); await this.processToolCalls( toolCalls, nudgeMessages, @@ -1432,6 +1458,7 @@ export class ChatView extends ItemView { if (toolCalls.length === 0 && shouldFallbackToReadTools) { const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools); if (autoToolCalls.length > 0) { + this.showActivityIndicator('Using tools…'); toolCalls = autoToolCalls; fullResponse = ''; await this.processToolCalls( @@ -1509,6 +1536,7 @@ export class ChatView extends ItemView { this.syncMessagesToSession(); } finally { // Clean up streaming resources regardless of outcome + this.hideActivityIndicator(); this.cleanupStreamingResources(); } } @@ -1646,6 +1674,37 @@ export class ChatView extends ItemView { private showLogsClickHandler: (() => void) | null = null; private chatScrollHandler: (() => void) | null = null; + // Activity indicator + private activityIndicatorEl: HTMLElement | null = null; + + private showActivityIndicator(text: string): void { + if (!this.activityIndicatorEl) return; + const textEl = this.activityIndicatorEl.querySelector('.ollama-activity-text'); + if (textEl) { + textEl.textContent = text; + } + this.activityIndicatorEl.style.display = 'flex'; + if (this.sendButton && this.sendButton instanceof HTMLButtonElement) { + this.sendButton.disabled = true; + this.sendButton.textContent = 'Working…'; + } + if (this.inputEl) { + this.inputEl.disabled = true; + } + } + + private hideActivityIndicator(): void { + if (!this.activityIndicatorEl) return; + this.activityIndicatorEl.style.display = 'none'; + if (this.sendButton && this.sendButton instanceof HTMLButtonElement) { + this.sendButton.disabled = false; + this.sendButton.textContent = 'Send'; + } + if (this.inputEl) { + this.inputEl.disabled = false; + } + } + private createOllamaClient(model: string, settings: PluginSettings): OllamaClient { return new OllamaClient(settings.ollamaUrl, model, undefined, settings.cacheConfig); } diff --git a/styles.css b/styles.css index 0d0542f..ae88592 100644 --- a/styles.css +++ b/styles.css @@ -11,13 +11,41 @@ --ollama-log-bg: var(--background-primary-alt); } +/* Root chat view layout: flex column with input locked at bottom */ +.ollama-chat-view-content { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.ollama-new-chat-container { + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--size-4-1); + padding: var(--size-4-1) var(--size-4-2); + flex-shrink: 0; +} + .ollama-chat-container { display: flex; flex-direction: column; gap: var(--ollama-gap); padding: var(--size-4-2); overflow-y: auto; - flex: 1; + flex: 1 1 auto; + min-height: 0; +} + +.ollama-input-container { + display: flex; + gap: var(--size-4-1); + padding: var(--size-4-2); + border-top: 1px solid var(--ollama-border); + background-color: var(--background-primary); + align-items: flex-end; + flex-shrink: 0; } .ollama-message { @@ -280,14 +308,6 @@ } /* Agent Mode Selector */ -.ollama-new-chat-container { - display: flex; - justify-content: flex-end; - align-items: center; - gap: var(--size-4-1); - padding: var(--size-4-1) var(--size-4-2); -} - .ollama-mode-selector { padding: var(--size-4-1) var(--size-4-2); border-radius: var(--ollama-radius); @@ -419,3 +439,46 @@ color: var(--text-normal); word-break: break-word; } + +/* Activity Indicator */ +.ollama-activity-indicator { + display: none; + align-items: center; + gap: var(--size-4-1); + padding: var(--size-4-1) 0; + font-size: var(--font-ui-small); + color: var(--text-muted); + min-height: 1.5rem; +} + +.ollama-activity-dot { + display: inline-block; + width: 0.6rem; + height: 0.6rem; + border-radius: 50%; + background-color: var(--interactive-accent); + animation: ollama-pulse 1.2s ease-in-out infinite; +} + +.ollama-activity-text { + font-style: italic; +} + +@keyframes ollama-pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.4; + transform: scale(0.8); + } +} + +/* Disabled input/send during activity */ +.ollama-input:disabled, +.ollama-send-button:disabled { + opacity: 0.6; + cursor: not-allowed; +}