slidemind 0.3.12 → 0.3.14
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +86 -21
- package/README.md +14 -17
- package/bin/slidemind.mjs +65 -4969
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +113 -74
- package/skills/search/SKILL.md +25 -7
- package/src/bridge/build-and-push.mjs +108 -15
- package/src/bridge/bundle.mjs +69 -0
- package/src/bridge/capture.mjs +71 -8
- package/src/bridge/client/brands.mjs +622 -0
- package/src/bridge/client/engine-routing.mjs +237 -0
- package/src/bridge/client/engine-search.mjs +310 -0
- package/src/bridge/client/engine-slides.mjs +435 -0
- package/src/bridge/client/pane-ops.mjs +335 -0
- package/src/bridge/client/presentations.mjs +587 -0
- package/src/bridge/client/render.mjs +317 -0
- package/src/bridge/client/transport.mjs +695 -0
- package/src/bridge/client.mjs +80 -3452
- package/src/bridge/config.mjs +1 -1
- package/src/bridge/engine-download.mjs +15 -2
- package/src/builders/build.mjs +16 -669
- package/src/builders/draft-check.mjs +376 -0
- package/src/builders/lint.mjs +38 -2
- package/src/builders/preview.html +799 -0
- package/src/builders/review.mjs +23 -4
- package/src/builders/visual-families.mjs +6 -8
- package/src/cli/common.mjs +179 -0
- package/src/cli/concepts.mjs +310 -0
- package/src/cli/connection.mjs +147 -0
- package/src/cli/deck.mjs +608 -0
- package/src/cli/designs.mjs +113 -0
- package/src/cli/diagnose.mjs +141 -0
- package/src/cli/download-bundle.mjs +144 -0
- package/src/cli/engine-helpers.mjs +429 -0
- package/src/cli/engine-results.mjs +320 -0
- package/src/cli/help.mjs +201 -0
- package/src/cli/install.mjs +257 -0
- package/src/cli/read.mjs +120 -0
- package/src/cli/ready.mjs +805 -0
- package/src/cli/search.mjs +608 -0
- package/src/cli/session.mjs +177 -0
- package/src/cli/setup.mjs +282 -0
- package/src/cli/slide-reuse.mjs +334 -0
- package/src/cli/status.mjs +118 -0
- package/src/cli/workspace.mjs +302 -0
- package/src/installers/codex.mjs +1 -1
- package/src/update-check.mjs +231 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// Slide extraction, live insert, view and source-presentation reads off a search.
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { assertSearchId, downloadTimeoutMs, resultRoute, directEngineBackend } from "./engine-routing.mjs";
|
|
8
|
+
import { insertSlides } from "./pane-ops.mjs";
|
|
9
|
+
import { resolveBackendForPres, engineOpWithPaneWait } from "./presentations.mjs";
|
|
10
|
+
import { engineTimeoutMs, bridgeFetch, bridgeStreamToFile, backendSupportsSourcePresentation, graphHeaders } from "./transport.mjs";
|
|
11
|
+
|
|
12
|
+
// Stream a chosen slide-search result to `destPath` as a native one-slide .pptx.
|
|
13
|
+
// The size-unbounded sibling of `extractSlide`: the pane ack carries only the
|
|
14
|
+
// request, and the add-in server performs it and pipes the (base64-decoded) bytes
|
|
15
|
+
// here. `extractSlide` puts the whole slide through the pane's Socket.IO ack as
|
|
16
|
+
// base64, so it is capped at ~7 MB of real bytes — which real reused slides reach.
|
|
17
|
+
//
|
|
18
|
+
// With NO presId and a resolved `backendURL`/`brandId` this streams pane-free
|
|
19
|
+
// from `GET /api/cli/searches/:searchId/results/:selection/slide`.
|
|
20
|
+
export async function extractSlideToFile({
|
|
21
|
+
presId = null,
|
|
22
|
+
searchId,
|
|
23
|
+
selection,
|
|
24
|
+
destPath,
|
|
25
|
+
backendURL = null,
|
|
26
|
+
brandId = null,
|
|
27
|
+
onProgress = null,
|
|
28
|
+
}) {
|
|
29
|
+
const direct = !presId && Boolean(backendURL || brandId);
|
|
30
|
+
if (!presId && !direct) throw new Error("extractSlideToFile: presId is required");
|
|
31
|
+
assertSearchId("extractSlideToFile", searchId);
|
|
32
|
+
if (typeof selection !== "number") {
|
|
33
|
+
throw new Error("extractSlideToFile: selection (number) is required");
|
|
34
|
+
}
|
|
35
|
+
if (!destPath) throw new Error("extractSlideToFile: destPath is required");
|
|
36
|
+
const shape = (res) => {
|
|
37
|
+
const title = res.headers?.["x-slidemind-presentation-title"];
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
bytesWritten: res.bytesWritten,
|
|
41
|
+
slideId: res.headers?.["x-slidemind-slide-id"] ?? undefined,
|
|
42
|
+
presentationTitle: title ? decodeURIComponent(String(title)) : undefined,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
if (direct) {
|
|
46
|
+
const b = await directEngineBackend("extractSlideToFile", {
|
|
47
|
+
backendURL,
|
|
48
|
+
brandId,
|
|
49
|
+
});
|
|
50
|
+
const res = await bridgeStreamToFile(
|
|
51
|
+
resultRoute(searchId, selection, "slide"),
|
|
52
|
+
{
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers: await graphHeaders(b),
|
|
55
|
+
timeoutMs: downloadTimeoutMs(),
|
|
56
|
+
onProgress,
|
|
57
|
+
},
|
|
58
|
+
destPath,
|
|
59
|
+
b,
|
|
60
|
+
);
|
|
61
|
+
return shape(res);
|
|
62
|
+
}
|
|
63
|
+
const be = backendURL || (await resolveBackendForPres(presId));
|
|
64
|
+
return engineOpWithPaneWait(presId, be, async (pid, b) => {
|
|
65
|
+
const res = await bridgeStreamToFile(
|
|
66
|
+
`/api/cli/presentations/${encodeURIComponent(pid)}/engine/slide-extract-stream`,
|
|
67
|
+
{
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "content-type": "application/json" },
|
|
70
|
+
body: JSON.stringify({ searchId, selection }),
|
|
71
|
+
timeoutMs: downloadTimeoutMs(),
|
|
72
|
+
onProgress,
|
|
73
|
+
},
|
|
74
|
+
destPath,
|
|
75
|
+
b,
|
|
76
|
+
);
|
|
77
|
+
return shape(res);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract a chosen slide-search result as a standalone one-slide .pptx (base64),
|
|
82
|
+
// WITHOUT inserting it anywhere — the "fold into a deck" path. Relayed through
|
|
83
|
+
// the pane like search/download, and pane-only for the same reason as
|
|
84
|
+
// engineDownload: the pane-free route streams (extractSlideToFile above), and
|
|
85
|
+
// this capped ack path exists only for add-ins that predate it.
|
|
86
|
+
export async function extractSlide({
|
|
87
|
+
presId,
|
|
88
|
+
searchId,
|
|
89
|
+
selection,
|
|
90
|
+
backendURL = null,
|
|
91
|
+
}) {
|
|
92
|
+
if (!presId) throw new Error("extractSlide: presId is required");
|
|
93
|
+
assertSearchId("extractSlide", searchId);
|
|
94
|
+
if (typeof selection !== "number") {
|
|
95
|
+
throw new Error("extractSlide: selection (number) is required");
|
|
96
|
+
}
|
|
97
|
+
const be = backendURL || (await resolveBackendForPres(presId));
|
|
98
|
+
return engineOpWithPaneWait(presId, be, async (pid, b) => {
|
|
99
|
+
const res = await bridgeFetch(
|
|
100
|
+
`/api/cli/presentations/${encodeURIComponent(pid)}/engine/slide-extract`,
|
|
101
|
+
{
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "content-type": "application/json" },
|
|
104
|
+
body: JSON.stringify({ searchId, selection }),
|
|
105
|
+
timeoutMs:
|
|
106
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_ENGINE_TIMEOUT_MS || "90000", 10) ||
|
|
107
|
+
90000,
|
|
108
|
+
},
|
|
109
|
+
b,
|
|
110
|
+
);
|
|
111
|
+
return res.json(); // { ok, base64, slideId?, presentationTitle? }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Insert one or more chosen slide-search results live into the open
|
|
116
|
+
// presentation (the "reuse → insert live" path). Relayed through the pane.
|
|
117
|
+
//
|
|
118
|
+
// `fileName`/`brandId` identify the target WINDOW (brandId == the deck's presFP).
|
|
119
|
+
// They cost nothing on the happy path and are what lets the single retry find
|
|
120
|
+
// the same window again after a pane reconnect churned its presId — the same
|
|
121
|
+
// self-healing `push` does around its insert.
|
|
122
|
+
export async function insertSlideLive({
|
|
123
|
+
presId,
|
|
124
|
+
searchId,
|
|
125
|
+
selections,
|
|
126
|
+
at = null,
|
|
127
|
+
backendURL = null,
|
|
128
|
+
fileName = null,
|
|
129
|
+
brandId = null,
|
|
130
|
+
otherPresIds = null,
|
|
131
|
+
}) {
|
|
132
|
+
if (!presId) throw new Error("insertSlideLive: presId is required");
|
|
133
|
+
assertSearchId("insertSlideLive", searchId);
|
|
134
|
+
if (!Array.isArray(selections) || selections.length === 0) {
|
|
135
|
+
throw new Error("insertSlideLive: selections (number[]) is required");
|
|
136
|
+
}
|
|
137
|
+
const be = backendURL || (await resolveBackendForPres(presId));
|
|
138
|
+
// Live insert is a MUTATION, so it carries an operationId for the add-in's
|
|
139
|
+
// runMutationOnce dedup (the slide-insert handler keys an LRU on it). Generate
|
|
140
|
+
// it ONCE here, before engineOpWithPaneWait — the body is built once and
|
|
141
|
+
// reused across the wrapper's single pane_suspended retry, so the SAME id
|
|
142
|
+
// rides both attempts and a retry can never double-insert. Mirrors how
|
|
143
|
+
// buildAndPush makes one push operationId reused across attempts.
|
|
144
|
+
const operationId = `insert-${randomUUID()}`;
|
|
145
|
+
const body = { searchId, selections, operationId };
|
|
146
|
+
if (typeof at === "number") body.at = at;
|
|
147
|
+
return engineOpWithPaneWait(
|
|
148
|
+
presId,
|
|
149
|
+
be,
|
|
150
|
+
async (pid, b) => {
|
|
151
|
+
const res = await bridgeFetch(
|
|
152
|
+
`/api/cli/presentations/${encodeURIComponent(pid)}/engine/slide-insert`,
|
|
153
|
+
{
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "content-type": "application/json" },
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
timeoutMs:
|
|
158
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_ENGINE_TIMEOUT_MS || "90000", 10) ||
|
|
159
|
+
90000,
|
|
160
|
+
},
|
|
161
|
+
b,
|
|
162
|
+
);
|
|
163
|
+
// Tag the window the insert actually landed in: after a retarget it is no
|
|
164
|
+
// longer the presId the caller passed, and the caller reports it.
|
|
165
|
+
return { ...(await res.json()), presId: pid }; // { ok, insertedCount, insertedSlideIds, presId }
|
|
166
|
+
},
|
|
167
|
+
// A vanished presId is recoverable HERE (unlike the engine reads, whose
|
|
168
|
+
// 404s are about the search record): the insert never landed, so re-listing
|
|
169
|
+
// and retrying against the re-registered window is safe — and the same
|
|
170
|
+
// operationId rides along, so even an ambiguous first attempt can't
|
|
171
|
+
// double-insert.
|
|
172
|
+
{ fileName, brandId, otherPresIds, retargetOnMissing: true },
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Reuse chosen slide-search results live when the search record is the
|
|
177
|
+
// BACKEND's rather than a pane's.
|
|
178
|
+
//
|
|
179
|
+
// The pane's slide-insert handler resolves a selection out of the in-browser
|
|
180
|
+
// search context IT minted (a numeric id); a searchId from the pane-free search
|
|
181
|
+
// is server-owned and opaque, so relaying it would answer `searchId_required`.
|
|
182
|
+
// The two legs `add-slide --search` + `push` already run therefore do the job in
|
|
183
|
+
// the other order: extract each pick pane-free, then hand the BYTES to the
|
|
184
|
+
// window's own insert route. Live insert stays pane-bound (Office.js is the only
|
|
185
|
+
// thing that can type into an open deck) — only the searchId resolution moves.
|
|
186
|
+
export async function insertSlidesFromSearch({
|
|
187
|
+
presId,
|
|
188
|
+
searchId,
|
|
189
|
+
selections,
|
|
190
|
+
at = null,
|
|
191
|
+
backendURL = null,
|
|
192
|
+
engineBackendURL = null,
|
|
193
|
+
fileName = null,
|
|
194
|
+
brandId = null,
|
|
195
|
+
otherPresIds = null,
|
|
196
|
+
onProgress = null,
|
|
197
|
+
}) {
|
|
198
|
+
if (!presId) throw new Error("insertSlidesFromSearch: presId is required");
|
|
199
|
+
assertSearchId("insertSlidesFromSearch", searchId);
|
|
200
|
+
if (!Array.isArray(selections) || selections.length === 0) {
|
|
201
|
+
throw new Error("insertSlidesFromSearch: selections (number[]) is required");
|
|
202
|
+
}
|
|
203
|
+
const be = backendURL || (await resolveBackendForPres(presId));
|
|
204
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-reuse-"));
|
|
205
|
+
try {
|
|
206
|
+
let insertedCount = 0;
|
|
207
|
+
let livePresId = presId;
|
|
208
|
+
let liveBackendURL = be;
|
|
209
|
+
const insertedSlideIds = [];
|
|
210
|
+
for (const [i, selection] of selections.entries()) {
|
|
211
|
+
const slidePath = path.join(dir, `pick-${selection}.pptx`);
|
|
212
|
+
const extracted = await extractSlideToFile({
|
|
213
|
+
searchId,
|
|
214
|
+
selection,
|
|
215
|
+
destPath: slidePath,
|
|
216
|
+
backendURL: engineBackendURL,
|
|
217
|
+
brandId,
|
|
218
|
+
onProgress,
|
|
219
|
+
});
|
|
220
|
+
if (!extracted?.bytesWritten) {
|
|
221
|
+
throw new Error(`selection ${selection} produced no slide bytes`);
|
|
222
|
+
}
|
|
223
|
+
// One insert per pick, each with its own dedup key, so a retry inside
|
|
224
|
+
// engineOpWithPaneWait re-sends THAT pick and nothing else. `at` counts
|
|
225
|
+
// forward so the picks land in the order they were named.
|
|
226
|
+
const operationId = `insert-${randomUUID()}`;
|
|
227
|
+
const base64 = fs.readFileSync(slidePath).toString("base64");
|
|
228
|
+
const resp = await engineOpWithPaneWait(
|
|
229
|
+
livePresId,
|
|
230
|
+
liveBackendURL,
|
|
231
|
+
async (pid, b) => {
|
|
232
|
+
const ack = await insertSlides({
|
|
233
|
+
presId: pid,
|
|
234
|
+
base64,
|
|
235
|
+
insertAt: typeof at === "number" ? at + i : null,
|
|
236
|
+
sourceSlideIds: extracted.slideId ? [extracted.slideId] : null,
|
|
237
|
+
operationId,
|
|
238
|
+
backendURL: b,
|
|
239
|
+
});
|
|
240
|
+
return { ...ack, presId: pid, backendURL: b };
|
|
241
|
+
},
|
|
242
|
+
{ fileName, brandId, otherPresIds, retargetOnMissing: true },
|
|
243
|
+
);
|
|
244
|
+
// A retarget moves every remaining pick to the window that answered, so
|
|
245
|
+
// a multi-pick reuse cannot end up split across two presentations.
|
|
246
|
+
livePresId = resp.presId ?? livePresId;
|
|
247
|
+
liveBackendURL = resp.backendURL ?? liveBackendURL;
|
|
248
|
+
const landed = Array.isArray(resp.insertedSlideIds)
|
|
249
|
+
? resp.insertedSlideIds
|
|
250
|
+
: [];
|
|
251
|
+
insertedCount += landed.length || 1;
|
|
252
|
+
insertedSlideIds.push(...landed);
|
|
253
|
+
bestEffortRm(slidePath);
|
|
254
|
+
}
|
|
255
|
+
return { ok: true, insertedCount, insertedSlideIds, presId: livePresId };
|
|
256
|
+
} finally {
|
|
257
|
+
bestEffortRm(dir);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Remove a temp file/dir that bytes were JUST streamed into. Windows may still
|
|
262
|
+
// hold a handle on it at cleanup time (antivirus scanning the fresh write), so
|
|
263
|
+
// plain rmSync races into EBUSY/ENOTEMPTY there — retry, and swallow what's
|
|
264
|
+
// left: cleanup failure must never mask an operation that succeeded.
|
|
265
|
+
export function bestEffortRm(p) {
|
|
266
|
+
try {
|
|
267
|
+
fs.rmSync(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Fetch a chosen slide-search result's thumbnail/preview as a data URI for
|
|
272
|
+
// on-demand viewing (the agent doesn't have to extract the whole .pptx just to
|
|
273
|
+
// look at a slide). Relayed through the pane.
|
|
274
|
+
//
|
|
275
|
+
// With NO presId and a resolved `backendURL`/`brandId` this reads the pane-free
|
|
276
|
+
// `GET …/results/:selection/thumbnail`, which STREAMS: a thumbnail is small, so
|
|
277
|
+
// it is buffered back into the same `{ dataUri }` contract rather than forcing
|
|
278
|
+
// every caller to grow a file path.
|
|
279
|
+
export async function viewSlide({
|
|
280
|
+
presId = null,
|
|
281
|
+
searchId,
|
|
282
|
+
selection,
|
|
283
|
+
backendURL = null,
|
|
284
|
+
brandId = null,
|
|
285
|
+
}) {
|
|
286
|
+
const direct = !presId && Boolean(backendURL || brandId);
|
|
287
|
+
if (!presId && !direct) throw new Error("viewSlide: presId is required");
|
|
288
|
+
assertSearchId("viewSlide", searchId);
|
|
289
|
+
if (typeof selection !== "number") {
|
|
290
|
+
throw new Error("viewSlide: selection (number) is required");
|
|
291
|
+
}
|
|
292
|
+
if (direct) {
|
|
293
|
+
const b = await directEngineBackend("viewSlide", { backendURL, brandId });
|
|
294
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-thumb-"));
|
|
295
|
+
const tmp = path.join(dir, "thumbnail.bin");
|
|
296
|
+
try {
|
|
297
|
+
const res = await bridgeStreamToFile(
|
|
298
|
+
resultRoute(searchId, selection, "thumbnail"),
|
|
299
|
+
{
|
|
300
|
+
method: "GET",
|
|
301
|
+
headers: await graphHeaders(b),
|
|
302
|
+
timeoutMs: engineTimeoutMs(),
|
|
303
|
+
},
|
|
304
|
+
tmp,
|
|
305
|
+
b,
|
|
306
|
+
);
|
|
307
|
+
if (!res.streamed) {
|
|
308
|
+
// An add-in that answers with an inline preview passes straight through.
|
|
309
|
+
return res.body ? JSON.parse(res.body) : { ok: false };
|
|
310
|
+
}
|
|
311
|
+
const mimeType =
|
|
312
|
+
String(res.headers?.["content-type"] ?? "").split(";")[0].trim() ||
|
|
313
|
+
"image/png";
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
mimeType,
|
|
317
|
+
dataUri: `data:${mimeType};base64,${fs.readFileSync(tmp).toString("base64")}`,
|
|
318
|
+
};
|
|
319
|
+
} finally {
|
|
320
|
+
bestEffortRm(dir);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const be = backendURL || (await resolveBackendForPres(presId));
|
|
324
|
+
return engineOpWithPaneWait(presId, be, async (pid, b) => {
|
|
325
|
+
const res = await bridgeFetch(
|
|
326
|
+
`/api/cli/presentations/${encodeURIComponent(pid)}/engine/slide-view`,
|
|
327
|
+
{
|
|
328
|
+
method: "POST",
|
|
329
|
+
headers: { "content-type": "application/json" },
|
|
330
|
+
body: JSON.stringify({ searchId, selection }),
|
|
331
|
+
timeoutMs:
|
|
332
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_ENGINE_TIMEOUT_MS || "90000", 10) ||
|
|
333
|
+
90000,
|
|
334
|
+
},
|
|
335
|
+
b,
|
|
336
|
+
);
|
|
337
|
+
return res.json(); // { ok, dataUri, mimeType }
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Read the ORIGINAL deck a chosen slide-search result came from — NOT the open
|
|
342
|
+
// window (that is getPresentationContent below). Answers "what else is in the
|
|
343
|
+
// presentation this slide belongs to?": the surrounding narrative, the slides
|
|
344
|
+
// either side, the argument the source deck was making.
|
|
345
|
+
//
|
|
346
|
+
// The add-in reads it out of the slide index rather than downloading the source
|
|
347
|
+
// file, so this is fast and works the same for blob- and SharePoint-backed
|
|
348
|
+
// results — but the text is the last SYNCED extraction, not the live file. The
|
|
349
|
+
// response is Markdown, and the pane never carries it: the relay route resolves
|
|
350
|
+
// the selection through the pane and performs the backend read itself.
|
|
351
|
+
//
|
|
352
|
+
// searchId is presentation-scoped on the pane path, so that path relays through
|
|
353
|
+
// the window the search ran on, like viewSlide. Idempotent, so the
|
|
354
|
+
// pane_suspended retry is safe. With NO presId and a resolved
|
|
355
|
+
// `backendURL`/`brandId` it reads the pane-free
|
|
356
|
+
// `GET …/results/:selection/source-presentation` instead (the searchId is
|
|
357
|
+
// server-owned there, so no window is involved at all).
|
|
358
|
+
export async function readSourcePresentation({
|
|
359
|
+
presId = null,
|
|
360
|
+
searchId,
|
|
361
|
+
selection,
|
|
362
|
+
fromSlide = null,
|
|
363
|
+
toSlide = null,
|
|
364
|
+
backendURL = null,
|
|
365
|
+
brandId = null,
|
|
366
|
+
}) {
|
|
367
|
+
const direct = !presId && Boolean(backendURL || brandId);
|
|
368
|
+
if (!presId && !direct) {
|
|
369
|
+
throw new Error("readSourcePresentation: presId is required");
|
|
370
|
+
}
|
|
371
|
+
assertSearchId("readSourcePresentation", searchId);
|
|
372
|
+
if (typeof selection !== "number") {
|
|
373
|
+
throw new Error("readSourcePresentation: selection (number) is required");
|
|
374
|
+
}
|
|
375
|
+
// Both paths answer with Markdown plus the counts/truncation in headers.
|
|
376
|
+
const shape = async (res) => ({
|
|
377
|
+
ok: true,
|
|
378
|
+
markdown: await res.text(),
|
|
379
|
+
truncated: res.headers?.["x-slidemind-truncated"] === "1",
|
|
380
|
+
slideCount: Number(res.headers?.["x-slidemind-slide-count"] ?? 0) || 0,
|
|
381
|
+
totalSlideCount:
|
|
382
|
+
Number(res.headers?.["x-slidemind-total-slide-count"] ?? 0) || 0,
|
|
383
|
+
presentationTitle: res.headers?.["x-slidemind-presentation-title"]
|
|
384
|
+
? decodeURIComponent(String(res.headers["x-slidemind-presentation-title"]))
|
|
385
|
+
: undefined,
|
|
386
|
+
});
|
|
387
|
+
if (direct) {
|
|
388
|
+
// The direct route table carries source-presentation, so engineRoutes is the
|
|
389
|
+
// capability that gates it here (sourcePresentation gates the pane route).
|
|
390
|
+
const b = await directEngineBackend("readSourcePresentation", {
|
|
391
|
+
backendURL,
|
|
392
|
+
brandId,
|
|
393
|
+
});
|
|
394
|
+
const params = new URLSearchParams();
|
|
395
|
+
if (typeof fromSlide === "number") params.set("fromSlide", String(fromSlide));
|
|
396
|
+
if (typeof toSlide === "number") params.set("toSlide", String(toSlide));
|
|
397
|
+
const res = await bridgeFetch(
|
|
398
|
+
resultRoute(searchId, selection, "source-presentation", params),
|
|
399
|
+
{
|
|
400
|
+
method: "GET",
|
|
401
|
+
headers: { accept: "text/markdown, text/plain" },
|
|
402
|
+
timeoutMs: engineTimeoutMs(),
|
|
403
|
+
},
|
|
404
|
+
b,
|
|
405
|
+
);
|
|
406
|
+
return shape(res);
|
|
407
|
+
}
|
|
408
|
+
const be = backendURL || (await resolveBackendForPres(presId));
|
|
409
|
+
if (!(await backendSupportsSourcePresentation(be))) {
|
|
410
|
+
const err = new Error(
|
|
411
|
+
"This slidemind add-in cannot read a result's source presentation yet. Update the add-in, then try again.",
|
|
412
|
+
);
|
|
413
|
+
err.code = "source_presentation_unsupported";
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
const body = { searchId, selection };
|
|
417
|
+
if (typeof fromSlide === "number") body.fromSlide = fromSlide;
|
|
418
|
+
if (typeof toSlide === "number") body.toSlide = toSlide;
|
|
419
|
+
return engineOpWithPaneWait(presId, be, async (pid, b) => {
|
|
420
|
+
const res = await bridgeFetch(
|
|
421
|
+
`/api/cli/presentations/${encodeURIComponent(pid)}/source-presentation`,
|
|
422
|
+
{
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: {
|
|
425
|
+
"content-type": "application/json",
|
|
426
|
+
accept: "text/markdown, text/plain",
|
|
427
|
+
},
|
|
428
|
+
body: JSON.stringify(body),
|
|
429
|
+
timeoutMs: engineTimeoutMs(),
|
|
430
|
+
},
|
|
431
|
+
b,
|
|
432
|
+
);
|
|
433
|
+
return shape(res);
|
|
434
|
+
});
|
|
435
|
+
}
|