diff --git a/main.js b/main.js index 35bef4d..4330866 100644 --- a/main.js +++ b/main.js @@ -10855,7 +10855,7 @@ var ErrorHandler = class { // src/chat-view.ts var ChatView = class extends import_obsidian5.ItemView { - constructor(leaf, settings, vectorStore, structuredMemoryManager, telemetryManager) { + constructor(leaf, settings, vectorStore, structuredMemoryManager, telemetryManager, chatHistoryManager) { super(leaf); // State this.messages = []; @@ -10872,6 +10872,8 @@ var ChatView = class extends import_obsidian5.ItemView { this.newChatButtonClickWrapper = null; this.listenersAttached = false; this.modeSelectorEl = null; + this.historySelectEl = null; + this.historyDeleteButton = null; this.pendingActions = []; this.pendingReadResults = []; this.pendingFollowUpContext = null; @@ -10915,6 +10917,7 @@ var ChatView = class extends import_obsidian5.ItemView { ); this.structuredMemoryManager = structuredMemoryManager; this.telemetryManager = telemetryManager; + this.chatHistoryManager = chatHistoryManager; this.workflowEngine = new WorkflowEngine( this.app.vault, this.app, @@ -10927,6 +10930,7 @@ var ChatView = class extends import_obsidian5.ItemView { this.appendLogEntry(entry); } }); + this.restoreActiveSession(); } // Getters for testing getSendButtonClickHandler() { @@ -10938,6 +10942,9 @@ var ChatView = class extends import_obsidian5.ItemView { getNewChatButtonClickHandler() { return this.newChatButtonClickHandler; } + getHistorySelectEl() { + return this.historySelectEl; + } updateSettings(newSettings) { this.settings = newSettings; this.currentAgentMode = newSettings.agentMode ?? "ask"; @@ -10997,6 +11004,7 @@ var ChatView = class extends import_obsidian5.ItemView { this.updateSettings(newSettings); } async onClose() { + this.saveCurrentSession(); this.ollamaClient.cancelStream(); this.removeEventListeners(); this.cleanupStreamingResources(); @@ -11013,6 +11021,8 @@ var ChatView = class extends import_obsidian5.ItemView { this.chatContainer = null; this.showLogsButton = null; this.logsContainer = null; + this.historySelectEl = null; + this.historyDeleteButton = null; return Promise.resolve(); } cleanupStreamingResources() { @@ -11090,6 +11100,43 @@ var ChatView = class extends import_obsidian5.ItemView { } else { newChatContainer.appendChild(this.modeSelectorEl); } + if (!this.historySelectEl) { + this.historySelectEl = newChatContainer.createEl("select", { + cls: "ollama-history-selector" + }); + this.historySelectEl.addEventListener("change", () => { + const selectedId = this.historySelectEl.value; + if (selectedId === "__new__") { + this.clearConversation(); + } else if (selectedId) { + const session = this.chatHistoryManager?.getSession(selectedId); + if (session) { + this.chatHistoryManager?.setActiveSessionId(selectedId); + this.loadSession(session); + this.render(); + } + } + }); + } + this.populateHistoryDropdown(); + newChatContainer.appendChild(this.historySelectEl); + if (!this.historyDeleteButton) { + this.historyDeleteButton = newChatContainer.createEl("button", { + cls: "ollama-history-delete-button", + text: "Delete" + }); + this.historyDeleteButton.addEventListener("click", () => { + const selectedId = this.historySelectEl?.value; + if (selectedId && selectedId !== "__new__") { + const deleted = this.chatHistoryManager?.deleteSession(selectedId); + if (deleted) { + new import_obsidian5.Notice("Chat deleted."); + this.clearConversation(); + } + } + }); + } + newChatContainer.appendChild(this.historyDeleteButton); if (!this.newChatButton) { this.newChatButton = newChatContainer.createEl("button", { cls: "ollama-new-chat-button", @@ -11218,15 +11265,87 @@ var ChatView = class extends import_obsidian5.ItemView { this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode)); } clearConversation() { + this.saveCurrentSession(); this.messages = []; this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode)); + this.chatHistoryManager?.createSession(this.currentAgentMode); this.render(); } + restoreActiveSession() { + if (!this.chatHistoryManager) return; + const activeId = this.chatHistoryManager.getActiveSessionId(); + if (activeId) { + const session = this.chatHistoryManager.getSession(activeId); + if (session) { + this.loadSession(session); + return; + } + } + this.chatHistoryManager.createSession(this.currentAgentMode); + } + loadSession(session) { + this.messages = [...session.messages]; + this.currentAgentMode = session.agentMode; + this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode)); + for (const msg of this.messages) { + if (msg.role === "user" || msg.role === "assistant") { + this.conversationStateManager.updateShortTermContext({ + role: msg.role, + content: msg.content + }); + } + } + if (this.modeSelectorEl) { + this.modeSelectorEl.value = this.currentAgentMode; + } + } + saveCurrentSession() { + if (!this.chatHistoryManager) return; + const activeId = this.chatHistoryManager.getActiveSessionId(); + if (!activeId) return; + const nonStreamingMessages = this.messages.filter((msg) => !msg.isStreaming); + this.chatHistoryManager.updateSessionMessages(activeId, nonStreamingMessages); + } + syncMessagesToSession() { + if (!this.chatHistoryManager) return; + const activeId = this.chatHistoryManager.getActiveSessionId(); + if (!activeId) return; + const nonStreamingMessages = this.messages.filter((msg) => !msg.isStreaming); + this.chatHistoryManager.updateSessionMessages(activeId, nonStreamingMessages); + } + populateHistoryDropdown() { + if (!this.historySelectEl) return; + const previousValue = this.historySelectEl.value; + this.historySelectEl.innerHTML = ""; + const newOption = this.historySelectEl.createEl("option", { + text: "New Chat", + attr: { value: "__new__" } + }); + const sessions = this.chatHistoryManager?.getSessions() ?? []; + const activeId = this.chatHistoryManager?.getActiveSessionId(); + for (const session of sessions) { + const option = this.historySelectEl.createEl("option", { + text: session.title, + attr: { value: session.id } + }); + if (session.id === activeId) { + option.setAttribute("selected", "selected"); + } + } + if (previousValue && sessions.some((s) => s.id === previousValue)) { + this.historySelectEl.value = previousValue; + } else if (activeId) { + this.historySelectEl.value = activeId; + } else { + this.historySelectEl.value = "__new__"; + } + } updateMessageById(id, updates) { const index = this.messages.findIndex((m) => m.id === id); if (index !== -1) { this.messages[index] = { ...this.messages[index], ...updates }; this.render(); + this.syncMessagesToSession(); } } updateLastMessage(updates) { @@ -11236,6 +11355,7 @@ var ChatView = class extends import_obsidian5.ItemView { if (index !== -1) { this.messages[index] = { ...this.messages[index], ...updates }; this.render(); + this.syncMessagesToSession(); } } } @@ -11987,6 +12107,7 @@ ${actualMessage}` : actualMessage; this.messages = this.messages.slice(-this.settings.maxMessageHistory); } this.render(); + this.syncMessagesToSession(); } catch (error) { ErrorHandler.handleError(error, "ChatView.handleUserInput"); this.updateMessageById(assistantMessageId, { @@ -11994,6 +12115,7 @@ ${actualMessage}` : actualMessage; isStreaming: false, isThinking: false }); + this.syncMessagesToSession(); } finally { this.cleanupStreamingResources(); } @@ -13268,6 +13390,93 @@ var TelemetryManager = class { } }; +// src/chat-history.ts +var DEFAULT_MAX_SESSIONS = 50; +function createDefaultChatHistoryData() { + return { + sessions: [], + activeSessionId: void 0 + }; +} +var ChatHistoryManager = class { + constructor(data, maxSessions = DEFAULT_MAX_SESSIONS) { + this.data = data ?? createDefaultChatHistoryData(); + this.maxSessions = maxSessions; + } + getData() { + return this.data; + } + getSessions() { + return [...this.data.sessions]; + } + getSession(id) { + return this.data.sessions.find((s) => s.id === id); + } + getActiveSessionId() { + return this.data.activeSessionId; + } + setActiveSessionId(id) { + this.data.activeSessionId = id; + } + createSession(agentMode) { + const session = { + id: crypto.randomUUID?.() ?? `session-${Date.now()}-${Math.random()}`, + title: "New Chat", + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + agentMode + }; + this.data.sessions.unshift(session); + this.data.activeSessionId = session.id; + this.trimSessions(); + return session; + } + updateSession(id, updates) { + const index = this.data.sessions.findIndex((s) => s.id === id); + if (index === -1) return void 0; + const session = this.data.sessions[index]; + const updated = { ...session, ...updates, updatedAt: Date.now() }; + this.data.sessions[index] = updated; + this.data.sessions.splice(index, 1); + this.data.sessions.unshift(updated); + return updated; + } + updateSessionMessages(id, messages) { + const session = this.getSession(id); + if (!session) return void 0; + const title = this.deriveTitle(messages); + return this.updateSession(id, { messages: [...messages], title }); + } + deleteSession(id) { + const initialLength = this.data.sessions.length; + this.data.sessions = this.data.sessions.filter((s) => s.id !== id); + if (this.data.activeSessionId === id) { + this.data.activeSessionId = void 0; + } + return this.data.sessions.length < initialLength; + } + clearAll() { + this.data.sessions = []; + this.data.activeSessionId = void 0; + } + trimSessions() { + if (this.data.sessions.length > this.maxSessions) { + const removed = this.data.sessions.splice(this.maxSessions); + if (this.data.activeSessionId && removed.some((s) => s.id === this.data.activeSessionId)) { + this.data.activeSessionId = this.data.sessions[0]?.id; + } + } + } + deriveTitle(messages) { + const firstUser = messages.find((m) => m.role === "user"); + if (!firstUser) return "New Chat"; + const text = firstUser.content.trim(); + if (!text) return "New Chat"; + return text.length > 40 ? text.slice(0, 40) + "\u2026" : text; + } +}; + // src/main.ts var OllamaPlugin = class extends import_obsidian7.Plugin { constructor() { @@ -13286,7 +13495,8 @@ var OllamaPlugin = class extends import_obsidian7.Plugin { this.settings, this.vaultVectorStore, this.structuredMemoryManager, - this.telemetryManager + this.telemetryManager, + this.chatHistoryManager ) ); this.addRibbonIcon("bot", "Open Ollama Chat", async () => { @@ -13364,6 +13574,16 @@ var OllamaPlugin = class extends import_obsidian7.Plugin { new import_obsidian7.Notice("Tool telemetry cleared."); } }); + this.addCommand({ + id: "clear-chat-history", + name: "Clear Chat History", + callback: async () => { + this.chatHistoryManager?.clearAll(); + await this.saveSettings(); + new import_obsidian7.Notice("Chat history cleared."); + this.notifyChatViews(); + } + }); this.addSettingTab(new OllamaSettingTab(this.app, this)); if (this.settings.cacheConfig) { this.semanticCache = new SemanticCacheService( @@ -13400,12 +13620,15 @@ var OllamaPlugin = class extends import_obsidian7.Plugin { ); const telemetryData = data.toolTelemetry ?? createDefaultToolTelemetryData(); this.telemetryManager = new TelemetryManager(this.settings.toolTelemetryConfig, telemetryData); + const historyData = data.chatHistory ?? createDefaultChatHistoryData(); + this.chatHistoryManager = new ChatHistoryManager(historyData); } async saveSettings() { await this.saveData({ settings: this.settings, structuredMemory: this.structuredMemoryManager?.getData() ?? createDefaultStructuredMemoryData(), - toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData() + toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData(), + chatHistory: this.chatHistoryManager?.getData() ?? createDefaultChatHistoryData() }); } initializeAutoOrganizer() { diff --git a/src/chat-history.ts b/src/chat-history.ts new file mode 100644 index 0000000..75b1056 --- /dev/null +++ b/src/chat-history.ts @@ -0,0 +1,108 @@ +// src/chat-history.ts + +import { ChatSession, ChatHistoryData, ChatMessage, AgentMode } from './types'; + +const DEFAULT_MAX_SESSIONS = 50; + +export function createDefaultChatHistoryData(): ChatHistoryData { + return { + sessions: [], + activeSessionId: undefined, + }; +} + +export class ChatHistoryManager { + private data: ChatHistoryData; + private maxSessions: number; + + constructor(data?: ChatHistoryData, maxSessions: number = DEFAULT_MAX_SESSIONS) { + this.data = data ?? createDefaultChatHistoryData(); + this.maxSessions = maxSessions; + } + + getData(): ChatHistoryData { + return this.data; + } + + getSessions(): ChatSession[] { + return [...this.data.sessions]; + } + + getSession(id: string): ChatSession | undefined { + return this.data.sessions.find((s) => s.id === id); + } + + getActiveSessionId(): string | undefined { + return this.data.activeSessionId; + } + + setActiveSessionId(id: string | undefined): void { + this.data.activeSessionId = id; + } + + createSession(agentMode: AgentMode): ChatSession { + const session: ChatSession = { + id: crypto.randomUUID?.() ?? `session-${Date.now()}-${Math.random()}`, + title: 'New Chat', + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + agentMode, + }; + this.data.sessions.unshift(session); + this.data.activeSessionId = session.id; + this.trimSessions(); + return session; + } + + updateSession(id: string, updates: Partial): ChatSession | undefined { + const index = this.data.sessions.findIndex((s) => s.id === id); + if (index === -1) return undefined; + const session = this.data.sessions[index]; + const updated = { ...session, ...updates, updatedAt: Date.now() }; + this.data.sessions[index] = updated; + // Move to top so most recent sessions appear first + this.data.sessions.splice(index, 1); + this.data.sessions.unshift(updated); + return updated; + } + + updateSessionMessages(id: string, messages: ChatMessage[]): ChatSession | undefined { + const session = this.getSession(id); + if (!session) return undefined; + const title = this.deriveTitle(messages); + return this.updateSession(id, { messages: [...messages], title }); + } + + deleteSession(id: string): boolean { + const initialLength = this.data.sessions.length; + this.data.sessions = this.data.sessions.filter((s) => s.id !== id); + if (this.data.activeSessionId === id) { + this.data.activeSessionId = undefined; + } + return this.data.sessions.length < initialLength; + } + + clearAll(): void { + this.data.sessions = []; + this.data.activeSessionId = undefined; + } + + private trimSessions(): void { + if (this.data.sessions.length > this.maxSessions) { + const removed = this.data.sessions.splice(this.maxSessions); + if (this.data.activeSessionId && removed.some((s) => s.id === this.data.activeSessionId)) { + this.data.activeSessionId = this.data.sessions[0]?.id; + } + } + } + + private deriveTitle(messages: ChatMessage[]): string { + const firstUser = messages.find((m) => m.role === 'user'); + if (!firstUser) return 'New Chat'; + const text = firstUser.content.trim(); + if (!text) return 'New Chat'; + // Limit to ~40 chars with ellipsis + return text.length > 40 ? text.slice(0, 40) + '…' : text; + } +} diff --git a/src/chat-view.ts b/src/chat-view.ts index 4e4d2be..07599ba 100755 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -22,11 +22,13 @@ import { ChatMessage, ProposedAction, ToolResult, + ChatSession, } from './types'; import { ConversationStateManager } from './conversation-state'; import { ErrorHandler } from './error-handler'; import { StructuredMemoryManager } from './structured-memory'; import { TelemetryManager } from './tool-telemetry'; +import { ChatHistoryManager } from './chat-history'; import { Logger, LogEntry } from './utils'; export const VIEW_TYPE_OLLAMA_CHAT = 'ollama-chat-view'; @@ -45,12 +47,17 @@ export class ChatView extends ItemView { return this.newChatButtonClickHandler; } + getHistorySelectEl() { + return this.historySelectEl; + } + constructor( leaf: WorkspaceLeaf, settings: PluginSettings, vectorStore?: VaultVectorStore, structuredMemoryManager?: StructuredMemoryManager, - telemetryManager?: TelemetryManager + telemetryManager?: TelemetryManager, + chatHistoryManager?: ChatHistoryManager ) { super(leaf); this.messages = []; @@ -88,6 +95,7 @@ export class ChatView extends ItemView { ); this.structuredMemoryManager = structuredMemoryManager; this.telemetryManager = telemetryManager; + this.chatHistoryManager = chatHistoryManager; this.workflowEngine = new WorkflowEngine( this.app.vault, this.app, @@ -102,6 +110,9 @@ export class ChatView extends ItemView { this.appendLogEntry(entry); } }); + + // Restore active session if available + this.restoreActiveSession(); } updateSettings(newSettings: PluginSettings) { @@ -174,6 +185,7 @@ export class ChatView extends ItemView { } async onClose(): Promise { + this.saveCurrentSession(); this.ollamaClient.cancelStream(); this.removeEventListeners(); this.cleanupStreamingResources(); @@ -190,6 +202,8 @@ export class ChatView extends ItemView { this.chatContainer = null; this.showLogsButton = null; this.logsContainer = null; + this.historySelectEl = null; + this.historyDeleteButton = null; return Promise.resolve(); } @@ -287,6 +301,47 @@ export class ChatView extends ItemView { newChatContainer.appendChild(this.modeSelectorEl); } + // Setup chat history selector + if (!this.historySelectEl) { + this.historySelectEl = newChatContainer.createEl('select', { + cls: 'ollama-history-selector', + }); + this.historySelectEl.addEventListener('change', () => { + const selectedId = this.historySelectEl!.value; + if (selectedId === '__new__') { + this.clearConversation(); + } else if (selectedId) { + const session = this.chatHistoryManager?.getSession(selectedId); + if (session) { + this.chatHistoryManager?.setActiveSessionId(selectedId); + this.loadSession(session); + this.render(); + } + } + }); + } + this.populateHistoryDropdown(); + newChatContainer.appendChild(this.historySelectEl); + + // Setup delete history button + if (!this.historyDeleteButton) { + this.historyDeleteButton = newChatContainer.createEl('button', { + cls: 'ollama-history-delete-button', + text: 'Delete', + }); + this.historyDeleteButton.addEventListener('click', () => { + const selectedId = this.historySelectEl?.value; + if (selectedId && selectedId !== '__new__') { + const deleted = this.chatHistoryManager?.deleteSession(selectedId); + if (deleted) { + new Notice('Chat deleted.'); + this.clearConversation(); + } + } + }); + } + newChatContainer.appendChild(this.historyDeleteButton); + // Setup new chat button if (!this.newChatButton) { this.newChatButton = newChatContainer.createEl('button', { @@ -453,16 +508,103 @@ export class ChatView extends ItemView { } clearConversation(): void { + // Save the current session before clearing + this.saveCurrentSession(); this.messages = []; this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode)); + // Create a new session for the fresh conversation + this.chatHistoryManager?.createSession(this.currentAgentMode); this.render(); } + private restoreActiveSession(): void { + if (!this.chatHistoryManager) return; + const activeId = this.chatHistoryManager.getActiveSessionId(); + if (activeId) { + const session = this.chatHistoryManager.getSession(activeId); + if (session) { + this.loadSession(session); + return; + } + } + // No active session: create one + this.chatHistoryManager.createSession(this.currentAgentMode); + } + + private loadSession(session: ChatSession): void { + this.messages = [...session.messages]; + this.currentAgentMode = session.agentMode; + this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode)); + // Rebuild conversation state from messages + for (const msg of this.messages) { + if (msg.role === 'user' || msg.role === 'assistant') { + this.conversationStateManager.updateShortTermContext({ + role: msg.role, + content: msg.content, + }); + } + } + if (this.modeSelectorEl) { + this.modeSelectorEl.value = this.currentAgentMode; + } + } + + private saveCurrentSession(): void { + if (!this.chatHistoryManager) return; + const activeId = this.chatHistoryManager.getActiveSessionId(); + if (!activeId) return; + const nonStreamingMessages = this.messages.filter((msg) => !msg.isStreaming); + this.chatHistoryManager.updateSessionMessages(activeId, nonStreamingMessages); + } + + private syncMessagesToSession(): void { + if (!this.chatHistoryManager) return; + const activeId = this.chatHistoryManager.getActiveSessionId(); + if (!activeId) return; + const nonStreamingMessages = this.messages.filter((msg) => !msg.isStreaming); + this.chatHistoryManager.updateSessionMessages(activeId, nonStreamingMessages); + } + + private populateHistoryDropdown(): void { + if (!this.historySelectEl) return; + const previousValue = this.historySelectEl.value; + this.historySelectEl.innerHTML = ''; + + // New Chat option + const newOption = this.historySelectEl.createEl('option', { + text: 'New Chat', + attr: { value: '__new__' }, + }); + + const sessions = this.chatHistoryManager?.getSessions() ?? []; + const activeId = this.chatHistoryManager?.getActiveSessionId(); + + for (const session of sessions) { + const option = this.historySelectEl.createEl('option', { + text: session.title, + attr: { value: session.id }, + }); + if (session.id === activeId) { + option.setAttribute('selected', 'selected'); + } + } + + // If the previously selected value is still valid, keep it; otherwise select active or New Chat + if (previousValue && sessions.some((s) => s.id === previousValue)) { + this.historySelectEl.value = previousValue; + } else if (activeId) { + this.historySelectEl.value = activeId; + } else { + this.historySelectEl.value = '__new__'; + } + } + updateMessageById(id: string, updates: Partial): void { const index = this.messages.findIndex((m) => m.id === id); if (index !== -1) { this.messages[index] = { ...this.messages[index], ...updates }; this.render(); + this.syncMessagesToSession(); } } @@ -473,6 +615,7 @@ export class ChatView extends ItemView { if (index !== -1) { this.messages[index] = { ...this.messages[index], ...updates }; this.render(); + this.syncMessagesToSession(); } } } @@ -1350,6 +1493,7 @@ export class ChatView extends ItemView { } this.render(); + this.syncMessagesToSession(); } catch (error) { ErrorHandler.handleError(error, 'ChatView.handleUserInput'); this.updateMessageById(assistantMessageId, { @@ -1357,6 +1501,7 @@ export class ChatView extends ItemView { isStreaming: false, isThinking: false, }); + this.syncMessagesToSession(); } finally { // Clean up streaming resources regardless of outcome this.cleanupStreamingResources(); @@ -1388,9 +1533,12 @@ export class ChatView extends ItemView { private conversationStateManager: ConversationStateManager; private structuredMemoryManager?: StructuredMemoryManager; private telemetryManager?: TelemetryManager; + private chatHistoryManager?: ChatHistoryManager; private vectorStore?: VaultVectorStore; private modeSelectorEl: HTMLSelectElement | null = null; + private historySelectEl: HTMLSelectElement | null = null; + private historyDeleteButton: HTMLElement | null = null; private currentAgentMode: AgentMode; private pendingActions: ProposedAction[] = []; private pendingReadResults: (ToolResult & { id?: string })[] = []; diff --git a/src/main.ts b/src/main.ts index 19dcd94..e0feb4f 100755 --- a/src/main.ts +++ b/src/main.ts @@ -5,12 +5,13 @@ import { SemanticCacheService } from './semantic-cache'; import { VaultVectorStore } from './vault-vector-store'; import { VaultIndexer } from './vault-indexer'; import { AutoTagger, AutoLinker } from './auto-organizer'; -import { PluginSettings, StructuredMemoryData, ToolTelemetryData } from './types'; +import { PluginSettings, StructuredMemoryData, ToolTelemetryData, ChatHistoryData } from './types'; import { Logger } from './utils'; import { ALL_AGENT_MODES, getAgentModeLabel } from './agent-modes'; import { AgentMode } from './types'; import { StructuredMemoryManager, createDefaultStructuredMemoryData } from './structured-memory'; import { TelemetryManager, createDefaultToolTelemetryData } from './tool-telemetry'; +import { ChatHistoryManager, createDefaultChatHistoryData } from './chat-history'; export default class OllamaPlugin extends Plugin { settings: PluginSettings = DEFAULT_SETTINGS; @@ -20,6 +21,7 @@ export default class OllamaPlugin extends Plugin { autoLinker?: AutoLinker; structuredMemoryManager?: StructuredMemoryManager; telemetryManager?: TelemetryManager; + chatHistoryManager?: ChatHistoryManager; private indexingAbortController?: AbortController; private currentIndexingPromise?: Promise; @@ -40,7 +42,8 @@ export default class OllamaPlugin extends Plugin { this.settings, this.vaultVectorStore, this.structuredMemoryManager, - this.telemetryManager + this.telemetryManager, + this.chatHistoryManager ) ); @@ -136,6 +139,18 @@ export default class OllamaPlugin extends Plugin { new Notice('Tool telemetry cleared.'); }, }); + + // Add a command to clear chat history + this.addCommand({ + id: 'clear-chat-history', + name: 'Clear Chat History', + callback: async () => { + this.chatHistoryManager?.clearAll(); + await this.saveSettings(); + new Notice('Chat history cleared.'); + this.notifyChatViews(); + }, + }); this.addSettingTab(new OllamaSettingTab(this.app, this)); // Initialize the semantic cache @@ -189,6 +204,10 @@ export default class OllamaPlugin extends Plugin { const telemetryData: ToolTelemetryData = (data.toolTelemetry as ToolTelemetryData | undefined) ?? createDefaultToolTelemetryData(); this.telemetryManager = new TelemetryManager(this.settings.toolTelemetryConfig, telemetryData); + + const historyData: ChatHistoryData = + (data.chatHistory as ChatHistoryData | undefined) ?? createDefaultChatHistoryData(); + this.chatHistoryManager = new ChatHistoryManager(historyData); } async saveSettings() { @@ -197,6 +216,7 @@ export default class OllamaPlugin extends Plugin { structuredMemory: this.structuredMemoryManager?.getData() ?? createDefaultStructuredMemoryData(), toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData(), + chatHistory: this.chatHistoryManager?.getData() ?? createDefaultChatHistoryData(), }); } diff --git a/src/types.ts b/src/types.ts index 070e687..d180708 100644 --- a/src/types.ts +++ b/src/types.ts @@ -313,6 +313,20 @@ export interface VaultIndexConfig { similarityThreshold: number; } +export interface ChatSession { + id: string; + title: string; + createdAt: number; + updatedAt: number; + messages: ChatMessage[]; + agentMode: AgentMode; +} + +export interface ChatHistoryData { + sessions: ChatSession[]; + activeSessionId?: string; +} + export interface PluginSettings { ollamaUrl: string; chatModel: string; diff --git a/styles.css b/styles.css index c0a0cf6..0d0542f 100644 --- a/styles.css +++ b/styles.css @@ -303,6 +303,44 @@ border-color: var(--interactive-accent); } +/* Chat History Selector */ +.ollama-history-selector { + padding: var(--size-4-1) var(--size-4-2); + border-radius: var(--ollama-radius); + border: 1px solid var(--ollama-border); + background-color: var(--background-modifier-form-field); + color: var(--text-normal); + font-size: var(--font-ui-small); + cursor: pointer; + max-width: 12rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.ollama-history-selector:focus { + outline: none; + border-color: var(--interactive-accent); +} + +.ollama-history-delete-button { + display: inline-flex; + align-items: center; + gap: var(--size-4-1); + padding: var(--size-4-1) var(--size-4-2); + font-size: var(--font-smallest); + border-radius: var(--ollama-radius); + border: 1px solid var(--ollama-border); + background-color: var(--background-modifier-form-field); + color: var(--text-normal); + cursor: pointer; + transition: background-color 0.15s ease; +} + +.ollama-history-delete-button:hover { + background-color: var(--background-modifier-error-hover); + color: var(--text-error); +} + /* Show Logs Button */ .ollama-show-logs-button { display: inline-flex; diff --git a/tests/chat-view.test.ts b/tests/chat-view.test.ts index 33932d5..21d65df 100755 --- a/tests/chat-view.test.ts +++ b/tests/chat-view.test.ts @@ -1,6 +1,7 @@ import { ChatView } from '../src/chat-view'; import { PluginSettings, OllamaMessage, ChatMessage, OllamaTool, ToolCall } from '../src/types'; import { ErrorHandler } from '../src/error-handler'; +import { ChatHistoryManager } from '../src/chat-history'; // Mock Obsidian types interface MockVault { @@ -114,7 +115,14 @@ describe('ChatView', () => { app: mockApp, }; - view = new ChatView(mockLeaf as unknown as any, mockSettings); + view = new ChatView( + mockLeaf as unknown as any, + mockSettings, + undefined, + undefined, + undefined, + new ChatHistoryManager() + ); // Obsidian's contentEl has a createEl helper that standard DOM lacks // Unlike standard DOM, Obsidian elements can create nested elements with createEl const contentDiv = document.createElement('div') as any;