slidemind 0.1.4 → 0.1.6

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.
@@ -18,11 +18,32 @@ So this file owns only what is genuinely CLI-specific: the **host precondition**
18
18
  This skill drives the `slidemind` CLI + its MCP server (`slidemind_list_open_presentations`, `slidemind_push_deck`, `slidemind_search_engine`, `slidemind_download_result`). The CLI reaches PowerPoint through the add-in's **cloud relay** — it doesn't run any local server. Patch what's missing yourself, but the **pairing** step needs the user once (they hold the add-in's identity):
19
19
 
20
20
  - **CLI missing** (`slidemind` not on `PATH`, or the `slidemind_*` MCP tools absent): `npm install -g slidemind`, then `slidemind init --for <claude|codex|opencode|all>` (registers the MCP server + (re)deploys this skill). The harness may need a restart for the `slidemind_*` tools to appear.
21
- - **Not paired** (a command says *slidemind is not paired*): this is the one step you can't do for the user. Tell them: in the slidemind add-in click **"Connect CLI"**, then paste the shown `slidemind pair …` command into this terminal. It's one-time per environment (the code carries that backend's URL + a session token — no hardcoded URLs, no separate login; the add-in's own session is the auth). `slidemind whoami` lists every paired environment.
21
+ - **Not paired** (a command says *slidemind is not paired*): the one step you can't do for the user — they hold the add-in's identity. Walk them through **Pairing the CLI with the add-in** below (it carries the restart step the in-app wizard never mentions). `slidemind whoami` lists every paired environment.
22
22
  - `npx slidemind <command>` runs one-off without a global install; `slidemind --version` confirms PATH; `slidemind update` upgrades (`--ref <version>` to pin).
23
23
 
24
24
  After pairing once: **CLI paired + the add-in open ≡ it just works**, against whatever environment (dev / prod / a client deployment) the add-in is wired to. The CLI is **multi-environment**: pair once per backend (local / dev / prod) and every command fans out across all of them, tags each open window with its `env`, and routes to the right backend automatically — no manual switching. (Scope to one with `--backend <url|env>` when you need to.)
25
25
 
