summaryrefslogtreecommitdiffhomepage
path: root/src/chat-history.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-29 13:25:18 +0900
committerAdam Malczewski <[email protected]>2026-03-29 13:25:18 +0900
commit90346a91a81c317b90f4ca9a64cbaaf0ade7868b (patch)
tree0be851269904a3ef76f16cff9d384914face8927 /src/chat-history.ts
parentbffeac84cb7b3094f7a4b879b6bab6ceaec561ac (diff)
downloadai-pulse-obsidian-plugin-90346a91a81c317b90f4ca9a64cbaaf0ade7868b.tar.gz
ai-pulse-obsidian-plugin-90346a91a81c317b90f4ca9a64cbaaf0ade7868b.zip
add persistent chat history with cross-device sync
Diffstat (limited to 'src/chat-history.ts')
-rw-r--r--src/chat-history.ts136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/chat-history.ts b/src/chat-history.ts
new file mode 100644
index 0000000..ac29ed9
--- /dev/null
+++ b/src/chat-history.ts
@@ -0,0 +1,136 @@
+import type { App } from "obsidian";
+import type { ChatMessage } from "./ollama-client";
+
+/**
+ * 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}`;
+}
+
+/**
+ * Filter ChatMessage[] down to only persistable user/assistant messages.
+ */
+export function toPersistableMessages(messages: readonly ChatMessage[]): PersistedMessage[] {
+ const result: PersistedMessage[] = [];
+ for (const msg of messages) {
+ if (msg.role === "user" || msg.role === "assistant") {
+ result.push({ role: msg.role, content: msg.content });
+ }
+ }
+ return result;
+}
+
+/**
+ * Convert persisted messages back to ChatMessage[] for the LLM context.
+ */
+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;
+}