feat(plugins): add export-format registration hook to plugin API

Add context.formats.registerExportFormat(id, opts) to PluginContext,
backed by a new FormatRegistry (mirrors PluginRegistry's Map-based
shape). Plugins register namespaced (${pluginId}:${id}) export
formats with a label/extension/handler; the writing-studio built-in
plugin registers a trivial "sprint-summary" .txt export as a
worked example.

The plugin system lives entirely in the renderer process while the
Export menu is built in main.js, so wiring formats into the menu
required a small IPC round-trip: renderer sends format metadata to
main after plugin load (main rebuilds the menu via the already-
idempotent createMenu()), and a menu click sends the resolved save
path back to the renderer, which is the only process holding the
plugin's handler function.

Amit Haridas
This commit is contained in:
2026-08-23 19:31:33 +05:30
parent 44624cd4bf
commit d6baa2daf7
10 changed files with 304 additions and 3 deletions
@@ -29,6 +29,7 @@ class WritingStudioPlugin extends PluginAPI {
this._registerCommands(context);
this._registerStatusBar(context);
this._registerExportFormats(context);
}
_registerCommands(context) {
@@ -120,6 +121,42 @@ class WritingStudioPlugin extends PluginAPI {
});
}
// Example usage of context.formats.registerExportFormat (Task 17): adds
// a "Writing Studio Summary" entry to the Export menu that writes a
// plain-text snapshot of today's sprint/goal progress instead of going
// through Pandoc. Doubles as documentation for how a plugin can offer
// its own export target.
_registerExportFormats(context) {
const { sprintEngine, goalTracker } = this;
context.formats.registerExportFormat('sprint-summary', {
label: 'Writing Studio Summary (.txt)',
extension: 'txt',
handler: async (markdownContent, outputPath) => {
const fs = require('fs');
const goal = context.settings.get('dailyGoal') || 1000;
const progress = goalTracker.getDailyProgress(goal);
const streak = goalTracker.getStreak(goal);
const wordCount = (markdownContent || '').split(/\s+/).filter(Boolean).length;
const lines = [
'Writing Studio Summary',
'=======================',
`Generated: ${new Date().toISOString()}`,
'',
`Document word count: ${wordCount}`,
`Daily goal: ${goal}`,
`Words written today: ${progress.written} (${progress.pct}%)`,
`Current streak: ${streak} day(s)`,
`Sprint active: ${sprintEngine.isActive() ? 'yes' : 'no'}`,
'',
];
fs.writeFileSync(outputPath, lines.join('\n'), 'utf-8');
},
});
}
deactivate() {
if (this._sprintInterval) clearInterval(this._sprintInterval);
}
@@ -32,7 +32,14 @@
"shortcut": "Ctrl+Alt+G"
}
],
"statusBar": { "indicators": ["sprint-timer", "word-goal"] }
"statusBar": { "indicators": ["sprint-timer", "word-goal"] },
"exportFormats": [
{
"id": "sprint-summary",
"label": "Writing Studio Summary (.txt)",
"extension": "txt"
}
]
},
"settings": [
{ "key": "dailyGoal", "type": "number", "default": 1000, "label": "Daily word goal" },