diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/chat-history.ts | 116 | ||||
| -rw-r--r-- | src/chat-view.ts | 20 | ||||
| -rw-r--r-- | src/main.ts | 15 | ||||
| -rw-r--r-- | src/settings.ts | 12 |
4 files changed, 34 insertions, 129 deletions
diff --git a/src/chat-history.ts b/src/chat-history.ts index ac29ed9..ac7c6a8 100644 --- a/src/chat-history.ts +++ b/src/chat-history.ts @@ -1,34 +1,7 @@ -import type { App } from "obsidian"; import type { ChatMessage } from "./ollama-client"; +import type { PersistedMessage } from "./settings"; -/** - * Stored chat history format. - * Only user and assistant messages are persisted — system and tool messages - * are transient (injected per-request by the agent loop). - */ -export interface ChatHistoryData { - version: 1; - messages: PersistedMessage[]; -} - -/** - * A message stored in the chat history file. - * This is a subset of ChatMessage — we strip tool_calls, tool_name, - * and system/tool role messages since they are not meaningful across sessions. - */ -export interface PersistedMessage { - role: "user" | "assistant"; - content: string; -} - -const CHAT_HISTORY_FILENAME = "chat-history.json"; - -/** - * Resolve the full path to the chat history file inside the plugin folder. - */ -function getHistoryPath(app: App, pluginId: string): string { - return `${app.vault.configDir}/plugins/${pluginId}/${CHAT_HISTORY_FILENAME}`; -} +export type { PersistedMessage } from "./settings"; /** * Filter ChatMessage[] down to only persistable user/assistant messages. @@ -49,88 +22,3 @@ export function toPersistableMessages(messages: readonly ChatMessage[]): Persist export function toRuntimeMessages(messages: readonly PersistedMessage[]): ChatMessage[] { return messages.map((m) => ({ role: m.role, content: m.content })); } - -/** - * Load chat history from the plugin folder. - * Returns an empty array if the file doesn't exist or is corrupted. - */ -export async function loadChatHistory(app: App, pluginId: string): Promise<PersistedMessage[]> { - const path = getHistoryPath(app, pluginId); - - try { - const exists = await app.vault.adapter.exists(path); - if (!exists) { - return []; - } - - const raw = await app.vault.adapter.read(path); - const parsed = JSON.parse(raw) as unknown; - - if (!isValidChatHistory(parsed)) { - return []; - } - - return parsed.messages; - } catch { - return []; - } -} - -/** - * Save chat history to the plugin folder. - */ -export async function saveChatHistory( - app: App, - pluginId: string, - messages: readonly ChatMessage[], -): Promise<void> { - const path = getHistoryPath(app, pluginId); - const persistable = toPersistableMessages(messages); - - const data: ChatHistoryData = { - version: 1, - messages: persistable, - }; - - await app.vault.adapter.write(path, JSON.stringify(data, null, 2)); -} - -/** - * Clear the chat history by writing an empty messages array. - * Writing an empty file rather than deleting ensures Obsidian Sync - * propagates the "cleared" state to all devices. - */ -export async function clearChatHistory(app: App, pluginId: string): Promise<void> { - const path = getHistoryPath(app, pluginId); - - const data: ChatHistoryData = { - version: 1, - messages: [], - }; - - try { - await app.vault.adapter.write(path, JSON.stringify(data, null, 2)); - } catch { - // Silently ignore — clear is best-effort - } -} - -/** - * Type guard for validating the parsed chat history JSON. - */ -function isValidChatHistory(value: unknown): value is ChatHistoryData { - if (typeof value !== "object" || value === null) return false; - - const obj = value as Record<string, unknown>; - if (obj["version"] !== 1) return false; - if (!Array.isArray(obj["messages"])) return false; - - for (const msg of obj["messages"]) { - if (typeof msg !== "object" || msg === null) return false; - const m = msg as Record<string, unknown>; - if (m["role"] !== "user" && m["role"] !== "assistant") return false; - if (typeof m["content"] !== "string") return false; - } - - return true; -} diff --git a/src/chat-view.ts b/src/chat-view.ts index 72e7a32..dd27c63 100644 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -7,7 +7,7 @@ import { ToolModal } from "./tool-modal"; import { TOOL_REGISTRY } from "./tools"; import type { OllamaToolDefinition } from "./tools"; import { collectVaultContext, formatVaultContext } from "./vault-context"; -import { loadChatHistory, saveChatHistory, clearChatHistory, toRuntimeMessages, toPersistableMessages } from "./chat-history"; +import { toRuntimeMessages, toPersistableMessages } from "./chat-history"; import type { PersistedMessage } from "./chat-history"; export const VIEW_TYPE_CHAT = "ai-pulse-chat"; @@ -116,8 +116,9 @@ export class ChatView extends ItemView { if (this.messageContainer !== null) { this.messageContainer.empty(); } - void clearChatHistory(this.plugin.app, this.plugin.manifest.id); + this.plugin.settings.chatHistory = []; this.plugin.updateChatSnapshot([]); + void this.plugin.saveSettings(); (document.activeElement as HTMLElement)?.blur(); }); @@ -161,7 +162,8 @@ export class ChatView extends ItemView { this.saveDebounceTimer = null; } // Save any pending history before closing - void saveChatHistory(this.plugin.app, this.plugin.manifest.id, this.messages); + this.plugin.settings.chatHistory = toPersistableMessages(this.messages); + void this.plugin.saveSettings(); this.contentEl.empty(); this.messages = []; this.bubbleContent.clear(); @@ -762,12 +764,12 @@ export class ChatView extends ItemView { } this.saveDebounceTimer = setTimeout(() => { this.saveDebounceTimer = null; - void saveChatHistory(this.plugin.app, this.plugin.manifest.id, this.messages); + const persistable = toPersistableMessages(this.messages); + this.plugin.settings.chatHistory = persistable; // Update the plugin's snapshot so the sync checker doesn't treat // our own save as an external change. - this.plugin.updateChatSnapshot( - toPersistableMessages(this.messages), - ); + this.plugin.updateChatSnapshot(persistable); + void this.plugin.saveSettings(); }, 500); } @@ -775,7 +777,7 @@ export class ChatView extends ItemView { * Restore chat history from the persisted file and render messages. */ private async restoreChatHistory(): Promise<void> { - const persisted = await loadChatHistory(this.plugin.app, this.plugin.manifest.id); + const persisted = this.plugin.settings.chatHistory; if (persisted.length === 0) return; this.messages = toRuntimeMessages(persisted); @@ -828,7 +830,7 @@ export class ChatView extends ItemView { * Replaces the current messages and re-renders the UI. */ async reloadChatHistory(): Promise<void> { - const persisted = await loadChatHistory(this.plugin.app, this.plugin.manifest.id); + const persisted = this.plugin.settings.chatHistory; // Skip reload if we're currently streaming — avoid disrupting the UI if (this.abortController !== null) return; diff --git a/src/main.ts b/src/main.ts index 96eb207..e09cf56 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,6 @@ import { DEFAULT_SETTINGS } from "./settings"; import { ChatView, VIEW_TYPE_CHAT } from "./chat-view"; import { testConnection, listModels } from "./ollama-client"; import { getDefaultToolStates } from "./tools"; -import { loadChatHistory } from "./chat-history"; import type { PersistedMessage } from "./chat-history"; export default class AIPulse extends Plugin { @@ -39,7 +38,11 @@ export default class AIPulse extends Plugin { // We check when the app regains visibility (user switches back from another app/device). this.registerDomEvent(document, "visibilitychange", () => { if (document.visibilityState === "visible") { - void this.checkChatHistorySync(); + // Reload settings from disk in case Obsidian Sync updated data.json + // while the app was in the background. + void this.loadSettings().then(() => { + this.checkChatHistorySync(); + }); } }); } @@ -87,20 +90,20 @@ export default class AIPulse extends Plugin { /** * Called by Obsidian when data.json is modified externally (e.g., via Sync). - * This is a strong signal that other plugin files may also have been synced. + * Reloads settings (which now include chat history) and syncs the chat view. */ async onExternalSettingsChange(): Promise<void> { await this.loadSettings(); - void this.checkChatHistorySync(); + this.checkChatHistorySync(); } /** * Check if the persisted chat history has changed (e.g., from another device) * and reload the chat view if needed. */ - async checkChatHistorySync(): Promise<void> { + checkChatHistorySync(): void { try { - const persisted = await loadChatHistory(this.app, this.manifest.id); + const persisted = this.settings.chatHistory; const snapshot = buildChatSnapshot(persisted); if (snapshot === this.lastChatSnapshot) return; diff --git a/src/settings.ts b/src/settings.ts index c61af9d..ab20416 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,15 @@ import { getDefaultToolStates } from "./tools"; +/** + * A message stored in the persisted chat history. + * Only user and assistant messages are persisted — system and tool messages + * are transient (injected per-request by the agent loop). + */ +export interface PersistedMessage { + role: "user" | "assistant"; + content: string; +} + export interface AIPulseSettings { ollamaUrl: string; model: string; @@ -11,6 +21,7 @@ export interface AIPulseSettings { systemPromptFile: string; injectVaultContext: boolean; vaultContextRecentFiles: number; + chatHistory: PersistedMessage[]; } export const DEFAULT_SETTINGS: AIPulseSettings = { @@ -24,4 +35,5 @@ export const DEFAULT_SETTINGS: AIPulseSettings = { systemPromptFile: "agent.md", injectVaultContext: false, vaultContextRecentFiles: 20, + chatHistory: [], }; |
