import { App, Notice, PluginSettingTab, Setting } from 'obsidian'; import ClaudeContextPlugin from './main'; import { ContextSource, getSourceIcon, SourceRegistry } from './sources'; import { SourceModal } from './source-modal'; import { PromptTemplate, STARTER_TEMPLATES } from './templates'; import { TemplateModal, TemplateImportExportModal } from './template-modal'; import { HistorySettings, DEFAULT_HISTORY_SETTINGS } from './history'; export interface ClaudeContextSettings { contextFolder: string; separator: string; includeFilenames: boolean; showPreview: boolean; includeActiveNote: boolean; excludedFiles: string[]; sources: ContextSource[]; showSourceLabels: boolean; promptTemplates: PromptTemplate[]; defaultTemplateId: string | null; history: HistorySettings; } export const DEFAULT_SETTINGS: ClaudeContextSettings = { contextFolder: '_claude', separator: '---', includeFilenames: true, showPreview: false, includeActiveNote: false, excludedFiles: [], sources: [], showSourceLabels: true, promptTemplates: [], defaultTemplateId: null, history: DEFAULT_HISTORY_SETTINGS, }; export class ClaudeContextSettingTab extends PluginSettingTab { plugin: ClaudeContextPlugin; constructor(app: App, plugin: ClaudeContextPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); new Setting(containerEl) .setName('Context folder') .setDesc('Folder containing your context files') .addText(text => text .setPlaceholder('_claude') .setValue(this.plugin.settings.contextFolder) .onChange(async (value) => { this.plugin.settings.contextFolder = value; await this.plugin.saveSettings(); })); new Setting(containerEl) .setName('Separator') .setDesc('Text between files (e.g. "---" or "***")') .addText(text => text .setPlaceholder('---') .setValue(this.plugin.settings.separator) .onChange(async (value) => { this.plugin.settings.separator = value; await this.plugin.saveSettings(); })); new Setting(containerEl) .setName('Include filenames') .setDesc('Add "# === filename.md ===" headers before each file') .addToggle(toggle => toggle .setValue(this.plugin.settings.includeFilenames) .onChange(async (value) => { this.plugin.settings.includeFilenames = value; await this.plugin.saveSettings(); })); new Setting(containerEl) .setName('Show preview') .setDesc('Show preview modal before copying') .addToggle(toggle => toggle .setValue(this.plugin.settings.showPreview) .onChange(async (value) => { this.plugin.settings.showPreview = value; await this.plugin.saveSettings(); })); new Setting(containerEl) .setName('Include active note') .setDesc('Append currently open note to context') .addToggle(toggle => toggle .setValue(this.plugin.settings.includeActiveNote) .onChange(async (value) => { this.plugin.settings.includeActiveNote = value; await this.plugin.saveSettings(); })); new Setting(containerEl) .setName('Excluded files') .setDesc('Comma-separated filenames to exclude (e.g. "examples.md, drafts.md")') .addText(text => text .setPlaceholder('file1.md, file2.md') .setValue(this.plugin.settings.excludedFiles.join(', ')) .onChange(async (value) => { this.plugin.settings.excludedFiles = value .split(',') .map(s => s.trim()) .filter(s => s.length > 0); await this.plugin.saveSettings(); })); // === CONTEXT SOURCES SECTION === containerEl.createEl('h3', { text: 'Context Sources' }); const sourcesDesc = containerEl.createEl('p', { text: 'Add additional context sources like freetext, external files, or shell command output.', cls: 'setting-item-description' }); sourcesDesc.style.marginBottom = '10px'; const buttonContainer = containerEl.createDiv({ cls: 'sources-button-container' }); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '8px'; buttonContainer.style.marginBottom = '15px'; const addFreetextBtn = buttonContainer.createEl('button', { text: '+ Freetext' }); addFreetextBtn.addEventListener('click', () => { new SourceModal(this.app, this.plugin, 'freetext', null, () => this.display()).open(); }); const addFileBtn = buttonContainer.createEl('button', { text: '+ File' }); addFileBtn.addEventListener('click', () => { new SourceModal(this.app, this.plugin, 'file', null, () => this.display()).open(); }); const addShellBtn = buttonContainer.createEl('button', { text: '+ Shell' }); addShellBtn.addEventListener('click', () => { new SourceModal(this.app, this.plugin, 'shell', null, () => this.display()).open(); }); // Sources list const sourcesContainer = containerEl.createDiv({ cls: 'sources-list-container' }); this.renderSourcesList(sourcesContainer); new Setting(containerEl) .setName('Show source labels') .setDesc('Add position and name labels to source output') .addToggle(toggle => toggle .setValue(this.plugin.settings.showSourceLabels) .onChange(async (value) => { this.plugin.settings.showSourceLabels = value; await this.plugin.saveSettings(); })); // === PROMPT TEMPLATES SECTION === containerEl.createEl('h3', { text: 'Prompt Templates' }); const templatesDesc = containerEl.createEl('p', { text: 'Create reusable prompt templates that wrap around your context.', cls: 'setting-item-description' }); templatesDesc.style.marginBottom = '10px'; const templateButtonContainer = containerEl.createDiv({ cls: 'templates-button-container' }); templateButtonContainer.style.display = 'flex'; templateButtonContainer.style.gap = '8px'; templateButtonContainer.style.marginBottom = '15px'; const addTemplateBtn = templateButtonContainer.createEl('button', { text: '+ New Template' }); addTemplateBtn.addEventListener('click', () => { new TemplateModal(this.app, this.plugin, null, () => this.display()).open(); }); const importBtn = templateButtonContainer.createEl('button', { text: 'Import' }); importBtn.addEventListener('click', () => { new TemplateImportExportModal(this.app, this.plugin, 'import', () => this.display()).open(); }); const exportBtn = templateButtonContainer.createEl('button', { text: 'Export' }); exportBtn.addEventListener('click', () => { new TemplateImportExportModal(this.app, this.plugin, 'export', () => {}).open(); }); // Starter templates const hasStarterTemplates = this.plugin.settings.promptTemplates.some(t => t.isBuiltin); if (!hasStarterTemplates) { const starterContainer = containerEl.createDiv(); starterContainer.style.padding = '10px'; starterContainer.style.backgroundColor = 'var(--background-secondary)'; starterContainer.style.borderRadius = '4px'; starterContainer.style.marginBottom = '15px'; const starterText = starterContainer.createEl('p', { text: `Add ${STARTER_TEMPLATES.length} starter templates to get started quickly.` }); starterText.style.margin = '0 0 10px 0'; const addStarterBtn = starterContainer.createEl('button', { text: 'Add Starter Templates' }); addStarterBtn.addEventListener('click', async () => { this.plugin.settings.promptTemplates.push(...STARTER_TEMPLATES); await this.plugin.saveSettings(); new Notice(`Added ${STARTER_TEMPLATES.length} starter templates`); this.display(); }); } // Templates list const templatesContainer = containerEl.createDiv({ cls: 'templates-list-container' }); this.renderTemplatesList(templatesContainer); // Default template setting new Setting(containerEl) .setName('Default template') .setDesc('Template to use by default when copying context') .addDropdown(dropdown => { dropdown.addOption('', 'None (plain context)'); for (const template of this.plugin.settings.promptTemplates) { dropdown.addOption(template.id, template.name); } dropdown.setValue(this.plugin.settings.defaultTemplateId || ''); dropdown.onChange(async (value) => { this.plugin.settings.defaultTemplateId = value || null; await this.plugin.saveSettings(); }); }); // === HISTORY SECTION === containerEl.createEl('h3', { text: 'Context History' }); const historyDesc = containerEl.createEl('p', { text: 'Track and compare previously generated contexts. Useful for iterative LLM workflows.', cls: 'setting-item-description' }); historyDesc.style.marginBottom = '10px'; new Setting(containerEl) .setName('Enable history') .setDesc('Save generated contexts for later review and comparison') .addToggle(toggle => toggle .setValue(this.plugin.settings.history.enabled) .onChange(async (value) => { this.plugin.settings.history.enabled = value; await this.plugin.saveSettings(); this.display(); })); if (this.plugin.settings.history.enabled) { new Setting(containerEl) .setName('Storage folder') .setDesc('Folder in your vault where history entries are stored') .addText(text => text .setPlaceholder('.context-history') .setValue(this.plugin.settings.history.storageFolder) .onChange(async (value) => { this.plugin.settings.history.storageFolder = value || '.context-history'; await this.plugin.saveSettings(); })); new Setting(containerEl) .setName('Maximum entries') .setDesc('Oldest entries will be deleted when limit is exceeded') .addText(text => text .setPlaceholder('50') .setValue(String(this.plugin.settings.history.maxEntries)) .onChange(async (value) => { const num = parseInt(value, 10); if (!isNaN(num) && num > 0) { this.plugin.settings.history.maxEntries = num; await this.plugin.saveSettings(); } })); new Setting(containerEl) .setName('Auto-cleanup (days)') .setDesc('Delete entries older than this many days (0 = disabled)') .addText(text => text .setPlaceholder('30') .setValue(String(this.plugin.settings.history.autoCleanupDays)) .onChange(async (value) => { const num = parseInt(value, 10); if (!isNaN(num) && num >= 0) { this.plugin.settings.history.autoCleanupDays = num; await this.plugin.saveSettings(); } })); // History actions const historyActions = containerEl.createDiv(); historyActions.style.display = 'flex'; historyActions.style.gap = '8px'; historyActions.style.marginTop = '10px'; const viewHistoryBtn = historyActions.createEl('button', { text: 'View History' }); viewHistoryBtn.addEventListener('click', () => { this.plugin.openHistory(); }); const cleanupBtn = historyActions.createEl('button', { text: 'Run Cleanup Now' }); cleanupBtn.addEventListener('click', async () => { const deleted = await this.plugin.runHistoryCleanup(); new Notice(`Cleaned up ${deleted} old entries`); }); } } private renderSourcesList(container: HTMLElement) { container.empty(); if (this.plugin.settings.sources.length === 0) { const emptyMsg = container.createEl('p', { text: 'No sources configured yet.', cls: 'setting-item-description' }); emptyMsg.style.fontStyle = 'italic'; return; } const list = container.createDiv({ cls: 'sources-list' }); list.style.border = '1px solid var(--background-modifier-border)'; list.style.borderRadius = '4px'; list.style.marginBottom = '15px'; for (const source of this.plugin.settings.sources) { const row = list.createDiv({ cls: 'source-row' }); row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.padding = '8px 12px'; row.style.borderBottom = '1px solid var(--background-modifier-border)'; row.style.gap = '10px'; // Icon const icon = row.createEl('span', { text: getSourceIcon(source.type) }); icon.style.fontSize = '16px'; // Name const name = row.createEl('span', { text: source.name }); name.style.flex = '1'; name.style.fontWeight = '500'; // Position badge const position = row.createEl('span', { text: source.position }); position.style.padding = '2px 6px'; position.style.borderRadius = '3px'; position.style.fontSize = '11px'; position.style.backgroundColor = 'var(--background-modifier-hover)'; // Enabled toggle const toggleContainer = row.createDiv(); const toggle = toggleContainer.createEl('input', { type: 'checkbox' }); toggle.checked = source.enabled; toggle.addEventListener('change', async () => { source.enabled = toggle.checked; await this.plugin.saveSettings(); }); // Edit button const editBtn = row.createEl('button', { text: '✎' }); editBtn.style.padding = '2px 8px'; editBtn.addEventListener('click', () => { new SourceModal(this.app, this.plugin, source.type, source, () => this.display()).open(); }); // Delete button const deleteBtn = row.createEl('button', { text: '✕' }); deleteBtn.style.padding = '2px 8px'; deleteBtn.addEventListener('click', async () => { this.plugin.settings.sources = this.plugin.settings.sources.filter(s => s.id !== source.id); await this.plugin.saveSettings(); this.display(); }); // Test button const testBtn = row.createEl('button', { text: '▶' }); testBtn.title = 'Test source'; testBtn.style.padding = '2px 8px'; testBtn.addEventListener('click', async () => { const registry = new SourceRegistry(); const result = await registry.resolveSource(source); if (result.error) { new (await import('obsidian')).Notice(`Error: ${result.error}`); } else { const preview = result.content.substring(0, 200) + (result.content.length > 200 ? '...' : ''); new (await import('obsidian')).Notice(`Success (${result.content.length} chars):\n${preview}`); } }); } // Remove bottom border from last item const lastRow = list.lastElementChild as HTMLElement; if (lastRow) { lastRow.style.borderBottom = 'none'; } } private renderTemplatesList(container: HTMLElement) { container.empty(); if (this.plugin.settings.promptTemplates.length === 0) { const emptyMsg = container.createEl('p', { text: 'No templates configured yet.', cls: 'setting-item-description' }); emptyMsg.style.fontStyle = 'italic'; return; } const list = container.createDiv({ cls: 'templates-list' }); list.style.border = '1px solid var(--background-modifier-border)'; list.style.borderRadius = '4px'; list.style.marginBottom = '15px'; for (const template of this.plugin.settings.promptTemplates) { const row = list.createDiv({ cls: 'template-row' }); row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.padding = '8px 12px'; row.style.borderBottom = '1px solid var(--background-modifier-border)'; row.style.gap = '10px'; // Icon const icon = row.createEl('span', { text: template.isBuiltin ? '📦' : '📝' }); icon.style.fontSize = '16px'; // Name and description const textContainer = row.createDiv(); textContainer.style.flex = '1'; const name = textContainer.createEl('span', { text: template.name }); name.style.fontWeight = '500'; if (template.description) { const desc = textContainer.createEl('div', { text: template.description }); desc.style.fontSize = '11px'; desc.style.color = 'var(--text-muted)'; } // Builtin badge if (template.isBuiltin) { const badge = row.createEl('span', { text: 'builtin' }); badge.style.padding = '2px 6px'; badge.style.borderRadius = '3px'; badge.style.fontSize = '11px'; badge.style.backgroundColor = 'var(--background-modifier-hover)'; } // Edit button (only for non-builtin) if (!template.isBuiltin) { const editBtn = row.createEl('button', { text: '✎' }); editBtn.style.padding = '2px 8px'; editBtn.addEventListener('click', () => { new TemplateModal(this.app, this.plugin, template, () => this.display()).open(); }); } // Duplicate button const duplicateBtn = row.createEl('button', { text: '⧉' }); duplicateBtn.title = 'Duplicate'; duplicateBtn.style.padding = '2px 8px'; duplicateBtn.addEventListener('click', async () => { const { generateTemplateId } = await import('./templates'); const duplicate: PromptTemplate = { id: generateTemplateId(), name: `${template.name} (copy)`, description: template.description, content: template.content, isBuiltin: false, }; this.plugin.settings.promptTemplates.push(duplicate); await this.plugin.saveSettings(); new Notice(`Duplicated template: ${template.name}`); this.display(); }); // Delete button const deleteBtn = row.createEl('button', { text: '✕' }); deleteBtn.style.padding = '2px 8px'; deleteBtn.addEventListener('click', async () => { this.plugin.settings.promptTemplates = this.plugin.settings.promptTemplates.filter( t => t.id !== template.id ); // Clear default if this was it if (this.plugin.settings.defaultTemplateId === template.id) { this.plugin.settings.defaultTemplateId = null; } await this.plugin.saveSettings(); this.display(); }); } // Remove bottom border from last item const lastRow = list.lastElementChild as HTMLElement; if (lastRow) { lastRow.style.borderBottom = 'none'; } } }