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.
This commit is contained in:
@@ -10885,6 +10885,8 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.removeLogListener = null;
|
this.removeLogListener = null;
|
||||||
this.showLogsClickHandler = null;
|
this.showLogsClickHandler = null;
|
||||||
this.chatScrollHandler = null;
|
this.chatScrollHandler = null;
|
||||||
|
// Activity indicator
|
||||||
|
this.activityIndicatorEl = null;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.lastMessageEl = null;
|
this.lastMessageEl = null;
|
||||||
this.newChatButton = null;
|
this.newChatButton = null;
|
||||||
@@ -11182,9 +11184,27 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
} else {
|
} else {
|
||||||
inputContainer.appendChild(this.sendButton);
|
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(newChatContainer);
|
||||||
this.contentEl.appendChild(inputContainer);
|
|
||||||
this.contentEl.appendChild(container);
|
this.contentEl.appendChild(container);
|
||||||
|
this.contentEl.appendChild(inputContainer);
|
||||||
if (this.shouldAutoScroll && container) {
|
if (this.shouldAutoScroll && container) {
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
@@ -11940,11 +11960,14 @@ ${action.preview.after.slice(0, 500)}`
|
|||||||
this.lastMessageEl = previousStreamingEl;
|
this.lastMessageEl = previousStreamingEl;
|
||||||
}
|
}
|
||||||
if (isWorkflowCommand) {
|
if (isWorkflowCommand) {
|
||||||
|
this.showActivityIndicator("Running workflow\u2026");
|
||||||
await this.handleWorkflowRequest(actualMessage, assistantMessageId);
|
await this.handleWorkflowRequest(actualMessage, assistantMessageId);
|
||||||
|
this.hideActivityIndicator();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
this.showActivityIndicator("Thinking\u2026");
|
||||||
const noteContext = await this.noteContextBuilder.buildContext(
|
const noteContext = await this.noteContextBuilder.buildContext(
|
||||||
actualMessage,
|
actualMessage,
|
||||||
this.settings.vaultSearchLimit,
|
this.settings.vaultSearchLimit,
|
||||||
@@ -12003,6 +12026,7 @@ ${actualMessage}` : actualMessage;
|
|||||||
durationMs: llmDurationMs
|
durationMs: llmDurationMs
|
||||||
});
|
});
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
|
this.showActivityIndicator("Using tools\u2026");
|
||||||
await this.processToolCalls(
|
await this.processToolCalls(
|
||||||
toolCalls,
|
toolCalls,
|
||||||
messagesWithMemory,
|
messagesWithMemory,
|
||||||
@@ -12016,6 +12040,7 @@ ${actualMessage}` : actualMessage;
|
|||||||
if (toolCalls.length === 0 && toolCapableModes.includes(this.currentAgentMode) && fullResponse.trim().length > 0) {
|
if (toolCalls.length === 0 && toolCapableModes.includes(this.currentAgentMode) && fullResponse.trim().length > 0) {
|
||||||
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
||||||
if (shouldFallbackToReadTools) {
|
if (shouldFallbackToReadTools) {
|
||||||
|
this.showActivityIndicator("Thinking\u2026");
|
||||||
const nudgeMessages = [
|
const nudgeMessages = [
|
||||||
...messagesWithMemory,
|
...messagesWithMemory,
|
||||||
{ role: "assistant", content: fullResponse },
|
{ role: "assistant", content: fullResponse },
|
||||||
@@ -12041,6 +12066,7 @@ ${actualMessage}` : actualMessage;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
|
this.showActivityIndicator("Using tools\u2026");
|
||||||
await this.processToolCalls(
|
await this.processToolCalls(
|
||||||
toolCalls,
|
toolCalls,
|
||||||
nudgeMessages,
|
nudgeMessages,
|
||||||
@@ -12057,6 +12083,7 @@ ${actualMessage}` : actualMessage;
|
|||||||
if (toolCalls.length === 0 && shouldFallbackToReadTools) {
|
if (toolCalls.length === 0 && shouldFallbackToReadTools) {
|
||||||
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
||||||
if (autoToolCalls.length > 0) {
|
if (autoToolCalls.length > 0) {
|
||||||
|
this.showActivityIndicator("Using tools\u2026");
|
||||||
toolCalls = autoToolCalls;
|
toolCalls = autoToolCalls;
|
||||||
fullResponse = "";
|
fullResponse = "";
|
||||||
await this.processToolCalls(
|
await this.processToolCalls(
|
||||||
@@ -12120,6 +12147,7 @@ ${actualMessage}` : actualMessage;
|
|||||||
});
|
});
|
||||||
this.syncMessagesToSession();
|
this.syncMessagesToSession();
|
||||||
} finally {
|
} finally {
|
||||||
|
this.hideActivityIndicator();
|
||||||
this.cleanupStreamingResources();
|
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();
|
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) {
|
createOllamaClient(model, settings) {
|
||||||
return new OllamaClient(settings.ollamaUrl, model, void 0, settings.cacheConfig);
|
return new OllamaClient(settings.ollamaUrl, model, void 0, settings.cacheConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-1
@@ -398,10 +398,30 @@ export class ChatView extends ItemView {
|
|||||||
inputContainer.appendChild(this.sendButton);
|
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
|
// 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(newChatContainer);
|
||||||
this.contentEl.appendChild(inputContainer);
|
|
||||||
this.contentEl.appendChild(container);
|
this.contentEl.appendChild(container);
|
||||||
|
this.contentEl.appendChild(inputContainer);
|
||||||
|
|
||||||
// Auto-scroll chat to bottom if enabled
|
// Auto-scroll chat to bottom if enabled
|
||||||
if (this.shouldAutoScroll && container) {
|
if (this.shouldAutoScroll && container) {
|
||||||
@@ -1287,12 +1307,15 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isWorkflowCommand) {
|
if (isWorkflowCommand) {
|
||||||
|
this.showActivityIndicator('Running workflow…');
|
||||||
await this.handleWorkflowRequest(actualMessage, assistantMessageId);
|
await this.handleWorkflowRequest(actualMessage, assistantMessageId);
|
||||||
|
this.hideActivityIndicator();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.showActivityIndicator('Thinking…');
|
||||||
const noteContext = await this.noteContextBuilder.buildContext(
|
const noteContext = await this.noteContextBuilder.buildContext(
|
||||||
actualMessage,
|
actualMessage,
|
||||||
this.settings.vaultSearchLimit,
|
this.settings.vaultSearchLimit,
|
||||||
@@ -1369,6 +1392,7 @@ export class ChatView extends ItemView {
|
|||||||
|
|
||||||
// Process tool calls if any
|
// Process tool calls if any
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
|
this.showActivityIndicator('Using tools…');
|
||||||
await this.processToolCalls(
|
await this.processToolCalls(
|
||||||
toolCalls,
|
toolCalls,
|
||||||
messagesWithMemory,
|
messagesWithMemory,
|
||||||
@@ -1388,6 +1412,7 @@ export class ChatView extends ItemView {
|
|||||||
) {
|
) {
|
||||||
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
||||||
if (shouldFallbackToReadTools) {
|
if (shouldFallbackToReadTools) {
|
||||||
|
this.showActivityIndicator('Thinking…');
|
||||||
const nudgeMessages: OllamaMessage[] = [
|
const nudgeMessages: OllamaMessage[] = [
|
||||||
...messagesWithMemory,
|
...messagesWithMemory,
|
||||||
{ role: 'assistant', content: fullResponse },
|
{ role: 'assistant', content: fullResponse },
|
||||||
@@ -1414,6 +1439,7 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
|
this.showActivityIndicator('Using tools…');
|
||||||
await this.processToolCalls(
|
await this.processToolCalls(
|
||||||
toolCalls,
|
toolCalls,
|
||||||
nudgeMessages,
|
nudgeMessages,
|
||||||
@@ -1432,6 +1458,7 @@ export class ChatView extends ItemView {
|
|||||||
if (toolCalls.length === 0 && shouldFallbackToReadTools) {
|
if (toolCalls.length === 0 && shouldFallbackToReadTools) {
|
||||||
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
||||||
if (autoToolCalls.length > 0) {
|
if (autoToolCalls.length > 0) {
|
||||||
|
this.showActivityIndicator('Using tools…');
|
||||||
toolCalls = autoToolCalls;
|
toolCalls = autoToolCalls;
|
||||||
fullResponse = '';
|
fullResponse = '';
|
||||||
await this.processToolCalls(
|
await this.processToolCalls(
|
||||||
@@ -1509,6 +1536,7 @@ export class ChatView extends ItemView {
|
|||||||
this.syncMessagesToSession();
|
this.syncMessagesToSession();
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up streaming resources regardless of outcome
|
// Clean up streaming resources regardless of outcome
|
||||||
|
this.hideActivityIndicator();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1646,6 +1674,37 @@ export class ChatView extends ItemView {
|
|||||||
private showLogsClickHandler: (() => void) | null = null;
|
private showLogsClickHandler: (() => void) | null = null;
|
||||||
private chatScrollHandler: (() => 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 {
|
private createOllamaClient(model: string, settings: PluginSettings): OllamaClient {
|
||||||
return new OllamaClient(settings.ollamaUrl, model, undefined, settings.cacheConfig);
|
return new OllamaClient(settings.ollamaUrl, model, undefined, settings.cacheConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-9
@@ -11,13 +11,41 @@
|
|||||||
--ollama-log-bg: var(--background-primary-alt);
|
--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 {
|
.ollama-chat-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--ollama-gap);
|
gap: var(--ollama-gap);
|
||||||
padding: var(--size-4-2);
|
padding: var(--size-4-2);
|
||||||
overflow-y: auto;
|
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 {
|
.ollama-message {
|
||||||
@@ -280,14 +308,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Agent Mode Selector */
|
/* 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 {
|
.ollama-mode-selector {
|
||||||
padding: var(--size-4-1) var(--size-4-2);
|
padding: var(--size-4-1) var(--size-4-2);
|
||||||
border-radius: var(--ollama-radius);
|
border-radius: var(--ollama-radius);
|
||||||
@@ -419,3 +439,46 @@
|
|||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
word-break: break-word;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user