@markdstage/markdstage 2.6.0 → 3.0.0

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/README.md CHANGED
@@ -16,8 +16,8 @@ exported PDF, or hybrid editable PowerPoint deck.
16
16
  ## Install
17
17
 
18
18
  ```console
19
- npx @markdstage/markdstage presentation slides.md
20
19
  npx @markdstage/markdstage present slides.md
20
+ npx @markdstage/markdstage preview slides.md
21
21
  npm install --global @markdstage/markdstage
22
22
  ```
23
23
 
@@ -31,8 +31,8 @@ npm install --global .\markdstage-markdstage-<version>.tgz
31
31
  ## Commands
32
32
 
33
33
  ```console
34
- markdstage presentation slides.md
35
- markdstage present slides.md --watch
34
+ markdstage present slides.md
35
+ markdstage preview slides.md --watch
36
36
  markdstage validate slides.md --json
37
37
  markdstage inspect slides.md --json
38
38
  markdstage capture slides.md --pages 2,4
@@ -45,8 +45,8 @@ markdstage skill install --target claude
45
45
 
46
46
  | Command | Description |
47
47
  | --- | --- |
48
- | `presentation` | Opens the presenter view with the current slide, next-slide preview, speaker notes, and navigation. Select **Start presentation** to open the synchronized audience-facing window; select **End presentation** to close it. `--watch` reloads on save, and `--no-open` serves the presenter URL without launching a browser. |
49
- | `present` | Serves the deck on loopback and opens the MarkdStage presenter window: navigation, presenter view, next-slide preview, speaker notes, overview, custom themes, Mermaid, Architecture DSL, and local assets. `--watch` reloads on save while preserving the current slide, keeps the last valid deck when a save is broken, and enables Architecture editing. Without `--watch`, the source is read-only. `--no-open` serves the deck only. |
48
+ | `present` | Opens the presenter view with the current slide, next-slide preview, speaker notes, and navigation. Select **Start presentation** to open the synchronized audience-facing window; select **End presentation** to close it. `--watch` reloads on save, and `--no-open` serves the presenter URL without launching a browser. |
49
+ | `preview` | Serves the deck on loopback and opens the MarkdStage preview: navigation, presenter view, next-slide preview, speaker notes, overview, custom themes, Mermaid, Architecture DSL, and local assets. `--watch` reloads on save while preserving the current slide, keeps the last valid deck when a save is broken, and enables Architecture editing. Without `--watch`, the source is read-only. `--no-open` serves the deck only. |
50
50
  | `validate` | Checks deck structure, Architecture DSL blocks, themes, and theme paths. |
51
51
  | `inspect` | Reports the same compact 1280x720 clipping diagnostics as the canvas `inspect_layout` action. `--slide <n>` limits it to one page, `--all` includes slides that fit, `--fail-on-issues` exits with code 5. |
52
52
  | `capture` | Writes 1280x720 PNG files. Without `--pages` only the slides reported as clipped are captured. |
@@ -60,7 +60,7 @@ Global options: `--workspace <dir>`, `--theme <name>`, `--theme-file <path>`,
60
60
 
61
61
  ## Architecture editing
62
62
 
63
- Run `markdstage present slides.md --watch` for the live authoring workflow. The
63
+ Run `markdstage preview slides.md --watch` for the live authoring workflow. The
64
64
  browser starts in viewing mode. Select the pencil control to move Architecture
65
65
  elements; those placement changes are saved atomically to the matching
66
66
  `architecture` fence. Select **Advanced edit** to add, update, duplicate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdstage/markdstage",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
4
4
  "description": "Present, validate, inspect, capture, and export MarkdStage Markdown decks from the command line — no Copilot canvas required.",
5
5
  "license": "MIT",
6
6
  "author": "runceel",
package/shared/README.md CHANGED
@@ -207,10 +207,31 @@ When the extension detects a presentation-related prompt, it adds a short hint
207
207
  to use `markdstage_guide` at most once per session. No hint is added after the
208
208
  tool has already been called, and session-end cleanup removes the state.
209
209
 
210
- ### Slide fragment format
210
+ ### Markdown file syntax
211
211
 
212
- Each element in open / `load_deck` `slides` is one Markdown string. It may start
213
- with front matter delimited by `---`, followed by GFM-compatible content.
212
+ A Markdown file represents the complete deck. At the top level, a line containing
213
+ only `---` after a blank line separates slides. A leading `---` block is file
214
+ front matter, and `---` inside a fenced code block is not a separator.
215
+
216
+ Use only the top-level `---` syntax for slide boundaries. Custom markers such as
217
+ `<!-- slide -->` are not slide separators and must not be used.
218
+
219
+ Do not place a top-level HTML comment immediately before a slide separator. End
220
+ the comment, leave a blank line, and then write `---`; without the blank line,
221
+ the separator may not be recognized as a slide boundary.
222
+
223
+ To have MarkdStage read and split a workspace Markdown file, use **More controls >
224
+ Open Markdown**. This file-loading workflow is separate from the Canvas API.
225
+
226
+ ### Canvas API `slides` array
227
+
228
+ For canvas `open` and `load_deck`, each element of the `slides` array is exactly
229
+ one slide. The caller must read, split, or generate the deck before invoking the
230
+ Canvas API. Do not pass a complete multi-slide Markdown file as one array element
231
+ and expect its `---` lines to be split.
232
+
233
+ Each array element is one Markdown string. It may start with front matter
234
+ delimited by `---`, followed by GFM-compatible content.
214
235
 
215
236
  | Front matter | Purpose |
216
237
  | --- | --- |
@@ -245,14 +266,6 @@ First explain the **prerequisites**.
245
266
  -->
246
267
  ```
