feat: add live preview pane and status bar to generator modal
Collapsible preview shows first 30 lines of generated output with real-time updates (300ms debounce). Status bar displays file count, token estimate, active target, and output format — all updating as options change. Token count warns when exceeding target limit. Clicking target scrolls to config, clicking format cycles options. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
08b8a180ca
commit
2f3574c63f
3 changed files with 304 additions and 17 deletions
246
src/generator.ts
246
src/generator.ts
|
|
@ -2,7 +2,7 @@ import { App, Modal, Notice, Setting, TFile, TFolder } from 'obsidian';
|
||||||
import ClaudeContextPlugin from './main';
|
import ClaudeContextPlugin from './main';
|
||||||
import { createFreetextSource, SourcePosition } from './sources';
|
import { createFreetextSource, SourcePosition } from './sources';
|
||||||
import { estimateTokens } from './history';
|
import { estimateTokens } from './history';
|
||||||
import { OutputTarget, getTargetIcon, formatTokenCount } from './targets';
|
import { OutputTarget, OutputFormat, getTargetIcon, formatTokenCount } from './targets';
|
||||||
|
|
||||||
interface FolderConfig {
|
interface FolderConfig {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -87,10 +87,26 @@ export class ContextGeneratorModal extends Modal {
|
||||||
selectedTemplateId: string | null = null;
|
selectedTemplateId: string | null = null;
|
||||||
selectedTargetIds: Set<string> = new Set();
|
selectedTargetIds: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Preview pane state
|
||||||
|
private previewContent: HTMLElement | null = null;
|
||||||
|
private previewLineInfo: HTMLElement | null = null;
|
||||||
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private previewOpen: boolean = false;
|
||||||
|
private static readonly PREVIEW_LINES = 30;
|
||||||
|
private static readonly DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
|
// Status bar DOM references
|
||||||
|
private statusFileCount: HTMLElement | null = null;
|
||||||
|
private statusTokens: HTMLElement | null = null;
|
||||||
|
private statusTarget: HTMLElement | null = null;
|
||||||
|
private statusFormat: HTMLElement | null = null;
|
||||||
|
private targetsHeadingEl: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(app: App, plugin: ClaudeContextPlugin) {
|
constructor(app: App, plugin: ClaudeContextPlugin) {
|
||||||
super(app);
|
super(app);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
this.config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
||||||
|
this.previewOpen = this.plugin.settings.generatorPreviewOpen ?? false;
|
||||||
this.config.folders = this.scanVaultStructure().map(name => ({ name, purpose: '' }));
|
this.config.folders = this.scanVaultStructure().map(name => ({ name, purpose: '' }));
|
||||||
// Initialize with default template
|
// Initialize with default template
|
||||||
this.selectedTemplateId = this.plugin.settings.defaultTemplateId;
|
this.selectedTemplateId = this.plugin.settings.defaultTemplateId;
|
||||||
|
|
@ -108,6 +124,39 @@ export class ContextGeneratorModal extends Modal {
|
||||||
|
|
||||||
contentEl.createEl('h2', { text: 'Context Generator' });
|
contentEl.createEl('h2', { text: 'Context Generator' });
|
||||||
|
|
||||||
|
// === STATUS BAR ===
|
||||||
|
const statusBar = contentEl.createDiv({ cls: 'cc-gen-statusbar' });
|
||||||
|
|
||||||
|
this.statusFileCount = statusBar.createEl('span', { cls: 'cc-statusbar-item' });
|
||||||
|
statusBar.createEl('span', { cls: 'cc-statusbar-sep', text: '\u00B7' });
|
||||||
|
this.statusTokens = statusBar.createEl('span', { cls: 'cc-statusbar-item cc-statusbar-tokens' });
|
||||||
|
statusBar.createEl('span', { cls: 'cc-statusbar-sep', text: '\u00B7' });
|
||||||
|
this.statusTarget = statusBar.createEl('span', { cls: 'cc-statusbar-item cc-statusbar-clickable' });
|
||||||
|
statusBar.createEl('span', { cls: 'cc-statusbar-sep', text: '\u00B7' });
|
||||||
|
this.statusFormat = statusBar.createEl('span', { cls: 'cc-statusbar-item cc-statusbar-clickable' });
|
||||||
|
|
||||||
|
// Click target label → scroll to Output Targets section
|
||||||
|
this.statusTarget.addEventListener('click', () => {
|
||||||
|
if (this.targetsHeadingEl) {
|
||||||
|
this.targetsHeadingEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click format label → cycle through formats on primary target
|
||||||
|
this.statusFormat.addEventListener('click', () => {
|
||||||
|
const primary = this.getPrimaryTarget();
|
||||||
|
if (!primary) return;
|
||||||
|
const formats: OutputFormat[] = ['markdown', 'xml', 'plain'];
|
||||||
|
const idx = formats.indexOf(primary.format);
|
||||||
|
primary.format = formats[(idx + 1) % formats.length] ?? 'markdown';
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
this.updateStatusBar();
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial status bar render
|
||||||
|
this.updateStatusBar();
|
||||||
|
|
||||||
// === TWO-ZONE GRID LAYOUT ===
|
// === TWO-ZONE GRID LAYOUT ===
|
||||||
const layout = contentEl.createDiv({ cls: 'cc-gen-layout' });
|
const layout = contentEl.createDiv({ cls: 'cc-gen-layout' });
|
||||||
const selectionZone = layout.createDiv({ cls: 'cc-gen-zone cc-gen-selection' });
|
const selectionZone = layout.createDiv({ cls: 'cc-gen-zone cc-gen-selection' });
|
||||||
|
|
@ -124,31 +173,31 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.setName('conventions.md')
|
.setName('conventions.md')
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.config.generateFiles.conventions)
|
.setValue(this.config.generateFiles.conventions)
|
||||||
.onChange(v => this.config.generateFiles.conventions = v));
|
.onChange(v => { this.config.generateFiles.conventions = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(selectionZone)
|
new Setting(selectionZone)
|
||||||
.setName('structure.md')
|
.setName('structure.md')
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.config.generateFiles.structure)
|
.setValue(this.config.generateFiles.structure)
|
||||||
.onChange(v => this.config.generateFiles.structure = v));
|
.onChange(v => { this.config.generateFiles.structure = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(selectionZone)
|
new Setting(selectionZone)
|
||||||
.setName('workflows.md')
|
.setName('workflows.md')
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.config.generateFiles.workflows)
|
.setValue(this.config.generateFiles.workflows)
|
||||||
.onChange(v => this.config.generateFiles.workflows = v));
|
.onChange(v => { this.config.generateFiles.workflows = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(selectionZone)
|
new Setting(selectionZone)
|
||||||
.setName('templates.md')
|
.setName('templates.md')
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.config.generateFiles.templates)
|
.setValue(this.config.generateFiles.templates)
|
||||||
.onChange(v => this.config.generateFiles.templates = v));
|
.onChange(v => { this.config.generateFiles.templates = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(selectionZone)
|
new Setting(selectionZone)
|
||||||
.setName('examples.md')
|
.setName('examples.md')
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.config.generateFiles.examples)
|
.setValue(this.config.generateFiles.examples)
|
||||||
.onChange(v => this.config.generateFiles.examples = v));
|
.onChange(v => { this.config.generateFiles.examples = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
// === FOLDER STRUCTURE ===
|
// === FOLDER STRUCTURE ===
|
||||||
selectionZone.createEl('h3', { text: 'Folder structure' });
|
selectionZone.createEl('h3', { text: 'Folder structure' });
|
||||||
|
|
@ -170,7 +219,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addOption('flat', 'Flat (#tag)')
|
.addOption('flat', 'Flat (#tag)')
|
||||||
.addOption('none', 'No tags')
|
.addOption('none', 'No tags')
|
||||||
.setValue(this.config.tagsStyle)
|
.setValue(this.config.tagsStyle)
|
||||||
.onChange(v => this.config.tagsStyle = v));
|
.onChange(v => { this.config.tagsStyle = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(selectionZone)
|
new Setting(selectionZone)
|
||||||
.setName('Predefined tags')
|
.setName('Predefined tags')
|
||||||
|
|
@ -180,6 +229,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.setValue(this.config.customTags.join(', '))
|
.setValue(this.config.customTags.join(', '))
|
||||||
.onChange(v => {
|
.onChange(v => {
|
||||||
this.config.customTags = v.split(',').map(s => s.trim()).filter(s => s);
|
this.config.customTags = v.split(',').map(s => s.trim()).filter(s => s);
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// === FRONTMATTER SECTION ===
|
// === FRONTMATTER SECTION ===
|
||||||
|
|
@ -192,6 +242,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.setValue(this.config.frontmatterFields.join(', '))
|
.setValue(this.config.frontmatterFields.join(', '))
|
||||||
.onChange(v => {
|
.onChange(v => {
|
||||||
this.config.frontmatterFields = v.split(',').map(s => s.trim()).filter(s => s);
|
this.config.frontmatterFields = v.split(',').map(s => s.trim()).filter(s => s);
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// === NOTE TEMPLATES ===
|
// === NOTE TEMPLATES ===
|
||||||
|
|
@ -229,7 +280,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addTextArea(text => {
|
.addTextArea(text => {
|
||||||
text.setPlaceholder('e.g. Personal Zettelkasten for development and knowledge management')
|
text.setPlaceholder('e.g. Personal Zettelkasten for development and knowledge management')
|
||||||
.setValue(this.config.vaultDescription)
|
.setValue(this.config.vaultDescription)
|
||||||
.onChange(v => this.config.vaultDescription = v);
|
.onChange(v => { this.config.vaultDescription = v; this.schedulePreviewUpdate(); });
|
||||||
text.inputEl.rows = 2;
|
text.inputEl.rows = 2;
|
||||||
text.inputEl.style.width = '100%';
|
text.inputEl.style.width = '100%';
|
||||||
});
|
});
|
||||||
|
|
@ -240,7 +291,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addOption('english', 'English')
|
.addOption('english', 'English')
|
||||||
.addOption('german', 'Deutsch')
|
.addOption('german', 'Deutsch')
|
||||||
.setValue(this.config.language)
|
.setValue(this.config.language)
|
||||||
.onChange(v => this.config.language = v));
|
.onChange(v => { this.config.language = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
// === FORMATTING ===
|
// === FORMATTING ===
|
||||||
configZone.createEl('h3', { text: 'Formatting' });
|
configZone.createEl('h3', { text: 'Formatting' });
|
||||||
|
|
@ -253,7 +304,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addOption('camelCase', 'camelCase')
|
.addOption('camelCase', 'camelCase')
|
||||||
.addOption('free', 'Free / no convention')
|
.addOption('free', 'Free / no convention')
|
||||||
.setValue(this.config.fileNaming)
|
.setValue(this.config.fileNaming)
|
||||||
.onChange(v => this.config.fileNaming = v));
|
.onChange(v => { this.config.fileNaming = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(configZone)
|
new Setting(configZone)
|
||||||
.setName('Link style')
|
.setName('Link style')
|
||||||
|
|
@ -261,7 +312,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addOption('wikilinks', '[[Wikilinks]]')
|
.addOption('wikilinks', '[[Wikilinks]]')
|
||||||
.addOption('markdown', '[Markdown](links)')
|
.addOption('markdown', '[Markdown](links)')
|
||||||
.setValue(this.config.linkStyle)
|
.setValue(this.config.linkStyle)
|
||||||
.onChange(v => this.config.linkStyle = v));
|
.onChange(v => { this.config.linkStyle = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(configZone)
|
new Setting(configZone)
|
||||||
.setName('Heading depth')
|
.setName('Heading depth')
|
||||||
|
|
@ -271,7 +322,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addOption('h4', 'H1 - H4')
|
.addOption('h4', 'H1 - H4')
|
||||||
.addOption('h6', 'Unlimited')
|
.addOption('h6', 'Unlimited')
|
||||||
.setValue(this.config.headingDepth)
|
.setValue(this.config.headingDepth)
|
||||||
.onChange(v => this.config.headingDepth = v));
|
.onChange(v => { this.config.headingDepth = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(configZone)
|
new Setting(configZone)
|
||||||
.setName('Date format')
|
.setName('Date format')
|
||||||
|
|
@ -280,7 +331,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addOption('DD.MM.YYYY', 'DD.MM.YYYY')
|
.addOption('DD.MM.YYYY', 'DD.MM.YYYY')
|
||||||
.addOption('MM/DD/YYYY', 'MM/DD/YYYY')
|
.addOption('MM/DD/YYYY', 'MM/DD/YYYY')
|
||||||
.setValue(this.config.dateFormat)
|
.setValue(this.config.dateFormat)
|
||||||
.onChange(v => this.config.dateFormat = v));
|
.onChange(v => { this.config.dateFormat = v; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
// === RULES ===
|
// === RULES ===
|
||||||
configZone.createEl('h3', { text: 'Rules' });
|
configZone.createEl('h3', { text: 'Rules' });
|
||||||
|
|
@ -293,6 +344,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.setValue(this.config.customRules.join('\n'))
|
.setValue(this.config.customRules.join('\n'))
|
||||||
.onChange(v => {
|
.onChange(v => {
|
||||||
this.config.customRules = v.split('\n').map(s => s.trim()).filter(s => s);
|
this.config.customRules = v.split('\n').map(s => s.trim()).filter(s => s);
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
text.inputEl.rows = 3;
|
text.inputEl.rows = 3;
|
||||||
text.inputEl.style.width = '100%';
|
text.inputEl.style.width = '100%';
|
||||||
|
|
@ -305,6 +357,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.setValue(this.config.forbiddenActions.join(', '))
|
.setValue(this.config.forbiddenActions.join(', '))
|
||||||
.onChange(v => {
|
.onChange(v => {
|
||||||
this.config.forbiddenActions = v.split(',').map(s => s.trim()).filter(s => s);
|
this.config.forbiddenActions = v.split(',').map(s => s.trim()).filter(s => s);
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// === ADDITIONAL CONTEXT ===
|
// === ADDITIONAL CONTEXT ===
|
||||||
|
|
@ -316,7 +369,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addTextArea(text => {
|
.addTextArea(text => {
|
||||||
text.setPlaceholder('Enter additional context that will be included when copying...')
|
text.setPlaceholder('Enter additional context that will be included when copying...')
|
||||||
.setValue(this.temporaryFreetext)
|
.setValue(this.temporaryFreetext)
|
||||||
.onChange(v => this.temporaryFreetext = v);
|
.onChange(v => { this.temporaryFreetext = v; this.schedulePreviewUpdate(); });
|
||||||
text.inputEl.rows = 4;
|
text.inputEl.rows = 4;
|
||||||
text.inputEl.style.width = '100%';
|
text.inputEl.style.width = '100%';
|
||||||
});
|
});
|
||||||
|
|
@ -327,7 +380,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
.addOption('prefix', 'Prefix (before vault content)')
|
.addOption('prefix', 'Prefix (before vault content)')
|
||||||
.addOption('suffix', 'Suffix (after vault content)')
|
.addOption('suffix', 'Suffix (after vault content)')
|
||||||
.setValue(this.temporaryPosition)
|
.setValue(this.temporaryPosition)
|
||||||
.onChange(v => this.temporaryPosition = v as SourcePosition));
|
.onChange(v => { this.temporaryPosition = v as SourcePosition; this.schedulePreviewUpdate(); }));
|
||||||
|
|
||||||
new Setting(configZone)
|
new Setting(configZone)
|
||||||
.setName('Save as default')
|
.setName('Save as default')
|
||||||
|
|
@ -357,6 +410,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
dropdown.setValue(this.selectedTemplateId || '');
|
dropdown.setValue(this.selectedTemplateId || '');
|
||||||
dropdown.onChange(v => {
|
dropdown.onChange(v => {
|
||||||
this.selectedTemplateId = v || null;
|
this.selectedTemplateId = v || null;
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -369,7 +423,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
|
|
||||||
// === OUTPUT TARGETS ===
|
// === OUTPUT TARGETS ===
|
||||||
if (this.plugin.settings.targets.length > 0) {
|
if (this.plugin.settings.targets.length > 0) {
|
||||||
configZone.createEl('h3', { text: 'Output Targets' });
|
this.targetsHeadingEl = configZone.createEl('h3', { text: 'Output Targets' });
|
||||||
|
|
||||||
const targetsContainer = configZone.createDiv({ cls: 'targets-checkboxes' });
|
const targetsContainer = configZone.createDiv({ cls: 'targets-checkboxes' });
|
||||||
targetsContainer.style.display = 'flex';
|
targetsContainer.style.display = 'flex';
|
||||||
|
|
@ -397,6 +451,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
} else {
|
} else {
|
||||||
this.selectedTargetIds.delete(target.id);
|
this.selectedTargetIds.delete(target.id);
|
||||||
}
|
}
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
const icon = row.createEl('span', { text: getTargetIcon(target.format) });
|
const icon = row.createEl('span', { text: getTargetIcon(target.format) });
|
||||||
|
|
@ -436,6 +491,42 @@ export class ContextGeneratorModal extends Modal {
|
||||||
// === TOKEN ESTIMATE ===
|
// === TOKEN ESTIMATE ===
|
||||||
this.renderTokenEstimate(configZone);
|
this.renderTokenEstimate(configZone);
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// PREVIEW PANE (full-width, between grid and footer)
|
||||||
|
// =============================================
|
||||||
|
const previewSection = contentEl.createDiv({
|
||||||
|
cls: `cc-section cc-preview-section${this.previewOpen ? '' : ' is-collapsed'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewHeader = previewSection.createDiv({ cls: 'cc-section-header' });
|
||||||
|
const previewTitle = previewHeader.createEl('span', {
|
||||||
|
text: this.previewOpen ? 'Hide Preview' : 'Show Preview',
|
||||||
|
cls: 'cc-section-title',
|
||||||
|
});
|
||||||
|
previewHeader.createEl('span', { text: '\u203A', cls: 'cc-section-chevron' });
|
||||||
|
|
||||||
|
const previewBody = previewSection.createDiv({ cls: 'cc-section-content cc-preview-body' });
|
||||||
|
this.previewContent = previewBody.createEl('pre', { cls: 'cc-preview-code' });
|
||||||
|
this.previewLineInfo = previewBody.createDiv({ cls: 'cc-preview-line-info' });
|
||||||
|
|
||||||
|
previewHeader.addEventListener('click', async () => {
|
||||||
|
this.previewOpen = !this.previewOpen;
|
||||||
|
if (this.previewOpen) {
|
||||||
|
previewSection.removeClass('is-collapsed');
|
||||||
|
previewTitle.textContent = 'Hide Preview';
|
||||||
|
this.updatePreview();
|
||||||
|
} else {
|
||||||
|
previewSection.addClass('is-collapsed');
|
||||||
|
previewTitle.textContent = 'Show Preview';
|
||||||
|
}
|
||||||
|
this.plugin.settings.generatorPreviewOpen = this.previewOpen;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.previewOpen) {
|
||||||
|
this.updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// FOOTER (full-width, below both zones)
|
// FOOTER (full-width, below both zones)
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|
@ -507,6 +598,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
input.style.flex = '1';
|
input.style.flex = '1';
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
folder.purpose = input.value;
|
folder.purpose = input.value;
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -526,6 +618,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
nameInput.style.flex = '1';
|
nameInput.style.flex = '1';
|
||||||
nameInput.addEventListener('input', () => {
|
nameInput.addEventListener('input', () => {
|
||||||
template.name = nameInput.value;
|
template.name = nameInput.value;
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
const folderInput = row.createEl('input', { type: 'text' });
|
const folderInput = row.createEl('input', { type: 'text' });
|
||||||
|
|
@ -534,6 +627,7 @@ export class ContextGeneratorModal extends Modal {
|
||||||
folderInput.style.flex = '1';
|
folderInput.style.flex = '1';
|
||||||
folderInput.addEventListener('input', () => {
|
folderInput.addEventListener('input', () => {
|
||||||
template.folder = folderInput.value;
|
template.folder = folderInput.value;
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
const tagInput = row.createEl('input', { type: 'text' });
|
const tagInput = row.createEl('input', { type: 'text' });
|
||||||
|
|
@ -542,12 +636,14 @@ export class ContextGeneratorModal extends Modal {
|
||||||
tagInput.style.flex = '1';
|
tagInput.style.flex = '1';
|
||||||
tagInput.addEventListener('input', () => {
|
tagInput.addEventListener('input', () => {
|
||||||
template.tag = tagInput.value;
|
template.tag = tagInput.value;
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeBtn = row.createEl('button', { text: '✕' });
|
const removeBtn = row.createEl('button', { text: '✕' });
|
||||||
removeBtn.addEventListener('click', () => {
|
removeBtn.addEventListener('click', () => {
|
||||||
this.config.templates.splice(index, 1);
|
this.config.templates.splice(index, 1);
|
||||||
this.renderTemplates(container);
|
this.renderTemplates(container);
|
||||||
|
this.schedulePreviewUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -850,7 +946,125 @@ Here is the content with a link to ${link}.
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPrimaryTarget(): OutputTarget | null {
|
||||||
|
const primaryId = this.plugin.settings.primaryTargetId;
|
||||||
|
const targets = this.plugin.settings.targets;
|
||||||
|
if (primaryId) {
|
||||||
|
return targets.find(t => t.id === primaryId && this.selectedTargetIds.has(t.id)) ?? null;
|
||||||
|
}
|
||||||
|
return targets.find(t => this.selectedTargetIds.has(t.id)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly FORMAT_LABELS: Record<OutputFormat, string> = {
|
||||||
|
markdown: 'Markdown',
|
||||||
|
xml: 'XML',
|
||||||
|
plain: 'Plain Text',
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateStatusBar() {
|
||||||
|
if (!this.statusFileCount || !this.statusTokens || !this.statusTarget || !this.statusFormat) return;
|
||||||
|
|
||||||
|
// File count: VAULT.md + enabled optional files
|
||||||
|
const gf = this.config.generateFiles;
|
||||||
|
const fileCount = 1
|
||||||
|
+ (gf.conventions ? 1 : 0)
|
||||||
|
+ (gf.structure ? 1 : 0)
|
||||||
|
+ (gf.workflows ? 1 : 0)
|
||||||
|
+ (gf.templates ? 1 : 0)
|
||||||
|
+ (gf.examples ? 1 : 0);
|
||||||
|
this.statusFileCount.textContent = `${fileCount} file${fileCount !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
// Token estimate from generated output
|
||||||
|
const output = this.generatePreviewOutput();
|
||||||
|
const tokens = estimateTokens(output);
|
||||||
|
const primary = this.getPrimaryTarget();
|
||||||
|
|
||||||
|
const tokenText = `~${(tokens / 1000).toFixed(1)}k tokens`;
|
||||||
|
this.statusTokens.textContent = tokenText;
|
||||||
|
|
||||||
|
// Token warning color
|
||||||
|
if (primary && tokens > primary.maxTokens) {
|
||||||
|
this.statusTokens.addClass('is-warning');
|
||||||
|
} else {
|
||||||
|
this.statusTokens.removeClass('is-warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target name
|
||||||
|
this.statusTarget.textContent = primary
|
||||||
|
? `Target: ${primary.name}`
|
||||||
|
: 'Target: None';
|
||||||
|
|
||||||
|
// Format
|
||||||
|
if (primary) {
|
||||||
|
this.statusFormat.textContent = `Format: ${ContextGeneratorModal.FORMAT_LABELS[primary.format]}`;
|
||||||
|
} else {
|
||||||
|
this.statusFormat.textContent = 'Format: —';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private schedulePreviewUpdate() {
|
||||||
|
if (this.debounceTimer) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
}
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
this.debounceTimer = null;
|
||||||
|
this.updatePreview();
|
||||||
|
this.updateStatusBar();
|
||||||
|
}, ContextGeneratorModal.DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePreview() {
|
||||||
|
if (!this.previewOpen || !this.previewContent || !this.previewLineInfo) return;
|
||||||
|
|
||||||
|
const fullOutput = this.generatePreviewOutput();
|
||||||
|
const allLines = fullOutput.split('\n');
|
||||||
|
const maxLines = ContextGeneratorModal.PREVIEW_LINES;
|
||||||
|
const truncated = allLines.length > maxLines;
|
||||||
|
const displayLines = truncated ? allLines.slice(0, maxLines) : allLines;
|
||||||
|
|
||||||
|
this.previewContent.textContent = displayLines.join('\n');
|
||||||
|
this.previewLineInfo.textContent = truncated
|
||||||
|
? `Showing ${maxLines} of ~${allLines.length} lines`
|
||||||
|
: `${allLines.length} lines`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generatePreviewOutput(): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// VAULT.md is always included
|
||||||
|
parts.push(this.generateVaultMd());
|
||||||
|
|
||||||
|
if (this.config.generateFiles.conventions) {
|
||||||
|
parts.push(this.generateConventionsMd());
|
||||||
|
}
|
||||||
|
if (this.config.generateFiles.structure) {
|
||||||
|
parts.push(this.generateStructureMd());
|
||||||
|
}
|
||||||
|
if (this.config.generateFiles.workflows) {
|
||||||
|
parts.push(this.generateWorkflowsMd());
|
||||||
|
}
|
||||||
|
if (this.config.generateFiles.templates) {
|
||||||
|
parts.push(this.generateTemplatesMd());
|
||||||
|
}
|
||||||
|
if (this.config.generateFiles.examples) {
|
||||||
|
parts.push(this.generateExamplesMd());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('\n\n---\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
|
if (this.debounceTimer) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
this.debounceTimer = null;
|
||||||
|
}
|
||||||
|
this.previewContent = null;
|
||||||
|
this.previewLineInfo = null;
|
||||||
|
this.statusFileCount = null;
|
||||||
|
this.statusTokens = null;
|
||||||
|
this.statusTarget = null;
|
||||||
|
this.statusFormat = null;
|
||||||
|
this.targetsHeadingEl = null;
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export interface ClaudeContextSettings {
|
||||||
targetOutputFolder: string;
|
targetOutputFolder: string;
|
||||||
lastSettingsTab: string;
|
lastSettingsTab: string;
|
||||||
collapsedSections: string[];
|
collapsedSections: string[];
|
||||||
|
generatorPreviewOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ClaudeContextSettings = {
|
export const DEFAULT_SETTINGS: ClaudeContextSettings = {
|
||||||
|
|
@ -44,6 +45,7 @@ export const DEFAULT_SETTINGS: ClaudeContextSettings = {
|
||||||
targetOutputFolder: '_claude/outputs',
|
targetOutputFolder: '_claude/outputs',
|
||||||
lastSettingsTab: 'general',
|
lastSettingsTab: 'general',
|
||||||
collapsedSections: [],
|
collapsedSections: [],
|
||||||
|
generatorPreviewOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history';
|
export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history';
|
||||||
|
|
|
||||||
73
styles.css
73
styles.css
|
|
@ -88,7 +88,7 @@
|
||||||
|
|
||||||
.cc-gen-modal .modal-content {
|
.cc-gen-modal .modal-content {
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +131,77 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Generator status bar */
|
||||||
|
.cc-gen-statusbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-statusbar-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-statusbar-sep {
|
||||||
|
color: var(--text-faint);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-statusbar-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-statusbar-clickable:hover {
|
||||||
|
background: var(--background-modifier-hover);
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-statusbar-tokens.is-warning {
|
||||||
|
color: var(--text-warning, #e0a526);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generator preview pane */
|
||||||
|
.cc-preview-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-preview-body {
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-preview-code {
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cc-preview-line-info {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Stack on narrow viewports */
|
/* Stack on narrow viewports */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.cc-gen-modal {
|
.cc-gen-modal {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue