slidemind 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/slidemind.mjs CHANGED
@@ -336,15 +336,20 @@ async function cmdSearch(rest) {
336
336
  subtype = "image";
337
337
  }
338
338
 
339
- const res = await client.engineSearch({
340
- presId,
339
+ // Window-agnostic: tries the picked pane, then fails over to any other live
340
+ // window on the same backend if it's suspended/frozen. The answering window
341
+ // (res.presId) is what reuse must target — the searchId is bound to it.
342
+ const res = await client.engineSearchAnyPane({
343
+ registry,
344
+ explicitPresId: to,
341
345
  engineId,
342
346
  query,
343
347
  subtype,
344
348
  intent: intent ?? null,
345
349
  top: top ?? null,
346
- backendURL,
347
350
  });
351
+ const ranPresId = res?.presId ?? presId;
352
+ const ranEntry = registry.find((p) => p.presId === ranPresId) ?? entry;
348
353
  if (asJson) {
349
354
  process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
350
355
  return;
@@ -387,14 +392,14 @@ async function cmdSearch(rest) {
387
392
  // add-ins that don't echo a top-level subtype.
388
393
  const isSlideResults =
389
394
  res.subtype === "slide" || results.some((r) => r.kind === "slide");
390
- const envTag = entry.env ? ` [${entry.env}]` : "";
395
+ const envTag = ranEntry.env ? ` [${ranEntry.env}]` : "";
391
396
  if (isSlideResults) {
392
397
  // The searchId is bound to the window the search ran on — reuse must target
393
398
  // that same window, so spell out its presId in the next-step commands.
394
399
  process.stdout.write(
395
- `searchId ${res.searchId} ran on window ${presId}${envTag} — reuse from THIS window:\n` +
396
- ` slidemind insert-slide --search ${res.searchId} --pick <n> --to ${presId}\n` +
397
- ` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${presId}\n`,
400
+ `searchId ${res.searchId} ran on window ${ranPresId}${envTag} — reuse from THIS window:\n` +
401
+ ` slidemind insert-slide --search ${res.searchId} --pick <n> --to ${ranPresId}\n` +
402
+ ` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${ranPresId}\n`,
398
403
  );
399
404
  } else {
400
405
  process.stdout.write(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,19 @@ function listTimeoutMs() {
28
28
  );
29
29
  }
30
30
 
31
+ // Engine ops (list / search / download / extract) are RELAYED through the task
32
+ // pane: the server waits up to ~25s for a suspended pane to reconnect, then up
33
+ // to 60s for the pane's ack. Budget above that sum so a typed pane error
34
+ // (pane_suspended / pane_timeout) wins over a client timeout. Note `ls engines`
35
+ // is pane-relayed too — using the 8s list timeout there masked the real
36
+ // pane_suspended as a bare "timed out after 8000ms".
37
+ function engineTimeoutMs() {
38
+ return (
39
+ parseInt(process.env.SLIDEMIND_BRIDGE_ENGINE_TIMEOUT_MS || "90000", 10) ||
40
+ 90000
41
+ );
42
+ }
43
+
31
44
  const NOT_PAIRED_MESSAGE =
32
45
  'slidemind is not paired. In the slidemind add-in, click "Connect CLI" ' +
33
46
  "and run the shown `slidemind pair …` command, then retry.";
@@ -483,7 +496,9 @@ async function listEnginesForBackend(backendURL, { type = null, subtype = null }
483
496
  const qs = params.toString();
484
497
  const res = await bridgeFetch(
485
498
  `/api/cli/engines${qs ? `?${qs}` : ""}`,
486
- { timeoutMs: listTimeoutMs() },
499
+ // Pane-relayed on the add-in side — needs the engine budget, not the 8s
500
+ // list timeout (which bailed before the pane could ever answer).
501
+ { timeoutMs: engineTimeoutMs() },
487
502
  backendURL,
488
503
  );
489
504
  const data = await res.json();
@@ -837,6 +852,11 @@ export async function engineSearch({
837
852
  intent = null,
838
853
  top = null,
839
854
  backendURL = null,
855
+ // When the chosen pane is suspended, wait CLIENT-side for THIS window to wake
856
+ // (the single-window default). engineSearchAnyPane sets this false for all but
857
+ // the last candidate so it can fail over to another live window instead.
858
+ waitOnSuspend = true,
859
+ timeoutMs = null,
840
860
  }) {
841
861
  if (!presId) throw new Error("engineSearch: presId is required");
842
862
  if (!engineId) throw new Error("engineSearch: engineId is required");
@@ -846,24 +866,96 @@ export async function engineSearch({
846
866
  if (subtype) body.subtype = subtype;
847
867
  if (intent) body.intent = intent;
848
868
  if (typeof top === "number") body.top = top;
849
- return engineOpWithPaneWait(presId, be, async (pid, b) => {
869
+ const run = async (pid, b) => {
850
870
  const res = await bridgeFetch(
851
871
  `/api/cli/presentations/${encodeURIComponent(pid)}/engine/search`,
852
872
  {
853
873
  method: "POST",
854
874
  headers: { "content-type": "application/json" },
855
875
  body: JSON.stringify(body),
856
- // Engine ops relay through the pane: the server may wait up to ~25s for
857
- // a suspended pane to reconnect, then up to 60s for the pane's ack —
858
- // stay above that sum so typed errors win over client timeouts.
859
- timeoutMs:
860
- parseInt(process.env.SLIDEMIND_BRIDGE_ENGINE_TIMEOUT_MS || "90000", 10) ||
861
- 90000,
876
+ timeoutMs: timeoutMs ?? engineTimeoutMs(),
862
877
  },
863
878
  b,
864
879
  );
865
880
  return res.json(); // { ok, searchId, engineId, subtype, intent, resultCount, gridDataUri?, results }
866
- });
881
+ };
882
+ return waitOnSuspend ? engineOpWithPaneWait(presId, be, run) : run(presId, be);
883
+ }
884
+
885
+ // A suspended/throttled background webview must not strand a window-AGNOSTIC
886
+ // engine search: the catalog is per-user, so any connected pane can answer it.
887
+ // Try the picked pane first, then every other connected pane on the same
888
+ // backend (non-suspended first), failing fast past each unresponsive one; the
889
+ // LAST candidate keeps the normal "click the pane to wake it" wait so a genuine
890
+ // single-window/all-asleep case still surfaces the actionable prompt. Returns
891
+ // the search result tagged with the presId/backendURL that actually answered —
892
+ // the searchId is bound to THAT window, so callers must thread it into
893
+ // follow-up view/insert/add-slide. An explicit presId pins the window (no
894
+ // failover — the caller asked for that specific one).
895
+ export async function engineSearchAnyPane({
896
+ registry,
897
+ explicitPresId = null,
898
+ engineId,
899
+ query,
900
+ subtype = null,
901
+ intent = null,
902
+ top = null,
903
+ }) {
904
+ const primary = pickAnyPresentationEntry(registry, explicitPresId);
905
+ const primaryBackend = primary.backendURL ?? null;
906
+ let candidates;
907
+ if (explicitPresId) {
908
+ candidates = [primary];
909
+ } else {
910
+ const others = (Array.isArray(registry) ? registry : [])
911
+ .filter(
912
+ (p) =>
913
+ p.presId !== primary.presId &&
914
+ p.connected !== false &&
915
+ (p.backendURL ?? null) === primaryBackend,
916
+ )
917
+ .sort((a, b) => (a.suspendedAt ? 1 : 0) - (b.suspendedAt ? 1 : 0));
918
+ candidates = [primary, ...others];
919
+ }
920
+ const failoverTimeout = envInt(
921
+ "SLIDEMIND_BRIDGE_ENGINE_FAILOVER_TIMEOUT_MS",
922
+ 25000,
923
+ );
924
+ let lastErr = null;
925
+ for (let i = 0; i < candidates.length; i++) {
926
+ const cand = candidates[i];
927
+ const isLast = i === candidates.length - 1;
928
+ try {
929
+ const res = await engineSearch({
930
+ presId: cand.presId,
931
+ engineId,
932
+ query,
933
+ subtype,
934
+ intent,
935
+ top,
936
+ backendURL: cand.backendURL ?? primaryBackend,
937
+ waitOnSuspend: isLast,
938
+ timeoutMs: isLast ? null : failoverTimeout,
939
+ });
940
+ return {
941
+ ...res,
942
+ presId: cand.presId,
943
+ backendURL: cand.backendURL ?? primaryBackend,
944
+ };
945
+ } catch (err) {
946
+ lastErr = err;
947
+ // A suspended/frozen/slow pane is recoverable by trying the next window;
948
+ // anything else (bad engineId, auth, 4xx) is real — surface it now.
949
+ const recoverable =
950
+ err?.code === "pane_suspended" ||
951
+ err?.code === "pane_timeout" ||
952
+ err?.code === "bridge_timeout" ||
953
+ err?.status === 503 ||
954
+ err?.status === 504;
955
+ if (isLast || !recoverable) throw err;
956
+ }
957
+ }
958
+ throw lastErr ?? new Error("engineSearchAnyPane: no candidate panes");
867
959
  }
868
960
 
869
961
  // Fetch a chosen engine result's bytes through the local bridge. For image
@@ -353,15 +353,20 @@ server.registerTool(
353
353
  engineId = pick.id;
354
354
  subtype = "image";
355
355
  }
356
- const res = await client.engineSearch({
357
- presId,
356
+ // Window-agnostic: tries the picked pane, then fails over to any other
357
+ // live window on the same backend if it's suspended/frozen. res.presId is
358
+ // the window that actually answered — echo THAT back (the searchId is
359
+ // bound to it) so follow-up view/insert/add target the right pane.
360
+ const res = await client.engineSearchAnyPane({
361
+ registry,
362
+ explicitPresId: presentation_id ?? null,
358
363
  engineId,
359
364
  query,
360
365
  subtype,
361
366
  intent: intent ?? null,
362
367
  top: top ?? null,
363
- backendURL,
364
368
  });
369
+ const ranPresId = res?.presId ?? presId;
365
370
  const content = [];
366
371
  const grid = imageContentFromDataUri(res?.gridDataUri);
367
372
  if (grid) content.push(grid);
@@ -385,9 +390,10 @@ server.registerTool(
385
390
  {
386
391
  ok: res?.ok ?? false,
387
392
  // The window the search ran on (auto-attached when the caller didn't
388
- // pin one). The searchId is bound to it — pass this presentation_id
389
- // to slidemind_view_slide / _insert_slide / _add_slide_to_deck.
390
- presentation_id: presId,
393
+ // pin one; may have failed over from the picked pane). The searchId
394
+ // is bound to it — pass this presentation_id to slidemind_view_slide
395
+ // / _insert_slide / _add_slide_to_deck.
396
+ presentation_id: ranPresId,
391
397
  engineId,
392
398
  subtype: res?.subtype ?? null,
393
399
  intent: res?.intent ?? null,