obsidian-promptfire/src/source-modal.ts
Luca G. Oelfke cc2d196911
feat: rename plugin from "Claude Context" to "Promptfire"
Rename before community plugin submission (plugin IDs are permanent).
Updates plugin ID, display name, package name, all class names,
CSS prefix (cc- → pf-), default folders (_claude → _context),
built-in target ID, and all documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 12:42:16 +01:00

358 lines
9 KiB
TypeScript

import { App, Modal, Notice, Setting } from 'obsidian';
import PromptfirePlugin from './main';
import {
ContextSource,
SourceType,
SourcePosition,
FreetextConfig,
FileConfig,
ShellConfig,
generateSourceId,
SourceRegistry,
} from './sources';
export class SourceModal extends Modal {
plugin: PromptfirePlugin;
sourceType: SourceType;
existingSource: ContextSource | null;
onSave: () => void;
// Form state
name: string = '';
enabled: boolean = true;
position: SourcePosition = 'prefix';
// Freetext config
freetextContent: string = '';
// File config
filePath: string = '';
recursive: boolean = false;
filePattern: string = '*';
// Shell config
shellCommand: string = '';
shellArgs: string = '';
shellCwd: string = '';
shellTimeout: number = 5000;
constructor(
app: App,
plugin: PromptfirePlugin,
sourceType: SourceType,
existingSource: ContextSource | null,
onSave: () => void
) {
super(app);
this.plugin = plugin;
this.sourceType = sourceType;
this.existingSource = existingSource;
this.onSave = onSave;
// Load existing source data if editing
if (existingSource) {
this.name = existingSource.name;
this.enabled = existingSource.enabled;
this.position = existingSource.position;
switch (existingSource.type) {
case 'freetext':
this.freetextContent = (existingSource.config as FreetextConfig).content;
break;
case 'file':
case 'folder':
const fileConfig = existingSource.config as FileConfig;
this.filePath = fileConfig.path;
this.recursive = fileConfig.recursive || false;
this.filePattern = fileConfig.pattern || '*';
break;
case 'shell':
const shellConfig = existingSource.config as ShellConfig;
this.shellCommand = shellConfig.command;
this.shellArgs = shellConfig.args.join(' ');
this.shellCwd = shellConfig.cwd || '';
this.shellTimeout = shellConfig.timeout || 5000;
break;
}
}
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('promptfire-source-modal');
const title = this.existingSource ? 'Edit Source' : 'Add Source';
const typeLabel = this.getTypeLabel();
contentEl.createEl('h2', { text: `${title}: ${typeLabel}` });
// Common fields
new Setting(contentEl)
.setName('Name')
.setDesc('Display name for this source')
.addText(text => text
.setPlaceholder('My Context')
.setValue(this.name)
.onChange(v => this.name = v));
new Setting(contentEl)
.setName('Position')
.setDesc('Where to insert this content')
.addDropdown(dropdown => dropdown
.addOption('prefix', 'Prefix (before vault content)')
.addOption('suffix', 'Suffix (after vault content)')
.setValue(this.position)
.onChange(v => this.position = v as SourcePosition));
new Setting(contentEl)
.setName('Enabled')
.addToggle(toggle => toggle
.setValue(this.enabled)
.onChange(v => this.enabled = v));
// Type-specific fields
contentEl.createEl('hr');
switch (this.sourceType) {
case 'freetext':
this.renderFreetextFields(contentEl);
break;
case 'file':
case 'folder':
this.renderFileFields(contentEl);
break;
case 'shell':
this.renderShellFields(contentEl);
break;
}
// Buttons
contentEl.createEl('hr');
const buttonContainer = contentEl.createDiv({ cls: 'button-container' });
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.gap = '10px';
const testBtn = buttonContainer.createEl('button', { text: 'Test' });
testBtn.addEventListener('click', () => this.testSource());
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => this.close());
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'mod-cta' });
saveBtn.addEventListener('click', () => this.save());
}
private getTypeLabel(): string {
switch (this.sourceType) {
case 'freetext': return 'Freetext';
case 'file': return 'File';
case 'folder': return 'Folder';
case 'shell': return 'Shell Command';
default: return 'Unknown';
}
}
private renderFreetextFields(container: HTMLElement) {
new Setting(container)
.setName('Content')
.setDesc('Text content to include')
.addTextArea(text => {
text.setPlaceholder('Enter your context text here...')
.setValue(this.freetextContent)
.onChange(v => this.freetextContent = v);
text.inputEl.rows = 8;
text.inputEl.style.width = '100%';
});
}
private renderFileFields(container: HTMLElement) {
new Setting(container)
.setName('Path')
.setDesc('Absolute path to file or folder')
.addText(text => text
.setPlaceholder('/home/user/project/README.md')
.setValue(this.filePath)
.onChange(v => this.filePath = v));
new Setting(container)
.setName('Recursive')
.setDesc('Read folder contents recursively')
.addToggle(toggle => toggle
.setValue(this.recursive)
.onChange(v => this.recursive = v));
new Setting(container)
.setName('Pattern')
.setDesc('Glob pattern for file matching (e.g. *.md, *.ts)')
.addText(text => text
.setPlaceholder('*')
.setValue(this.filePattern)
.onChange(v => this.filePattern = v));
}
private renderShellFields(container: HTMLElement) {
new Setting(container)
.setName('Command')
.setDesc('Command to execute (e.g. git, ls, cat)')
.addText(text => text
.setPlaceholder('git')
.setValue(this.shellCommand)
.onChange(v => this.shellCommand = v));
new Setting(container)
.setName('Arguments')
.setDesc('Space-separated arguments')
.addText(text => text
.setPlaceholder('log --oneline -10')
.setValue(this.shellArgs)
.onChange(v => this.shellArgs = v));
new Setting(container)
.setName('Working directory')
.setDesc('Optional working directory for the command')
.addText(text => text
.setPlaceholder('/home/user/project')
.setValue(this.shellCwd)
.onChange(v => this.shellCwd = v));
new Setting(container)
.setName('Timeout (ms)')
.setDesc('Maximum execution time')
.addText(text => text
.setPlaceholder('5000')
.setValue(String(this.shellTimeout))
.onChange(v => {
const num = parseInt(v, 10);
if (!isNaN(num) && num > 0) {
this.shellTimeout = num;
}
}));
}
private buildSource(): ContextSource {
let config;
let type: SourceType = this.sourceType;
switch (this.sourceType) {
case 'freetext':
config = { content: this.freetextContent } as FreetextConfig;
break;
case 'file':
case 'folder':
config = {
path: this.filePath,
recursive: this.recursive,
pattern: this.filePattern,
} as FileConfig;
type = this.recursive ? 'folder' : 'file';
break;
case 'shell':
config = {
command: this.shellCommand,
args: this.shellArgs.split(/\s+/).filter(s => s),
cwd: this.shellCwd || undefined,
timeout: this.shellTimeout,
} as ShellConfig;
break;
default:
throw new Error(`Unknown source type: ${this.sourceType}`);
}
return {
id: this.existingSource?.id || generateSourceId(),
type,
name: this.name || this.getDefaultName(),
enabled: this.enabled,
position: this.position,
config,
};
}
private getDefaultName(): string {
switch (this.sourceType) {
case 'freetext':
return 'Freetext';
case 'file':
case 'folder':
return this.filePath.split('/').pop() || 'File';
case 'shell':
return `${this.shellCommand} ${this.shellArgs}`.trim();
default:
return 'Source';
}
}
private async testSource() {
const source = this.buildSource();
const registry = new SourceRegistry();
try {
const result = await registry.resolveSource(source);
if (result.error) {
new Notice(`Error: ${result.error}`);
} else {
const preview = result.content.substring(0, 500);
const suffix = result.content.length > 500 ? '\n\n... (truncated)' : '';
new Notice(`Success (${result.content.length} chars):\n${preview}${suffix}`, 10000);
}
} catch (error) {
new Notice(`Test failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
private async save() {
if (!this.validate()) {
return;
}
const source = this.buildSource();
if (this.existingSource) {
// Update existing
const index = this.plugin.settings.sources.findIndex(s => s.id === this.existingSource!.id);
if (index >= 0) {
this.plugin.settings.sources[index] = source;
}
} else {
// Add new
this.plugin.settings.sources.push(source);
}
await this.plugin.saveSettings();
this.onSave();
this.close();
}
private validate(): boolean {
switch (this.sourceType) {
case 'freetext':
if (!this.freetextContent.trim()) {
new Notice('Content is required');
return false;
}
break;
case 'file':
case 'folder':
if (!this.filePath.trim()) {
new Notice('Path is required');
return false;
}
break;
case 'shell':
if (!this.shellCommand.trim()) {
new Notice('Command is required');
return false;
}
break;
}
return true;
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}