summaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/chat-history.ts116
-rw-r--r--src/chat-view.ts20
-rw-r--r--src/main.ts15
-rw-r--r--src/settings.ts12
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: [],
};