Snapshots capture the file list, template, and formatting config from a history entry so the context can be regenerated from current vault state. Stored as JSON in .context-snapshots/ with no auto-cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
7 KiB
TypeScript
230 lines
7 KiB
TypeScript
import { App, Modal, Notice, Setting } from 'obsidian';
|
|
import PromptfirePlugin from './main';
|
|
import { HistoryEntry, formatRelativeTime } from './history';
|
|
import { ContextSnapshot, SnapshotManager, generateSnapshotId } from './snapshots';
|
|
|
|
export class SaveSnapshotModal extends Modal {
|
|
private plugin: PromptfirePlugin;
|
|
private entry: HistoryEntry;
|
|
private snapshotManager: SnapshotManager;
|
|
private name = '';
|
|
private description = '';
|
|
|
|
constructor(app: App, plugin: PromptfirePlugin, entry: HistoryEntry) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.entry = entry;
|
|
this.snapshotManager = plugin.snapshotManager;
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass('promptfire-save-snapshot');
|
|
|
|
contentEl.createEl('h2', { text: 'Save as Snapshot' });
|
|
|
|
// Summary
|
|
const summary = contentEl.createDiv();
|
|
summary.style.marginBottom = '15px';
|
|
summary.style.padding = '10px';
|
|
summary.style.backgroundColor = 'var(--background-secondary)';
|
|
summary.style.borderRadius = '4px';
|
|
summary.style.fontSize = '12px';
|
|
|
|
const fileCount = this.entry.metadata.includedFiles.length;
|
|
const templateName = this.entry.metadata.templateName || 'None';
|
|
const activeNote = this.entry.metadata.activeNote || 'None';
|
|
|
|
summary.createEl('div', { text: `Files: ${fileCount}` });
|
|
summary.createEl('div', { text: `Template: ${templateName}` });
|
|
summary.createEl('div', { text: `Active note: ${activeNote}` });
|
|
|
|
// Name field
|
|
let nameInput: HTMLInputElement;
|
|
new Setting(contentEl)
|
|
.setName('Name')
|
|
.setDesc('A short name for this snapshot (required)')
|
|
.addText(text => {
|
|
nameInput = text.inputEl;
|
|
text.setPlaceholder('e.g., "Auth refactor context"')
|
|
.onChange(value => { this.name = value; });
|
|
});
|
|
|
|
// Description field
|
|
new Setting(contentEl)
|
|
.setName('Description')
|
|
.setDesc('Optional notes about when to use this snapshot')
|
|
.addTextArea(text => {
|
|
text.setPlaceholder('e.g., "Use when working on auth module"')
|
|
.onChange(value => { this.description = value; });
|
|
text.inputEl.rows = 2;
|
|
text.inputEl.style.width = '100%';
|
|
});
|
|
|
|
// Buttons
|
|
const buttonContainer = contentEl.createDiv();
|
|
buttonContainer.style.display = 'flex';
|
|
buttonContainer.style.justifyContent = 'flex-end';
|
|
buttonContainer.style.gap = '10px';
|
|
buttonContainer.style.marginTop = '20px';
|
|
|
|
const saveBtn = buttonContainer.createEl('button', { text: 'Save Snapshot', cls: 'mod-cta' });
|
|
saveBtn.addEventListener('click', async () => {
|
|
if (!this.name.trim()) {
|
|
new Notice('Name is required');
|
|
return;
|
|
}
|
|
|
|
const snapshot: ContextSnapshot = {
|
|
id: generateSnapshotId(),
|
|
name: this.name.trim(),
|
|
description: this.description.trim() || undefined,
|
|
createdAt: Date.now(),
|
|
notePaths: this.entry.metadata.includedFiles,
|
|
activeNotePath: this.entry.metadata.activeNote,
|
|
templateId: this.entry.metadata.templateId,
|
|
includeFilenames: this.plugin.settings.includeFilenames,
|
|
separator: this.plugin.settings.separator,
|
|
};
|
|
|
|
const success = await this.snapshotManager.saveSnapshot(snapshot);
|
|
if (success) {
|
|
new Notice(`Snapshot saved: ${snapshot.name}`);
|
|
this.close();
|
|
} else {
|
|
new Notice('Failed to save snapshot');
|
|
}
|
|
});
|
|
|
|
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
|
|
cancelBtn.addEventListener('click', () => this.close());
|
|
}
|
|
|
|
onClose() {
|
|
this.contentEl.empty();
|
|
}
|
|
}
|
|
|
|
export class SnapshotListModal extends Modal {
|
|
private plugin: PromptfirePlugin;
|
|
private snapshotManager: SnapshotManager;
|
|
|
|
constructor(app: App, plugin: PromptfirePlugin) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.snapshotManager = plugin.snapshotManager;
|
|
}
|
|
|
|
async onOpen() {
|
|
await this.loadAndRender();
|
|
}
|
|
|
|
async loadAndRender() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass('promptfire-snapshot-list');
|
|
|
|
contentEl.createEl('h2', { text: 'Context Snapshots' });
|
|
|
|
const snapshots = await this.snapshotManager.loadSnapshots();
|
|
|
|
if (snapshots.length === 0) {
|
|
const notice = contentEl.createEl('p', {
|
|
text: 'No snapshots yet. Open a history entry and click "Save as Snapshot" to create one.'
|
|
});
|
|
notice.style.fontStyle = 'italic';
|
|
notice.style.color = 'var(--text-muted)';
|
|
return;
|
|
}
|
|
|
|
const countInfo = contentEl.createEl('p', {
|
|
text: `${snapshots.length} snapshot${snapshots.length !== 1 ? 's' : ''}`
|
|
});
|
|
countInfo.style.color = 'var(--text-muted)';
|
|
countInfo.style.marginBottom = '10px';
|
|
|
|
const list = contentEl.createDiv({ cls: 'snapshot-list' });
|
|
list.style.maxHeight = '400px';
|
|
list.style.overflow = 'auto';
|
|
list.style.border = '1px solid var(--background-modifier-border)';
|
|
list.style.borderRadius = '4px';
|
|
|
|
for (const snapshot of snapshots) {
|
|
const row = list.createDiv({ cls: 'snapshot-row' });
|
|
row.style.display = 'flex';
|
|
row.style.alignItems = 'center';
|
|
row.style.padding = '10px 12px';
|
|
row.style.borderBottom = '1px solid var(--background-modifier-border)';
|
|
row.style.gap = '10px';
|
|
|
|
// Info
|
|
const infoContainer = row.createDiv();
|
|
infoContainer.style.flex = '1';
|
|
|
|
const header = infoContainer.createDiv();
|
|
header.style.display = 'flex';
|
|
header.style.alignItems = 'center';
|
|
header.style.gap = '8px';
|
|
|
|
const nameEl = header.createEl('span', { text: snapshot.name });
|
|
nameEl.style.fontWeight = '500';
|
|
|
|
const timeEl = header.createEl('span', { text: formatRelativeTime(snapshot.createdAt) });
|
|
timeEl.style.color = 'var(--text-muted)';
|
|
timeEl.style.fontSize = '12px';
|
|
|
|
// Stats
|
|
const stats = infoContainer.createDiv();
|
|
stats.style.fontSize = '12px';
|
|
stats.style.color = 'var(--text-muted)';
|
|
stats.style.marginTop = '4px';
|
|
|
|
const fileCount = snapshot.notePaths.length;
|
|
const templateName = snapshot.templateId
|
|
? (this.plugin.settings.promptTemplates.find(t => t.id === snapshot.templateId)?.name || 'Unknown template')
|
|
: 'No template';
|
|
stats.setText(`${fileCount} files · ${templateName}`);
|
|
|
|
// Description
|
|
if (snapshot.description) {
|
|
const desc = infoContainer.createDiv({ text: snapshot.description });
|
|
desc.style.fontSize = '12px';
|
|
desc.style.fontStyle = 'italic';
|
|
desc.style.marginTop = '4px';
|
|
}
|
|
|
|
// Actions
|
|
const actions = row.createDiv();
|
|
actions.style.display = 'flex';
|
|
actions.style.gap = '4px';
|
|
|
|
const replayBtn = actions.createEl('button', { text: '\u25B6' });
|
|
replayBtn.title = 'Replay snapshot';
|
|
replayBtn.style.padding = '4px 8px';
|
|
replayBtn.addEventListener('click', async () => {
|
|
this.close();
|
|
await this.plugin.replaySnapshot(snapshot);
|
|
});
|
|
|
|
const deleteBtn = actions.createEl('button', { text: '\u2715' });
|
|
deleteBtn.title = 'Delete snapshot';
|
|
deleteBtn.style.padding = '4px 8px';
|
|
deleteBtn.addEventListener('click', async () => {
|
|
await this.snapshotManager.deleteSnapshot(snapshot.id);
|
|
new Notice('Snapshot deleted');
|
|
await this.loadAndRender();
|
|
});
|
|
}
|
|
|
|
// Remove bottom border from last row
|
|
const lastRow = list.lastElementChild as HTMLElement;
|
|
if (lastRow) {
|
|
lastRow.style.borderBottom = 'none';
|
|
}
|
|
}
|
|
|
|
onClose() {
|
|
this.contentEl.empty();
|
|
}
|
|
}
|