pindeck 3.4.0 → 4.1.0-beta.1

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/cli.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  (add --issues for the issues only: layout 9 10 11 --issues)
6
6
  node cli.mjs shot <slide>... screenshots with pin markers; prints the PNG paths
7
7
  node cli.mjs slides slide numbers and ids
8
+ node cli.mjs element <eid>... the HTML of elements, by data-eid, as written in the deck
8
9
  node cli.mjs pdf handout PDF with one page per slide (build steps in their final state)
9
10
  node cli.mjs pdf presentation PDF with one page per build step
10
11
  (add "all" to include hidden slides: pdf handout all)
@@ -13,6 +14,11 @@
13
14
  from the top left), as PNG: without a name in the screenshots
14
15
  folder to look at, with one saved into the deck
15
16
 
17
+ node cli.mjs resolve <comment> [what was done]
18
+ marks a review comment resolved (its id, c-…, or its number, C3)
19
+ node cli.mjs reply <comment> <text>
20
+ answers a review comment and leaves it open (why it was not done, a question)
21
+
16
22
  node cli.mjs git-setup [deck] keep PinDeck's data-eid ids out of git (no server needed; a folder or deck file)
17
23
 
18
24
  <slide> is a slide id (data-eid), a slide number (1-based) or "current". The assistant's commands reach its
@@ -59,8 +65,9 @@ if (cmd === "git-setup") {
59
65
  }
60
66
  const issuesOnly = cmd === "layout" && rest.includes("--issues");
61
67
  const slides = rest.filter((a) => a !== "--issues");
