feat: markdown rendering + font size slider

- Chat messages now render full markdown via Obsidian's MarkdownRenderer
  (headers, bold, italic, code blocks, lists, links)
- Streaming shows plain text, swaps to rendered markdown on completion
- User messages render instantly as markdown
- New Chat font size slider in plugin settings (10-24px, default 14)
- Live-applies via CSS custom property --hermes-chat-font-size
- No restart needed
This commit is contained in:
Helm 2026-07-10 11:58:46 -04:00
parent 16fb2238dd
commit 695a8e0050
5 changed files with 45 additions and 14 deletions

16
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
import { ItemView, WorkspaceLeaf, MarkdownView } from "obsidian"; import { ItemView, WorkspaceLeaf, MarkdownView, MarkdownRenderer } from "obsidian";
import type { Message, ChatState, ChatCompletionResponse, SlashCommand } from "./types"; import type { Message, ChatState, ChatCompletionResponse, SlashCommand } from "./types";
import { ContextBridge } from "./context-bridge"; import { ContextBridge } from "./context-bridge";
@ -192,8 +192,13 @@ export class ChatView extends ItemView {
this.slashCommands = commands; this.slashCommands = commands;
} }
/** Set chat font size via CSS custom property on the container. */
setChatFontSize(size: number): void {
this.containerEl.style.setProperty("--hermes-chat-font-size", `${size}px`);
}
/** Append a message bubble to the chat display and scroll to bottom. */ /** Append a message bubble to the chat display and scroll to bottom. */
appendMessage(role: "user" | "assistant" | "system", content: string): void { async appendMessage(role: "user" | "assistant" | "system", content: string): Promise<void> {
if (!this.messagesContainer) return; if (!this.messagesContainer) return;
const wrapper = this.messagesContainer.createDiv("chat-message"); const wrapper = this.messagesContainer.createDiv("chat-message");
@ -205,7 +210,7 @@ export class ChatView extends ItemView {
); );
const contentEl = wrapper.createDiv("chat-message-content"); const contentEl = wrapper.createDiv("chat-message-content");
contentEl.setText(content); await MarkdownRenderer.render(this.app, content, contentEl, "", this);
this.scrollToBottom(); this.scrollToBottom();
} }
@ -399,11 +404,11 @@ export class ChatView extends ItemView {
const userInput = this.resolveSlashCommand(rawInput); const userInput = this.resolveSlashCommand(rawInput);
// Display user message (show the resolved prompt, not the /trigger) // Display user message (show the resolved prompt, not the /trigger)
this.appendMessage("user", userInput); await this.appendMessage("user", userInput);
// Check API client availability // Check API client availability
if (!this.apiClient) { if (!this.apiClient) {
this.appendMessage( await this.appendMessage(
"assistant", "assistant",
"[API client not configured. Check plugin settings.]" "[API client not configured. Check plugin settings.]"
); );
@ -461,6 +466,12 @@ export class ChatView extends ItemView {
} }
); );
// Replace streaming plain text with rendered markdown
if (contentDiv) {
contentDiv.empty();
await MarkdownRenderer.render(this.app, fullContent, contentDiv, "", this);
}
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

@ -22,6 +22,7 @@ export default class HermesChatPlugin extends Plugin {
const view = new ChatView(leaf, client, this.contextBridge!); const view = new ChatView(leaf, client, this.contextBridge!);
view.setContextActive(this.settings.enableContextInjection); view.setContextActive(this.settings.enableContextInjection);
view.setSlashCommands(this.getAllCommands()); view.setSlashCommands(this.getAllCommands());
view.setChatFontSize(this.settings.chatFontSize);
return view; return view;
} }
); );
@ -147,6 +148,7 @@ export default class HermesChatPlugin extends Plugin {
if (leaf.view instanceof ChatView) { if (leaf.view instanceof ChatView) {
leaf.view.setContextActive(this.settings.enableContextInjection); leaf.view.setContextActive(this.settings.enableContextInjection);
leaf.view.setSlashCommands(commands); leaf.view.setSlashCommands(commands);
leaf.view.setChatFontSize(this.settings.chatFontSize);
} }
} }
} }

View file

@ -6,6 +6,7 @@ export interface HermesChatSettings {
apiKey: string; apiKey: string;
showModelInfo: boolean; showModelInfo: boolean;
enableContextInjection: boolean; enableContextInjection: boolean;
chatFontSize: number;
customCommands: { trigger: string; description: string; prompt: string }[]; customCommands: { trigger: string; description: string; prompt: string }[];
} }
@ -33,6 +34,7 @@ export const DEFAULT_SETTINGS: HermesChatSettings = {
apiKey: "0f224b188b52e8728d135ec339f8db8dfbf82d89a60021dcfe20d91c6174cf9b", apiKey: "0f224b188b52e8728d135ec339f8db8dfbf82d89a60021dcfe20d91c6174cf9b",
showModelInfo: true, showModelInfo: true,
enableContextInjection: true, enableContextInjection: true,
chatFontSize: 14,
customCommands: [], customCommands: [],
}; };
@ -101,6 +103,20 @@ export class HermesChatSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerEl)
.setName("Chat font size")
.setDesc("Font size for chat messages and input (10—24 px)")
.addSlider((slider) =>
slider
.setLimits(10, 24, 1)
.setValue(this.plugin.settings.chatFontSize)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.chatFontSize = value;
await this.plugin.saveData(this.plugin.settings);
})
);
// --- Slash commands --- // --- Slash commands ---
containerEl.createEl("h3", { text: "Slash Commands" }); containerEl.createEl("h3", { text: "Slash Commands" });
containerEl.createEl("p", { containerEl.createEl("p", {

View file

@ -8,6 +8,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
--hermes-chat-font-size: 14px;
} }
/* --- Message list --- */ /* --- Message list --- */
@ -45,6 +46,7 @@
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
color: var(--text-normal); color: var(--text-normal);
font-size: var(--hermes-chat-font-size);
} }
/* --- Loading indicator --- */ /* --- Loading indicator --- */
@ -74,7 +76,7 @@
max-height: 120px; max-height: 120px;
padding: 0.5rem; padding: 0.5rem;
font-family: inherit; font-family: inherit;
font-size: 0.9rem; font-size: var(--hermes-chat-font-size);
outline: none; outline: none;
transition: border-color 0.15s; transition: border-color 0.15s;
} }