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>
358 lines
9 KiB
TypeScript
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();
|
|
}
|
|
}
|