import { App, MarkdownView } from "obsidian"; /** * Reads the active Obsidian note and produces structured context for Hermes. * * Strategy: instead of embedding full note content (wastes context tokens), * we send a vault-relative path reference. Hermes reads the file on-demand * via mcp_obsidian_get_vault_document — zero context burned, same result. * * If the user has text selected, we embed that selection directly (it's small * and intentional). Otherwise we send a path reference. */ export class ContextBridge { private app: App; constructor(app: App) { this.app = app; } /** * Get the active note as a vault-relative reference. * Returns null if no markdown editor is active. */ getActiveNoteContext(): { vault: string; path: string; title: string; } | null { const activeView = this.app.workspace.activeEditor; if (!activeView || !activeView.editor) return null; // Verify it's a Markdown file (not canvas, PDF, etc.) const leaf = this.app.workspace.activeLeaf; if (!leaf || !(leaf.view instanceof MarkdownView)) return null; const file = leaf.view.file; if (!file) return null; return { vault: this.app.vault.getName(), path: file.path, title: file.basename, }; } /** * Snapshot the active note context AND selection in one atomic read. * Call this when the chat view gains focus — captures the note state * before the user's focus move clears activeEditor / deselects text. * * Returns null if no markdown editor is active. */ snapshot(): { vault: string; path: string; title: string; selection: string | null; } | null { const noteRef = this.getActiveNoteContext(); if (!noteRef) return null; return { ...noteRef, selection: this.getSelectionContext(), }; } /** * Get the currently selected text in the active editor. * Returns null if no text is selected. */ getSelectionContext(): string | null { const activeView = this.app.workspace.activeEditor; if (!activeView || !activeView.editor) return null; const selection = activeView.editor.getSelection(); return selection || null; } /** * Format a note reference as a system message that tells Hermes * which MCP tool call to use to read the file on Helm. */ formatContextMessage(ref: { vault: string; path: string; title: string; }): { role: string; content: string } { return { role: "system", content: [ `[Current note: ${ref.vault}/${ref.path}]`, `Use mcp_obsidian_get_vault_document(vault_name="${ref.vault}", doc_path="${ref.path}") to read the full contents.`, ].join("\n"), }; } /** * Build the messages array for an API call. * * - If text is selected in the active note: embed it directly as context. * - Otherwise if context injection is enabled: send a path reference * so Hermes can read the file via MCP on Helm. * - Always appends the user's chat message. */ buildMessages( userInput: string, includeContext: boolean ): { role: string; content: string }[] { const messages: { role: string; content: string }[] = []; if (includeContext) { // Prefer selected text (small, intentional, works anywhere) const selection = this.getSelectionContext(); if (selection) { const ref = this.getActiveNoteContext(); const source = ref ? `${ref.vault}/${ref.path}` : "current note"; messages.push({ role: "system", content: `[Selection from ${source}]\n\n${selection}`, }); } else { // No selection — send path reference for Hermes to read via MCP const ref = this.getActiveNoteContext(); if (ref) { messages.push(this.formatContextMessage(ref)); } } } messages.push({ role: "user", content: userInput }); return messages; } }