62
- if (!["layout", "shot", "slides", "pdf", "pdf-figure"].includes(cmd)) {
63
- console.error("Usage: node cli.mjs layout|shot <slide>... [--issues] | node cli.mjs slides | node cli.mjs pdf handout|presentation | " +
68
+ if (!["layout", "shot", "slides", "element", "pdf", "pdf-figure", "resolve", "reply"].includes(cmd)) {
69
+ console.error("Usage: node cli.mjs layout|shot <slide>... [--issues] | node cli.mjs slides | node cli.mjs element <eid>... | node cli.mjs pdf handout|presentation | " +
70
+ "node cli.mjs resolve <comment> [what was done] | node cli.mjs reply <comment> <text> | " +
64
71
  "node cli.mjs pdf-figure <pdf> <page> [x,y,w,h] [figures/name.png] [--width px] | node cli.mjs git-setup [deck]");
65
72
  process.exit(2);
66
73
  }
@@ -89,6 +96,8 @@ if (!url || !token) {
89
96
  /* layout and shot take several slides, so the assistant needs no shell loop or pipe (which PinDeck refuses) */
90
97
  const queries = cmd === "pdf-figure" ? [figureQuery([...rest])]
91
98
  : cmd === "pdf" ? [{ slide: rest[0] || "handout", ...(rest[1] === "all" ? { all: "1" } : {}) }]
99
+ : cmd === "resolve" || cmd === "reply" ? [{ id: rest[0] || "", note: rest.slice(1).join(" ") }]
100
+ : cmd === "element" ? (rest.length ? rest : [""]).map((eid) => ({ eid }))
92
101
  : cmd === "slides" ? [{}] : (slides.length ? slides : ["current"]).map((slide) => ({ slide }));
93
102
  let ok = true;
94
103
  for (const q of queries) {
@@ -0,0 +1,51 @@
1
+ # Configuration
2
+
3
+ [← README](../README.md)
4
+
5
+ ## Models, providers and configuration
6
+
7
+ Open the existing model menu in the chat header to choose a model under **Claude** or **OpenAI / ChatGPT**. Choices come from the installed backends: Claude's initialization response and Codex's paginated `model/list`. Hidden or explicitly unavailable entries are omitted. Effort choices appear in a separate reasoning-effort selector beside the model and follow its reported capabilities. Readable labels preserve the exact backend values (for example, Extra high sends `xhigh`). The selector shows provider descriptions where available and distinguishes **Model default** from a **PinDeck default** fallback. Models without effort controls show a disabled Effort N/A selector. Effort choices are remembered per provider/model in `effortPreferences` in config.json. A model with no saved choice uses its default; an unsupported saved choice falls back to the current default. The picker defaults to GPT-6 models (including 6.x) and the newest discovered Fable, Opus, Sonnet, and Haiku model in each family. Claude versions are compared numerically from discovered names; a rolling family alias wins ties, avoiding duplicate entries. **Show all available models** reveals the full discovered catalog; this display preference is remembered in the browser. A currently selected older model remains visible and is never switched automatically by the filter. **Refresh models** asks the backends again; otherwise the lists are shared by the decks you open for 10 minutes (`models.json` in the state folder), so opening a deck starts no backend just to list models. A missing CLI, missing sign-in, or discovery failure is shown in its provider group. These are backend-reported choices, not a guarantee of current quota or entitlement; a later rejected request reports its error. Older CLIs may expose fewer models than a provider's website.
8
+
9
+ Changing models within a provider resumes that provider's conversation. Changing providers opens **Switch with summary**, **Start fresh**, and **Cancel**. A switch prepares the new backend before replacing the old one. Chat remains visible with a divider, and deck files and undo/redo are retained. Switching back to a provider creates another new session; it does not silently restore old context.
10
+
11
+ The current handover is a local, bounded digest of recent user/assistant messages and notices, not an additional model-generated summary. It excludes tool output, needs no extra inference request, and is sent with the first message to the new session. Earlier messages may be omitted. **Start fresh** sends no conversation digest; the assistant still receives PinDeck instructions and the current deck context.
12
+
13
+ Each deck keeps its own provider, model and effort with its conversation (in its `chat.json`), so a choice in one deck never moves another deck's conversation to another backend. The last choice is also saved in `~/.pindeck-v4/config.json` (or `$PINDECK_HOME/config.json`), as the start for decks that have none yet:
14
+
15
+ ```json
16
+ {
17
+ "provider": "claude",
18
+ "model": "sonnet",
19
+ "effort": "medium"
20
+ }
21
+ ```
22
+
23
+ Legacy configuration without `provider` is interpreted as Claude; a provider without a model gets that provider's default model. Once the backend has listed its models, a saved model it no longer offers gives way to its default model (the chat says so), and an effort the model does not offer to its saved or default one, before the first message. config.json is read again and merged whenever a choice is saved, so decks open at the same time keep each other's effort preferences. Provider-tagged chat/session records prevent one backend from resuming the other's IDs. `--provider` and `--model` override the deck's settings for that run; a `--provider` other than the deck's starts a new conversation (the chat says so), and the next run without it is back on the deck's own choice. A later menu selection is saved. `CLAUDE_BIN` and `CODEX_BIN` override executable paths. `CLAUDE_MODEL`, which PinDeck 2 read, is not used: PinDeck warns and names `--model`.
24
+
25
+ PinDeck 4 defaults to **`~/.pindeck-v4`**, apart from PinDeck 3 (`~/.pindeck-v3`) and PinDeck 2 (`~/.pindeck`), so the beta can run next to a stable version: their running servers, chats and Undo histories stay apart. Nothing is copied over automatically. To bring PinDeck 3's settings and chats along, copy its settings and decks (not `servers/`, which lists the running 3.x servers): `mkdir -p ~/.pindeck-v4 && cp -R ~/.pindeck-v3/config.json ~/.pindeck-v3/decks ~/.pindeck-v4/`. From PinDeck 2, `config.json` and `decks/*/chat.json` can be copied over by hand (asset roots too: they are in config.json). Never give both versions the same `PINDECK_HOME`: they keep files of the same names in different formats and overwrite each other's settings and chats. PinDeck warns at start when its folder holds a chat that PinDeck 2 wrote. To run both versions at once, use another port, for example:
26
+
27
+ ```bash
28
+ node front.mjs /path/to/a/deck-copy --port 5183 --provider codex
29
+ ```
30
+
31
+ ## Backend permissions and attachments
32
+
33
+ Claude uses its native Read/Edit tools with PinDeck's narrow allowlist, safe mode, and no inherited MCP configuration. Codex runs with a read-only sandbox, command execution disabled, and no approval grants. Inherited MCP servers and plugins are disabled in the thread configuration. Codex receives four host-controlled tools: `pindeck_read`, `pindeck_edit`, `pindeck_write` and `pindeck_command`. Both assistants may edit and create the files of the deck folder: the deck file, `theme.css` and its other files. Never `brand.css`, `vendor/`, `.git/`, or the other decks in the folder (its other `.html` files, and subfolders that hold a deck of their own). Linked writable files and paths outside the allowed roots are rejected. Commands are restricted to PinDeck's layout, shot, slides, pdf and pdf-figure helpers, executed with argument arrays rather than a shell. `brand.css` also retains the server's post-turn restoration safeguard.
34
+
35
+ Codex reads text, PNG/JPEG/WebP/GIF images and screenshots through the bridge. SVG is read as text. Attached PDFs are handled page by page: a rendered page image (PDFKit on macOS, Poppler on Linux), plus the page's text on macOS. Audio is embedded by path and is not transcribed. Read operations have a 12 MB limit; long text can be read in chunks.
36
+
37
+ ### Files outside the deck folder
38
+
39
+ A deck can use files from a folder around it, such as a style sheet or figures shared by several talks: start PinDeck with `--asset-root <folder>` (repeatable), or list the folders in config.json as `"assetRoots": ["~/talks"]`. When the folder contains the deck, PinDeck serves the deck from inside it, so relative paths such as `../shared/style.css` work, in the editor, in screenshots and in PDF exports. Claude may read those files but does not edit them.
40
+
41
+ Symlinks: PinDeck serves a file only if, with symlinks followed, it lies inside the deck folder or an asset root. A symlink in the deck that leads anywhere else is refused (403). To use a symlinked library folder, add the folder it points to as an asset root; it does not have to contain the deck. PinDeck never writes or deletes through a symlink out of the deck folder: with a `figures/` or `export/` that links elsewhere, uploads, exports and deleting a picture's file are refused with the reason.
42
+
43
+ ## Keeping the ids out of git
44
+
45
+ PinDeck adds `data-eid` attributes to the deck it opens. If the deck is in git, run once per clone:
46
+
47
+ ```bash
48
+ node cli.mjs git-setup path/to/deck
49
+ ```
50
+
51
+ This adds a line like `path/to/deck/*.html filter=pindeck-ids` to `.gitattributes` (commit it) and defines the filter in the clone's git config: git then strips the ids whenever it reads the deck's HTML. The ids stay in the file on disk, where PinDeck and the assistant need them, but commits and diffs never contain them, and a change that only adds ids does not show in `git status`. Your own edits show and commit as usual. Clones without the filter configured simply see the file as it is.
@@ -0,0 +1,56 @@
1
+ # How it works
2
+
3
+ [← README](../README.md)
4
+
5
+ ## How it works
6
+
7
+ * **One port, a server per deck.** `pindeck` starts the front (`front.mjs`): it holds the port, the login and the deck list, and serves each open deck at `/d/<deck>/`. Every deck runs its own deck server (`server.mjs`) on a private port of 127.0.0.1 with a token of its own; the front passes each request on with that token. A deck's server, its assistant and its undo history are those of that deck alone. The editor tabs of a browser share one event stream from the front (a shared worker), since browsers open only six connections to one port; if a deck server stops, the front opens the deck again and the tab catches up. Each PinDeck process notes its address in `~/.pindeck-v4/servers/` for `pindeck login` and `cli.mjs`, and a deck server holds its deck's `open.json`, so a deck opens in one PinDeck only.
8
+ * **Deck pages are sandboxed.** The deck page in the editor's frames runs with a sandbox policy in an origin of its own, at a per-run address that serves the deck's files and nothing else. A script in a deck, a template, or written by the assistant cannot reach the login cookie, the API, the chat or other decks. The editor and its bridge in the slide frame talk by `postMessage`. Present opens the same address on `localhost` without the sandbox, so reveal.js's speaker view (`S`) works: browsers treat `localhost` and `127.0.0.1` as different sites, PinDeck takes no login and no cookie there, and the tab is opened without a handle on the editor. Headless Chrome (screenshots, layout, PDFs) does not log in at all; the page it asks for is served without the sandbox, which its screenshot and print modes cannot handle, only with a key made for that one run.
9
+ * **Element ids.** On start, the server gives every slide and every visible element a readable `data-eid` attribute (`summary`, `summary/callout`, `summary/li-2`). Only the attribute is inserted; the rest of the file stays as it was. A backup of the file before the first change is kept in `~/.pindeck-v4/decks/<deck>/backups/`. Ids survive edits, so pins, layout reports and the assistant all name the same element.
10
+ * **Hand edits.** A move is stored as the CSS `translate` property and a resize as `width`/`height`, inline on the element in `index.html`. `translate` moves an element without changing the layout around it, for elements in the flow and absolutely positioned ones alike. Only that one style attribute is rewritten; the rest of the file stays as it was. The assistant gets a note about each hand edit. Brand-locked elements (footer, logos) cannot be selected.
11
+ * **Text edits.** While you type, the element holds its source HTML from `index.html`, not the rendered slide, so math stays TeX and nothing that KaTeX or the deck script generates ends up in the file. On save only that element's content is replaced. Browser editing artefacts are cleaned up: `<b>`/`<i>` become `<strong>`/`<em>`, styled spans from formatting are removed, trailing `<br>`s dropped.
12
+ * **Pins.** A pin records the element id, its box in slide coordinates (1280 x 720), the click point, the clicked word and, in tables, the cell. Pins on the footer or logos are marked as locked.
13
+ * **Layout instead of screenshots.** With each message the editor measures the current slide and every pinned slide in your browser: position and size of every element, font sizes, and issues (content below the footer rule, outside the slide, overlaps, clipped text). The assistant gets this as text. To check its own work it runs the PinDeck CLI's `layout <slide>...` (`--issues` for the issues only). Screenshots are only taken when you tick the box or the assistant needs a visual judgement.
14
+ * **One selected backend.** Claude runs as a long-lived `claude -p` stream-json process; Codex runs as a local app-server over stdio. Both run in the deck folder under your own login, with the instructions from `lib/system-prompt.md` and the deck's `CLAUDE.md` and `AGENTS.md`. Codex model and effort changes reuse the active thread writer and apply to the next turn. Claude changes and provider switches prepare a replacement process before retiring the old one. Events from replaced sessions are ignored.
15
+ * **Permissions.** The assistant may read the deck folder, its screenshots, attached PDFs and asset roots, edit and create files in the deck folder, and run the editor's CLI. Nothing else: no other commands, and no edits to `brand.css`, `vendor/`, `.git/` or, for a named deck file, the other decks in its folder. Claude Code loads no settings files and no MCP servers, so no allow rule in your own, the deck's or a parent folder's settings widens this. Codex has no shell and no file access of its own: it reads and edits through PinDeck's own tools, which apply the same rules and refuse symlinks out of the deck. If `brand.css` changes during a turn anyway, the server restores it and says so in the chat. Subfolders that hold a deck of their own count as other decks.
16
+ * **Undo.** The server keeps a copy of every file a change touched, from before and after it (an AI turn, which may change, create or delete several files of the deck, or a hand edit). Files over 2 MB are not copied: the chat says when a turn changed one, and Undo leaves it as it is. Hidden files, `vendor/` and `export/` are not part of undo. Undo and redo walk that history and tell the assistant on the next message. They first check that the files are still as that change left them; if something else changed them since, they do nothing and say which file differs, rather than overwrite that work. Decks that share a folder share its files: while another deck's assistant works there too, a turn keeps only the files its own assistant said it wrote, and says which it left out.
17
+ * **Saving.** Every file PinDeck writes into the deck is written to a temporary file next to it and then renamed into place, so other programs never see half a file. Each change from the editor carries the revision of the files it was made on; if the files have changed since, the change is refused, the slide reloads, and edited text can be copied from the message to type in again.
18
+
19
+ ## Files
20
+
21
+ | File | Purpose |
22
+ |---|---|
23
+ | `front.mjs` | What `pindeck` starts (127.0.0.1 only, random token per run, one-time login links): the login, the deck list and one deck server per open deck at `/d/<deck>/`; `login`. |
24
+ | `server.mjs` | The server for one deck: UI, deck files, events, backend session, undo, CLI endpoints. Behind the front, or on its own (`node server.mjs <deck>`). |
25
+ | `lib/login.mjs`, `lib/deck-target.mjs`, `lib/deck-list.mjs` | Login links and cookies, which deck a path names (and its state folder), and the decks the menu lists. |
26
+ | `lib/servers.mjs` | The PinDecks running for this user, and which one serves a deck (`pindeck login`, `cli.mjs`). |
27
+ | `cli.mjs` | `layout`, `shot`, `slides`, `element`, `pdf handout` / `pdf presentation` and `pdf-figure` commands for the backends (and for you), and `git-setup`. |
28
+ | `public/` | The editor page; `bridge.js`, which is injected into the deck in edit mode (never saved to it); `events-worker.js`, the event stream the tabs of a browser share. |
29
+ | `lib/pptx.mjs`, `lib/zip.mjs`, `lib/xml.mjs` | Reading `.pptx` files (no dependencies): slides, text, shapes and their positions, notes. |
30
+ | `lib/pptx-import.mjs` | Turns PowerPoint slides into slides of the deck: figures, tables, build steps, audio, layout, HTML in the deck's style. |
31
+ | `lib/waveform.mjs` | Waveform SVGs for imported audio. |
32
+ | `lib/omml.mjs` | PowerPoint equations (Office Math) to TeX. |
33
+ | `lib/pptx-render.mjs`, `lib/pdf-render.jxa.js` | Slide and PDF pictures: PowerPoint or Keynote (macOS) or LibreOffice (Linux) saves a PDF; macOS PDFKit or Poppler renders pages and regions of it. |
34
+ | `lib/pdf.mjs` | PDF export: reveal.js print layout in headless Chrome, then print to PDF. |
35
+ | `lib/eids.mjs` | Adds the `data-eid` attributes by editing the HTML as text. |
36
+ | `lib/claude.mjs`, `lib/codex.mjs`, `lib/agent-process.mjs` | Backend adapters and JSON-line transport. |
37
+ | `lib/models.mjs`, `lib/agent-tools.mjs` | Model/settings normalization, handover, and restricted Codex tools. |
38
+ | `lib/capabilities.mjs` | What this server can do on this system (macOS, Linux), for the editor, the API, the CLI and the assistant. |
39
+ | `lib/deck-files.mjs` | Atomic writes and the revision of the deck's files. |
40
+ | `lib/system-prompt.md` | What the assistant is told about the editor. |
41
+ | `CONTRIBUTING.md` | How to report bugs and contribute. |
42
+ | `docs/` | Images for this README. |
43
+ | `LICENSE` | MIT. |
44
+
45
+ ## Current Limitations
46
+
47
+ * reveal.js decks, and decks that name their slides with `<meta name="pindeck:slides">` (see [Decks made without reveal.js](preparing-decks.md#decks-made-without-revealjs)). Any other deck made with another framework (Marp, Slidev, impress.js, remark, Spectacle, Shower, deck.js, bespoke.js) is recognised and refused with its name before PinDeck changes anything.
48
+ * Quarto: the rendered HTML must be next to its source (no `output-dir` in `_quarto.yml`), and only reveal.js output is supported. A render takes a few seconds; the slide shows the previous render meanwhile.
49
+ * Text inside inline SVG diagrams cannot be edited in place; pin it and ask the assistant.
50
+ * Parts of an inline SVG move with the whole SVG; inline text (bold, spans) moves with its block.
51
+ * No indenting or outdenting of bullets with `Tab` yet, and `Backspace` at the start of a bullet does not merge it with the one above; ask the assistant for those.
52
+ * Moves are offsets (`translate`), so the space an element came from stays reserved. Ask the assistant to turn hand edits into proper CSS when a layout should really change.
53
+ * The undo history lives on the server and starts fresh when PinDeck restarts, or when a deck closes after 30 minutes without an open editor.
54
+ * One writer at a time: PinDeck is not made for editing the same deck from two editors or tools at once.
55
+ * A deck's own scripts run sandboxed in the editor: they cannot use `localStorage`, cookies or the editor page. A deck that needs them for its slides to work should catch the error. In Present they can, on `localhost`, where every deck of one PinDeck shares the same `localStorage`.
56
+ * Linux: pictures of PDF pages and PowerPoint slides, and waveforms of non-WAV audio, need Poppler, LibreOffice and ffmpeg on the server (see [Running on a Linux server](linux-server.md#running-on-a-linux-server)).
@@ -0,0 +1,36 @@
1
+ # Running on a Linux server
2
+
3
+ [← README](../README.md)
4
+
5
+ ## Running on a Linux server
6
+
7
+ PinDeck can run on a Linux machine where the deck lives, with the editor in your Mac's browser. The deck, PinDeck, the backend (Claude Code or Codex) and the deck's git clone all stay on the server; the browser only shows the editor. The server listens on 127.0.0.1 there, as on a Mac, and an SSH tunnel brings that port to your Mac.
8
+
9
+ On the server you need Node 20 or later (22 or 24 recommended), git if the deck is in a repository, and the `claude` or `codex` CLI installed and logged in under your account there, if you want the chat. Without one, editing by hand still works. Then, in one SSH session on the server:
10
+
11
+ ```bash
12
+ node front.mjs path/to/deck --port 5180
13
+ ```
14
+
15
+ and on your Mac, in another terminal (`myserver` is your usual SSH host):
16
+
17
+ ```bash
18
+ ssh -N -T -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -L 127.0.0.1:5180:127.0.0.1:5180 myserver
19
+ ```
20
+
21
+ Open the login link PinDeck printed on the server in the Mac's browser. The local port may differ from the server's: with `-L 127.0.0.1:8080:127.0.0.1:5180`, change 5180 in the link to 8080. PinDeck accepts any port, as long as the address is `127.0.0.1`: `localhost` is where decks are presented, and nobody logs in there. If the tunnel drops, a message you have already sent keeps running on the server. Restart the tunnel and the editor reconnects and shows the files as they are then; it never sends a message twice. If PinDeck restarts, the editor asks you to log in again: run `node front.mjs login path/to/deck` on the server for a new link.
22
+
23
+ Other users of a shared server cannot open the editor or read the deck through PinDeck: the token that protects it is only in your browser's cookie and in PinDeck's own files in `~/.pindeck-v4` (readable only by you), never in a page or on a command line.
24
+
25
+ What works on Linux: editing by hand, speaker notes, slides, pins, the chat with Claude or Codex, Undo/Redo, live reload when files change on the server, layout checks, attaching figures, audio and PDFs, `git-setup` and the PowerPoint import. Some features use programs on the server, and are on when they are installed (on Debian or Ubuntu: `sudo apt install poppler-utils libreoffice-impress ffmpeg`):
26
+
27
+ | Feature | Needs |
28
+ |---|---|
29
+ | Screenshots, layout checks without an open editor, PDF export | Chrome or Chromium (`PINDECK_LINUX_CHROME=0` turns these off) |
30
+ | Pictures of PDF pages (`pdf-figure`) | Poppler (`pdftoppm`, `pdfinfo`) |
31
+ | Pictures of PowerPoint slides in the import (thumbnails, diagrams) | LibreOffice (`soffice`) and Poppler |
32
+ | Waveforms of audio other than WAV in the import | ffmpeg |
33
+
34
+ Without them the editor says what is missing (hover over "Server: Linux"), and the assistant is told which commands are not available. LibreOffice may draw some slides a little differently from PowerPoint; the pictures are references and figures, the slides themselves are rebuilt from the file. Chromium from snap may not be allowed to use hidden folders in your home folder such as `~/.pindeck-v4`; if screenshots fail with it, set `PINDECK_HOME` to a folder that is not hidden (not the one PinDeck 2 uses), or install Chrome or Chromium from a package. Run as root (in a container, say), PinDeck starts Chrome with `--no-sandbox`, which Chrome needs there; a screenshot or PDF that fails gives Chrome's own reason.
35
+
36
+ One PinDeck serves all your decks on one port, and a deck can be open in only one PinDeck at a time (a second one says where it is open). While PinDeck or the assistant is working on a deck, do not edit it with other tools or switch git branches at the same time. Stop first, or wait until the assistant has finished. PinDeck notices changes made by other programs and reloads, and it refuses Undo, Redo or an edit that would overwrite them. Its checks cannot rule out every race with a program that writes at the same moment.
@@ -0,0 +1,88 @@
1
+ # Preparing decks
2
+
3
+ [← README](../README.md)
4
+
5
+ ## Preparing a deck
6
+
7
+ PinDeck works with reveal.js decks whose slides are `<section>` elements inside `.slides`, and with other decks that name their slides (see [Decks made without reveal.js](#decks-made-without-revealjs)). Two optional `<meta>` tags in the deck's `<head>` tell it about the design:
8
+
9
+ ```html
10
+ <meta name="pindeck:footer-top" content="652"> <!-- y of the footer rule: content must stay above it -->
11
+ <meta name="pindeck:locked" content=".slide-foot, .logo"> <!-- brand elements that cannot be selected or moved -->
12
+ ```
13
+
14
+ Hidden slides use reveal.js's `data-visibility="hidden"`. If the deck numbers its slides itself (as the example's `deck.js` does for the footer), count only slides without that attribute, or the numbers will jump where a slide is hidden.
15
+
16
+ Without them PinDeck runs no footer check and locks only `.slide-foot` and `.logo`. A file called `brand.css` in the deck folder is locked for both backends. See `ocean-example/` for a complete deck.
17
+
18
+ ### Decks made without reveal.js
19
+
20
+ A deck with a script of its own, or made with a framework whose slides are elements of their own (Shower, deck.js, a hand-written deck), can be edited too once it names its slides in its `<head>`:
21
+
22
+ ```html
23
+ <meta name="pindeck:slides" content="main.deck > section.slide"> <!-- a CSS selector for the slides -->
24
+ <meta name="pindeck:size" content="1440x1080"> <!-- their size in px -->
25
+ ```
26
+
27
+ The selector may use tag names, `#id`, `.class`, `[attr]` and `[attr="value"]`, joined by spaces or `>`, and a list of them separated by commas. In the editor, PinDeck shows the slides itself, one at a time and scaled to the window, and switches off the deck's own scaling, slide keys and inert or hidden marks while you edit; that changes nothing in the deck file. Everything in [Working with it](using.md#working-with-it) works as with a reveal.js deck, with these differences:
28
+
29
+ * Build steps (`.fragment`) belong to reveal.js: other decks show every element at once.
30
+ * Hidden slides (`data-visibility="hidden"`) are left out of PinDeck's PDF exports; whether presenting skips them is up to the deck's own script.
31
+ * Speaker notes are an `<aside class="notes">` in the slide, as in reveal.js. PinDeck hides them in the editor; the deck's CSS should hide them too (`aside.notes { display: none }`).
32
+ * **Present** opens the deck at `#<slide id>`, or `#<number>` (counting from 1) for a slide without an id. Present may open on the first slide if the deck does not go to slides by their address.
33
+ * **+ New slide** models a new slide on the deck's own (see [Making your own template](#making-your-own-template)): a content slide gets placeholder bullets in a `.slide-body`; without one it has only its title. A new or duplicated slide does not take the original's `id`.
34
+
35
+ Both tags are needed: PinDeck opens the slides at the given size for screenshots and PDF pages. A missing size, or a selector that finds no slides or that PinDeck cannot read, is reported before anything changes. While PinDeck runs, it reads the tags again for every screenshot, PDF and assistant session; the editor reads them when the slide reloads. If a tag stops working meanwhile, the editor shows the same message and the assistant is told. A session already running keeps the slide selector it started with; **New chat** starts one with the new selector.
36
+
37
+ ## Making your own template
38
+
39
+ PinDeck has no template generator; a template is simply a deck folder. Start from a copy of the example:
40
+
41
+ ```bash
42
+ cp -R ocean-example my-template
43
+ ```
44
+
45
+ 1. **Brand in `brand.css`:** palette, backgrounds, footer and logo positions. PinDeck locks this file for both backends, so edit it by hand outside PinDeck. Replace the logos in `assets/` too.
46
+ 2. **Everything else in `theme.css`:** type sizes, spacing, layouts and components. This can be done in PinDeck with the selected assistant.
47
+ 3. **`index.html`:** the footer text in the `data-` attributes of `<div class="reveal">`, and the `pindeck:footer-top` and `pindeck:locked` meta tags if the footer or the brand elements move. `deck.js` holds the math macros, the footer markup and the audio player.
48
+ 4. **`README.md`, `CLAUDE.md` and `AGENTS.md`:** both backends follow the deck's `CLAUDE.md` and `AGENTS.md`, so describe the new classes and rules there; the assistant can read `README.md` for details. Refer to PinDeck's commands as "the PinDeck CLI command given in the system prompt", as the example's `CLAUDE.md` does; PinDeck tells the assistant the right path wherever the deck is. An older `node ../cli.mjs` in a deck's `CLAUDE.md` is replaced with that command.
49
+
50
+ **Instructions for the assistant.** PinDeck gives both backends the deck folder's own `CLAUDE.md` and `AGENTS.md`. It does not load such files from parent folders, as Claude Code and Codex do in a terminal: those are usually written for coding (run the tests, commit), which PinDeck's assistant cannot do; they would change with where the deck happens to sit; and on a shared server a parent folder may be writable by others. To share rules between decks, say so in the deck's own file, in one of two ways:
51
+
52
+ - **Import:** `Follow @../shared/STYLE.md for wording.` includes that file's text. Paths are relative to the file they are in (or absolute, or `~/...`); imported files may import others, up to 5 levels, each file once, 200 KB in all. Only text files are imported; an e-mail address, an `@name` that is not a file, and anything in code are left as written.
53
+ - **Pointer:** `The style rules are in ../shared/STYLE.md; read them first.` The assistant reads the file when it needs it.
54
+
55
+ **New slide** and the PowerPoint import model new slides on the deck's own: keep a slide with class `title-slide`, one with `section-slide`, and a content slide with `.slide-head` (`h2`, optionally a `.kicker` or `.sub`) and `.slide-body`. Imported tables take the class of the deck's first table, and imported audio copies the first `.demo-row` (with a `data-target`). Without a title or section slide, those PowerPoint slides are imported as content slides; without a `.demo-row`, audio gets plain players.
56
+
57
+ ## Quarto decks
58
+
59
+ PinDeck opens reveal.js decks written in [Quarto](https://quarto.org): name the source, or the HTML Quarto made from it (the `.qmd` must be beside it).
60
+
61
+ ```bash
62
+ pindeck quarto-mathjax-example/talk.qmd
63
+ ```
64
+
65
+ * **The source is what changes.** The rendered `talk.html` and `talk_files/` are build output: locked for the assistant, which edits `talk.qmd` and the files it uses (its `.scss` theme, figures) instead. Its instructions explain how pins on the rendered slides map to the source.
66
+ * **PinDeck renders.** On start, if the HTML is older than the source, and after every change to a source file, by the assistant, by Undo or by you in any editor. The slide reloads when the render is done; the assistant's `shot`, `layout` and `slides` wait for it, and Quarto's errors go to the chat and to the assistant.
67
+ * **No hand edits.** Moving, resizing, text editing, notes, new, hidden, moved or deleted slides and the PowerPoint import would be overwritten by the next render, so they are off. Pins, comments, the chat, Undo of the assistant's changes, screenshots and PDF export work as usual.
68
+ * **The Ocean template in Quarto.** `format: ocean-revealjs` gives a Quarto deck the look of `ocean-example/` (title slide, dividers, kicker over the title, footer with slide number and logo). Copy `quarto-mathjax-example/_extensions/ocean/` next to your `.qmd`; its `README.md` lists the Markdown for each layout. As in an HTML deck, its `brand.css` and `vendor/` are locked for the assistant and its `theme.css` is not.
69
+ * `quarto-mathjax-example/` is a short signal-processing deck in Ocean (sampling, aliasing, a moving-average filter) with interactive Observable JS slides.
70
+ * `quarto-python-example/` is the same deck with a slide whose figure and numbers Python computes at each render. It needs Jupyter and matplotlib (`pip install -r quarto-python-example/requirements.txt`).
71
+
72
+ ## PowerPoint import
73
+
74
+ The import keeps what can be carried over exactly and makes pictures of the rest:
75
+
76
+ * **Text becomes HTML** in the deck's layouts: the title (and the subtitle, as a `.sub` line or a kicker, as the deck does it), bullets with their levels, numbered lists, bold, italics, coloured words (as `.hl`), superscripts, web and mail links and line breaks, other text as paragraphs, references at the bottom of a slide as `.refs`, and the speaker notes. Side notes and labels clearly smaller than the body text stay smaller.
77
+ * **Tables become HTML tables** with the class of the deck's own tables, merged header cells, header rows and a caption from the text above them. Cells merged down in the body become a label on the group's first row with empty cells below it, and each later group's first row gets `class="group"`. A table with shapes drawn over it (e.g. boxes marking rows) is kept as a picture instead.
78
+ * **Build steps:** shapes and paragraphs that appear (or disappear) on a click become reveal.js fragments in the same order, also when several things appear on one click. A picture made of several shapes appears on a click only if all of its shapes do.
79
+ * **Audio** goes into the deck's own audio player (a copy of its first `.demo-row`, with a waveform made from the sound), labelled with the nearest text on the slide; without such a player, plain players. **Video** keeps its place, with its poster frame. The first slide is filled into the deck's title slide (title, subtitle, authors, event, institute) and PowerPoint section headers into its section divider.
80
+ * **Pictures are copied** to `figures/` (`<file>-s04-1.png`, ...).
81
+ * **Everything drawn** (diagrams built from shapes and arrows, charts, pictures with labels on them, EMF graphics) becomes a picture cut from the slide as PowerPoint renders it. For this PinDeck asks PowerPoint (or else Keynote) to save the presentation as a PDF; macOS asks once whether PinDeck may control the app. The same PDF gives the thumbnails in the import dialog.
82
+ * **Layout:** blocks that sit side by side become `.cols` with the slide's proportions, others are stacked top to bottom; figures and, if needed, the text are scaled so that the slide fits.
83
+ * **Equations** (PowerPoint's equation editor) become TeX for the deck's KaTeX: `\( \)` in the text, `\[ \]` on their own line; fractions, scripts, roots, sums and integrals, functions and limits, accents, braces, matrices and the bold, italic and double-struck math letters are converted.
84
+ * **Hidden slides** stay hidden (`data-visibility="hidden"`).
85
+
86
+ **Hand-off to the assistant.** Slides the import can only approximate are tagged *complex* in the dialog: diagrams of many shapes or tables with shapes over them that became pictures, pictures placed inside text (often equations as images), build steps on parts of a diagram, slides without a title, and text that had to shrink to fit. With **Then ask the assistant to improve the complex slides** ticked, the assistant gets these slides right after the import, each with a picture of the original slide and the texts of its diagrams, and rebuilds diagrams as SVG, equations as TeX and tables as HTML in the deck's style. The request shows in the chat like your own messages, and its changes can be undone as usual.
87
+
88
+ Without PowerPoint or Keynote (or on other systems than macOS) only the text and the pictures are imported.
package/docs/using.md ADDED
@@ -0,0 +1,45 @@
1
+ # Using PinDeck
2
+
3
+ [← README](../README.md)
4
+
5
+ ## Several decks
6
+
7
+ Click the deck's name above the slide for the other decks: the `.html` files in the deck's folder and in the asset roots, and in their folders one level down (`lecture/index.html`). A deck opens in the same tab, or with ⌘-click in a new one, on the same port and login, so one SSH tunnel serves them all. Each keeps its own chat and undo history while it is open, and the assistant finishes a turn in a deck you leave. A file PinDeck cannot open is greyed out with the reason; listing a deck changes nothing, and only opening it adds its element ids. Symlinked decks and folders are listed too; a deck file that links to a deck in another folder is greyed out (open that deck itself). Started with a folder that has no `index.html`, PinDeck shows the list of its decks. A deck with no editor open for 30 minutes is closed (its undo history goes, as on a restart) and opens again when you choose it. Open as many decks in as many tabs as you like: the tabs of a browser share one connection for their live updates.
8
+
9
+ A deck is open in one PinDeck at a time: started again for a deck that another PinDeck (another port, or `server.mjs` on its own) has open, PinDeck says where it is open and stops, so their chats and undo histories never overwrite each other. `pindeck login <deck>` and `cli.mjs` find the PinDeck that serves the deck, whichever port it runs on.
10
+
11
+ ## Working with it
12
+
13
+ | Action | How |
14
+ |---|---|
15
+ | Select, move, resize | Select mode (`V`, the default): click an element to select it, drag to move, pull a handle to resize. Elements snap to the slide edges and centre, the footer rule and other elements (pink guides); hold `Alt` to turn snapping off, `Shift` to move along one axis. Images keep their aspect ratio (`Shift` toggles). Arrow keys nudge by 1 px, `Shift`+arrows by 10 px. `⌘`-click adds an element to the selection or takes it out: the elements move, nudge and are deleted together (they have no resize handles), and the assistant gets all of them. `Delete` removes the selected element after a confirmation; a picture goes with its caption, and files nothing else in the deck uses can be deleted with it. `⌘Z` brings both back. |
16
+ | Edit text | Double-click the text (or select it and press `Enter`). The element shows its source, so math appears as TeX, e.g. `\(\mu=0.5\)`. `Enter` or a click elsewhere saves, `Esc` cancels, `Shift+Enter` adds a line break, `⌘B` / `⌘I` make text bold or italic. In a bullet, `Enter` splits it at the cursor and continues in a new bullet below; `Enter` on an empty new bullet removes it and stops. Pasted text arrives as plain text. Table cells are edited one at a time. |
17
+ | Undo, redo | `⌘Z` / `⇧⌘Z` or the toolbar arrows. One history for your hand edits and the assistant's changes. |
18
+ | Pin elements | Press `P` (or the Pin button), click elements; each click gets a number. Click a marker to remove it, `Esc` to stop. |
19
+ | Ask the assistant | Write in the sidebar and refer to pins by number: "1: align with 2, make 3 smaller". The selected element is sent too, so "make this smaller" works; with several selected, "align these" does. `Enter` sends, `Shift+Enter` adds a new line. |
20
+ | Visual questions | **+** in the chat, then **Screenshot**: the current slide with the pins of that message is attached when you send. If Chrome cannot take it, the assistant is told. **Stop** during the screenshot stops at once. |
21
+ | Review | Each answer ends with the changed files and a **Diff** button; the most recent change still in effect also has an **Undo** button. After a restart, earlier changes keep their *undone* mark but have no Diff or Undo (the undo history ends with the run). |
22
+ | Stop | **Stop** interrupts the assistant; the conversation continues. **New chat** starts a new conversation; your slides, Undo and Redo stay as they are. |
23
+ | New slide | **+ New slide** above the slide list: a **content slide** (title and bullets), a **section divider**, or a **duplicate** of the current slide. It is added after the current slide, modelled on the deck's own slides; a new content slide or divider opens its title for typing. |
24
+ | Import from PowerPoint | **+ New slide** → **Import from PowerPoint…**, then choose a `.pptx` file and tick the slides you want (hidden PowerPoint slides start unticked). They are added after the current slide in this deck's own layouts, and the assistant can improve the complex ones; see [PowerPoint import](preparing-decks.md#powerpoint-import). `⌘Z` removes them again, with their figures. |
25
+ | Speaker notes | The **Notes** field under the slide shows the current slide's notes (the speaker view, `S`, shows them when presenting). Type to change them; they are saved when you click elsewhere or press `⌘Enter`, and `Esc` puts back what was there. A slide without notes gets them; emptying the field removes them. Notes show their formatting: `⌘B`, `⌘I` and `⌘U` format the selection, `Enter` starts a new line, and pasted text comes in plain. Notes with markup the field cannot show safely (other tags, styles, scripts) are edited as HTML. Drag the line above the notes to give them more or less room (double-click it for the default size); collapse them with their header. |
26
+ | Move a slide | Drag it up or down in the slide list; an orange line shows where it will go. It moves with its notes and the comment above it, and `⌘Z` puts it back. Slides inside vertical stacks cannot be moved yet. |
27
+ | Hide a slide | The eye on a slide in the list, or `H`. A hidden slide stays in the deck and in PinDeck (greyed out, with a banner) but is skipped when presenting and in PDFs; **Export ▾** can include hidden slides. Show it the same way. |
28
+ | Model and effort | Choose a discovered Claude or OpenAI model in the chat header, then use the separate reasoning-effort selector. Provider changes offer an optional handover; same-provider changes continue the conversation. The choice is saved in `~/.pindeck-v4/config.json`. |
29
+ | Attach figures and audio | **+** in the chat, then **File…**, drag and drop onto the chat or a slide, or paste an image. Files are saved in `<deck>/figures/` straight away; audio also gets a waveform SVG. Then ask the assistant, e.g. "put this figure on a new slide after 4". Removing an attachment before sending deletes the file again. |
30
+ | Attach a PDF | The same way: a figure or table to take over, or a paper or report with more details. PDFs stay outside the deck (in `~/.pindeck-v4`); the assistant reads them, rebuilds tables as HTML, and renders a figure (a page or a cropped part of one) into `<deck>/figures/` as PNG with `cli.mjs pdf-figure` (macOS, or Linux with Poppler). E.g. "put Fig. 3 of this paper on slide 7" or "add the results of Table 2 to slide 9". |
31
+ | Export PDF | **Export ▾** in the toolbar. **Handout PDF**: one page per slide, build steps shown complete. **Presentation PDF**: one page per click, so a slide with two build steps gives three pages. Files go to `<deck>/export/`. Needs Chrome or Chromium. |
32
+ | Navigate | Slide list, `←` `→`, or the toolbar. **Present** opens the deck in a new tab, at the current slide. |
33
+ | Comment for later | Write a note in the message box, pin one element (or none, for the whole slide) and press **Comment** instead of Send. The comment is kept, not sent: a blue marker `C1`, `C2`, … on the slide, and an entry in **Comments** (chat header). Click a marker to open its comment. See [Review comments](#review-comments). |
34
+ | Check from a terminal | `node cli.mjs layout 3 --issues`, `slides`, `shot 3` or `pdf handout`, in the deck's folder (or with `--deck <folder or file.html>`): asks the PinDeck that serves the deck. |
35
+
36
+ You can also edit `index.html` or `theme.css` in any editor; the slide reloads, and the assistant is told the files changed. CSS changes are swapped in without a reload. Typing in the slide is not lost meanwhile: when you finish, PinDeck refuses to save it over the other change and offers the text to copy.
37
+
38
+ ## Review comments
39
+
40
+ Review a deck first, change it later: comments are notes on slides and elements that wait, for you, for the assistant, or for a coding agent outside PinDeck (Claude Code in VS Code, say). Based on Sebastian J. Schlecht's review mode.
41
+
42
+ * **Kept outside the deck.** Comments are saved in `<deck folder>/.pindeck/<deck>.review.json`, never in the slides. They are no part of Undo, and the assistant cannot edit them.
43
+ * **Found again after changes.** Each comment remembers its slide (id attribute, title, position) and its element (id, tag and text, math as TeX). After an edit it is found again by them; one whose element or slide is gone, or whose text changed (perhaps because the comment was addressed), turns grey and says why. It is never attached to something else.
44
+ * **The Comments panel** lists them with their slide; click one to go there. **Reply**, **Edit**, **Resolve** (or **Reopen**) and **×** in each entry; **Edit** changes the text in place (`⌘↩` saves, `Esc` cancels). **Process Comments** puts all open comments into the message box as one request, to read over and send. The assistant marks each comment it has addressed as resolved, with a line on what it did (shown in the list under **Show resolved**). One it could not do, or found unclear, it leaves open with a reply on the comment saying why. **Reply** to answer it there, then **Process Comments** again: the thread goes with the comment. **Reopen** a resolved one if you disagree.
45
+ * **For an agent outside PinDeck**, `.pindeck/<deck>.review.md` lists the open comments with each slide's anchor, element and (for a Quarto deck) source line. It is rewritten whenever the comments or the deck change. The agent sets `"status": "resolved"` in the JSON file, or leaves the comment open and adds `{ "by": "agent", "text": "…" }` to its `replies`; the editor follows.
@@ -13,7 +13,7 @@ export const deckTools = [
13
13
  { type: 'function', name: 'pindeck_read', description: 'Read a deck text file, attached image, screenshot, or PDF page. For PDFs supply a 1-based page; returns page text and its rendered image. For text use offset to read beyond the first 120000 characters.', inputSchema: { ...schema({ path: str, page: { type: 'integer', minimum: 1 }, offset: { type: 'integer', minimum: 0 } }), required: ['path'] } },
14
14
  { type: 'function', name: 'pindeck_edit', description: 'Replace one exact unique text occurrence in a file of the deck folder: the deck file, theme.css or another of its files. Read the file first. brand.css, vendor/, .git/ and other decks in the folder are locked.', inputSchema: { ...schema({ path: str, oldText: str, newText: str }), required: ['path', 'oldText', 'newText'] } },
15
15
  { type: 'function', name: 'pindeck_write', description: 'Create a new file in the deck folder (missing folders are created), or replace all of an existing one. Prefer pindeck_edit for changes to existing files. The same files are locked as for pindeck_edit.', inputSchema: { ...schema({ path: str, content: str }), required: ['path', 'content'] } },
16
- { type: 'function', name: 'pindeck_command', description: 'Run a PinDeck command: layout, shot, slides, pdf, or pdf-figure. Pass individual arguments in an array, never a shell command. Use pindeck_read on returned image paths.', inputSchema: { ...schema({ command: { enum: ['layout', 'shot', 'slides', 'pdf', 'pdf-figure'] }, args: { type: 'array', items: str, maxItems: 100 } }), required: ['command', 'args'] } },
16
+ { type: 'function', name: 'pindeck_command', description: 'Run a PinDeck command: layout, shot, slides, element (the HTML of elements, by data-eid), pdf, pdf-figure, resolve (a review comment: its id, then what you did) or reply (to a review comment you leave open: its id, then why). Pass individual arguments in an array, never a shell command. Use pindeck_read on returned image paths.', inputSchema: { ...schema({ command: { enum: ['layout', 'shot', 'slides', 'element', 'pdf', 'pdf-figure', 'resolve', 'reply'] }, args: { type: 'array', items: str, maxItems: 100 } }), required: ['command', 'args'] } },
17
17
  ];
18
18
  /* Codex may read the deck, screenshots, attached PDFs and asset roots, and write any file of the deck folder
19
19
  except the locked ones: the same rules as Claude's --allowedTools / --disallowedTools. locked: patterns
@@ -47,7 +47,7 @@ export function createDeckTools({ deckDir, readDirs = [], cliFile, env, pdfTextF
47
47
  return full;
48
48
  }
49
49
  async function command(name, args, signal) {
50
- if (!['layout', 'shot', 'slides', 'pdf', 'pdf-figure'].includes(name) || !Array.isArray(args) || args.length > 100 || args.some(a => typeof a !== 'string' || a.length > 4096 || a.includes('\0'))) throw new Error('Invalid PinDeck command.');
50
+ if (!['layout', 'shot', 'slides', 'element', 'pdf', 'pdf-figure', 'resolve', 'reply'].includes(name) || !Array.isArray(args) || args.length > 100 || args.some(a => typeof a !== 'string' || a.length > 4096 || a.includes('\0'))) throw new Error('Invalid PinDeck command.');
51
51
  if (name === 'pdf-figure') {
52
52
  checked(args[0]);
53
53
  // Host renderer can create figures: disallow symlinked output directories too.
package/lib/deck-list.mjs CHANGED
@@ -13,10 +13,12 @@ const MAX_BYTES = 5 * 1024 * 1024;
13
13
 
14
14
  // symlinks count as what they point to
15
15
  const kind = (dir, e) => { if (!e.isSymbolicLink()) return e; try { return fs.statSync(path.join(dir, e.name)); } catch { return e; } };
16
+ // .html decks, and Quarto sources in place of what Quarto renders from them (as the editor names the deck)
16
17
  const htmlFiles = (dir) => {
17
18
  try {
18
- return fs.readdirSync(dir, { withFileTypes: true })
19
- .filter((e) => /\.html$/i.test(e.name) && !e.name.startsWith(".") && kind(dir, e).isFile()).map((e) => e.name).sort();
19
+ const files = fs.readdirSync(dir, { withFileTypes: true })
20
+ .filter((e) => /\.(html|qmd)$/i.test(e.name) && !e.name.startsWith(".") && kind(dir, e).isFile()).map((e) => e.name);
21
+ return files.filter((f) => /\.qmd$/i.test(f) || !files.includes(f.replace(/\.html$/i, ".qmd"))).sort();
20
22
  } catch { return []; }
21
23
  };
22
24
  const subfolders = (dir) => {
@@ -57,7 +59,7 @@ export function listDecks(roots) {
57
59
  const st = fs.statSync(abs), known = checked.get(abs);
58
60
  if (known && known.mtimeMs === st.mtimeMs && known.size === st.size) problem = known.problem;
59
61
  else {
60
- problem = st.size > MAX_BYTES ? `${file} is larger than 5 MB, too large for a deck.`
62
+ problem = /\.qmd$/i.test(file) ? null : st.size > MAX_BYTES ? `${file} is larger than 5 MB, too large for a deck.`
61
63
  : unsupportedMessage(fs.readFileSync(abs, "utf8"), file)?.replace(/ Nothing was changed\.$/, "") || null;
62
64
  checked.set(abs, { mtimeMs: st.mtimeMs, size: st.size, problem });
63
65
  }
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import fs from "node:fs";
7
7
  import crypto from "node:crypto";
8
8
 
9
- export const homeDir = (env = process.env) => env.PINDECK_HOME || path.join(os.homedir(), ".pindeck-v3");
9
+ export const homeDir = (env = process.env) => env.PINDECK_HOME || path.join(os.homedir(), ".pindeck-v4");
10
10
 
11
11
  /* PinDeck 2 keeps its chats and settings in files of the same names, in other formats: the two versions in one
12
12
  folder (one PINDECK_HOME for both) overwrite each other's. PinDeck 3 saves a provider with every chat and
@@ -20,16 +20,18 @@ export function homeWarning(home) {
20
20
  try { chat = JSON.parse(fs.readFileSync(path.join(home, "decks", d, "chat.json"), "utf8")); } catch { continue; }
21
21
  if (chat && typeof chat === "object" && !("provider" in chat))
22
22
  return `${home} holds a chat written by PinDeck 2 (decks/${d}/chat.json). If PinDeck 2 uses this folder too, ` +
23
- `give PinDeck 3 another one (PINDECK_HOME; by default ~/.pindeck-v3): the two versions overwrite each ` +
23
+ `give this PinDeck another one (PINDECK_HOME; by default ~/.pindeck-v4): the two versions overwrite each ` +
24
24
  `other's settings and chats. Chats copied over from PinDeck 2 are fine.`;
25
25
  }
26
26
  return null;
27
27
  }
28
28
 
29
29
  /* A file (talk.html), or a folder, which means its index.html. A path ending in .html that is
30
- not a folder is a file, so a missing file gets a clear message. */
30
+ not a folder is a file, so a missing file gets a clear message.
31
+ A Quarto deck is named by its source (talk.qmd) or by what Quarto renders from it (talk.html, beside it):
32
+ either way the deck is talk.html, with source "talk.qmd", and shares its state. */
31
33
  export function deckTarget(target) {
32
- const abs = path.resolve(target);
34
+ const abs = path.resolve(target).replace(/\.qmd$/i, ".html");
33
35
  const isFolder = (() => { try { return fs.statSync(abs).isDirectory(); } catch { return false; } })();
34
36
  const dir = isFolder || !/\.html$/i.test(abs) ? abs : path.dirname(abs);
35
37
  const file = dir === abs ? "index.html" : path.basename(abs);
@@ -39,5 +41,7 @@ export function deckTarget(target) {
39
41
  histories stay; a named file gets its own, so two decks in one folder never share chat or undo. */
40
42
  const key = (named ? `${path.basename(dir)}-${name}` : path.basename(dir)).replace(/[^\w.-]+/g, "_") + "-" +
41
43
  crypto.createHash("sha1").update(named ? path.join(dir, file) : dir).digest("hex").slice(0, 8);
42
- return { dir, file, named, name, key };
44
+ const qmd = file.replace(/\.html$/i, ".qmd");
45
+ const source = fs.existsSync(path.join(dir, qmd)) ? qmd : null;
46
+ return { dir, file, named, name, key, source };
43
47
  }
package/lib/eids.mjs CHANGED
@@ -126,7 +126,7 @@ export function decode(s) { // one pass, so "&amp;lt;" stays "&lt;"
126
126
  return s.replace(/&(?:(nbsp|amp|lt|gt|quot|apos)|#(\d+)|#x([0-9a-f]+));/gi, (m, name, d, x) =>
127
127
  name ? ENTITIES[name.toLowerCase()] : String.fromCodePoint(d ? +d : parseInt(x, 16)));
128
128
  }
129
- function textOf(html, node) {
129
+ export function textOf(html, node) {
130
130
  if (node.closeStart < 0) return "";
131
131
  return decode(html.slice(node.contentStart, node.closeStart).replace(/<[^>]*>/g, " "))
132
132
  .replace(/\\[()[\]]/g, " ").replace(/\s+/g, " ").trim();
@@ -250,6 +250,10 @@ export function findSlides(tree) {
250
250
  return out;
251
251
  }
252
252
 
253
+ /* A slide's kind from its classes (title-slide, section-slide, closing-slide, …). pandoc gives every level-1 slide
254
+ .title-slide: a more specific kind (.section-slide) wins. */
255
+ function kindOf(cls) { const k = cls.filter((c) => /-slide$/.test(c)); return k.find((c) => c !== "title-slide") || k[0] || "content"; }
256
+
253
257
  function slideTitle(html, section) {
254
258
  const cls = classesOf(section);
255
259
  const heading = find(section, (c) => /^h[1-3]$/.test(c.name));
@@ -260,7 +264,7 @@ function slideTitle(html, section) {
260
264
  main = textOf(html, heading);
261
265
  if (sub && main.endsWith(sub)) main = main.slice(0, -sub.length).trim();
262
266
  }
263
- return { main, sub, kind: cls.find((c) => /-slide$/.test(c)) || "content" };
267
+ return { main, sub, kind: kindOf(cls) };
264
268
  }
265
269
 
266
270
  function elementName(node) {
@@ -429,6 +433,12 @@ export function getInner(html, eid, path) {
429
433
  return node ? html.slice(node.contentStart, node.closeStart) : null;
430
434
  }
431
435
 
436
+ /* An element with its own tags, exactly as written in the source. */
437
+ export function getOuter(html, eid) {
438
+ const node = nodeAt(html, eid, [], { anyKind: true });
439
+ return node ? html.slice(node.start, elementEnd(html, node)) : null;
440
+ }
441
+
432
442
  /* Replaces the inner HTML of one element. Returns { html, changed } or null if not found. */
433
443
  export function setInner(html, eid, path, inner) {
434
444
  const node = nodeAt(html, eid, path);
@@ -515,7 +525,7 @@ export function fileRefs(fragment) {
515
525
  /* ---- New slides ---------------------------------------------------------------------------------- */
516
526
 
517
527
  export const classList = (n) => (n.attrs?.class?.value || "").split(/\s+/).filter(Boolean);
518
- const slideKind = (n) => classList(n).find((c) => /-slide$/.test(c)) || "content";
528
+ const slideKind = (n) => kindOf(classList(n));
519
529
 
520
530
  /* Inserts a new slide after the slide `afterEid`, modelled on a slide of the deck itself so it
521
531
  matches the design: