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
@@ -42,17 +42,20 @@ function newCommentId(now = Date.now()) {
* Load all comments for a document. Missing file/corrupt JSON → [] (a fresh
* document simply has no comments yet; a corrupt store must not crash the app).
*
* Async: `io.readFile`/`io.fileExists` are IPC-backed Promises in the app
* (plain values in tests — `await` handles both).
*
* @param {string} docPath Document path
* @param {object} io - { readFile: (path) => string|null, fileExists: (path) => bool }
* @param {Function} pathUtil injected path module
* @returns {Array<object>} comments sorted by line then createdAt
* @returns {Promise<Array<object>>} comments sorted by line then createdAt
*/
function loadComments(docPath, io, pathUtil) {
async function loadComments(docPath, io, pathUtil) {
if (!docPath) return [];
const file = commentsFilePathFor(docPath, pathUtil);
try {
if (!io.fileExists(file)) return [];
const raw = io.readFile(file);
if (!(await io.fileExists(file))) return [];
const raw = await io.readFile(file);
const data = JSON.parse(raw);
if (!Array.isArray(data.comments)) return [];
return normalizeComments(data.comments);
@@ -69,10 +72,10 @@ function loadComments(docPath, io, pathUtil) {
* @param {object} io - { writeFile, ensureDirectory }
* @param {Function} pathUtil injected path module
*/
function saveComments(docPath, comments, io, pathUtil) {
async function saveComments(docPath, comments, io, pathUtil) {
const file = commentsFilePathFor(docPath, pathUtil);
io.ensureDirectory(pathUtil.dirname(file));
io.writeFile(
await io.ensureDirectory(pathUtil.dirname(file));
await io.writeFile(
file,
JSON.stringify({ version: 1, doc: pathUtil.basename(docPath), comments }, null, 2)
);