- Streaming chat sidebar with OpenAI-compatible API client - Path-referencing context injection via MCP tools - Dark theme styling with Obsidian CSS variables - Settings: gateway URL, API key, context/model toggles - SSE streaming with fetch timeout protection - Session continuity via X-Hermes-Session-Id headers
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
import { Plugin, WorkspaceLeaf } from "obsidian";
|
|
import { HermesChatSettingTab, HermesChatSettings, DEFAULT_SETTINGS } from "./settings";
|
|
import { ChatView, VIEW_TYPE } from "./chat-view";
|
|
import { ApiClient } from "./api-client";
|
|
import { ContextBridge } from "./context-bridge";
|
|
import type { GatewayConfig } from "./types";
|
|
|
|
export default class HermesChatPlugin extends Plugin {
|
|
settings: HermesChatSettings;
|
|
private statusBarItem: HTMLElement | null = null;
|
|
private apiClient: ApiClient | null = null;
|
|
private contextBridge: ContextBridge | null = null;
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
|
|
// Register the chat view
|
|
this.registerView(
|
|
VIEW_TYPE,
|
|
(leaf: WorkspaceLeaf) => {
|
|
const view = new ChatView(leaf, this.apiClient!, this.contextBridge!);
|
|
view.setContextActive(this.settings.enableContextInjection);
|
|
return view;
|
|
}
|
|
);
|
|
|
|
// Add settings tab
|
|
this.addSettingTab(new HermesChatSettingTab(this.app, this));
|
|
|
|
// Ribbon icon to activate chat view
|
|
this.addRibbonIcon("message-square", "Open Hermes Chat", () => {
|
|
this.activateView();
|
|
});
|
|
|
|
// Status bar item showing model name
|
|
if (this.settings.showModelInfo) {
|
|
this.statusBarItem = this.addStatusBarItem();
|
|
this.statusBarItem.setText("Hermes: connecting...");
|
|
}
|
|
|
|
// Initialize API client from settings
|
|
const gatewayConfig: GatewayConfig = {
|
|
baseUrl: this.settings.gatewayUrl,
|
|
apiKey: this.settings.apiKey,
|
|
model: "hermes-agent",
|
|
};
|
|
this.apiClient = new ApiClient(gatewayConfig);
|
|
this.contextBridge = new ContextBridge(this.app);
|
|
|
|
// Command: Open Hermes Chat
|
|
this.addCommand({
|
|
id: "open-hermes-chat",
|
|
name: "Open Hermes Chat",
|
|
callback: () => {
|
|
this.activateView();
|
|
},
|
|
});
|
|
|
|
// Command: Send Current Note as Context
|
|
this.addCommand({
|
|
id: "send-note-as-context",
|
|
name: "Send Current Note as Context",
|
|
callback: () => {
|
|
const context = this.contextBridge?.getActiveNoteContext();
|
|
if (context) {
|
|
this.activateView();
|
|
// Note: actual context injection happens in chat-view's handleSend via the contextBridge
|
|
console.log(`Context ready: vault=${context.vault}, path=${context.path}`);
|
|
} else {
|
|
console.log("No active note to send as context");
|
|
}
|
|
},
|
|
});
|
|
|
|
// Check gateway connectivity
|
|
if (this.statusBarItem && this.apiClient) {
|
|
this.statusBarItem.setText("Hermes: checking...");
|
|
this.apiClient.healthCheck().then((healthy) => {
|
|
if (this.statusBarItem) {
|
|
this.statusBarItem.setText(healthy ? "Hermes: connected" : "Hermes: unreachable");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async onunload() {
|
|
// Detach the view when plugin is unloaded
|
|
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
|
|
}
|
|
|
|
async activateView() {
|
|
const { workspace } = this.app;
|
|
|
|
// Check if view already exists
|
|
const existing = workspace.getLeavesOfType(VIEW_TYPE);
|
|
if (existing.length > 0) {
|
|
workspace.revealLeaf(existing[0]);
|
|
return;
|
|
}
|
|
|
|
// Create new leaf in right sidebar
|
|
const leaf = workspace.getRightLeaf(false);
|
|
if (leaf) {
|
|
await leaf.setViewState({ type: VIEW_TYPE, active: true });
|
|
workspace.revealLeaf(leaf);
|
|
}
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
this.syncContextToViews();
|
|
}
|
|
|
|
/** Push enableContextInjection setting to any open ChatViews. */
|
|
private syncContextToViews(): void {
|
|
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE);
|
|
for (const leaf of leaves) {
|
|
if (leaf.view instanceof ChatView) {
|
|
leaf.view.setContextActive(this.settings.enableContextInjection);
|
|
}
|
|
}
|
|
}
|
|
}
|