Add chat session history with dropdown UI

Implement persistent chat history using a new ChatHistoryManager class.
Sessions are saved on view close, restored on open, and selectable via a
dropdown in the chat header. Includes delete and clear-all commands,
with automatic title generation from the first user message.
This commit is contained in:
2026-05-21 10:36:55 +02:00
parent 5c2078aebb
commit e7b753014c
7 changed files with 566 additions and 7 deletions
+226 -3
View File
@@ -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() {
+108
View File
@@ -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>): 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;
}
}
+149 -1
View File
@@ -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<void> {
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<ChatMessage>): 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 })[] = [];
+22 -2
View File
@@ -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<void>;
@@ -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(),
});
}
+14
View File
@@ -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;
+38
View File
@@ -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;
+9 -1
View File
@@ -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;