26
+ ## Pairing the CLI with the add-in (one-time; you guide the user)
27
+
28
+ Pairing is the **only** step that genuinely needs the user — the pairing code carries the add-in's own session, so only they can mint it. When a command fails with *slidemind is not paired* (or `slidemind whoami` lists nothing for this machine), don't retry blindly: walk the user through the steps below, then resume. It's one-time per environment; once paired, `slidemind whoami` shows it.
29
+
30
+ **1 — Mint a pairing code in the add-in.** Tell the user: in PowerPoint, open the **slidemind add-in** task pane → **Settings** → **General** → the **Connect CLI** section (it's marked *Preview*) → press **Pair**. That opens a small 3-step wizard with copy buttons:
31
+ 1. `npm install -g slidemind` — skip if the CLI is already installed.
32
+ 2. pick the coding agent(s) → the wizard prints the exact `slidemind init --for <…>` — skip if you already ran `init`.
33
+ 3. a **one-time** `slidemind pair <code>` command — this is the one that matters.
34
+
35
+ **2 — Run the pair command in this terminal.** Have the user copy the step-3 `slidemind pair <code>` line and paste it here; run it. The code is single-use and expires (~10 min) — if it fails as expired/invalid, have them press **Pair** again for a fresh one. On success the add-in's Connect CLI panel flips to **Connected**.
36
+
37
+ **3 — Restart the add-in once (the wizard does NOT tell the user this).** Pairing flips CLI integration on, but the task pane that's *already running* often won't pick up the live connection — so the freshly-paired window may not show up yet. **If, right after pairing, `slidemind presentations` still doesn't list the window** (or a push/search behaves as though nothing is connected) even though PowerPoint is open with the add-in loaded and a corporate design active, tell the user to **restart the add-in**: close the slidemind task pane and reopen it from the ribbon; if the window still doesn't appear, close and reopen the presentation (or PowerPoint). A fresh pane re-reads the now-enabled pairing and registers itself so the CLI can see it. Then re-run `slidemind presentations`. This is a one-time nudge right after the first pairing — once the window appears it stays, and the add-in can't trigger this restart itself, so you have to ask for it.
38
+
39
+ **Two states, two different fixes — don't conflate them:**
40
+
41
+ | What you see | What it means | What to tell the user |
42
+ |---|---|---|
43
+ | *slidemind is not paired* (or empty `whoami`) | this machine has no session | mint a code via **Connect CLI** and run `slidemind pair <code>` (steps 1–2) |
44
+ | `slidemind presentations` is **empty**, but PowerPoint is open with the add-in + a CD active, **and** this machine is already paired | the pane hasn't registered on the relay — typically a just-paired pane that needs a fresh boot | **restart the add-in** (step 3), then retry |
45
+ | `slidemind presentations` is **empty** and no PowerPoint is connected | nothing to talk to | open PowerPoint, load the add-in, pick/create a corporate design |
46
+
26
47
  ## Precondition gate (check this first, every time)
27
48
 
28
49
  Authoring and rendering hard-require an **open PowerPoint host with the slidemind add-in loaded and an active corporate design**. Without a host there is no bundle to read and no renderer to push to; `download-bundle`, `build-pptx`, and `push` all fail.
@@ -31,7 +52,7 @@ Authoring and rendering hard-require an **open PowerPoint host with the slidemin
31
52
  2. One registered → that's your target `presentation_id`. **Several → ask the user which**, by `title`/`fileName` (never raw ids) — once for **which presentation's corporate design to download**, and again at render time for **which presentation to push into** (the CD source and the push target may be different presentations).
32
53
  3. **None registered → stop.** Tell the user: *open PowerPoint, load the slidemind add-in, and pick or create a corporate design, then retry.*
33
54
 
34
- > **Empty list vs. not paired — different fixes.** An *empty* registry means no PowerPoint is connected (step 3 → ask the user to open it / enable Connect CLI). A *not paired* error means this machine has no session — ask the user to pair once via the add-in's **"Connect CLI"** (see Bootstrap); don't ask them to open PowerPoint for that.
55
+ > **Empty list vs. not paired — different fixes** (full triage in *Pairing the CLI with the add-in* above). An *empty* registry with no PowerPoint connected means open it / enable Connect CLI (step 3). A *not paired* error means this machine has no session — pair once via the add-in's **"Connect CLI"**; don't ask them to open PowerPoint for that. An *empty* registry **while** PowerPoint is open with the add-in **and** this machine is already paired usually means the just-paired pane never registered — ask the user to **restart the add-in** (close + reopen the task pane), then retry.
35
56
 
36
57
  **Backgrounded PowerPoint is expected to work.** Do not pre-emptively ask the user to focus PowerPoint or the task pane, and never ask them to keep it foregrounded while something runs. The render itself (`build-pptx`, the render half of `push`) **never needs the task pane awake** — it captures the slides with a local headless browser and assembles the `.pptx` server-side — streams its own `rendering slide n/m…` progress, and survives long decks. Only the pane-touching ops (the insert half of `push`, engine search/download) need the pane, and the CLI handles that itself: it waits up to ~2 min, prints one *"click the slidemind pane"* line when user help is genuinely needed, and retries safely. If you see that line, relay it once and keep waiting — do not rebuild, re-plan, or babysit. If a `push` still times out, the rendered `.pptx` was saved next to the deck (`paneSuspended` + `fallbackFile` in the result) — nothing is lost; retry the push after the user clicks the pane. Reserve the full "open PowerPoint…" instruction in (3) for when nothing is registered at all.
37
58
 
@@ -57,12 +78,13 @@ Keep planning about intent and content. Don't materialize the bundle or read bra
57
78
  3. **Study brand samples (before composing).** The materialized bundle's `samples/` dir holds the brand's own reference slides. If it's non-empty, pick **3 at random** and **Read** each (the Read tool renders images) *before* writing any JSX — this is the local-harness stand-in for the in-app persona's reference thumbnails, and it is what keeps a deck looking on-brand rather than merely compliant. Study them for whitespace rhythm, shape density, hierarchy, accent use, and layering, then compose in that spirit (never copy a sample 1:1). This is `composition.md`'s samples discipline; skip only when `samples/` is empty.
58
79
  4. **Read, then author.** Read the four create-stage references together — `workflow/create.md` (the procedure), `workflow/composition.md` (the composition baseline), `workflow/layouts.md` (palette + working area), and `workflow/authoring.md` (the substrate rules) — plus the deck's `design.md` and the specific `layouts/*.jsx` the plan uses. (That is the read set `workflow/create.md` → Context loading prescribes; follow it as written.) Then write `decks/<slug>/slides/NN-<topic>.jsx` — zero-padded numeric prefix from the first file on (play order is filename order here; see the slide-file row in the command mapping), one default export per slide, composing one bundle layout (Mode A) or a blank-content layout with freeform children (Mode B), exactly as those docs prescribe.
59
80
  5. **Imagery from the engine.** Per the `### Brand Image Sources` block in `design.md`, search the brand engine and place results. The engine search/reuse mechanics live in **`slidemind-search`** — use it to find and fetch imagery; the create-only icon-recolour detail is in *Recolouring engine icons* below. To pull in an **existing whole slide** (not just an image), use `slidemind-search`'s `add-slide` — it folds the slide into `decks/<slug>/slides/` as a native `.pptx` that coexists with your JSX in play order and survives re-push.
60
- 6. **Preview review, then push.** This is `build_and_push` in `workflow/build-and-export.md`, including its preview review gate — the local harness has a native port: before the FIRST export of new slides, run `slidemind review <deck>`. It builds the HTML preview, opens it in the browser with a spatial comment overlay, and **blocks until the user decides** (host-free — no open PowerPoint needed for the gate). The command then prints its verdict to stdout:
81
+ 6. **Look at your own draft first (you, the agent).** This is `see_draft` in `workflow/build-and-export.md` — the agent's pre-check, *before* any human sees the deck. The local-harness port is `slidemind view-draft <deck>` (MCP `slidemind_view_draft({ deck_path })`): it renders each authored `slides/*.jsx` to a per-slide PNG and hands the images back to you — **nothing is pushed, no `.pptx` is written, no review gate opens**. Read every image the way the audience will, fix anything visibly wrong **in the JSX** (re-express, never delete), and view again until each slide is sound. One round usually suffices. It runs locally against the materialized bundle, so it needs no awake pane and no open host once the bundle has been downloaded once. Two faithful limits to know: a slide on a **purpose-built (Mode A) layout** (cover/divider/agenda) and a **reused native `.pptx`** slide have no freeform region to capture, so they come back as a status line, not an image — judge those at the full-deck `review`/`build` step below. Do this self-check before the human review gate; don't push a draft you haven't looked at.
82
+ 7. **Preview review, then push.** This is `build_and_push` in `workflow/build-and-export.md`, including its preview review gate — the local harness has a native port: before the FIRST export of new slides, run `slidemind review <deck>`. It builds the HTML preview, opens it in the browser with a spatial comment overlay, and **blocks until the user decides** (host-free — no open PowerPoint needed for the gate). The command then prints its verdict to stdout:
61
83
  - `DECISION: approved` → proceed to the export the user actually asked for: `slidemind push <deck>` (into PowerPoint) or `slidemind build-pptx <deck>` (to a file).
62
84
  - `DECISION: comments` → the user left spatially-anchored notes, each citing a `slides/<file>.jsx:line:col`, the source line, and the click position (plus a readonly-layout note where the click landed on a layout slot). **Edit the cited files in place at the given line**, then **re-run `slidemind review <deck>`** — the prior round shows as read-only ghost pins so the user can confirm each note was addressed. Loop until approved. Nothing is exported on a comments round.
63
85
 
64
86
  `slidemind review` is the only step that pauses; `push`/`build-pptx` never pause — they render pane-free (local capture + server-side assembly, with live per-slide progress). One approval covers the task: push follow-up fixes without re-running review unless the user wants another look. If several presentations are open, confirm **which one to push into** first (it can differ from the CD source). After approval, run the push without asking; it works while PowerPoint is backgrounded, and the CLI itself waits + retries if the pane is needed (see the precondition gate). If a push still times out, the rendered `.pptx` is saved next to the deck — re-run `push` after the user clicks the pane, without rebuilding.
65
- 7. **Sanity pass.** Run the checks in `workflow/create.md` before reporting done.
87
+ 8. **Sanity pass.** Run the checks in `workflow/create.md` before reporting done.
66
88
 
67
89
  ## Recolouring engine icons (create-authoring specifics)
68
90
 
@@ -83,11 +105,13 @@ The bundle's `workflow/*.md` are written for the in-app design agent and name **
83
105
  | In `workflow/*.md` (in-app agent) | Your equivalent (local harness) |
84
106
  |---|---|
85
107
  | `build_and_push` | `slidemind push <deck>` (into the open PowerPoint) · `slidemind build-pptx <deck>` (to a `.pptx` file) — both render server-side via the bridge |
108
+ | `read_presentation_context` (read the live deck) | `slidemind read [presId] [--mode summary\|raw]` · MCP `slidemind_read_presentation({ presentation_id?, mode? })` — pull the current content of an open presentation (`summary` = per-slide outline; `raw` = each slide's plain text) to inspect what's there before editing. Plain text, not authoring markup |
86
109
  | `engine_search` / `engine_add` / `insert_image` | **`slidemind-search`** owns the engine search + reuse mechanics (`slidemind_search_engine` → `slidemind_download_result`, plus whole-slide reuse via `add-slide`). For placing a fetched icon in your JSX, pass `icon_color` / batch with `selections: [{ selection, icon_color }, …]` and colour per *Recolouring engine icons* below |
87
110
  | writing `slides/<topic>.jsx` in the chat workspace — unprefixed kebab-case names; play order = the ordered list passed to `build_and_push` | write `decks/<slug>/slides/NN-<topic>.jsx` on disk with a **zero-padded numeric prefix** (`01-cover.jsx`, `02-agenda.jsx`, …) — over the CLI play order is **numeric-aware filename order**; there is no ordered-list channel, so reordering = renaming files. The bundle's "don't pad with index prefixes" rule is in-app-only; here it inverts |
88
111
  | reading workspace files (`design.md`, `layouts/`, `workflow/*`) | read them from the materialized bundle dir (`<bundle>/…`) and your `decks/<slug>/` |
89
112
  | reference-slide thumbnails injected at `add_slide` (in-app persona) | **Read** 3 random images from the materialized bundle's `samples/` dir — the Read tool renders them (see *The create flow* step 3) |
90
113
  | `present_plan_for_review` (manager-owned) | **you** run intake → plan with the user (above); persist `decks/<slug>/plan.md` for decks |
114
+ | `see_draft` (agent looks at its OWN draft before pushing) | `slidemind view-draft <deck>` · MCP `slidemind_view_draft({ deck_path })` — renders each `slides/*.jsx` to a per-slide PNG and returns the images to **you**; **no push, no `.pptx`, no review gate**. Look, fix the JSX, view again. Host-free once the bundle is materialized. Freeform slides only — purpose-built (Mode A) and reused native `.pptx` slides report as a status line (judge those at `review`/`build`). This is the agent self-check; `slidemind review` is the *human* gate |
91
115
  | the `build_and_push` **preview review gate** (pause on rendered previews → approve / per-element comments) | `slidemind review <deck>` — a native port of the same gate: serves the HTML preview with a spatial comment overlay and **blocks until the user decides**, then prints `DECISION: approved` or `DECISION: comments` (each comment cited to `slides/<file>.jsx:line:col`). On approve → run the export (`slidemind push` / `build-pptx`); on comments → edit the cited files and re-run `slidemind review` (prior round shows as ghost pins). One approval covers the task. `push`/`build-pptx` never pause; only `review` does. |
92
116
 
93
117
  Everything else in `workflow/*.md` — the composition craft, the substrate/authoring rules, the layout discovery and slot/working-area rules, the sanity bar — applies to you unchanged.
@@ -14,9 +14,10 @@ So this file owns only what is genuinely CLI-specific: the **host precondition**
14
14
  This skill drives the `slidemind` CLI + its MCP server (`slidemind_list_open_presentations`, `slidemind_list_engines`, `slidemind_search_engine`, `slidemind_view_slide`, `slidemind_insert_slide`, `slidemind_add_slide_to_deck`, `slidemind_download_result`). The CLI reaches PowerPoint through the add-in's **cloud relay** — it doesn't run any local server. Patch what's missing yourself; the **pairing** step needs the user once (they hold the add-in's identity):
15
15
 
16
16
  - **CLI missing** (`slidemind` not on `PATH`, or the `slidemind_*` MCP tools absent): `npm install -g slidemind`, then `slidemind init --for <claude|codex|opencode|all>` (registers the MCP server + (re)deploys this skill). The harness may need a restart for the `slidemind_*` tools to appear.
17
- - **Not paired** (a command says *slidemind is not paired*): the one step you can't do for the user. Tell them: in the slidemind add-in click **"Connect CLI"**, then paste the shown `slidemind pair …` command into this terminal. It's one-time per environment. `slidemind whoami` lists every paired environment.
17
+ - **Not paired** (a command says *slidemind is not paired*): the one step you can't do for the user. Tell them: in the slidemind add-in open **Settings → General → Connect CLI**, press **Pair**, and run the shown one-time `slidemind pair <code>` command in this terminal (the code expires in ~10 min — re-press **Pair** for a fresh one). It's one-time per environment; `slidemind whoami` lists every paired environment.
18
+ - **Restart the add-in once after pairing** — the wizard never says this. If, right after pairing, `slidemind presentations` still doesn't list the window even though PowerPoint is open with the add-in and a corporate design active, ask the user to **restart the add-in** (close + reopen the slidemind task pane; if it still doesn't appear, reopen PowerPoint) so the freshly-paired pane registers on the relay. Then retry.
18
19
 