247
268
 
248
- Write local images as `![Alternative text](/assets/foo.png)` and pass the source
249
- Markdown's workspace-relative path as `sourceName`. Lookup tries adjacent
250
- `assets/` before workspace-root `assets/`. Architecture `icon` and `image.src`
251
- use `assets/foo.svg` without a leading slash and follow the same lookup order.
252
- `sourceName` supplies this resolution base only; it does not read or watch the
253
- Markdown file.
254
- Specifically, lookup checks `assets/` beside the Markdown before `assets/` at the workspace root.
255
-
256
269
  On a standard slide, the first H1/H2 is fixed in the top title area, so its
257
270
  position does not move with body length. Later headings remain in the body.
258
271
  Specialized `title`, `section`, and `backcover` layouts retain their own
@@ -287,6 +300,20 @@ layout: section
287
300
  ## Key GitHub Copilot features
288
301
  ```
289
302
 
303
+ ### `sourceName` role
304
+
305
+ `sourceName` is workspace-relative metadata used to resolve adjacent themes and
306
+ images and to derive output filenames. It does not read, parse, split, or watch
307
+ Markdown content. Passing `sourceName` does not load a deck; non-empty canvas
308
+ input must still include the complete `slides` array.
309
+
310
+ To have MarkdStage load a Markdown file, use **More controls > Open Markdown**.
311
+
312
+ Write local images as `![Alternative text](/assets/foo.png)`. With `sourceName`,
313
+ lookup tries `assets/` beside the Markdown before workspace-root `assets/`.
314
+ Architecture `icon` and `image.src` use `assets/foo.svg` without a leading slash
315
+ and follow the same lookup order.
316
+
290
317
  ### Choosing a theme
291
318
 
292
319
  Set the deck-wide `theme`; use `dark` when omitted.
@@ -745,8 +772,8 @@ Architecture diagrams can be moved directly over the rendered result. For decks
745
772
  imported through **More controls > Open Markdown**, edits write back to the source `architecture` fence and
746
773
  survive re-import. Decks supplied directly through open / `load_deck` cannot be
747
774
  reversibly mapped to a source file, so they save only to canvas deck state.
748
- In the standalone CLI, `markdstage present slides.md --watch` enables the same
749
- source-backed editing workflow; `present` without `--watch` is read-only.
775
+ In the standalone CLI, `markdstage preview slides.md --watch` enables the same
776
+ source-backed editing workflow; `preview` without `--watch` is read-only.
750
777
 
751
778
  **Placement editing is a stable part of Architecture DSL v1.**
752
779
 
@@ -837,7 +864,7 @@ blocks.
837
864
  For a source-backed deck imported through **More controls > Open Markdown**,
838
865
  **Shape editing** opens this editor directly. If the slide contains multiple
839
866
  Architecture blocks, select one from the accessible picker first. CLI
840
- `present --watch` opens the same editor in a browser window. Agents can open
867
+ `preview --watch` opens the same editor in a browser window. Agents can open
841
868
  the editor canvas directly:
842
869
 
843
870
  ```json
