import { App, PluginSettingTab, Setting } from "obsidian"; import type HermesChatPlugin from "./main"; import type { Message } from "./types"; export interface HermesChatSettings { gatewayUrl: string; apiKey: string; showModelInfo: boolean; enableContextInjection: boolean; chatFontSize: number; customCommands: { trigger: string; description: string; prompt: string }[]; chatMessages: Message[]; chatSessionId: string; } /** Built-in slash commands shipped with the plugin. */ export const BUILTIN_COMMANDS: { trigger: string; description: string; prompt: string }[] = [ { trigger: "/threads", description: "Crawl the campaign wiki for unresolved plot threads", prompt: "Crawl the campaign wiki for unresolved plot threads relevant to the next session. Cross-reference current party location, active PCs, and open story hooks. Rank by narrative urgency and suggest specific integration points.", }, { trigger: "/npc", description: "Generate a detailed NPC with full backstory, beliefs, and ties", prompt: "Generate a detailed D&D NPC with full backstory, core beliefs, motivations, ties, a secret, and a voice note. Use the npc-generator skill in detailed mode. Context:", }, { trigger: "/npc-fast", description: "Quick NPC — name, race, age, description only (mid-session)", prompt: "Generate a quick D&D NPC — just name, race, age range, and 3 bullet descriptions. Use the npc-generator skill in fast mode. Context:", }, { trigger: "/location", description: "Generate a detailed location with atmosphere, features, and wiki ties", prompt: "Generate a detailed D&D location with atmosphere, notable features, potential encounters, and wiki connections. Use the location-generator skill in detailed mode. Context:", }, { trigger: "/location-fast", description: "Quick location — name, vibe, one interesting detail (mid-session)", prompt: "Generate a quick D&D location — just name, type, 3-sentence atmosphere, and one notable detail. Use the location-generator skill in fast mode. Context:", }, { trigger: "/scene", description: "Generate atmospheric read-aloud text for a D&D scene", prompt: "Generate atmospheric read-aloud text for this D&D scene. Pull details from the campaign wiki if the location exists there. Use the scene-setter skill. Context:", }, { trigger: "/relations", description: "Map all wiki-documented relationships for an NPC", prompt: "Map all wiki-documented relationships, connections, session appearances, and faction ties for this campaign NPC. Use the relation-mapper skill. NPC:", }, { trigger: "/hook", description: "Generate quest hooks tied to existing campaign threads", prompt: "Generate 2-3 quest hooks tied to existing campaign wiki threads. Every hook must have a documented source. Use the hook-generator skill. Context:", }, { trigger: "/encounter", description: "Build a themed encounter with creatures, hazards, and wiki ties", prompt: "Build a thematically appropriate D&D encounter with creatures, environmental features, and a wiki connection. Include multiple resolution paths. Use the encounter-builder skill. Context:", }, { trigger: "/loot", description: "Generate themed treasure — coins, items, and a signature piece with a story hook", prompt: "Generate D&D treasure scaled to party level and themed to the context. Include coin, 1-2 items, and one signature item with a story hook. Use the loot-generator skill. Context:", }, { trigger: "/lint", description: "Scan the campaign wiki for lore inconsistencies and stale data", prompt: "Scan the campaign wiki for lore inconsistencies — temporal contradictions, spatial impossibilities, one-way relationships, factual conflicts, orphaned entities, stale records, and dead plot threads. Flag issues for GM resolution. Use the lore-lint skill. Scope:", }, { trigger: "/prep-session", description: "Prep the next D&D session", prompt: "Run the full session-prep pipeline for the next SKT session: scan recent session notes, identify loose threads, generate a session guide with modular flow-chart sections.", }, { trigger: "/sync-content", description: "Sync Content Sink notes to vault", prompt: "/run content-sink-sync", }, { trigger: "/lint-wiki", description: "[Legacy] Scan the campaign wiki (use /lint instead)", prompt: "Scan the campaign wiki for lore inconsistencies and stale data. Use the lore-lint skill. Scope:", }, ]; export const DEFAULT_SETTINGS: HermesChatSettings = { gatewayUrl: "http://192.168.1.12:8642/v1", apiKey: "0f224b188b52e8728d135ec339f8db8dfbf82d89a60021dcfe20d91c6174cf9b", showModelInfo: true, enableContextInjection: true, chatFontSize: 14, customCommands: [], chatMessages: [], chatSessionId: "", }; export class HermesChatSettingTab extends PluginSettingTab { plugin: HermesChatPlugin; constructor(app: App, plugin: HermesChatPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h2", { text: "Hermes Chat Settings" }); new Setting(containerEl) .setName("Gateway URL") .setDesc("Base URL of your Hermes Gateway API server (e.g. http://192.168.1.12:8642/v1)") .addText((text) => text .setPlaceholder("http://192.168.1.12:8642/v1") .setValue(this.plugin.settings.gatewayUrl) .onChange(async (value) => { this.plugin.settings.gatewayUrl = value; await this.plugin.saveData(this.plugin.settings); }) ); new Setting(containerEl) .setName("API Key") .setDesc("API key for authenticating with the Hermes Gateway") .addText((text) => text .setPlaceholder("Enter your API key") .setValue(this.plugin.settings.apiKey) .onChange(async (value) => { this.plugin.settings.apiKey = value; await this.plugin.saveData(this.plugin.settings); }) ); new Setting(containerEl) .setName("Show model info") .setDesc("Display the current model name in the status bar") .addToggle((toggle) => toggle .setValue(this.plugin.settings.showModelInfo) .onChange(async (value) => { this.plugin.settings.showModelInfo = value; await this.plugin.saveData(this.plugin.settings); }) ); new Setting(containerEl) .setName("Enable context injection") .setDesc("Automatically send the active note as context with each message") .addToggle((toggle) => toggle .setValue(this.plugin.settings.enableContextInjection) .onChange(async (value) => { this.plugin.settings.enableContextInjection = value; await this.plugin.saveData(this.plugin.settings); }) ); 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.saveSettings(); }) ); // --- Slash commands --- containerEl.createEl("h3", { text: "Slash Commands" }); containerEl.createEl("p", { text: "Type / in the chat input to see available commands. Built-in: /prep-session, /sync-content, /lint-wiki. Add custom commands below as JSON.", }); const commandsDesc = containerEl.createDiv("setting-item-description"); commandsDesc.setText( 'Format: [{"trigger":"/my-cmd","description":"What it does","prompt":"Full prompt text..."}]' ); const textarea = containerEl.createEl("textarea"); textarea.setAttribute("rows", "6"); textarea.style.width = "100%"; textarea.style.fontFamily = "monospace"; textarea.style.fontSize = "12px"; textarea.value = JSON.stringify(this.plugin.settings.customCommands, null, 2); textarea.addEventListener("change", async () => { try { const parsed = JSON.parse(textarea.value); if (Array.isArray(parsed)) { this.plugin.settings.customCommands = parsed; await this.plugin.saveData(this.plugin.settings); } } catch { // Invalid JSON — don't save, user is still typing } }); } }