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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user