@@ -110,12 +110,18 @@ export async function readGuide(topic = "overview") {
110
110
  section(readme, "## How it works"),
111
111
  "",
112
112
  "Users can load workspace Markdown directly with **More controls > Open Markdown** (deterministic splitting without AI; natural-language summarization remains the AI's responsibility). The workspace root is the Git repository root when available, otherwise the folder opened for the current session.",
113
- "Use **More controls > Shape editing** to adjust the placement of an existing Architecture diagram. In the CLI, run `markdstage present slides.md --watch`; it starts in viewing mode and enables the same placement editor plus the detailed Architecture designer. CLI `present` without `--watch` is read-only. Comprehensive edits affect the source Markdown only when explicitly saved.",
113
+ "Use **More controls > Shape editing** to adjust the placement of an existing Architecture diagram. In the CLI, run `markdstage preview slides.md --watch`; it starts in viewing mode and enables the same placement editor plus the detailed Architecture designer. CLI `preview` without `--watch` is read-only. Comprehensive edits affect the source Markdown only when explicitly saved.",
114
114
  "",
115
115
  "For details, request `slide-format`, `themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or `architecture-schema`.",
116
116
  ].join("\n");
117
117
  case "slide-format":
118
- return section(readme, "### Slide fragment format");
118
+ return [
119
+ section(readme, "### Markdown file syntax"),
120
+ "",
121
+ section(readme, "### Canvas API `slides` array"),
122
+ "",
123
+ section(readme, "### `sourceName` role"),
124
+ ].join("\n");
119
125
  case "themes":
120
126
  return section(readme, "### Choosing a theme");
121
127
  case "custom-themes":
@@ -2148,6 +2148,7 @@ let currentVersion = -1;
2148
2148
  let knownDeckVersion = -1;
2149
2149
  let deckSlides = [];
2150
2150
  let deckTitles = [];
2151
+ let deckLayouts = [];
2151
2152
  let navIndex = 0;
2152
2153
  let navTotal = 0;
2153
2154
  let navMode = "deck";
@@ -2189,6 +2190,11 @@ function deriveTitle(md) {
2189
2190
  return fallback ? trimTitle(fallback) : "(Untitled)";
2190
2191
  }
2191
2192
 
