fix: font resize live-update + chat message persistence

- Font size slider now calls saveSettings() instead of saveData()
  so changes propagate to open views immediately
- Chat messages persist across plugin reload via chatMessages
  array in plugin settings — ChatPersistence interface bridges
  ChatView to Plugin data layer
- Messages auto-save on append and on close
This commit is contained in:
Helm 2026-07-10 13:09:00 -04:00
parent 695a8e0050
commit a819e7dd2f
4 changed files with 61 additions and 11 deletions

12
main.js

File diff suppressed because one or more lines are too long

View file

@ -16,11 +16,18 @@ export interface ApiClient {
): Promise<ChatCompletionResponse>; ): Promise<ChatCompletionResponse>;
} }
/** Persistence interface — saves/loads chat messages via plugin data. */
export interface ChatPersistence {
loadMessages(): Message[];
saveMessages(messages: Message[]): Promise<void>;
}
export const VIEW_TYPE = "hermes-chat-view"; export const VIEW_TYPE = "hermes-chat-view";
export class ChatView extends ItemView { export class ChatView extends ItemView {
private apiClient: ApiClient | null = null; private apiClient: ApiClient | null = null;
private contextBridge: ContextBridge | null = null; private contextBridge: ContextBridge | null = null;
private persistence: ChatPersistence | null = null;
private chatState: ChatState; private chatState: ChatState;
private messagesContainer: HTMLElement | null = null; private messagesContainer: HTMLElement | null = null;
private statusBar: HTMLElement | null = null; private statusBar: HTMLElement | null = null;
@ -43,10 +50,11 @@ export class ChatView extends ItemView {
private suggestIndex: number = -1; private suggestIndex: number = -1;
private filteredCommands: SlashCommand[] = []; private filteredCommands: SlashCommand[] = [];
constructor(leaf: WorkspaceLeaf, apiClient?: ApiClient, contextBridge?: ContextBridge) { constructor(leaf: WorkspaceLeaf, apiClient?: ApiClient, contextBridge?: ContextBridge, persistence?: ChatPersistence) {
super(leaf); super(leaf);
this.apiClient = apiClient ?? null; this.apiClient = apiClient ?? null;
this.contextBridge = contextBridge ?? null; this.contextBridge = contextBridge ?? null;
this.persistence = persistence ?? null;
this.chatState = { this.chatState = {
sessionId: null, sessionId: null,
messages: [], messages: [],
@ -167,10 +175,20 @@ export class ChatView extends ItemView {
// --- Status bar --- // --- Status bar ---
this.statusBar = container.createDiv("chat-status-bar"); this.statusBar = container.createDiv("chat-status-bar");
this.updateStatus(this.chatState.model, null); this.updateStatus(this.chatState.model, null);
// Restore saved messages from previous session
if (this.persistence) {
const saved = this.persistence.loadMessages();
if (saved.length > 0) {
for (const msg of saved) {
await this.appendMessage(msg.role, msg.content);
}
}
}
} }
async onClose(): Promise<void> { async onClose(): Promise<void> {
// No persistent cleanup needed await this.persistence?.saveMessages(this.chatState.messages);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -201,6 +219,17 @@ export class ChatView extends ItemView {
async appendMessage(role: "user" | "assistant" | "system", content: string): Promise<void> { async appendMessage(role: "user" | "assistant" | "system", content: string): Promise<void> {
if (!this.messagesContainer) return; if (!this.messagesContainer) return;
// Track in state for persistence
const msg: Message = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
role,
content,
timestamp: Date.now(),
};
this.chatState.messages.push(msg);
// Fire-and-forget save — don't block rendering
this.persistence?.saveMessages(this.chatState.messages);
const wrapper = this.messagesContainer.createDiv("chat-message"); const wrapper = this.messagesContainer.createDiv("chat-message");
wrapper.addClass(`chat-message-${role}`); wrapper.addClass(`chat-message-${role}`);
@ -472,6 +501,16 @@ export class ChatView extends ItemView {
await MarkdownRenderer.render(this.app, fullContent, contentDiv, "", this); await MarkdownRenderer.render(this.app, fullContent, contentDiv, "", this);
} }
// Track assistant response in state for persistence
const assistantMsg: Message = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
role: "assistant",
content: fullContent,
timestamp: Date.now(),
};
this.chatState.messages.push(assistantMsg);
this.persistence?.saveMessages(this.chatState.messages);
if (response.sessionId) { if (response.sessionId) {
this.chatState.sessionId = response.sessionId; this.chatState.sessionId = response.sessionId;
this.updateStatus(response.model, response.sessionId); this.updateStatus(response.model, response.sessionId);

View file

@ -1,6 +1,6 @@
import { Plugin, WorkspaceLeaf } from "obsidian"; import { Plugin, WorkspaceLeaf } from "obsidian";
import { HermesChatSettingTab, HermesChatSettings, DEFAULT_SETTINGS, BUILTIN_COMMANDS } from "./settings"; import { HermesChatSettingTab, HermesChatSettings, DEFAULT_SETTINGS, BUILTIN_COMMANDS } from "./settings";
import { ChatView, VIEW_TYPE } from "./chat-view"; import { ChatView, VIEW_TYPE, ChatPersistence } from "./chat-view";
import { ApiClient } from "./api-client"; import { ApiClient } from "./api-client";
import { ContextBridge } from "./context-bridge"; import { ContextBridge } from "./context-bridge";
import type { GatewayConfig, SlashCommand } from "./types"; import type { GatewayConfig, SlashCommand } from "./types";
@ -19,7 +19,14 @@ export default class HermesChatPlugin extends Plugin {
VIEW_TYPE, VIEW_TYPE,
(leaf: WorkspaceLeaf) => { (leaf: WorkspaceLeaf) => {
const client = new ApiClient(this.getGatewayConfig()); const client = new ApiClient(this.getGatewayConfig());
const view = new ChatView(leaf, client, this.contextBridge!); const persistence: ChatPersistence = {
loadMessages: () => this.settings.chatMessages,
saveMessages: async (messages) => {
this.settings.chatMessages = messages;
await this.saveData(this.settings);
},
};
const view = new ChatView(leaf, client, this.contextBridge!, persistence);
view.setContextActive(this.settings.enableContextInjection); view.setContextActive(this.settings.enableContextInjection);
view.setSlashCommands(this.getAllCommands()); view.setSlashCommands(this.getAllCommands());
view.setChatFontSize(this.settings.chatFontSize); view.setChatFontSize(this.settings.chatFontSize);

View file

@ -1,6 +1,8 @@
import { App, PluginSettingTab, Setting } from "obsidian"; import { App, PluginSettingTab, Setting } from "obsidian";
import type HermesChatPlugin from "./main"; import type HermesChatPlugin from "./main";
import type { Message } from "./types";
export interface HermesChatSettings { export interface HermesChatSettings {
gatewayUrl: string; gatewayUrl: string;
apiKey: string; apiKey: string;
@ -8,6 +10,7 @@ export interface HermesChatSettings {
enableContextInjection: boolean; enableContextInjection: boolean;
chatFontSize: number; chatFontSize: number;
customCommands: { trigger: string; description: string; prompt: string }[]; customCommands: { trigger: string; description: string; prompt: string }[];
chatMessages: Message[];
} }
/** Built-in slash commands shipped with the plugin. */ /** Built-in slash commands shipped with the plugin. */
@ -36,6 +39,7 @@ export const DEFAULT_SETTINGS: HermesChatSettings = {
enableContextInjection: true, enableContextInjection: true,
chatFontSize: 14, chatFontSize: 14,
customCommands: [], customCommands: [],
chatMessages: [],
}; };
export class HermesChatSettingTab extends PluginSettingTab { export class HermesChatSettingTab extends PluginSettingTab {
@ -113,7 +117,7 @@ export class HermesChatSettingTab extends PluginSettingTab {
.setDynamicTooltip() .setDynamicTooltip()
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.chatFontSize = value; this.plugin.settings.chatFontSize = value;
await this.plugin.saveData(this.plugin.settings); await this.plugin.saveSettings();
}) })
); );