19
- This is the same bootstrap as `slidemind-create-slides`; if you set it up there it's already set up here.
20
+ This is the same bootstrap as `slidemind-create-slides` (whose **Pairing the CLI with the add-in** section has the full step-by-step + triage table); if you set it up there it's already set up here.
20
21
 
21
22
  ## Precondition gate (check this first, every time)
22
23
 
@@ -26,7 +27,7 @@ Engine search and slide reuse are **pane-touching, host-required** operations
26
27
  2. **≥1 registered → you're set.** The engine catalog is **per-user, not bound to a window**, so search and discovery attach to **any** connected window automatically. **Never ask the user which window to search with**, and don't pass `--to`/`presentation_id` to search just to disambiguate — leave it off and let the CLI grab one. (A window only matters when you *insert a slide live*, and even then it's simply the window the search already ran on — see Reuse.)
27
28
  3. **None registered → stop.** Tell the user: *open PowerPoint, load the slidemind add-in, and pick or create a corporate design, then retry.*
28
29
 
29
- > **Empty list vs. not paired — different fixes.** An *empty* registry means no PowerPoint is connected (ask the user to open it / enable Connect CLI). A *not paired* error means this machine has no session — ask the user to pair once via **"Connect CLI"** (see Bootstrap). They are different problems; don't conflate them.
30
+ > **Empty list vs. not paired — different fixes.** An *empty* registry with no PowerPoint connected means open it / enable Connect CLI. A *not paired* error means this machine has no session — pair once via **"Connect CLI"** (see Bootstrap). An *empty* registry **while** PowerPoint is open with the add-in **and** this machine is already paired usually means a just-paired pane that never registered — ask the user to **restart the add-in** (see Bootstrap), then retry. Three different problems; don't conflate them.
30
31
 
31
32
  The pane-touching ops here (`search`, `view_slide`, `insert-slide`, `download-result`) wait for the pane themselves: the CLI waits up to ~2 min, prints one *"click the slidemind pane"* line when user help is genuinely needed, and retries safely. Relay that line once and keep waiting — don't re-plan or babysit.
32
33
 
@@ -49,6 +50,8 @@ A found slide can be reused two ways. Both extract the picked result(s) as nativ
49
50
 
50
51
  > **The durable rule:** a slide folded into a deck with `add-slide` is **deck source-of-truth**. The render pipeline splices native `slides/*.pptx` into the deck **by play-order filename**, alongside any authored `.jsx`, so a reused slide **survives every `build-pptx` and `push` re-render** — it is never lost on re-push. Reorder it the same way you reorder JSX: rename its numeric prefix.
51
52
 
53
+ > **Building a `.pptx` file from found slides → one combined deck, always.** When the user wants the reused slides as a **`.pptx` file** (rather than a live insert), collect **every** picked slide into **one** deck and render **one** file — never one `.pptx` per slide and never a deck per slide. Fold all picks into the same deck — pass every selection in a single `add-slide` call (`--pick n1,n2,…`), repeating only to cover picks that came from different `searchId`s (always the same `--deck`) — then run `slidemind build-pptx <deck>` once. The render splices every native `slides/*.pptx` into a **single `decks/<slug>/<slug>.pptx`** in play-order, so all the found slides land in one file. That combined `.pptx` is the deliverable; hand back its path. (Need new authored slides interleaved with the reused ones in that same file? That's still one deck — hand off to `slidemind-create-slides` to add the JSX, then `build-pptx` the whole deck once.)
54
+
52
55
  For brand **imagery** (icons/images, not whole slides), the reuse step is `slidemind_download_result` — it writes the asset into the deck's `assets/`. That mechanism (including the icon-recolor descriptor) is documented where it's used, in `slidemind-create-slides`.
53
56
 
54
57
  ## Working with slidemind-create-slides
@@ -1,5 +1,9 @@
1
1
  // Render a deck server-side via the bridge, then push the bytes into a running
2
- // PowerPoint window (or write them to a file).
2
+ // PowerPoint window (or write them to a file). Also hosts `viewDraft` — the
3
+ // push-free, .pptx-free draft preview (the CLI analog of the persona's
4
+ // `see_draft`): it runs the shipped capture engine locally and returns the
5
+ // per-slide preview PNGs it already produces, so the agent can look before the
6
+ // `review` gate.
3
7
  //
4
8
  // The CLI ships ZERO exporter code: the authored slide JSX + deck-local assets
5
9
  // are POSTed to the add-in's `render-pptx` route, which renders them against
@@ -17,7 +21,7 @@ import {
17
21
  pickPresentationEntry,
18
22
  renderPptx,
19
23
  replaceSlides,
20
- requireHost,
24
+ requireRenderHost,
21
25
  translateDeck,
22
26
  waitForPaneReady,
23
27
  } from "./client.mjs";
@@ -177,6 +181,181 @@ export async function renderDeckToBase64(
177
181
  return result; // { ok, base64, slideCount, warnings, skippedSlides }
178
182
  }
179
183
 
184
+ // Map ONE captured slide to a draft-view entry. The capture engine produces a
185
+ // `previewDataUrl` (the 1280×720 stage PNG) only for FREEFORM slides that go
186
+ // through the Chrome box capture — purpose-built (Mode A) layouts have no
187
+ // freeform region and so carry no preview, exactly like the add-in's see_draft.
188
+ // Degraded renders still yield a flat raster the agent can look at; lint/parse
189
+ // failures surface as a status line so the agent is never blind.
190
+ function mapCapturedSlide(fileName, cap, warnings) {
191
+ if (!cap) {
192
+ return {
193
+ fileName,
194
+ status: "missing",
195
+ note: "the capture engine produced no result for this slide",
196
+ };
197
+ }
198
+ for (const w of cap.lint?.warnLines ?? []) warnings.push(`${fileName}: ${w}`);
199
+ for (const w of cap.render?.warnings ?? []) warnings.push(`${fileName}: ${w}`);
200
+
201
+ if (cap.previewDataUrl) {
202
+ return { fileName, status: "ok", dataUri: cap.previewDataUrl };
203
+ }
204
+ // A degraded render is a non-editable raster; push/build-pptx will REJECT it.
205
+ // Attach it anyway (so the agent isn't blind) but flag the named cause.
206
+ if (cap.render?.degraded && cap.render?.fallbackPngBase64) {
207
+ return {
208
+ fileName,
209
+ status: "degraded",
210
+ dataUri: `data:image/png;base64,${cap.render.fallbackPngBase64}`,
211
+ note:
212
+ `did NOT render cleanly (degraded ${cap.render.degraded} raster, not editable shapes) — ` +
213
+ `push/build-pptx will reject it as-is. Fix the named cause in the JSX: ` +
214
+ (cap.render.errorText ?? "unknown render failure"),
215
+ };
216
+ }
217
+ if (cap.lint?.errorMessage) {
218
+ return { fileName, status: "failed", note: cap.lint.errorMessage };
219
+ }
220
+ if (cap.captureError) {
221
+ return { fileName, status: "failed", note: cap.captureError };
222
+ }
223
+ // No preview and no error: a purpose-built (Mode A) layout has no freeform
224
+ // region to capture — there is simply nothing to draft-preview for it.
225
+ return {
226
+ fileName,
227
+ status: "no-preview",
228
+ note: "no preview rendered — a purpose-built layout has no freeform region to capture (use slidemind review for a full-deck look)",
229
+ };
230
+ }
231
+
232
+ // Render a deck's authored slides to per-slide preview PNGs WITHOUT pushing,
233
+ // translating, or assembling a .pptx — the CLI analog of the add-in persona's
234
+ // `see_draft`. It runs the add-in's shipped capture engine on THIS machine's
235
+ // Chrome (the same engine the local-capture render path uses) and returns the
236
+ // `previewDataUrl` it already produces, so the agent can LOOK at its slides and
237
+ // self-correct before the user ever sees them at the `review` gate.
238
+ //
239
+ // Host-free once the bundle is materialized: the capture runs locally against
240
+ // the cached bundle, so no open PowerPoint is needed. A host is required ONLY
241
+ // when the bundle (and its capture engine) isn't cached yet — then we fetch it
242
+ // like `download-bundle` does. Returns { slides: [{fileName, dataUri?, status,
243
+ // note?}], warnings: [] }.
244
+ export async function viewDraft(
245
+ deckDir,
246
+ { presentationId = null, onProgress = null } = {},
247
+ ) {
248
+ if (!deckDir) throw new Error("viewDraft: deckDir is required");
249
+ const slidesDir = path.join(deckDir, "slides");
250
+ if (!fs.existsSync(slidesDir) || !fs.statSync(slidesDir).isDirectory()) {
251
+ throw new Error(`Expected 'slides/' folder in ${deckDir}`);
252
+ }
253
+ // Authored JSX + reused native .pptx share one play-order namespace, exactly
254
+ // as the render path sees them — walk them in that order so the draft view
255
+ // lines up with the deck the user will get.
256
+ const all = sortSlideFilenames(
257
+ fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx")),
258
+ );
259
+ if (all.length === 0) {
260
+ throw new Error(`No .jsx or .pptx slides under ${slidesDir}`);
261
+ }
262
+ const hasJsx = all.some((f) => f.endsWith(".jsx"));
263
+
264
+ // Resolve the materialized bundle. Local capture needs the bundle's capture
265
+ // engine; once it's cached this whole call is offline. Only reach for a host
266
+ // (to fetch the bundle) when there are JSX slides and no usable cached engine.
267
+ let ref = readDeckBundleRef(deckDir);
268
+ let bundleDir = resolveBundleDirForDeck(deckDir);
269
+ if (hasJsx && (!bundleDir || !bundleHasCaptureEngine(bundleDir))) {
270
+ const registry = await requireRenderHost();
271
+ const entry = pickPresentationEntry(registry, presentationId, {
272
+ preferBrandId: ref?.presFP ?? null,
273
+ });
274
+ const mat = await materializeBundle(entry.presId, {
275
+ backendURL: entry.backendURL ?? null,
276
+ preferBrandId: ref?.presFP ?? null,
277
+ });
278
+ writeDeckBundleRef(deckDir, { presFP: mat.presFP, etag: mat.etag });
279
+ bundleDir = resolveBundleDirForDeck(deckDir);
280
+ }
281
+
282
+ const warnings = [];
283
+
284
+ // A native-only deck (just reused .pptx) has no authored JSX to draft-preview.
285
+ if (!hasJsx) {
286
+ return {
287
+ slides: all.map((fileName) => ({
288
+ fileName,
289
+ status: "native",
290
+ note: "reused native slide — not authored JSX, no draft preview rendered",
291
+ })),
292
+ warnings,
293
+ };
294
+ }
295
+
296
+ if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) {
297
+ throw new Error(
298
+ "draft preview needs the corporate design's capture engine, which ships in the " +
299
+ "materialized bundle. Run `slidemind download-bundle <deck>` against an open " +
300
+ "PowerPoint host once, then retry — or use `slidemind build <deck>` for a " +
301
+ "host-free HTML preview.",
302
+ );
303
+ }
304
+
305
+ let captured;
306
+ try {
307
+ captured = await captureDeckLocally({ deckDir, bundleDir, onProgress });
308
+ } catch (err) {
309
+ throw new Error(
310
+ `local draft capture failed (${err instanceof Error ? err.message : String(err)}). ` +
311
+ "It runs a local headless Chrome; if Chrome can't launch here, use " +
312
+ "`slidemind build <deck>` for an HTML preview instead.",
313
+ );
314
+ }
315
+
316
+ const byFile = new Map((captured?.slides ?? []).map((s) => [s.fileName, s]));
317
+ const slides = all.map((fileName) =>
318
+ fileName.endsWith(".pptx")
319
+ ? {
320
+ fileName,
321
+ status: "native",
322
+ note: "reused native slide — not authored JSX, no draft preview rendered",
323
+ }
324
+ : mapCapturedSlide(fileName, byFile.get(fileName), warnings),
325
+ );
326
+
327
+ return { slides, warnings };
328
+ }
329
+
330
+ async function renderWithRetarget(deckDir, entry, onProgress) {
331
+ const run = (target) =>
332
+ renderDeckToBase64(deckDir, {
333
+ presId: target.presId,
334
+ backendURL: target.backendURL ?? null,
335
+ onProgress,
336
+ });
337
+ try {
338
+ return { entry, result: await run(entry) };
339
+ } catch (err) {
340
+ const recoverable =
341
+ err?.status === 404 || err?.code === "unknown_presentation";
342
+ if (!recoverable) throw err;
343
+ const live = await waitForPaneReady(entry.presId, {
344
+ backendURL: entry.backendURL ?? null,
345
+ fileName: entry.fileName ?? entry.title ?? null,
346
+ brandId: entry.brandId ?? null,
347
+ timeoutMs: 15000,
348
+ onWait: (seconds) =>
349
+ process.stderr.write(
350
+ "render target changed — waiting for the slidemind pane to " +
351
+ `re-register (up to ${seconds}s)…\n`,
352
+ ),
353
+ });
354
+ if (!live) throw err;
355
+ return { entry: live, result: await run(live) };
356
+ }
357
+ }
358
+
180
359
  export async function buildAndPush({
181
360
  deckDir,
182
361
  presentationId = null,
@@ -200,18 +379,15 @@ export async function buildAndPush({
200
379
  }
201
380
 
202
381
  const deckRef = readDeckBundleRef(deckDir);
203
- const registry = await requireHost();
382
+ const registry = await requireRenderHost();
204
383
  const entry = pickPresentationEntry(registry, presentationId, {
205
384
  preferBrandId: deckRef?.presFP ?? null,
206
385
  });
207
- const presId = entry.presId;
208
- const backendURL = entry.backendURL ?? null;
209
-
210
- const result = await renderDeckToBase64(deckDir, {
211
- presId,
212
- backendURL,
213
- onProgress,
214
- });
386
+ const rendered = await renderWithRetarget(deckDir, entry, onProgress);
387
+ const renderEntry = rendered.entry;
388
+ const presId = renderEntry.presId;
389
+ const backendURL = renderEntry.backendURL ?? null;
390
+ const result = rendered.result;
215
391
  const base64 = result.base64;
216
392
  const bytes = Buffer.byteLength(base64, "base64");
217
393
  const mode = replace ? "replace" : "insert";
@@ -255,8 +431,8 @@ export async function buildAndPush({
255
431
  if (!recoverable) throw err;
256
432
  const live = await waitForPaneReady(presId, {
257
433
  backendURL,
258
- fileName: entry.fileName ?? entry.title ?? null,
259
- brandId: entry.brandId ?? null,
434
+ fileName: renderEntry.fileName ?? renderEntry.title ?? null,
435
+ brandId: renderEntry.brandId ?? null,
260
436
  onWait: (seconds) =>
261
437
  process.stderr.write(
262
438
  "task pane suspended — click the slidemind pane in PowerPoint to " +
@@ -309,16 +485,16 @@ export async function buildPptxFile({
309
485
  }) {
310
486
  if (!deckDir) throw new Error("buildPptxFile: deckDir is required");
311
487
  const deckRef = readDeckBundleRef(deckDir);
312
- const registry = await requireHost();
488
+ const registry = await requireRenderHost();
313
489
  const entry = pickPresentationEntry(registry, presentationId, {
314
490
  preferBrandId: deckRef?.presFP ?? null,
315
491
  });
316
- const presId = entry.presId;
317
- const result = await renderDeckToBase64(deckDir, {
318
- presId,
319
- backendURL: entry.backendURL ?? null,
492
+ const { entry: renderEntry, result } = await renderWithRetarget(
493
+ deckDir,
494
+ entry,
320
495
  onProgress,
321
- });
496
+ );
497
+ const presId = renderEntry.presId;
322
498
  const out =
323
499
  outPath || path.join(deckDir, `${path.basename(deckDir)}.pptx`);
324
500
  fs.writeFileSync(out, Buffer.from(result.base64, "base64"));