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)
This commit is contained in:
2026-09-05 21:53:34 +05:30
parent efca458495
commit 7ab5a0ddb4
20 changed files with 426 additions and 225 deletions
@@ -1,3 +1,9 @@
/**
* 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) }
@@ -8,17 +14,17 @@ class SnapshotManager {
this.storeKey = storeKey;
}
_getAll() {
const raw = this.store.get(this.storeKey);
async _getAll() {
const raw = await this.store.get(this.storeKey);
return raw ? JSON.parse(raw) : [];
}
_saveAll(snaps) {
this.store.set(this.storeKey, JSON.stringify(snaps));
async _saveAll(snaps) {
await this.store.set(this.storeKey, JSON.stringify(snaps));
}
create(content, label = 'manual') {
const snaps = this._getAll();
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(),
@@ -27,31 +33,31 @@ class SnapshotManager {
label,
};
snaps.unshift(snap);
this._saveAll(snaps);
await this._saveAll(snaps);
return snap;
}
list() {
async list() {
return this._getAll();
}
getById(id) {
return this._getAll().find((s) => s.id === id) || null;
async getById(id) {
return (await this._getAll()).find((s) => s.id === id) || null;
}
restore(id) {
const snap = this.getById(id);
async restore(id) {
const snap = await this.getById(id);
if (!snap) throw new Error('Snapshot not found');
return snap.content;
}
delete(id) {
const snaps = this._getAll().filter((s) => s.id !== id);
this._saveAll(snaps);
async delete(id) {
const snaps = (await this._getAll()).filter((s) => s.id !== id);
await this._saveAll(snaps);
}
diff(id, currentContent) {
const snap = this.getById(id);
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');
@@ -68,9 +74,9 @@ class SnapshotManager {
return { added, removed };
}
prune(keepCount) {
const snaps = this._getAll();
this._saveAll(snaps.slice(0, keepCount));
async prune(keepCount) {
const snaps = await this._getAll();
await this._saveAll(snaps.slice(0, keepCount));
}
}