95a6954b50
Expand the tool executor with create_note, append_to_note, replace_note_section, update_frontmatter, rename_note, move_note, delete_note, and insert_link tools. Rename create_file to create_note for consistency and add helper methods for common file operations. Update tests to cover the new tools and renamed functionality.
761 lines
24 KiB
TypeScript
Executable File
761 lines
24 KiB
TypeScript
Executable File
import { ItemView, Notice, WorkspaceLeaf } from 'obsidian';
|
|
import { OllamaClient } from './ollama-client';
|
|
import { VaultIndexer } from './vault-indexer';
|
|
import { VaultVectorStore } from './vault-vector-store';
|
|
import { ToolExecutor } from './tool-executor';
|
|
import { PluginSettings, OllamaMessage, OllamaTool, OllamaToolCall, ChatMessage } from './types';
|
|
import { ConversationStateManager } from './conversation-state';
|
|
import { ErrorHandler } from './error-handler';
|
|
|
|
export const VIEW_TYPE_OLLAMA_CHAT = 'ollama-chat-view';
|
|
|
|
export class ChatView extends ItemView {
|
|
// Getters for testing
|
|
getSendButtonClickHandler() {
|
|
return this.sendButtonClickHandler;
|
|
}
|
|
|
|
getInputKeyDownHandler() {
|
|
return this.inputKeyDownHandler;
|
|
}
|
|
|
|
getNewChatButtonClickHandler() {
|
|
return this.newChatButtonClickHandler;
|
|
}
|
|
|
|
constructor(leaf: WorkspaceLeaf, settings: PluginSettings, vectorStore?: VaultVectorStore) {
|
|
super(leaf);
|
|
this.messages = [];
|
|
this.lastMessageEl = null;
|
|
this.newChatButton = null;
|
|
this.sendButton = null;
|
|
this.inputEl = null;
|
|
this.chatContainer = null;
|
|
this.sendButtonClickHandler = null;
|
|
this.inputKeyDownHandler = null;
|
|
this.newChatButtonClickHandler = null;
|
|
this.sendButtonClickWrapper = null;
|
|
this.inputKeyDownWrapper = null;
|
|
this.newChatButtonClickWrapper = null;
|
|
this.listenersAttached = false;
|
|
this.settings = settings;
|
|
this.ollamaClient = new OllamaClient(
|
|
settings.ollamaUrl,
|
|
settings.model,
|
|
undefined,
|
|
settings.cacheConfig
|
|
);
|
|
this.vaultIndexer = new VaultIndexer(this.app.vault, undefined, vectorStore);
|
|
this.toolExecutor = new ToolExecutor(this.app.vault, this.app);
|
|
this.conversationStateManager = new ConversationStateManager();
|
|
}
|
|
|
|
updateSettings(newSettings: PluginSettings) {
|
|
this.settings = newSettings;
|
|
this.ollamaClient = new OllamaClient(
|
|
newSettings.ollamaUrl,
|
|
newSettings.model,
|
|
undefined,
|
|
newSettings.cacheConfig
|
|
);
|
|
void this.ollamaClient.initializeCache().catch(() => {
|
|
new Notice(
|
|
'Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.'
|
|
);
|
|
});
|
|
}
|
|
|
|
setVectorStore(vectorStore: VaultVectorStore | undefined) {
|
|
this.vectorStore = vectorStore;
|
|
this.vaultIndexer.setVectorStore(vectorStore);
|
|
}
|
|
|
|
async clearCache(): Promise<void> {
|
|
await this.ollamaClient.clearCache();
|
|
}
|
|
|
|
getViewType(): string {
|
|
return 'ollama-chat-view';
|
|
}
|
|
|
|
getDisplayText(): string {
|
|
return 'Ollama Chat';
|
|
}
|
|
|
|
getIcon(): string {
|
|
return 'bot';
|
|
}
|
|
|
|
async onOpen(): Promise<void> {
|
|
try {
|
|
await this.ollamaClient.initializeCache();
|
|
} catch {
|
|
new Notice(
|
|
'Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.'
|
|
);
|
|
}
|
|
this.render();
|
|
this.removeEventListeners(); // Clean up any existing listeners before reattaching
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
onSettingsChange(newSettings: PluginSettings): void {
|
|
this.updateSettings(newSettings);
|
|
}
|
|
|
|
async onClose(): Promise<void> {
|
|
this.ollamaClient.cancelStream();
|
|
this.removeEventListeners();
|
|
this.cleanupStreamingResources();
|
|
this.lastMessageEl = null;
|
|
this.sendButton = null;
|
|
this.inputEl = null;
|
|
this.chatContainer = null;
|
|
return Promise.resolve();
|
|
}
|
|
|
|
cleanupStreamingResources(): void {
|
|
// Only cleanup if there's still an active streaming message
|
|
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
|
if (streamingMessage && this.lastMessageEl && this.lastMessageEl.parentElement) {
|
|
this.lastMessageEl.parentElement.removeChild(this.lastMessageEl);
|
|
this.lastMessageEl = null;
|
|
}
|
|
}
|
|
|
|
render(): void {
|
|
const container =
|
|
this.chatContainer || this.contentEl.createEl('div', { cls: 'ollama-chat-container' });
|
|
this.chatContainer = container;
|
|
const inputContainer =
|
|
this.contentEl.querySelector('.ollama-input-container') ||
|
|
this.contentEl.createEl('div', { cls: 'ollama-input-container' });
|
|
const newChatContainer =
|
|
this.contentEl.querySelector('.ollama-new-chat-container') ||
|
|
this.contentEl.createEl('div', { cls: 'ollama-new-chat-container' });
|
|
|
|
const messagesSnapshot = [...this.messages];
|
|
const nonStreamingMessages = messagesSnapshot.filter((msg) => !msg.isStreaming);
|
|
const existingMessages = container.querySelectorAll('.ollama-message');
|
|
|
|
// Remove messages that are no longer in the array
|
|
for (const el of Array.from(existingMessages)) {
|
|
const id = el.getAttribute('data-msg-id');
|
|
if (!id || !nonStreamingMessages.some((m) => m.id === id)) {
|
|
el.remove();
|
|
}
|
|
}
|
|
|
|
// Render non-streaming messages
|
|
for (const msg of nonStreamingMessages) {
|
|
const existingEl = container.querySelector(`.ollama-message[data-msg-id="${msg.id}"]`);
|
|
if (existingEl) {
|
|
const contentEl = existingEl.querySelector('.ollama-message-content');
|
|
if (contentEl) {
|
|
if (msg.isThinking) {
|
|
contentEl.innerHTML = '<span class="ollama-thinking-indicator">Thinking…</span>';
|
|
} else {
|
|
contentEl.textContent = msg.content;
|
|
}
|
|
}
|
|
} else {
|
|
const messageEl = container.createEl('div', {
|
|
cls: `ollama-message ollama-message-${msg.role}`,
|
|
});
|
|
messageEl.setAttribute('data-msg-id', msg.id);
|
|
|
|
const headerEl = messageEl.createEl('div', { cls: 'ollama-message-header' });
|
|
headerEl.createEl('span', { cls: 'ollama-message-role', text: msg.role });
|
|
|
|
const contentEl = messageEl.createEl('div', { cls: 'ollama-message-content' });
|
|
if (msg.isThinking) {
|
|
contentEl.innerHTML = '<span class="ollama-thinking-indicator">Thinking…</span>';
|
|
} else {
|
|
contentEl.textContent = msg.content;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-attach streaming message if it exists
|
|
const streamingMessage = messagesSnapshot.find((msg) => msg.isStreaming);
|
|
if (streamingMessage && this.lastMessageEl) {
|
|
const existingStreamingEl = container.querySelector(
|
|
`.ollama-message[data-msg-id="${streamingMessage.id}"]`
|
|
);
|
|
if (!existingStreamingEl) {
|
|
container.appendChild(this.lastMessageEl);
|
|
}
|
|
}
|
|
|
|
// 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 input area
|
|
if (!this.inputEl) {
|
|
this.inputEl = inputContainer.createEl('textarea', {
|
|
cls: 'ollama-input',
|
|
attr: { placeholder: 'Type your message...' },
|
|
});
|
|
} else {
|
|
inputContainer.appendChild(this.inputEl);
|
|
}
|
|
|
|
// Setup send button
|
|
if (!this.sendButton) {
|
|
this.sendButton = inputContainer.createEl('button', {
|
|
cls: 'ollama-send-button',
|
|
text: 'Send',
|
|
});
|
|
} else {
|
|
inputContainer.appendChild(this.sendButton);
|
|
}
|
|
|
|
// Append containers to contentEl
|
|
this.contentEl.appendChild(newChatContainer);
|
|
this.contentEl.appendChild(inputContainer);
|
|
this.contentEl.appendChild(container);
|
|
|
|
// Focus input on open
|
|
this.inputEl.focus();
|
|
}
|
|
|
|
setupEventListeners(): void {
|
|
if (this.listenersAttached) {
|
|
return;
|
|
}
|
|
|
|
this.sendButtonClickHandler = () => {
|
|
void this.handleUserInput(this.inputEl?.value);
|
|
};
|
|
|
|
this.inputKeyDownHandler = (event: KeyboardEvent) => {
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
void this.handleUserInput(this.inputEl?.value);
|
|
}
|
|
};
|
|
|
|
this.newChatButtonClickHandler = () => {
|
|
this.clearConversation();
|
|
};
|
|
|
|
if (this.sendButton && this.sendButtonClickHandler) {
|
|
this.sendButton.addEventListener('click', this.sendButtonClickHandler);
|
|
}
|
|
|
|
if (this.inputEl && this.inputKeyDownHandler) {
|
|
this.inputEl.addEventListener('keydown', this.inputKeyDownHandler);
|
|
}
|
|
|
|
if (this.newChatButton && this.newChatButtonClickHandler) {
|
|
this.newChatButton.addEventListener('click', this.newChatButtonClickHandler);
|
|
}
|
|
|
|
this.listenersAttached = true;
|
|
}
|
|
|
|
removeEventListeners(): void {
|
|
if (!this.listenersAttached) {
|
|
return;
|
|
}
|
|
|
|
if (this.sendButton && this.sendButtonClickHandler) {
|
|
this.sendButton.removeEventListener('click', this.sendButtonClickHandler);
|
|
}
|
|
|
|
if (this.inputEl && this.inputKeyDownHandler) {
|
|
this.inputEl.removeEventListener('keydown', this.inputKeyDownHandler);
|
|
}
|
|
|
|
if (this.newChatButton && this.newChatButtonClickHandler) {
|
|
this.newChatButton.removeEventListener('click', this.newChatButtonClickHandler);
|
|
}
|
|
|
|
this.listenersAttached = false;
|
|
}
|
|
|
|
clearConversation(): void {
|
|
this.messages = [];
|
|
this.conversationStateManager.clear();
|
|
this.render();
|
|
}
|
|
|
|
updateMessageById(id: string, updates: Partial<ChatMessage>): void {
|
|
const index = this.messages.findIndex((m) => m.id === id);
|
|
if (index !== -1) {
|
|
this.messages[index] = { ...this.messages[index], ...updates };
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
updateLastMessage(updates: Partial<ChatMessage>): void {
|
|
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
|
if (streamingMessage) {
|
|
const index = this.messages.findIndex((msg) => msg.id === streamingMessage.id);
|
|
if (index !== -1) {
|
|
this.messages[index] = { ...this.messages[index], ...updates };
|
|
this.render();
|
|
}
|
|
}
|
|
}
|
|
|
|
getTools(): OllamaTool[] {
|
|
return [
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_note',
|
|
description:
|
|
'Creates a new note in the vault at the specified path with the given content',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'The path to the new note (e.g., "Projects/My Note.md")',
|
|
},
|
|
content: {
|
|
type: 'string',
|
|
description: 'The markdown content for the new note',
|
|
},
|
|
},
|
|
required: ['path', 'content'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
description: 'Reads the content of a file from the vault',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'The path to the file to read',
|
|
},
|
|
},
|
|
required: ['path'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
description: 'Searches for files in the vault that match a given query',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
query: {
|
|
type: 'string',
|
|
description: 'The search query to use',
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
description: 'The maximum number of results to return',
|
|
},
|
|
},
|
|
required: ['query'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'append_to_note',
|
|
description: 'Appends content to the end of an existing note',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'The path to the note',
|
|
},
|
|
content: {
|
|
type: 'string',
|
|
description: 'The content to append',
|
|
},
|
|
},
|
|
required: ['path', 'content'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'replace_note_section',
|
|
description: 'Replaces the body of a section under the specified heading in a note',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'The path to the note',
|
|
},
|
|
heading: {
|
|
type: 'string',
|
|
description: 'The heading text of the section to replace',
|
|
},
|
|
content: {
|
|
type: 'string',
|
|
description: 'The new content for the section (heading will be preserved)',
|
|
},
|
|
},
|
|
required: ['path', 'heading', 'content'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'update_frontmatter',
|
|
description:
|
|
'Updates YAML frontmatter fields in a note. Adds, updates, or removes fields.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'The path to the note',
|
|
},
|
|
fields: {
|
|
type: 'object',
|
|
description:
|
|
'An object of frontmatter key-value pairs to set. Use null to remove a field.',
|
|
},
|
|
},
|
|
required: ['path', 'fields'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'rename_note',
|
|
description: 'Renames a note to a new path within the vault',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
oldPath: {
|
|
type: 'string',
|
|
description: 'The current path to the note',
|
|
},
|
|
newPath: {
|
|
type: 'string',
|
|
description: 'The new path for the note',
|
|
},
|
|
},
|
|
required: ['oldPath', 'newPath'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'move_note',
|
|
description: 'Moves a note into a different folder',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'The current path to the note',
|
|
},
|
|
folder: {
|
|
type: 'string',
|
|
description: 'The target folder path (e.g., "Projects"). Use "" for vault root.',
|
|
},
|
|
},
|
|
required: ['path', 'folder'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'delete_note',
|
|
description: 'Deletes a note from the vault',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'The path to the note to delete',
|
|
},
|
|
},
|
|
required: ['path'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'insert_link',
|
|
description: 'Inserts a wikilink to another note at the end of a source note',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
sourcePath: {
|
|
type: 'string',
|
|
description: 'The path to the note that will contain the link',
|
|
},
|
|
targetPath: {
|
|
type: 'string',
|
|
description: 'The path to the note being linked to',
|
|
},
|
|
anchorText: {
|
|
type: 'string',
|
|
description: 'Optional display text for the link',
|
|
},
|
|
},
|
|
required: ['sourcePath', 'targetPath'],
|
|
},
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
buildMessages(userMessageContent: string, tools?: OllamaTool[]): OllamaMessage[] {
|
|
const systemContent = `You are an assistant that can help answer questions using the contents of a vault.
|
|
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
|
|
Vault context includes note titles, content, and any tags (shown as "Tags: ..." at the top of a note entry).
|
|
When organizing or categorizing notes, pay attention to tags as they reflect the note's topics and categories.
|
|
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
|
|
Only use the tools if you need to access vault content that is not already in the context.`;
|
|
const systemMessage: OllamaMessage = {
|
|
role: 'system',
|
|
content: systemContent,
|
|
};
|
|
|
|
const userMessage: OllamaMessage = {
|
|
role: 'user',
|
|
content: userMessageContent,
|
|
};
|
|
|
|
const messages: OllamaMessage[] = [systemMessage, userMessage];
|
|
|
|
if (tools && tools.length > 0) {
|
|
messages.push({
|
|
role: 'assistant',
|
|
content: 'I have access to the following tools to help answer your questions:',
|
|
});
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
async processToolCalls(
|
|
toolCalls: OllamaToolCall[],
|
|
messages: OllamaMessage[],
|
|
tools: OllamaTool[],
|
|
fullResponse: string,
|
|
assistantMessageId: string
|
|
): Promise<void> {
|
|
const toolResults = (
|
|
await Promise.all(
|
|
toolCalls.slice(0, MAX_TOOL_CALLS).map(async (toolCall) => {
|
|
try {
|
|
const toolResult = await this.toolExecutor.handleToolCall(toolCall);
|
|
return { ...toolResult, id: toolCall.id };
|
|
} catch (error) {
|
|
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
|
return null;
|
|
}
|
|
})
|
|
)
|
|
).filter((result): result is NonNullable<typeof result> => result !== null);
|
|
|
|
const followUpMessages: OllamaMessage[] = toolResults.map((result) => {
|
|
return {
|
|
role: 'tool',
|
|
content: JSON.stringify(result),
|
|
tool_call_id: result.id ?? '',
|
|
};
|
|
});
|
|
|
|
const followUp: OllamaMessage = {
|
|
role: 'assistant',
|
|
content: 'I have processed your request using the following tools. Here are the results:',
|
|
tool_calls: toolCalls,
|
|
};
|
|
|
|
if (followUpMessages.length > 0) {
|
|
const finalMessages = [...messages, followUp, ...followUpMessages];
|
|
const response = await this.ollamaClient.chat(finalMessages, tools);
|
|
const finalResponse = response.content || fullResponse;
|
|
this.updateMessageById(assistantMessageId, {
|
|
content: finalResponse,
|
|
isStreaming: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
async handleUserInput(inputValue?: string): Promise<void> {
|
|
const userMessage = (inputValue ?? this.inputEl?.value ?? '').trim();
|
|
if (!userMessage) {
|
|
return;
|
|
}
|
|
|
|
const maxContextLength = this.settings.maxContextLength;
|
|
const tools = this.getTools();
|
|
const messageId = crypto.randomUUID();
|
|
const userMessageId = `${messageId}-user`;
|
|
const assistantMessageId = `${messageId}-assistant`;
|
|
|
|
const userChatMessage: ChatMessage = {
|
|
id: userMessageId,
|
|
role: 'user',
|
|
content: userMessage,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
const assistantMessage: ChatMessage = {
|
|
id: assistantMessageId,
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: Date.now(),
|
|
isStreaming: true,
|
|
isThinking: true,
|
|
};
|
|
|
|
const previousStreamingEl = this.lastMessageEl;
|
|
this.messages = [...this.messages, userChatMessage, assistantMessage];
|
|
this.render();
|
|
if (this.inputEl) {
|
|
this.inputEl.value = '';
|
|
}
|
|
|
|
// Add the assistant message to the DOM to enable streaming
|
|
this.lastMessageEl =
|
|
this.chatContainer?.querySelector(`.ollama-message[data-msg-id="${assistantMessageId}"]`) ??
|
|
null;
|
|
if (!this.lastMessageEl && previousStreamingEl) {
|
|
previousStreamingEl.classList.add('ollama-message');
|
|
previousStreamingEl.setAttribute('data-msg-id', assistantMessageId);
|
|
this.contentEl.appendChild(previousStreamingEl);
|
|
this.lastMessageEl = previousStreamingEl;
|
|
}
|
|
|
|
try {
|
|
const entries = await this.vaultIndexer.searchVault(
|
|
userMessage,
|
|
this.settings.vaultSearchLimit
|
|
);
|
|
const context = entries
|
|
.map((entry) => {
|
|
const parts: string[] = [];
|
|
if (entry.tags) {
|
|
parts.push(`Tags: ${entry.tags}`);
|
|
}
|
|
parts.push(entry.title);
|
|
parts.push(entry.content);
|
|
return parts.join('\n');
|
|
})
|
|
.join('\n\n')
|
|
.slice(0, maxContextLength);
|
|
const userMessageWithContext = context
|
|
? `Relevant vault context:\n${context}\n\nUser question:\n${userMessage}`
|
|
: userMessage;
|
|
|
|
// Get the complete messages array for the LLM with all context layers
|
|
const completeMessages =
|
|
this.conversationStateManager.getCompleteMessages(userMessageWithContext);
|
|
|
|
const stream = this.ollamaClient.streamChat(completeMessages, tools);
|
|
|
|
let fullResponse = '';
|
|
let toolCalls: OllamaToolCall[] = [];
|
|
|
|
for await (const chunk of stream) {
|
|
if (chunk.content) {
|
|
fullResponse += chunk.content;
|
|
this.updateLastMessage({
|
|
content: fullResponse,
|
|
isStreaming: true,
|
|
isThinking: false,
|
|
});
|
|
}
|
|
|
|
if (chunk.tool_calls) {
|
|
toolCalls = [...toolCalls, ...chunk.tool_calls];
|
|
}
|
|
}
|
|
|
|
// Process tool calls if any
|
|
if (toolCalls.length > 0) {
|
|
await this.processToolCalls(
|
|
toolCalls,
|
|
completeMessages,
|
|
tools,
|
|
fullResponse,
|
|
assistantMessageId
|
|
);
|
|
}
|
|
|
|
// Update assistant message immutably — only if no tool calls were processed
|
|
if (toolCalls.length === 0) {
|
|
this.updateMessageById(assistantMessageId, {
|
|
content: fullResponse,
|
|
isStreaming: false,
|
|
});
|
|
}
|
|
|
|
// Update conversation state with the assistant's response
|
|
this.conversationStateManager.updateShortTermContext({ role: 'user', content: userMessage });
|
|
this.conversationStateManager.updateShortTermContext({
|
|
role: 'assistant',
|
|
content: fullResponse,
|
|
});
|
|
|
|
// Limit conversation history to prevent memory issues
|
|
if (this.messages.length > this.settings.maxMessageHistory) {
|
|
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
|
|
}
|
|
|
|
this.render();
|
|
} catch (error) {
|
|
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
|
this.updateMessageById(assistantMessageId, {
|
|
content: 'An error occurred while processing your request.',
|
|
isStreaming: false,
|
|
isThinking: false,
|
|
});
|
|
} finally {
|
|
// Clean up streaming resources regardless of outcome
|
|
this.cleanupStreamingResources();
|
|
}
|
|
}
|
|
|
|
// State
|
|
private messages: ChatMessage[] = [];
|
|
private lastMessageEl: HTMLElement | null = null;
|
|
private newChatButton: HTMLElement | null = null;
|
|
private sendButton: HTMLElement | null = null;
|
|
private inputEl: HTMLTextAreaElement | null = null;
|
|
private chatContainer: HTMLElement | null = null;
|
|
private sendButtonClickHandler: (() => void) | null = null;
|
|
private inputKeyDownHandler: ((event: KeyboardEvent) => void) | null = null;
|
|
private newChatButtonClickHandler: (() => void) | null = null;
|
|
private sendButtonClickWrapper: (() => void) | null = null;
|
|
private inputKeyDownWrapper: ((event: KeyboardEvent) => void) | null = null;
|
|
private newChatButtonClickWrapper: (() => void) | null = null;
|
|
private listenersAttached: boolean = false;
|
|
private settings: PluginSettings;
|
|
private ollamaClient: OllamaClient;
|
|
private vaultIndexer: VaultIndexer;
|
|
private toolExecutor: ToolExecutor;
|
|
private conversationStateManager: ConversationStateManager;
|
|
private vectorStore?: VaultVectorStore;
|
|
}
|
|
|
|
const MAX_TOOL_CALLS = 5;
|