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
|
// src/chat-view.ts
|
||||||
var ChatView = class extends import_obsidian5.ItemView {
|
var ChatView = class extends import_obsidian5.ItemView {
|
||||||
constructor(leaf, settings, vectorStore, structuredMemoryManager, telemetryManager) {
|
constructor(leaf, settings, vectorStore, structuredMemoryManager, telemetryManager, chatHistoryManager) {
|
||||||
super(leaf);
|
super(leaf);
|
||||||
// State
|
// State
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
@@ -10872,6 +10872,8 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.newChatButtonClickWrapper = null;
|
this.newChatButtonClickWrapper = null;
|
||||||
this.listenersAttached = false;
|
this.listenersAttached = false;
|
||||||
this.modeSelectorEl = null;
|
this.modeSelectorEl = null;
|
||||||
|
this.historySelectEl = null;
|
||||||
|
this.historyDeleteButton = null;
|
||||||
this.pendingActions = [];
|
this.pendingActions = [];
|
||||||
this.pendingReadResults = [];
|
this.pendingReadResults = [];
|
||||||
this.pendingFollowUpContext = null;
|
this.pendingFollowUpContext = null;
|
||||||
@@ -10915,6 +10917,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
);
|
);
|
||||||
this.structuredMemoryManager = structuredMemoryManager;
|
this.structuredMemoryManager = structuredMemoryManager;
|
||||||
this.telemetryManager = telemetryManager;
|
this.telemetryManager = telemetryManager;
|
||||||
|
this.chatHistoryManager = chatHistoryManager;
|
||||||
this.workflowEngine = new WorkflowEngine(
|
this.workflowEngine = new WorkflowEngine(
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.app,
|
this.app,
|
||||||
@@ -10927,6 +10930,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.appendLogEntry(entry);
|
this.appendLogEntry(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.restoreActiveSession();
|
||||||
}
|
}
|
||||||
// Getters for testing
|
// Getters for testing
|
||||||
getSendButtonClickHandler() {
|
getSendButtonClickHandler() {
|
||||||
@@ -10938,6 +10942,9 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
getNewChatButtonClickHandler() {
|
getNewChatButtonClickHandler() {
|
||||||
return this.newChatButtonClickHandler;
|
return this.newChatButtonClickHandler;
|
||||||
}
|
}
|
||||||
|
getHistorySelectEl() {
|
||||||
|
return this.historySelectEl;
|
||||||
|
}
|
||||||
updateSettings(newSettings) {
|
updateSettings(newSettings) {
|
||||||
this.settings = newSettings;
|
this.settings = newSettings;
|
||||||
this.currentAgentMode = newSettings.agentMode ?? "ask";
|
this.currentAgentMode = newSettings.agentMode ?? "ask";
|
||||||
@@ -10997,6 +11004,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.updateSettings(newSettings);
|
this.updateSettings(newSettings);
|
||||||
}
|
}
|
||||||
async onClose() {
|
async onClose() {
|
||||||
|
this.saveCurrentSession();
|
||||||
this.ollamaClient.cancelStream();
|
this.ollamaClient.cancelStream();
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
@@ -11013,6 +11021,8 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.chatContainer = null;
|
this.chatContainer = null;
|
||||||
this.showLogsButton = null;
|
this.showLogsButton = null;
|
||||||
this.logsContainer = null;
|
this.logsContainer = null;
|
||||||
|
this.historySelectEl = null;
|
||||||
|
this.historyDeleteButton = null;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
cleanupStreamingResources() {
|
cleanupStreamingResources() {
|
||||||
@@ -11090,6 +11100,43 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
} else {
|
} else {
|
||||||
newChatContainer.appendChild(this.modeSelectorEl);
|
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) {
|
if (!this.newChatButton) {
|
||||||
this.newChatButton = newChatContainer.createEl("button", {
|
this.newChatButton = newChatContainer.createEl("button", {
|
||||||
cls: "ollama-new-chat-button",
|
cls: "ollama-new-chat-button",
|
||||||
@@ -11218,15 +11265,87 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode));
|
this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode));
|
||||||
}
|
}
|
||||||
clearConversation() {
|
clearConversation() {
|
||||||
|
this.saveCurrentSession();
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode));
|
this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode));
|
||||||
|
this.chatHistoryManager?.createSession(this.currentAgentMode);
|
||||||
this.render();
|
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) {
|
updateMessageById(id, updates) {
|
||||||
const index = this.messages.findIndex((m) => m.id === id);
|
const index = this.messages.findIndex((m) => m.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.messages[index] = { ...this.messages[index], ...updates };
|
this.messages[index] = { ...this.messages[index], ...updates };
|
||||||
this.render();
|
this.render();
|
||||||
|
this.syncMessagesToSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateLastMessage(updates) {
|
updateLastMessage(updates) {
|
||||||
@@ -11236,6 +11355,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.messages[index] = { ...this.messages[index], ...updates };
|
this.messages[index] = { ...this.messages[index], ...updates };
|
||||||
this.render();
|
this.render();
|
||||||
|
this.syncMessagesToSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11987,6 +12107,7 @@ ${actualMessage}` : actualMessage;
|
|||||||
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
|
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
|
this.syncMessagesToSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handleError(error, "ChatView.handleUserInput");
|
ErrorHandler.handleError(error, "ChatView.handleUserInput");
|
||||||
this.updateMessageById(assistantMessageId, {
|
this.updateMessageById(assistantMessageId, {
|
||||||
@@ -11994,6 +12115,7 @@ ${actualMessage}` : actualMessage;
|
|||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
isThinking: false
|
isThinking: false
|
||||||
});
|
});
|
||||||
|
this.syncMessagesToSession();
|
||||||
} finally {
|
} finally {
|
||||||
this.cleanupStreamingResources();
|
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
|
// src/main.ts
|
||||||
var OllamaPlugin = class extends import_obsidian7.Plugin {
|
var OllamaPlugin = class extends import_obsidian7.Plugin {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -13286,7 +13495,8 @@ var OllamaPlugin = class extends import_obsidian7.Plugin {
|
|||||||
this.settings,
|
this.settings,
|
||||||
this.vaultVectorStore,
|
this.vaultVectorStore,
|
||||||
this.structuredMemoryManager,
|
this.structuredMemoryManager,
|
||||||
this.telemetryManager
|
this.telemetryManager,
|
||||||
|
this.chatHistoryManager
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this.addRibbonIcon("bot", "Open Ollama Chat", async () => {
|
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.");
|
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));
|
this.addSettingTab(new OllamaSettingTab(this.app, this));
|
||||||
if (this.settings.cacheConfig) {
|
if (this.settings.cacheConfig) {
|
||||||
this.semanticCache = new SemanticCacheService(
|
this.semanticCache = new SemanticCacheService(
|
||||||
@@ -13400,12 +13620,15 @@ var OllamaPlugin = class extends import_obsidian7.Plugin {
|
|||||||
);
|
);
|
||||||
const telemetryData = data.toolTelemetry ?? createDefaultToolTelemetryData();
|
const telemetryData = data.toolTelemetry ?? createDefaultToolTelemetryData();
|
||||||
this.telemetryManager = new TelemetryManager(this.settings.toolTelemetryConfig, telemetryData);
|
this.telemetryManager = new TelemetryManager(this.settings.toolTelemetryConfig, telemetryData);
|
||||||
|
const historyData = data.chatHistory ?? createDefaultChatHistoryData();
|
||||||
|
this.chatHistoryManager = new ChatHistoryManager(historyData);
|
||||||
}
|
}
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
await this.saveData({
|
await this.saveData({
|
||||||
settings: this.settings,
|
settings: this.settings,
|
||||||
structuredMemory: this.structuredMemoryManager?.getData() ?? createDefaultStructuredMemoryData(),
|
structuredMemory: this.structuredMemoryManager?.getData() ?? createDefaultStructuredMemoryData(),
|
||||||
toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData()
|
toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData(),
|
||||||
|
chatHistory: this.chatHistoryManager?.getData() ?? createDefaultChatHistoryData()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
initializeAutoOrganizer() {
|
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,
|
ChatMessage,
|
||||||
ProposedAction,
|
ProposedAction,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
|
ChatSession,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { ConversationStateManager } from './conversation-state';
|
import { ConversationStateManager } from './conversation-state';
|
||||||
import { ErrorHandler } from './error-handler';
|
import { ErrorHandler } from './error-handler';
|
||||||
import { StructuredMemoryManager } from './structured-memory';
|
import { StructuredMemoryManager } from './structured-memory';
|
||||||
import { TelemetryManager } from './tool-telemetry';
|
import { TelemetryManager } from './tool-telemetry';
|
||||||
|
import { ChatHistoryManager } from './chat-history';
|
||||||
import { Logger, LogEntry } from './utils';
|
import { Logger, LogEntry } from './utils';
|
||||||
|
|
||||||
export const VIEW_TYPE_OLLAMA_CHAT = 'ollama-chat-view';
|
export const VIEW_TYPE_OLLAMA_CHAT = 'ollama-chat-view';
|
||||||
@@ -45,12 +47,17 @@ export class ChatView extends ItemView {
|
|||||||
return this.newChatButtonClickHandler;
|
return this.newChatButtonClickHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHistorySelectEl() {
|
||||||
|
return this.historySelectEl;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
leaf: WorkspaceLeaf,
|
leaf: WorkspaceLeaf,
|
||||||
settings: PluginSettings,
|
settings: PluginSettings,
|
||||||
vectorStore?: VaultVectorStore,
|
vectorStore?: VaultVectorStore,
|
||||||
structuredMemoryManager?: StructuredMemoryManager,
|
structuredMemoryManager?: StructuredMemoryManager,
|
||||||
telemetryManager?: TelemetryManager
|
telemetryManager?: TelemetryManager,
|
||||||
|
chatHistoryManager?: ChatHistoryManager
|
||||||
) {
|
) {
|
||||||
super(leaf);
|
super(leaf);
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
@@ -88,6 +95,7 @@ export class ChatView extends ItemView {
|
|||||||
);
|
);
|
||||||
this.structuredMemoryManager = structuredMemoryManager;
|
this.structuredMemoryManager = structuredMemoryManager;
|
||||||
this.telemetryManager = telemetryManager;
|
this.telemetryManager = telemetryManager;
|
||||||
|
this.chatHistoryManager = chatHistoryManager;
|
||||||
this.workflowEngine = new WorkflowEngine(
|
this.workflowEngine = new WorkflowEngine(
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.app,
|
this.app,
|
||||||
@@ -102,6 +110,9 @@ export class ChatView extends ItemView {
|
|||||||
this.appendLogEntry(entry);
|
this.appendLogEntry(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restore active session if available
|
||||||
|
this.restoreActiveSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSettings(newSettings: PluginSettings) {
|
updateSettings(newSettings: PluginSettings) {
|
||||||
@@ -174,6 +185,7 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onClose(): Promise<void> {
|
async onClose(): Promise<void> {
|
||||||
|
this.saveCurrentSession();
|
||||||
this.ollamaClient.cancelStream();
|
this.ollamaClient.cancelStream();
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
@@ -190,6 +202,8 @@ export class ChatView extends ItemView {
|
|||||||
this.chatContainer = null;
|
this.chatContainer = null;
|
||||||
this.showLogsButton = null;
|
this.showLogsButton = null;
|
||||||
this.logsContainer = null;
|
this.logsContainer = null;
|
||||||
|
this.historySelectEl = null;
|
||||||
|
this.historyDeleteButton = null;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +301,47 @@ export class ChatView extends ItemView {
|
|||||||
newChatContainer.appendChild(this.modeSelectorEl);
|
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
|
// Setup new chat button
|
||||||
if (!this.newChatButton) {
|
if (!this.newChatButton) {
|
||||||
this.newChatButton = newChatContainer.createEl('button', {
|
this.newChatButton = newChatContainer.createEl('button', {
|
||||||
@@ -453,16 +508,103 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearConversation(): void {
|
clearConversation(): void {
|
||||||
|
// Save the current session before clearing
|
||||||
|
this.saveCurrentSession();
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode));
|
this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode));
|
||||||
|
// Create a new session for the fresh conversation
|
||||||
|
this.chatHistoryManager?.createSession(this.currentAgentMode);
|
||||||
this.render();
|
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 {
|
updateMessageById(id: string, updates: Partial<ChatMessage>): void {
|
||||||
const index = this.messages.findIndex((m) => m.id === id);
|
const index = this.messages.findIndex((m) => m.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.messages[index] = { ...this.messages[index], ...updates };
|
this.messages[index] = { ...this.messages[index], ...updates };
|
||||||
this.render();
|
this.render();
|
||||||
|
this.syncMessagesToSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +615,7 @@ export class ChatView extends ItemView {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.messages[index] = { ...this.messages[index], ...updates };
|
this.messages[index] = { ...this.messages[index], ...updates };
|
||||||
this.render();
|
this.render();
|
||||||
|
this.syncMessagesToSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1350,6 +1493,7 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
|
this.syncMessagesToSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
||||||
this.updateMessageById(assistantMessageId, {
|
this.updateMessageById(assistantMessageId, {
|
||||||
@@ -1357,6 +1501,7 @@ export class ChatView extends ItemView {
|
|||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
isThinking: false,
|
isThinking: false,
|
||||||
});
|
});
|
||||||
|
this.syncMessagesToSession();
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up streaming resources regardless of outcome
|
// Clean up streaming resources regardless of outcome
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
@@ -1388,9 +1533,12 @@ export class ChatView extends ItemView {
|
|||||||
private conversationStateManager: ConversationStateManager;
|
private conversationStateManager: ConversationStateManager;
|
||||||
private structuredMemoryManager?: StructuredMemoryManager;
|
private structuredMemoryManager?: StructuredMemoryManager;
|
||||||
private telemetryManager?: TelemetryManager;
|
private telemetryManager?: TelemetryManager;
|
||||||
|
private chatHistoryManager?: ChatHistoryManager;
|
||||||
private vectorStore?: VaultVectorStore;
|
private vectorStore?: VaultVectorStore;
|
||||||
|
|
||||||
private modeSelectorEl: HTMLSelectElement | null = null;
|
private modeSelectorEl: HTMLSelectElement | null = null;
|
||||||
|
private historySelectEl: HTMLSelectElement | null = null;
|
||||||
|
private historyDeleteButton: HTMLElement | null = null;
|
||||||
private currentAgentMode: AgentMode;
|
private currentAgentMode: AgentMode;
|
||||||
private pendingActions: ProposedAction[] = [];
|
private pendingActions: ProposedAction[] = [];
|
||||||
private pendingReadResults: (ToolResult & { id?: string })[] = [];
|
private pendingReadResults: (ToolResult & { id?: string })[] = [];
|
||||||
|
|||||||
+22
-2
@@ -5,12 +5,13 @@ import { SemanticCacheService } from './semantic-cache';
|
|||||||
import { VaultVectorStore } from './vault-vector-store';
|
import { VaultVectorStore } from './vault-vector-store';
|
||||||
import { VaultIndexer } from './vault-indexer';
|
import { VaultIndexer } from './vault-indexer';
|
||||||
import { AutoTagger, AutoLinker } from './auto-organizer';
|
import { AutoTagger, AutoLinker } from './auto-organizer';
|
||||||
import { PluginSettings, StructuredMemoryData, ToolTelemetryData } from './types';
|
import { PluginSettings, StructuredMemoryData, ToolTelemetryData, ChatHistoryData } from './types';
|
||||||
import { Logger } from './utils';
|
import { Logger } from './utils';
|
||||||
import { ALL_AGENT_MODES, getAgentModeLabel } from './agent-modes';
|
import { ALL_AGENT_MODES, getAgentModeLabel } from './agent-modes';
|
||||||
import { AgentMode } from './types';
|
import { AgentMode } from './types';
|
||||||
import { StructuredMemoryManager, createDefaultStructuredMemoryData } from './structured-memory';
|
import { StructuredMemoryManager, createDefaultStructuredMemoryData } from './structured-memory';
|
||||||
import { TelemetryManager, createDefaultToolTelemetryData } from './tool-telemetry';
|
import { TelemetryManager, createDefaultToolTelemetryData } from './tool-telemetry';
|
||||||
|
import { ChatHistoryManager, createDefaultChatHistoryData } from './chat-history';
|
||||||
|
|
||||||
export default class OllamaPlugin extends Plugin {
|
export default class OllamaPlugin extends Plugin {
|
||||||
settings: PluginSettings = DEFAULT_SETTINGS;
|
settings: PluginSettings = DEFAULT_SETTINGS;
|
||||||
@@ -20,6 +21,7 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
autoLinker?: AutoLinker;
|
autoLinker?: AutoLinker;
|
||||||
structuredMemoryManager?: StructuredMemoryManager;
|
structuredMemoryManager?: StructuredMemoryManager;
|
||||||
telemetryManager?: TelemetryManager;
|
telemetryManager?: TelemetryManager;
|
||||||
|
chatHistoryManager?: ChatHistoryManager;
|
||||||
private indexingAbortController?: AbortController;
|
private indexingAbortController?: AbortController;
|
||||||
private currentIndexingPromise?: Promise<void>;
|
private currentIndexingPromise?: Promise<void>;
|
||||||
|
|
||||||
@@ -40,7 +42,8 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
this.settings,
|
this.settings,
|
||||||
this.vaultVectorStore,
|
this.vaultVectorStore,
|
||||||
this.structuredMemoryManager,
|
this.structuredMemoryManager,
|
||||||
this.telemetryManager
|
this.telemetryManager,
|
||||||
|
this.chatHistoryManager
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -136,6 +139,18 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
new Notice('Tool telemetry cleared.');
|
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));
|
this.addSettingTab(new OllamaSettingTab(this.app, this));
|
||||||
|
|
||||||
// Initialize the semantic cache
|
// Initialize the semantic cache
|
||||||
@@ -189,6 +204,10 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
const telemetryData: ToolTelemetryData =
|
const telemetryData: ToolTelemetryData =
|
||||||
(data.toolTelemetry as ToolTelemetryData | undefined) ?? createDefaultToolTelemetryData();
|
(data.toolTelemetry as ToolTelemetryData | undefined) ?? createDefaultToolTelemetryData();
|
||||||
this.telemetryManager = new TelemetryManager(this.settings.toolTelemetryConfig, telemetryData);
|
this.telemetryManager = new TelemetryManager(this.settings.toolTelemetryConfig, telemetryData);
|
||||||
|
|
||||||
|
const historyData: ChatHistoryData =
|
||||||
|
(data.chatHistory as ChatHistoryData | undefined) ?? createDefaultChatHistoryData();
|
||||||
|
this.chatHistoryManager = new ChatHistoryManager(historyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
@@ -197,6 +216,7 @@ export default class OllamaPlugin extends Plugin {
|
|||||||
structuredMemory:
|
structuredMemory:
|
||||||
this.structuredMemoryManager?.getData() ?? createDefaultStructuredMemoryData(),
|
this.structuredMemoryManager?.getData() ?? createDefaultStructuredMemoryData(),
|
||||||
toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData(),
|
toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData(),
|
||||||
|
chatHistory: this.chatHistoryManager?.getData() ?? createDefaultChatHistoryData(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,6 +313,20 @@ export interface VaultIndexConfig {
|
|||||||
similarityThreshold: number;
|
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 {
|
export interface PluginSettings {
|
||||||
ollamaUrl: string;
|
ollamaUrl: string;
|
||||||
chatModel: string;
|
chatModel: string;
|
||||||
|
|||||||
+38
@@ -303,6 +303,44 @@
|
|||||||
border-color: var(--interactive-accent);
|
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 */
|
/* Show Logs Button */
|
||||||
.ollama-show-logs-button {
|
.ollama-show-logs-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ChatView } from '../src/chat-view';
|
import { ChatView } from '../src/chat-view';
|
||||||
import { PluginSettings, OllamaMessage, ChatMessage, OllamaTool, ToolCall } from '../src/types';
|
import { PluginSettings, OllamaMessage, ChatMessage, OllamaTool, ToolCall } from '../src/types';
|
||||||
import { ErrorHandler } from '../src/error-handler';
|
import { ErrorHandler } from '../src/error-handler';
|
||||||
|
import { ChatHistoryManager } from '../src/chat-history';
|
||||||
|
|
||||||
// Mock Obsidian types
|
// Mock Obsidian types
|
||||||
interface MockVault {
|
interface MockVault {
|
||||||
@@ -114,7 +115,14 @@ describe('ChatView', () => {
|
|||||||
app: mockApp,
|
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
|
// Obsidian's contentEl has a createEl helper that standard DOM lacks
|
||||||
// Unlike standard DOM, Obsidian elements can create nested elements with createEl
|
// Unlike standard DOM, Obsidian elements can create nested elements with createEl
|
||||||
const contentDiv = document.createElement('div') as any;
|
const contentDiv = document.createElement('div') as any;
|
||||||
|
|||||||
Reference in New Issue
Block a user