Add stop button to cancel in-flight requests
Swap the Send button for a Stop button while a response is streaming, letting users abort the current operation cleanly. Cancelled requests now display "Stopped." instead of an error message. Also includes two smaller fixes: preserve the original assistant text and full tool_calls array when executing write tools for follow-up context, and add 'research' to agentic modes.
This commit is contained in:
@@ -10914,6 +10914,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.lastMessageEl = null;
|
this.lastMessageEl = null;
|
||||||
this.newChatButton = null;
|
this.newChatButton = null;
|
||||||
this.sendButton = null;
|
this.sendButton = null;
|
||||||
|
this.stopButton = null;
|
||||||
this.inputEl = null;
|
this.inputEl = null;
|
||||||
this.chatContainer = null;
|
this.chatContainer = null;
|
||||||
this.sendButtonClickHandler = null;
|
this.sendButtonClickHandler = null;
|
||||||
@@ -10923,6 +10924,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.inputKeyDownWrapper = null;
|
this.inputKeyDownWrapper = null;
|
||||||
this.newChatButtonClickWrapper = null;
|
this.newChatButtonClickWrapper = null;
|
||||||
this.listenersAttached = false;
|
this.listenersAttached = false;
|
||||||
|
this.isCancelled = false;
|
||||||
this.modeSelectorEl = null;
|
this.modeSelectorEl = null;
|
||||||
this.modelSelectorEl = null;
|
this.modelSelectorEl = null;
|
||||||
this.historySelectEl = null;
|
this.historySelectEl = null;
|
||||||
@@ -11074,6 +11076,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
}
|
}
|
||||||
this.lastMessageEl = null;
|
this.lastMessageEl = null;
|
||||||
this.sendButton = null;
|
this.sendButton = null;
|
||||||
|
this.stopButton = null;
|
||||||
this.inputEl = null;
|
this.inputEl = null;
|
||||||
this.chatContainer = null;
|
this.chatContainer = null;
|
||||||
this.showLogsButton = null;
|
this.showLogsButton = null;
|
||||||
@@ -11268,6 +11271,17 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
} else {
|
} else {
|
||||||
inputContainer.appendChild(this.sendButton);
|
inputContainer.appendChild(this.sendButton);
|
||||||
}
|
}
|
||||||
|
if (!this.stopButton) {
|
||||||
|
this.stopButton = inputContainer.createEl("button", {
|
||||||
|
cls: "ollama-stop-button",
|
||||||
|
text: "Stop"
|
||||||
|
});
|
||||||
|
this.stopButton.addEventListener("click", () => {
|
||||||
|
this.cancelCurrentOperation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
inputContainer.appendChild(this.stopButton);
|
||||||
|
this.stopButton.style.display = "none";
|
||||||
if (this.contentEl.addClass) {
|
if (this.contentEl.addClass) {
|
||||||
this.contentEl.addClass("ollama-chat-view-content");
|
this.contentEl.addClass("ollama-chat-view-content");
|
||||||
} else {
|
} else {
|
||||||
@@ -12279,15 +12293,25 @@ ${actualMessage}` : actualMessage;
|
|||||||
this.render();
|
this.render();
|
||||||
this.syncMessagesToSession();
|
this.syncMessagesToSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handleError(error, "ChatView.handleUserInput");
|
if (this.isCancelled) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
this.updateMessageById(assistantMessageId, {
|
||||||
this.updateMessageById(assistantMessageId, {
|
content: "Stopped.",
|
||||||
content: `An error occurred: ${errorMsg}`,
|
isStreaming: false,
|
||||||
isStreaming: false,
|
isThinking: false
|
||||||
isThinking: false
|
});
|
||||||
});
|
this.syncMessagesToSession();
|
||||||
this.syncMessagesToSession();
|
} else {
|
||||||
|
ErrorHandler.handleError(error, "ChatView.handleUserInput");
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
this.updateMessageById(assistantMessageId, {
|
||||||
|
content: `An error occurred: ${errorMsg}`,
|
||||||
|
isStreaming: false,
|
||||||
|
isThinking: false
|
||||||
|
});
|
||||||
|
this.syncMessagesToSession();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
this.isCancelled = false;
|
||||||
this.hideActivityIndicator();
|
this.hideActivityIndicator();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
}
|
}
|
||||||
@@ -12407,9 +12431,11 @@ ${actualMessage}` : actualMessage;
|
|||||||
textEl.textContent = text;
|
textEl.textContent = text;
|
||||||
}
|
}
|
||||||
this.activityIndicatorEl.style.display = "flex";
|
this.activityIndicatorEl.style.display = "flex";
|
||||||
if (this.sendButton && this.sendButton instanceof HTMLButtonElement) {
|
if (this.sendButton) {
|
||||||
this.sendButton.disabled = true;
|
this.sendButton.style.display = "none";
|
||||||
this.sendButton.textContent = "Working\u2026";
|
}
|
||||||
|
if (this.stopButton) {
|
||||||
|
this.stopButton.style.display = "inline-flex";
|
||||||
}
|
}
|
||||||
if (this.inputEl) {
|
if (this.inputEl) {
|
||||||
this.inputEl.disabled = true;
|
this.inputEl.disabled = true;
|
||||||
@@ -12418,14 +12444,23 @@ ${actualMessage}` : actualMessage;
|
|||||||
hideActivityIndicator() {
|
hideActivityIndicator() {
|
||||||
if (!this.activityIndicatorEl) return;
|
if (!this.activityIndicatorEl) return;
|
||||||
this.activityIndicatorEl.style.display = "none";
|
this.activityIndicatorEl.style.display = "none";
|
||||||
if (this.sendButton && this.sendButton instanceof HTMLButtonElement) {
|
if (this.sendButton) {
|
||||||
this.sendButton.disabled = false;
|
this.sendButton.style.display = "inline-flex";
|
||||||
this.sendButton.textContent = "Send";
|
}
|
||||||
|
if (this.stopButton) {
|
||||||
|
this.stopButton.style.display = "none";
|
||||||
}
|
}
|
||||||
if (this.inputEl) {
|
if (this.inputEl) {
|
||||||
this.inputEl.disabled = false;
|
this.inputEl.disabled = false;
|
||||||
|
this.inputEl.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cancelCurrentOperation() {
|
||||||
|
this.isCancelled = true;
|
||||||
|
this.ollamaClient.cancelStream();
|
||||||
|
this.agentOllamaClient.cancelStream();
|
||||||
|
new import_obsidian5.Notice("Stopping\u2026");
|
||||||
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-20
@@ -202,6 +202,7 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
this.lastMessageEl = null;
|
this.lastMessageEl = null;
|
||||||
this.sendButton = null;
|
this.sendButton = null;
|
||||||
|
this.stopButton = null;
|
||||||
this.inputEl = null;
|
this.inputEl = null;
|
||||||
this.chatContainer = null;
|
this.chatContainer = null;
|
||||||
this.showLogsButton = null;
|
this.showLogsButton = null;
|
||||||
@@ -435,6 +436,19 @@ export class ChatView extends ItemView {
|
|||||||
inputContainer.appendChild(this.sendButton);
|
inputContainer.appendChild(this.sendButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup stop button
|
||||||
|
if (!this.stopButton) {
|
||||||
|
this.stopButton = inputContainer.createEl('button', {
|
||||||
|
cls: 'ollama-stop-button',
|
||||||
|
text: 'Stop',
|
||||||
|
});
|
||||||
|
this.stopButton.addEventListener('click', () => {
|
||||||
|
this.cancelCurrentOperation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
inputContainer.appendChild(this.stopButton);
|
||||||
|
this.stopButton.style.display = 'none';
|
||||||
|
|
||||||
// Append containers to contentEl
|
// Append containers to contentEl
|
||||||
if (this.contentEl.addClass) {
|
if (this.contentEl.addClass) {
|
||||||
this.contentEl.addClass('ollama-chat-view-content');
|
this.contentEl.addClass('ollama-chat-view-content');
|
||||||
@@ -1015,7 +1029,7 @@ export class ChatView extends ItemView {
|
|||||||
// Store pending state for apply/cancel
|
// Store pending state for apply/cancel
|
||||||
this.pendingActions = writePreviews;
|
this.pendingActions = writePreviews;
|
||||||
this.pendingReadResults = readResults;
|
this.pendingReadResults = readResults;
|
||||||
this.pendingFollowUpContext = { messages, tools, assistantMessageId };
|
this.pendingFollowUpContext = { messages, tools, assistantMessageId, allToolCalls: toolCalls, assistantText: fullResponse };
|
||||||
|
|
||||||
this.updateMessageById(assistantMessageId, {
|
this.updateMessageById(assistantMessageId, {
|
||||||
content: `${fullResponse}\n\n*Proposed actions:*\n${writePreviews.map((a) => `- ${a.description}`).join('\n')}`,
|
content: `${fullResponse}\n\n*Proposed actions:*\n${writePreviews.map((a) => `- ${a.description}`).join('\n')}`,
|
||||||
@@ -1112,7 +1126,7 @@ export class ChatView extends ItemView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { messages, tools, assistantMessageId } = this.pendingFollowUpContext;
|
const { messages, tools, assistantMessageId, allToolCalls, assistantText } = this.pendingFollowUpContext;
|
||||||
|
|
||||||
// Execute write tools
|
// Execute write tools
|
||||||
const writeResults = (
|
const writeResults = (
|
||||||
@@ -1139,8 +1153,8 @@ export class ChatView extends ItemView {
|
|||||||
|
|
||||||
const followUp: OllamaMessage = {
|
const followUp: OllamaMessage = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'I have processed your request using the following tools. Here are the results:',
|
content: assistantText,
|
||||||
tool_calls: this.pendingActions.map((a) => a.toolCall),
|
tool_calls: allToolCalls,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (followUpMessages.length > 0) {
|
if (followUpMessages.length > 0) {
|
||||||
@@ -1535,7 +1549,9 @@ export class ChatView extends ItemView {
|
|||||||
this.showActivityIndicator('Thinking…');
|
this.showActivityIndicator('Thinking…');
|
||||||
const nudgeMessages: OllamaMessage[] = [
|
const nudgeMessages: OllamaMessage[] = [
|
||||||
...messagesWithMemory,
|
...messagesWithMemory,
|
||||||
...(priorResponse.trim() ? [{ role: 'assistant' as const, content: priorResponse }] : []),
|
...(priorResponse.trim()
|
||||||
|
? [{ role: 'assistant' as const, content: priorResponse }]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content:
|
content:
|
||||||
@@ -1641,16 +1657,27 @@ export class ChatView extends ItemView {
|
|||||||
this.render();
|
this.render();
|
||||||
this.syncMessagesToSession();
|
this.syncMessagesToSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
if (this.isCancelled) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
// User-initiated stop — show clean message, not an error
|
||||||
this.updateMessageById(assistantMessageId, {
|
this.updateMessageById(assistantMessageId, {
|
||||||
content: `An error occurred: ${errorMsg}`,
|
content: 'Stopped.',
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
isThinking: false,
|
isThinking: false,
|
||||||
});
|
});
|
||||||
this.syncMessagesToSession();
|
this.syncMessagesToSession();
|
||||||
|
} else {
|
||||||
|
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
this.updateMessageById(assistantMessageId, {
|
||||||
|
content: `An error occurred: ${errorMsg}`,
|
||||||
|
isStreaming: false,
|
||||||
|
isThinking: false,
|
||||||
|
});
|
||||||
|
this.syncMessagesToSession();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up streaming resources regardless of outcome
|
// Clean up streaming resources regardless of outcome
|
||||||
|
this.isCancelled = false;
|
||||||
this.hideActivityIndicator();
|
this.hideActivityIndicator();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
}
|
}
|
||||||
@@ -1797,6 +1824,7 @@ export class ChatView extends ItemView {
|
|||||||
private lastMessageEl: HTMLElement | null = null;
|
private lastMessageEl: HTMLElement | null = null;
|
||||||
private newChatButton: HTMLElement | null = null;
|
private newChatButton: HTMLElement | null = null;
|
||||||
private sendButton: HTMLElement | null = null;
|
private sendButton: HTMLElement | null = null;
|
||||||
|
private stopButton: HTMLElement | null = null;
|
||||||
private inputEl: HTMLTextAreaElement | null = null;
|
private inputEl: HTMLTextAreaElement | null = null;
|
||||||
private chatContainer: HTMLElement | null = null;
|
private chatContainer: HTMLElement | null = null;
|
||||||
private sendButtonClickHandler: (() => void) | null = null;
|
private sendButtonClickHandler: (() => void) | null = null;
|
||||||
@@ -1806,6 +1834,7 @@ export class ChatView extends ItemView {
|
|||||||
private inputKeyDownWrapper: ((event: KeyboardEvent) => void) | null = null;
|
private inputKeyDownWrapper: ((event: KeyboardEvent) => void) | null = null;
|
||||||
private newChatButtonClickWrapper: (() => void) | null = null;
|
private newChatButtonClickWrapper: (() => void) | null = null;
|
||||||
private listenersAttached: boolean = false;
|
private listenersAttached: boolean = false;
|
||||||
|
private isCancelled: boolean = false;
|
||||||
private settings: PluginSettings;
|
private settings: PluginSettings;
|
||||||
private ollamaClient: OllamaClient;
|
private ollamaClient: OllamaClient;
|
||||||
private agentOllamaClient: OllamaClient;
|
private agentOllamaClient: OllamaClient;
|
||||||
@@ -1832,6 +1861,8 @@ export class ChatView extends ItemView {
|
|||||||
messages: OllamaMessage[];
|
messages: OllamaMessage[];
|
||||||
tools: OllamaTool[];
|
tools: OllamaTool[];
|
||||||
assistantMessageId: string;
|
assistantMessageId: string;
|
||||||
|
allToolCalls: OllamaToolCall[];
|
||||||
|
assistantText: string;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
// Auto-scroll & logs UI
|
// Auto-scroll & logs UI
|
||||||
@@ -1853,9 +1884,11 @@ export class ChatView extends ItemView {
|
|||||||
textEl.textContent = text;
|
textEl.textContent = text;
|
||||||
}
|
}
|
||||||
this.activityIndicatorEl.style.display = 'flex';
|
this.activityIndicatorEl.style.display = 'flex';
|
||||||
if (this.sendButton && this.sendButton instanceof HTMLButtonElement) {
|
if (this.sendButton) {
|
||||||
this.sendButton.disabled = true;
|
this.sendButton.style.display = 'none';
|
||||||
this.sendButton.textContent = 'Working…';
|
}
|
||||||
|
if (this.stopButton) {
|
||||||
|
this.stopButton.style.display = 'inline-flex';
|
||||||
}
|
}
|
||||||
if (this.inputEl) {
|
if (this.inputEl) {
|
||||||
this.inputEl.disabled = true;
|
this.inputEl.disabled = true;
|
||||||
@@ -1865,15 +1898,25 @@ export class ChatView extends ItemView {
|
|||||||
private hideActivityIndicator(): void {
|
private hideActivityIndicator(): void {
|
||||||
if (!this.activityIndicatorEl) return;
|
if (!this.activityIndicatorEl) return;
|
||||||
this.activityIndicatorEl.style.display = 'none';
|
this.activityIndicatorEl.style.display = 'none';
|
||||||
if (this.sendButton && this.sendButton instanceof HTMLButtonElement) {
|
if (this.sendButton) {
|
||||||
this.sendButton.disabled = false;
|
this.sendButton.style.display = 'inline-flex';
|
||||||
this.sendButton.textContent = 'Send';
|
}
|
||||||
|
if (this.stopButton) {
|
||||||
|
this.stopButton.style.display = 'none';
|
||||||
}
|
}
|
||||||
if (this.inputEl) {
|
if (this.inputEl) {
|
||||||
this.inputEl.disabled = false;
|
this.inputEl.disabled = false;
|
||||||
|
this.inputEl.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cancelCurrentOperation(): void {
|
||||||
|
this.isCancelled = true;
|
||||||
|
this.ollamaClient.cancelStream();
|
||||||
|
this.agentOllamaClient.cancelStream();
|
||||||
|
new Notice('Stopping…');
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -1896,7 +1939,7 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isAgenticMode(mode: AgentMode): boolean {
|
private isAgenticMode(mode: AgentMode): boolean {
|
||||||
return mode === 'edit' || mode === 'organize' || mode === 'workflow';
|
return mode === 'edit' || mode === 'organize' || mode === 'research' || mode === 'workflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLogEntry(entry: LogEntry, container: HTMLElement): void {
|
private renderLogEntry(entry: LogEntry, container: HTMLElement): void {
|
||||||
|
|||||||
+25
@@ -181,6 +181,31 @@
|
|||||||
filter: brightness(0.95);
|
filter: brightness(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stop button */
|
||||||
|
.ollama-stop-button {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--size-4-1) var(--size-4-3);
|
||||||
|
border-radius: var(--ollama-radius);
|
||||||
|
border: none;
|
||||||
|
background-color: var(--text-error);
|
||||||
|
color: var(--text-on-accent);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-stop-button:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ollama-stop-button:active {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
/* Thinking indicator */
|
/* Thinking indicator */
|
||||||
.ollama-thinking-indicator {
|
.ollama-thinking-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user