2193
+ function deriveLayout(md) {
2194
+ const { meta } = splitFrontMatter(typeof md === "string" ? md : "");
2195
+ return typeof meta.layout === "string" ? meta.layout.trim().toLowerCase() : "";
2196
+ }
2197
+
2192
2198
  function trimTitle(text) {
2193
2199
  const stripped = text
2194
2200
  .replace(/[*_`>#~]/g, "")
@@ -2205,6 +2211,7 @@ async function fetchDeck() {
2205
2211
  if (Array.isArray(data.slides)) {
2206
2212
  deckSlides = data.slides;
2207
2213
  deckTitles = deckSlides.map(deriveTitle);
2214
+ deckLayouts = deckSlides.map(deriveLayout);
2208
2215
  }
2209
2216
  if (typeof data.deckVersion === "number") knownDeckVersion = data.deckVersion;
2210
2217
  buildOverview();
@@ -3032,24 +3039,51 @@ function buildOverview() {
3032
3039
  const list = document.getElementById("overviewList");
3033
3040
  if (!list) return;
3034
3041
  list.replaceChildren();
3042
+ let insideSection = false;
3035
3043
  deckTitles.forEach((title, i) => {
3044
+ const layout = deckLayouts[i] || "";
3045
+ if (layout === "title" || layout === "backcover") insideSection = false;
3046
+ const sectionChild =
3047
+ insideSection && layout !== "title" && layout !== "section" && layout !== "backcover";
3048
+
3036
3049
  const li = document.createElement("li");
3037
3050
  li.className = "overview-item";
3051
+ if (layout === "title" || layout === "section") {
3052
+ li.classList.add(`overview-item-${layout}`);
3053
+ }
3054
+ if (sectionChild) li.classList.add("overview-item-section-child");
3038
3055
  li.dataset.index = String(i);
3056
+ if (layout) li.dataset.layout = layout;
3057
+
3039
3058
  const btn = document.createElement("button");
3040
3059
  btn.type = "button";
3041
3060
  btn.className = "overview-link";
3061
+ btn.setAttribute(
3062
+ "aria-label",
3063
+ `${i + 1} ${title}${layout === "title" || layout === "section" ? `, ${layout} slide` : ""}`,
3064
+ );
3065
+
3042
3066
  const num = document.createElement("span");
3043
3067
  num.className = "overview-num";
3044
3068
  num.textContent = String(i + 1);
3045
3069
  const label = document.createElement("span");
3046
3070
  label.className = "overview-label";
3047
3071
  label.textContent = title;
3072
+
3048
3073
  btn.appendChild(num);
3074
+ if (layout === "title" || layout === "section") {
3075
+ const kind = document.createElement("span");
3076
+ kind.className = "overview-kind";
3077
+ kind.textContent = layout === "title" ? "Title" : "Section";
3078
+ kind.setAttribute("aria-hidden", "true");
3079
+ btn.appendChild(kind);
3080
+ }
3049
3081
  btn.appendChild(label);
3050
3082
  btn.addEventListener("click", () => goToIndex(i));
3051
3083
  li.appendChild(btn);
3052
3084
  list.appendChild(li);
3085
+
3086
+ if (layout === "section") insideSection = true;
3053
3087
  });
3054
3088
  highlightOverview();
3055
3089
  }
@@ -3478,13 +3512,9 @@ function init() {
3478
3512
  return;
3479
3513
  }
3480
3514
  if (params.get("print") === "1") {
3481
- // Print mode never reaches editing-mode branches. Removing this return would
3482
- // bake the editing UI into PDFs, so a regression test protects it.
3483
- //
3484
- // This early return is also **the primary fix for the #12 hang**. Only print
3485
- // mode avoids connectEvents() (an unclosed SSE) and the two-second setInterval,
3486
- // allowing the page to become idle and --print-to-pdf to complete.
3487
- // Removing the return makes printing hang forever.
3515
+ // Print mode renders one immutable output snapshot and never reaches editing,
3516
+ // SSE, or polling branches. The output runtime waits for its ready report
3517
+ // before asking Chromium to create the PDF.
3488
3518
  initPrint(params).catch(reportPrintBootstrapFailure);
3489
3519
  return;
3490
3520
  }
@@ -547,8 +547,22 @@ body.presenter-view-mode .layout-warning{display:none;}
547
547
  align-items:center;justify-content:center;border-radius:6px;
548
548
  font-size:.82em;font-weight:700;font-variant-numeric:tabular-nums;
549
549
  color:var(--muted);background:var(--code);border:1px solid var(--border);}
550
+ .overview-kind{flex:0 0 auto;padding:.18em .48em;border-radius:999px;
551
+ color:var(--accent-strong);background:var(--surface);border:1px solid var(--accent);
552
+ font-size:.66em;font-weight:750;letter-spacing:.06em;text-transform:uppercase;}
550
553
  .overview-label{flex:1 1 auto;font-size:.96em;line-height:1.35;
551
554
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
555
+ .overview-item-title{margin-bottom:.45em;}
556
+ .overview-item-title .overview-link{background:color-mix(in srgb,var(--accent-soft) 74%,transparent);
557
+ border:1px solid color-mix(in srgb,var(--accent) 48%,var(--border));}
558
+ .overview-item-title .overview-label{color:var(--fg);font-weight:700;}
559
+ .overview-item-section{margin-top:.65em;padding-top:.55em;border-top:1px solid var(--border);}
560
+ .overview-item-section .overview-link{padding-block:.65em;background:var(--accent-soft);
561
+ border-left:4px solid var(--accent);}
562
+ .overview-item-section .overview-num{color:var(--fg);border-color:var(--accent);}
563
+ .overview-item-section .overview-label{color:var(--fg);font-weight:700;}
564
+ .overview-item-section-child .overview-link{width:calc(100% - 1.2em);margin-left:1.2em;
565
+ border-left:1px solid color-mix(in srgb,var(--accent) 42%,var(--border));border-radius:0 8px 8px 0;}
552
566
  .overview-item.current .overview-link{background:var(--accent-soft);}
553
567
  .overview-item.current .overview-num{color:var(--fg);background:var(--accent);
554
568
  border-color:var(--accent);}
@@ -6,7 +6,7 @@
6
6
  // because the Extension is distributed as a folder ZIP.
7
7
 
8
8
  import { existsSync } from "node:fs";
9
- import { readFile, open, rm, stat } from "node:fs/promises";
9
+ import { readFile, open, rm, stat, writeFile } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
  import { execFileSync, spawn } from "node:child_process";
12
12
 
@@ -173,84 +173,6 @@ export function delay(milliseconds) {
173
173
  return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
174
174
  }
175
175
 
176
- /**
177
- * Run a headless browser once with `--print-to-pdf`.
178
- *
179
- * ⚠️ **`pageUrl` must include `?print=1&token=...` (#12).**
180
- *
181
- * `--print-to-pdf` completes only when the page becomes idle. In renderer `init()`,
182
- * only print mode returns early. Normal and presenter views keep an unclosed SSE
183
- * (`new EventSource("./events")`) and a two-second `setInterval` running.
184
- * Passing a URL without `?print=1` therefore means **the browser never exits**.
185
- *
186
- * Observed results, using Chrome arguments byte-for-byte identical to this function:
187
- *
188
- * | URL | Result |
189
- * | -------------------------- | --------------------------------------- |
190
- * | `/?print=1&token=<valid>` | exit 0 @ 2.4s (valid PDF) |
191
- * | `/?print=1&token=` (empty) | exit 0 @ 1.9s (blank; renderer reports failure) |
192
- * | `/` (normal view) | **HANG** (still running after 120 seconds) |
193
- * | `/?present=1` | **HANG** |
194
- * | `/nope-404` (no renderer) | exit 0 @ 3.0s |
195
- *
196
- * ⚠️ **`--virtual-time-budget` is effectively ignored by `--headless=new`.**
197
- * The `--virtual-time-budget=12000` argument below does not stop this hang.
198
- * Adding `--timeout=8000` is also ineffective, as verified empirically.
199
- * The argument is harmless and remains in place, but **do not treat it as a
200
- * wall-clock timeout**. Only Node's `PDF_RENDER_TIMEOUT_MS` and
201
- * `terminateProcessTree` enforce a limit, and failure may take up to 60 seconds.
202
- */
203
- export async function runHeadlessBrowser(browser, args, failureLabel) {
204
- await new Promise((resolvePromise, rejectPromise) => {
205
- const child = spawn(browser, args, {
206
- detached: process.platform !== "win32",
207
- windowsHide: true,
208
- stdio: ["ignore", "pipe", "pipe"],
209
- });
210
- let diagnostics = "";
211
- let settled = false;
212
- let timedOut = false;
213
- const appendDiagnostics = (chunk) => {
214
- diagnostics = `${diagnostics}${chunk.toString()}`.slice(-12_000);
215
- };
216
- child.stdout.on("data", appendDiagnostics);
217
- child.stderr.on("data", appendDiagnostics);
218
-
219
- const settle = (error) => {
220
- if (settled) return;
221
- settled = true;
222
- clearTimeout(timer);
223
- if (error) rejectPromise(error);
224
- else resolvePromise();
225
- };
226
- const timer = setTimeout(async () => {
227
- if (settled) return;
228
- timedOut = true;
229
- await terminateProcessTree(child);
230
- settle(new Error(`${failureLabel} timed out after ${PDF_RENDER_TIMEOUT_MS / 1000}s.`));
231
- }, PDF_RENDER_TIMEOUT_MS);
232
-
233
- child.once("error", (error) => {
234
- if (!timedOut) settle(error);
235
- });
236
- child.once("exit", (code, signal) => {
237
- if (timedOut) return;
238
- if (code === 0) {
239
- settle();
240
- return;
241
- }
242
- const detail = diagnostics.trim();
243
- settle(
244
- new Error(
245
- `${failureLabel} failed (${signal ? `signal ${signal}` : `exit ${code}`})${
246
- detail ? `: ${detail}` : "."
247
- }`,
248
- ),
249
- );
250
- });
251
- });
252
- }
253
-
254
176
  function withSandboxFallback(args) {
255
177
  if (
256
178
  process.platform !== "win32" &&
@@ -262,35 +184,38 @@ function withSandboxFallback(args) {
262
184
  return args;
263
185
  }
264
186
 
265
- export async function runPdfBrowser(browser, pageUrl, outputPath, profileDir) {
266
- // Enforce the contract at runtime. Otherwise it silently waits 60 seconds
267
- // before timing out, obscuring the cause; fail immediately with an explanation.
187
+ export async function runPdfBrowser(browser, pageUrl, outputPath, profileDir, job) {
268
188
  if (new URL(pageUrl).searchParams.get("print") !== "1") {
269
189
  throw new Error(
270
- `Refusing to run --print-to-pdf against a non-print URL (${pageUrl}): only ?print=1 stops the renderer's SSE and polling loops, so any other page hangs the browser forever.`,
190
+ `Refusing to render PDF from a non-print URL (${pageUrl}).`,
271
191
  );
272
192
  }
273
- const args = withSandboxFallback([
274
- "--headless=new",
275
- "--disable-gpu",
276
- "--disable-background-networking",
277
- "--disable-component-update",
278
- "--disable-default-apps",
279
- "--disable-extensions",
280
- "--force-color-profile=srgb",
281
- "--hide-scrollbars",
282
- "--no-first-run",
283
- "--no-pdf-header-footer",
284
- "--print-to-pdf-no-header",
285
- "--run-all-compositor-stages-before-draw",
286
- // Ineffective (#12): --headless=new ignores it. See the JSDoc above.
287
- // The actual safeguards are PDF_RENDER_TIMEOUT_MS + terminateProcessTree.
288
- "--virtual-time-budget=12000",
289
- `--user-data-dir=${profileDir}`,
290
- `--print-to-pdf=${outputPath}`,
291
- pageUrl,
292
- ]);
293
- await runHeadlessBrowser(browser, args, "Browser PDF rendering");
193
+ if (!job || typeof job !== "object") {
194
+ throw new Error("PDF rendering requires an output job.");
195
+ }
196
+
197
+ const { cdp, child } = await openCdpOutputPage(browser, pageUrl, profileDir, job);
198
+ try {
199
+ // The renderer reports ready only after Mermaid, images, fonts, and layout
200
+ // have settled. Give Chromium two compositor frames before printing.
201
+ await cdp.send("Runtime.evaluate", {
202
+ expression:
203
+ "new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))",
204
+ awaitPromise: true,
205
+ });
206
+ const pdf = await cdp.send("Page.printToPDF", {
207
+ displayHeaderFooter: false,
208
+ printBackground: true,
209
+ preferCSSPageSize: true,
210
+ transferMode: "ReturnAsBase64",
211
+ });
212
+ if (typeof pdf.data !== "string" || pdf.data.length === 0) {
213
+ throw new Error("Chromium DevTools did not return PDF data.");
214
+ }
215
+ await writeFile(outputPath, Buffer.from(pdf.data, "base64"));
216
+ } finally {
217
+ await closeCdpOutputPage(cdp, child);
218
+ }
294
219
  }
