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