Files
markdown-converter/src/plugins/built-in/writing-studio/snapshot-manager.js
T
amitwh 7ab5a0ddb4 feat(ai): add Anthropic-compatible provider; fix async plugin backends
Anthropic-compatible provider:
- New 'anthropic-compatible' option for any base URL speaking the Anthropic
  messages schema (LiteLLM proxies, Bedrock gateways, local servers)
- Sends x-api-key AND Bearer auth when a key is set (gateway-friendly,
  harmless for the official API); keyless proxies supported
- Tolerates base URLs with or without a trailing /v1 segment
- Settings modal, manifest, and provider docs updated

Runtime bug fixes found by booting the app (run-to-verify pass):
- PDF editor: File > Open PDF sends operation=null which matched no switch
  case and crashed on getElementById(undefined); now defaults to the merge
  section
- backlinks-panel: wrong require depth (../../utils -> ../utils) threw at
  panel registration time
- writing-studio stack was written against a synchronous settings backend but
  the real one is IPC-backed: GoalTracker/SnapshotManager/ProjectManager and
  all four panels now await; JSON.parse(Promise) crashes eliminated
- manuscript panel used window.prompt (unavailable in Electron); replaced
  with an inline dialog
- collaboration comment-store/save-load made async to match its IPC IO

617/617 tests green; 4 consecutive clean app boots (no uncaught errors)
2026-09-05 21:53:34 +05:30

84 lines
2.2 KiB
JavaScript

/**
* Content snapshots with a rolling window, stored via the plugin settings
* backend. All methods are async because the real backend is the IPC-backed
* SettingsStore (get/set return Promises); `await` also works against the
* synchronous fakes used in unit tests.
*/
class SnapshotManager {
/**
* @param {object} store - { get(key), set(key, value) }
* @param {string} storeKey - settings key for snapshots
*/
constructor(store, storeKey = 'plugins.writing-studio.snapshots') {
this.store = store;
this.storeKey = storeKey;
}
async _getAll() {
const raw = await this.store.get(this.storeKey);
return raw ? JSON.parse(raw) : [];
}
async _saveAll(snaps) {
await this.store.set(this.storeKey, JSON.stringify(snaps));
}
async create(content, label = 'manual') {
const snaps = await this._getAll();
const snap = {
id: 'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
timestamp: new Date().toISOString(),
content,
wordCount: content.split(/\s+/).filter(Boolean).length,
label,
};
snaps.unshift(snap);
await this._saveAll(snaps);
return snap;
}
async list() {
return this._getAll();
}
async getById(id) {
return (await this._getAll()).find((s) => s.id === id) || null;
}
async restore(id) {
const snap = await this.getById(id);
if (!snap) throw new Error('Snapshot not found');
return snap.content;
}
async delete(id) {
const snaps = (await this._getAll()).filter((s) => s.id !== id);
await this._saveAll(snaps);
}
async diff(id, currentContent) {
const snap = await this.getById(id);
if (!snap) throw new Error('Snapshot not found');
const oldLines = snap.content.split('\n');
const newLines = currentContent.split('\n');
const oldSet = new Set(oldLines);
const newSet = new Set(newLines);
let added = 0;
let removed = 0;
for (const line of newLines) {
if (!oldSet.has(line)) added++;
}
for (const line of oldLines) {
if (!newSet.has(line)) removed++;
}
return { added, removed };
}
async prune(keepCount) {
const snaps = await this._getAll();
await this._saveAll(snaps.slice(0, keepCount));
}
}
module.exports = { SnapshotManager };