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; +}