295
220
 
296
221
  async function waitForDevToolsPort(profileDir, child, diagnostics) {
@@ -497,14 +497,14 @@ export async function exportPdf(inst, requestedPath, requestedTheme) {
497
497
  profileDir = await mkdtemp(join(tmpdir(), "markdstage-pdf-"));
498
498
  const outputBase = basename(outputPath, extname(outputPath)) || "markdstage";
499
499
  temporaryOutputPath = join(outputParent, `.${outputBase}.${token}.tmp.pdf`);
500
- inst.exportJobs.set(token, createOutputJob(snapshot, "pdf"));
500
+ const exportJob = createOutputJob(snapshot, "pdf");
501
+ inst.exportJobs.set(token, exportJob);
501
502
 
502
503
  const pageUrl = pageUrlFor(inst, { print: 1, token });
503
- await runPdfBrowser(browser, pageUrl, temporaryOutputPath, profileDir);
504
- const exportJob = inst.exportJobs.get(token);
505
- if (exportJob?.status !== "ready") {
504
+ await runPdfBrowser(browser, pageUrl, temporaryOutputPath, profileDir, exportJob);
505
+ if (exportJob.status !== "ready") {
506
506
  throw new Error(
507
- exportJob?.error || "The print renderer did not finish before the browser exited.",
507
+ exportJob.error || "The print renderer did not finish before PDF generation.",
508
508
  );
509
509
  }
510
510
  const bytes = await verifyPdf(temporaryOutputPath);
package/src/cli.mjs CHANGED
@@ -36,8 +36,8 @@ import {
36
36
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
37
37
 
38
38
  const COMMANDS = [
39
- ["presentation", "Open presenter view and launch the audience view from it."],
40
- ["present", "Serve a deck on loopback and open it in a browser window."],
39
+ ["present", "Open presenter view and launch the audience view from it."],
40
+ ["preview", "Serve a deck on loopback and open it in a browser window."],
41
41
  ["validate", "Check deck structure, Architecture DSL blocks, and themes."],
42
42
  ["inspect", "Report 1280x720 clipping diagnostics for a deck."],
43
43
  ["capture", "Write 1280x720 PNG files for selected or clipped slides."],
@@ -59,8 +59,8 @@ const GLOBAL_OPTIONS = {
59
59
  };
60
60
 
61
61
  const COMMAND_OPTIONS = {
62
- presentation: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
63
62
  present: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
63
+ preview: { watch: { type: "boolean" }, "no-open": { type: "boolean" } },
64
64
  validate: {},
65
65
  inspect: { slide: { type: "string" }, all: { type: "boolean" }, "fail-on-issues": { type: "boolean" } },
66
66
  capture: { pages: { type: "string" }, output: { type: "string" } },
@@ -103,8 +103,8 @@ function usage(command) {
103
103
  return lines.join("\n");
104
104
  }
105
105
  const help = {
106
- presentation: [
107
- "Usage: markdstage presentation <file.md> [options]",
106
+ present: [
107
+ "Usage: markdstage present <file.md> [options]",
108
108
  "",
109
109
  "Opens presenter view with the current slide, next-slide preview, and speaker notes.",
110
110
  "Use Start presentation in that view to open the synchronized audience window.",
@@ -114,16 +114,16 @@ function usage(command) {
114
114
  "",
115
115
  "Presentation requires an installed Microsoft Edge, Google Chrome, or Chromium.",
116
116
  ],
117
- present: [
118
- "Usage: markdstage present <file.md> [options]",
117
+ preview: [
118
+ "Usage: markdstage preview <file.md> [options]",
119
119
  "",
120
120
  " --watch Reload on save and enable Architecture editing.",
121
121
  " --no-open Serve the deck without launching a browser.",
122
122
  "",
123
- "Without --watch, presentation is read-only. Watch mode starts in normal viewing mode;",
123
+ "Without --watch, preview is read-only. Watch mode starts in normal viewing mode;",
124
124
  "use the pencil control to edit Architecture diagrams and open the detailed designer.",
125
125
  "",
126
- "Presenting requires an installed Microsoft Edge, Google Chrome, or Chromium.",
126
+ "Preview requires an installed Microsoft Edge, Google Chrome, or Chromium.",
127
127
  ],
128
128
  validate: [
129
129
  "Usage: markdstage validate <file.md> [--json]",
@@ -262,8 +262,8 @@ export async function run(argv, io = {}) {
262
262
 
263
263
  try {
264
264
  switch (command) {
265
- case "present": {
266
- const file = requireFile(positionals, "present");
265
+ case "preview": {
266
+ const file = requireFile(positionals, "preview");
267
267
  const report = await presentCommand(
268
268
  { ...deckOptions(file, values), watch: values.watch, open: !values["no-open"], until: io.until },
269
269
  {
@@ -277,8 +277,8 @@ export async function run(argv, io = {}) {
277
277
  if (values.json) json(report);
278
278
  return EXIT_OK;
279
279
  }
280
- case "presentation": {
281
- const file = requireFile(positionals, "presentation");
280
+ case "present": {
281
+ const file = requireFile(positionals, "present");
282
282
  const report = await presentCommand(
283
283
  {
284
284
  ...deckOptions(file, values),
@@ -1,4 +1,4 @@
1
- // markdstage present serve the deck on loopback and open it in a browser.
1
+ // Shared server for the markdstage preview and present commands.
2
2
 
3
3
  import { mkdtemp, readFile, rm } from "node:fs/promises";
4
4
  import { join, resolve } from "node:path";
@@ -139,7 +139,9 @@ export async function presentCommand(options, io) {
139
139
  });
140
140
  }
141
141
 
142
- io.print(`MarkdStage is presenting ${session.sourceName || session.file}`);
142
+ io.print(
143
+ `MarkdStage is ${options.presenterView ? "presenting" : "previewing"} ${session.sourceName || session.file}`,
144
+ );
143
145
  io.print(` slides: ${session.slides.length}`);
144
146
  io.print(` theme: ${session.theme}`);
145
147
  io.print(` workspace: ${resolve(session.workspaceRoot)}`);
package/src/skills.mjs CHANGED
@@ -78,7 +78,7 @@ what the MarkdStage canvas and MarkdStage Desktop render.
78
78
  \`references/slide-format.md\`).
79
79
  4. Validate structure, themes, and Architecture DSL before visual review:
80
80
  \`markdstage validate slides.md --json\`.
81
- 5. Use \`markdstage present slides.md --watch\` for live source-backed authoring.
81
+ 5. Use \`markdstage preview slides.md --watch\` for live source-backed authoring.
82
82
  It reloads on save without losing the current slide and keeps the last valid
83
83
  deck while a save is incomplete.
84
84
  6. Check fixed 16:9 output with \`markdstage inspect slides.md --json\`. Use
@@ -88,14 +88,14 @@ what the MarkdStage canvas and MarkdStage Desktop render.
88
88
  whose balance, spacing, or diagrams need visual judgment.
89
89
  8. Revise Markdown and repeat validation plus targeted inspection until the deck
90
90
  is valid, unclipped, concise, and visually balanced.
91
- 9. Deliver from the same source with \`markdstage presentation slides.md\`,
91
+ 9. Deliver from the same source with \`markdstage present slides.md\`,
92
92
  \`markdstage export slides.md --output slides.pdf\`, or
93
93
  \`markdstage export slides.md --output slides.pptx\`.
94
94
 
95
- The browser in \`present --watch\` starts in viewing mode. The user can activate
95
+ The browser in \`preview --watch\` starts in viewing mode. The user can activate
96
96
  the pencil control to move Architecture elements, then choose **Advanced edit**
97
97
  for the detailed designer. Placement changes save immediately, while the
98
- detailed designer saves only when the user selects **Save**. \`present\` without
98
+ detailed designer saves only when the user selects **Save**. \`preview\` without
99
99
  \`--watch\` is read-only.
100
100
 
101
101
  Never hand-write HTML or CSS for a slide. Fix layout problems by shortening the
@@ -106,8 +106,8 @@ and layout diagnostics over capturing every slide.
106
106
 
107
107
  | Command | Purpose |
108
108
  | --- | --- |
109
- | \`markdstage presentation <file> [--watch]\` | Open presenter view with the current slide, next-slide preview, speaker notes, and controls for a synchronized audience window. |
110
- | \`markdstage present <file> [--watch]\` | Serve the deck on loopback and open it in a browser window. \`--watch\` reloads on save, keeps the current slide, and enables Architecture placement and detailed editing. Without it, the source is read-only. |
109
+ | \`markdstage present <file> [--watch]\` | Open presenter view with the current slide, next-slide preview, speaker notes, and controls for a synchronized audience window. |
110
+ | \`markdstage preview <file> [--watch]\` | Serve the deck on loopback and open it in a browser window. \`--watch\` reloads on save, keeps the current slide, and enables Architecture placement and detailed editing. Without it, the source is read-only. |
111
111
  | \`markdstage validate <file> [--json]\` | Check deck structure, Architecture DSL blocks, and themes. |
112
112
  | \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide; use \`--fail-on-issues\` for quality gates. |
113
113
  | \`markdstage capture <file> [--pages 2,4]\` | Write 1280x720 PNG files; without \`--pages\` only clipped slides are captured. |