Add model change callback and improve chat UI robustness

- Persist selected model to settings when changed via dropdown
- Reorder chat controls: model selector before mode selector, new chat before delete
- Add null guards for modelSelectorEl during async populate
- Fix streaming state and empty response handling in tool nudge loop
- Expand vault operation keyword detection
- Enable text selection in chat messages
- Include original error message in chat error display
This commit is contained in:
2026-05-21 16:47:08 +02:00
parent f6edc321e3
commit 624df32c37
5 changed files with 155 additions and 76 deletions
+67 -32
View File
@@ -57,7 +57,8 @@ export class ChatView extends ItemView {
vectorStore?: VaultVectorStore,
structuredMemoryManager?: StructuredMemoryManager,
telemetryManager?: TelemetryManager,
chatHistoryManager?: ChatHistoryManager
chatHistoryManager?: ChatHistoryManager,
onModelChange?: (model: string) => void
) {
super(leaf);
this.messages = [];
@@ -96,6 +97,7 @@ export class ChatView extends ItemView {
this.structuredMemoryManager = structuredMemoryManager;
this.telemetryManager = telemetryManager;
this.chatHistoryManager = chatHistoryManager;
this.onModelChange = onModelChange;
this.workflowEngine = new WorkflowEngine(
this.app.vault,
this.app,
@@ -129,6 +131,8 @@ export class ChatView extends ItemView {
(newSettings.agentModel ?? newSettings.model) === (newSettings.chatModel ?? newSettings.model)
? this.ollamaClient
: this.createOllamaClient(newSettings.agentModel ?? newSettings.model, newSettings);
// Refresh model dropdown so it reflects the newly saved model
void this.populateModelDropdown();
this.workflowEngine = new WorkflowEngine(
this.app.vault,
this.app,
@@ -202,6 +206,7 @@ export class ChatView extends ItemView {
this.chatContainer = null;
this.showLogsButton = null;
this.logsContainer = null;
this.modelSelectorEl = null;
this.historySelectEl = null;
this.historyDeleteButton = null;
return Promise.resolve();
@@ -280,6 +285,23 @@ export class ChatView extends ItemView {
}
}
// Setup model selector
if (!this.modelSelectorEl) {
this.modelSelectorEl = newChatContainer.createEl('select', {
cls: 'ollama-model-selector',
});
this.modelSelectorEl.addEventListener('change', () => {
const selectedModel = this.modelSelectorEl!.value;
this.ollamaClient.setModel(selectedModel);
if (this.agentOllamaClient !== this.ollamaClient) {
this.agentOllamaClient.setModel(selectedModel);
}
this.onModelChange?.(selectedModel);
});
}
this.populateModelDropdown();
newChatContainer.appendChild(this.modelSelectorEl);
// Setup mode selector
if (!this.modeSelectorEl) {
this.modeSelectorEl = newChatContainer.createEl('select', {
@@ -301,22 +323,6 @@ export class ChatView extends ItemView {
newChatContainer.appendChild(this.modeSelectorEl);
}
// Setup model selector
if (!this.modelSelectorEl) {
this.modelSelectorEl = newChatContainer.createEl('select', {
cls: 'ollama-model-selector',
});
this.modelSelectorEl.addEventListener('change', () => {
const selectedModel = this.modelSelectorEl!.value;
this.ollamaClient.setModel(selectedModel);
if (this.agentOllamaClient !== this.ollamaClient) {
this.agentOllamaClient.setModel(selectedModel);
}
});
}
this.populateModelDropdown();
newChatContainer.appendChild(this.modelSelectorEl);
// Setup chat history selector
if (!this.historySelectEl) {
this.historySelectEl = newChatContainer.createEl('select', {
@@ -339,6 +345,16 @@ export class ChatView extends ItemView {
this.populateHistoryDropdown();
newChatContainer.appendChild(this.historySelectEl);
// Setup new chat button
if (!this.newChatButton) {
this.newChatButton = newChatContainer.createEl('button', {
cls: 'ollama-new-chat-button',
text: 'New Chat',
});
} else {
newChatContainer.appendChild(this.newChatButton);
}
// Setup delete history button
if (!this.historyDeleteButton) {
this.historyDeleteButton = newChatContainer.createEl('button', {
@@ -358,16 +374,6 @@ export class ChatView extends ItemView {
}
newChatContainer.appendChild(this.historyDeleteButton);
// Setup new chat button
if (!this.newChatButton) {
this.newChatButton = newChatContainer.createEl('button', {
cls: 'ollama-new-chat-button',
text: 'New Chat',
});
} else {
newChatContainer.appendChild(this.newChatButton);
}
// Setup show-logs toggle button
if (!this.showLogsButton) {
this.showLogsButton = newChatContainer.createEl('button', {
@@ -641,6 +647,7 @@ export class ChatView extends ItemView {
this.modelSelectorEl.innerHTML = '';
const models = await this.ollamaClient.listModels();
if (!this.modelSelectorEl) return; // Guard: view may have closed while fetching
const currentModel = this.ollamaClient.getModel();
if (models.length === 0) {
@@ -654,6 +661,7 @@ export class ChatView extends ItemView {
}
for (const model of models) {
if (!this.modelSelectorEl) return;
const displayName = model.name;
const option = this.modelSelectorEl.createEl('option', {
text: displayName,
@@ -664,6 +672,7 @@ export class ChatView extends ItemView {
}
}
if (!this.modelSelectorEl) return;
// If the previously selected value is still valid, keep it
if (previousValue && models.some((m) => m.name === previousValue)) {
this.modelSelectorEl.value = previousValue;
@@ -1468,11 +1477,11 @@ export class ChatView extends ItemView {
if (modelMentionedActions || userWantsVaultOps) {
shouldFallbackToReadTools = true;
// Suppress the model's "Let me..." text — replace with actual tool execution
// Suppress the model's "Let me..." text — clear it from the DOM immediately
fullResponse = '';
this.updateMessageById(assistantMessageId, {
content: '',
isStreaming: true,
isStreaming: false,
isThinking: false,
});
@@ -1488,9 +1497,12 @@ export class ChatView extends ItemView {
attempts === 1 ? 'Thinking…' : `Retrying (${attempts}/${maxAttempts})…`
);
// Build nudge messages — skip empty assistant content to avoid API issues
const nudgeMessages: OllamaMessage[] = [
...currentMessages,
{ role: 'assistant', content: currentResponse },
...(currentResponse.trim()
? [{ role: 'assistant' as const, content: currentResponse }]
: []),
{
role: 'user',
content:
@@ -1553,7 +1565,7 @@ export class ChatView extends ItemView {
// Update assistant message — only if no tool calls were processed
if (toolCalls.length === 0) {
this.updateMessageById(assistantMessageId, {
content: fullResponse,
content: fullResponse || '(No response)',
isStreaming: false,
isThinking: false,
});
@@ -1607,8 +1619,9 @@ export class ChatView extends ItemView {
this.syncMessagesToSession();
} catch (error) {
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
const errorMsg = error instanceof Error ? error.message : String(error);
this.updateMessageById(assistantMessageId, {
content: 'An error occurred while processing your request.',
content: `An error occurred: ${errorMsg}`,
isStreaming: false,
isThinking: false,
});
@@ -1676,6 +1689,27 @@ export class ChatView extends ItemView {
'vault',
'notes',
'files',
'continue',
'go ahead',
'start',
'do it',
'execute',
'run',
'proceed',
'next',
'now',
'yes',
'ok',
'okay',
'sure',
'please',
'step',
'merge',
'clean',
'fix',
'update',
'implement',
'apply',
];
return operationPhrases.some((phrase) => lower.includes(phrase));
}
@@ -1772,6 +1806,7 @@ export class ChatView extends ItemView {
private telemetryManager?: TelemetryManager;
private chatHistoryManager?: ChatHistoryManager;
private vectorStore?: VaultVectorStore;
private onModelChange?: (model: string) => void;
private modeSelectorEl: HTMLSelectElement | null = null;
private modelSelectorEl: HTMLSelectElement | null = null;