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() {