pindeck 2.10.0 → 2.11.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 +5 -1
- package/package.json +1 -1
- package/public/editor.css +10 -0
- package/public/editor.js +24 -2
- package/public/index.html +4 -0
- package/server.mjs +16 -3
package/README.md
CHANGED
|
@@ -253,7 +253,7 @@ show and commit as usual. Clones without the filter configured simply see the fi
|
|
|
253
253
|
| Undo, redo | `⌘Z` / `⇧⌘Z` or the toolbar arrows. One history for your hand edits and Claude's changes. |
|
|
254
254
|
| Pin elements | Press `P` (or the Pin button), click elements; each click gets a number. Click a marker to remove it, `Esc` to stop. |
|
|
255
255
|
| Ask Claude | 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. `Enter` sends, `Shift+Enter` adds a new line. |
|
|
256
|
-
| Visual questions | **+** in the chat, then **Screenshot**: the current slide with its pin markers is attached when you send. |
|
|
256
|
+
| Visual questions | **+** in the chat, then **Screenshot**: the current slide with its pin markers is attached when you send. The chat shows it, and the screenshots Claude reads, as a small picture; click it to see it larger. |
|
|
257
257
|
| Review | Each answer ends with the changed files, a **Diff** button and an **Undo** button (most recent change). |
|
|
258
258
|
| Stop | **Stop** interrupts Claude; the conversation continues. **New chat** starts over. |
|
|
259
259
|
| 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, and its title opens for typing. |
|
|
@@ -396,6 +396,10 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
|
|
|
396
396
|
|
|
397
397
|
## Versions
|
|
398
398
|
|
|
399
|
+
* **2.11**: screenshots in the chat. A screenshot sent with a message, and one Claude reads, shows as
|
|
400
|
+
a small picture that opens larger with a click (Esc or a click closes it). PinDeck keeps the 40
|
|
401
|
+
newest screenshots of a deck; an older one says "no longer kept". Chats from before 2.11 keep their
|
|
402
|
+
plain label.
|
|
399
403
|
* **2.10**: decks with their own file names and shared files, decks made without reveal.js, and Claude
|
|
400
404
|
working on the whole deck.
|
|
401
405
|
- A deck file by name: `pindeck path/to/talk.html` (a folder still opens its `index.html`). Several
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pindeck",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "PinDeck: an editor for HTML slide decks on macOS, and on Linux (beta) through an SSH tunnel. Pin elements, describe the change, and Claude Code edits the slides.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"slides",
|
package/public/editor.css
CHANGED
|
@@ -207,6 +207,10 @@ body.resizing-notes .stage iframe { pointer-events: none; } /* keep the drag w
|
|
|
207
207
|
.chip .lock { color: var(--muted); }
|
|
208
208
|
.chip.sel .n { background: #1a73e8; border-radius: 4px; font-size: 10px; }
|
|
209
209
|
.chip .dim { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
210
|
+
.shot-chip { font: inherit; font-size: 12px; color: inherit; }
|
|
211
|
+
.shot-chip img { height: 28px; width: auto; max-width: 64px; border-radius: 3px; border: 1px solid var(--line); background: #fff; }
|
|
212
|
+
.msg.tool .shot-chip { margin: 0 0 0 4px; vertical-align: middle; padding: 1px; border-radius: 4px; }
|
|
213
|
+
.chip.gone { cursor: default; color: var(--muted); }
|
|
210
214
|
.msg.edit { font-size: 12.5px; color: var(--muted); display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
211
215
|
.msg.edit .files { color: var(--ink); }
|
|
212
216
|
.msg.edit .undone { text-decoration: line-through; }
|
|
@@ -264,6 +268,12 @@ body.resizing-notes .stage iframe { pointer-events: none; } /* keep the drag w
|
|
|
264
268
|
.import-dialog .import-notes .check { color: var(--ink); }
|
|
265
269
|
.import-list .tag.complex { border-color: var(--orange); color: var(--orange); }
|
|
266
270
|
|
|
271
|
+
/* ---- Screenshot ---- */
|
|
272
|
+
.shot-dialog { padding: 0; border: 0; background: none; max-width: 94vw; max-height: 92vh; cursor: zoom-out; }
|
|
273
|
+
.shot-dialog::backdrop { background: rgba(20,22,25,.6); }
|
|
274
|
+
.shot-dialog img { display: block; max-width: 94vw; max-height: 92vh; object-fit: contain; background: #fff;
|
|
275
|
+
border-radius: 6px; box-shadow: 0 20px 60px rgba(0,0,0,.35); }
|
|
276
|
+
|
|
267
277
|
/* ---- Diff ---- */
|
|
268
278
|
.diff-dialog { width: min(1000px, 92vw); max-height: 86vh; padding: 0; border: 1px solid var(--line); border-radius: 10px;
|
|
269
279
|
box-shadow: 0 20px 60px rgba(0,0,0,.25); }
|
package/public/editor.js
CHANGED
|
@@ -571,17 +571,25 @@ function md(s) {
|
|
|
571
571
|
}).join("");
|
|
572
572
|
return out.replace(/<p>\u0000(\d+)\u0000<\/p>|\u0000(\d+)\u0000/g, (_, a, b) => `<pre>${blocks[a ?? b]}</pre>`);
|
|
573
573
|
}
|
|
574
|
+
/* A screenshot in the chat: a small picture that opens larger. Chats from before 2.11 recorded no file
|
|
575
|
+
name (true); the server keeps only the newest 40, so an older one fails to load and says so. */
|
|
576
|
+
function shotChip(name, label = " screenshot") {
|
|
577
|
+
if (typeof name !== "string") return `<span class="chip">📷${label}</span>`;
|
|
578
|
+
const src = `/shots/${encodeURIComponent(name)}`;
|
|
579
|
+
return `<button type="button" class="chip shot-chip" data-shot="${esc(src)}" title="Open the screenshot">` +
|
|
580
|
+
`<img src="${esc(src)}" alt="">${label}</button>`;
|
|
581
|
+
}
|
|
574
582
|
function entryHtml(e) {
|
|
575
583
|
switch (e.kind) {
|
|
576
584
|
case "user":
|
|
577
585
|
return `${esc(e.text)}${e.pins?.length || e.screenshot || e.selection || e.attachments?.length ? `<div class="chips">${e.selection ?
|
|
578
586
|
`<span class="chip sel" title="${esc(e.selection.eid)}"><span class="n">▢</span> S${e.selection.slide} · ${esc(e.selection.label)}</span>` : ""}${(e.pins || []).map((p) =>
|
|
579
587
|
`<span class="chip" title="${esc(p.eid)}"><span class="n">${p.n}</span> S${p.slide} · ${esc(p.label || p.eid)}</span>`).join("")}${
|
|
580
|
-
e.screenshot ?
|
|
588
|
+
e.screenshot ? shotChip(e.screenshot) : ""}${(e.attachments || []).map((a) =>
|
|
581
589
|
`<span class="chip att-chip" title="${esc(a.path)}">${a.kind === "image" ? `<img src="/deck/${esc(a.path)}" alt="">` : a.kind === "pdf" ? "📄" : "🔊"} ${esc(a.path.split("/").pop())}</span>`).join("")}</div>` : ""}`;
|
|
582
590
|
case "assistant": return md(e.text);
|
|
583
591
|
case "tool":
|
|
584
|
-
return `<span><span class="tn">${esc(e.name)}</span> ${esc(e.detail || "")}${e.error ? `<span class="err">${esc(e.error)}</span>` : ""}</span>`;
|
|
592
|
+
return `<span><span class="tn">${esc(e.name)}</span> ${esc(e.detail || "")}${e.shot ? " " + shotChip(e.shot, "") : ""}${e.error ? `<span class="err">${esc(e.error)}</span>` : ""}</span>`;
|
|
585
593
|
case "edit": {
|
|
586
594
|
const t = state.turns.get(e.turnId);
|
|
587
595
|
return `<span class="files ${t?.undone ? "undone" : ""}">✎ ${esc(e.label)}</span>
|
|
@@ -641,6 +649,12 @@ chatEl.addEventListener("click", async (e) => {
|
|
|
641
649
|
$("diff-dialog").showModal();
|
|
642
650
|
return;
|
|
643
651
|
}
|
|
652
|
+
const shot = e.target.closest("[data-shot]");
|
|
653
|
+
if (shot) {
|
|
654
|
+
$("shot-img").src = shot.dataset.shot;
|
|
655
|
+
$("shot-dialog").showModal();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
644
658
|
const u = e.target.closest("[data-undo]");
|
|
645
659
|
if (u) {
|
|
646
660
|
u.disabled = true;
|
|
@@ -651,6 +665,14 @@ chatEl.addEventListener("click", async (e) => {
|
|
|
651
665
|
}
|
|
652
666
|
});
|
|
653
667
|
$("diff-close").onclick = () => $("diff-dialog").close();
|
|
668
|
+
// a picture that is gone: error events do not bubble, so they are caught on the way down
|
|
669
|
+
chatEl.addEventListener("error", (e) => {
|
|
670
|
+
const chip = e.target.closest?.(".shot-chip");
|
|
671
|
+
if (!chip) return;
|
|
672
|
+
chip.outerHTML = '<span class="chip gone" title="Only the 40 newest screenshots are kept">📷 no longer kept</span>';
|
|
673
|
+
}, true);
|
|
674
|
+
// Esc closes it (as any dialog); so does a click anywhere on it
|
|
675
|
+
$("shot-dialog").onclick = () => $("shot-dialog").close();
|
|
654
676
|
|
|
655
677
|
function setBusy(busy) {
|
|
656
678
|
state.busy = busy;
|
package/public/index.html
CHANGED
|
@@ -155,6 +155,10 @@
|
|
|
155
155
|
</form>
|
|
156
156
|
</dialog>
|
|
157
157
|
|
|
158
|
+
<dialog id="shot-dialog" class="shot-dialog" aria-label="Screenshot">
|
|
159
|
+
<img id="shot-img" alt="Screenshot of the slide">
|
|
160
|
+
</dialog>
|
|
161
|
+
|
|
158
162
|
<dialog id="diff-dialog" class="diff-dialog">
|
|
159
163
|
<div class="dialog-head"><span id="diff-title">Changes</span><button class="small" id="diff-close">Close</button></div>
|
|
160
164
|
<pre id="diff-body" class="diff"></pre>
|
package/server.mjs
CHANGED
|
@@ -83,6 +83,7 @@ const deckKey = (NAMED ? `${path.basename(DECK_DIR)}-${DECK_NAME}` : path.basena
|
|
|
83
83
|
crypto.createHash("sha1").update(NAMED ? path.join(DECK_DIR, DECK_FILE) : DECK_DIR).digest("hex").slice(0, 8);
|
|
84
84
|
const STATE_DIR = path.join(HOME_DIR, "decks", deckKey);
|
|
85
85
|
const SHOT_DIR = path.join(STATE_DIR, "shots");
|
|
86
|
+
const SHOT_NAME = /^[\w-]+\.png$/; // what takeShot writes, and all /shots/ serves
|
|
86
87
|
const PDF_DIR = path.join(STATE_DIR, "attachments"); // attached PDFs: reading material, not part of the deck
|
|
87
88
|
/* pindeck login: a new one-time login link from the running server, asked with the token in its server
|
|
88
89
|
file (readable only by this user) */
|
|
@@ -571,7 +572,12 @@ function toolSummary(name, input = {}) {
|
|
|
571
572
|
return !r || r.startsWith("..") ? path.basename(p) : r;
|
|
572
573
|
};
|
|
573
574
|
switch (name) {
|
|
574
|
-
case "Read":
|
|
575
|
+
case "Read": {
|
|
576
|
+
// a screenshot PinDeck took: the chat shows it, by its name in the shots folder
|
|
577
|
+
const f = input.file_path && path.resolve(DECK_DIR, input.file_path);
|
|
578
|
+
const shot = f && path.dirname(f) === SHOT_DIR && SHOT_NAME.test(path.basename(f)) ? path.basename(f) : undefined;
|
|
579
|
+
return { name: "Read", detail: base(input.file_path), shot };
|
|
580
|
+
}
|
|
575
581
|
case "Edit": case "MultiEdit": case "Write": return { name: "Edit", detail: base(input.file_path) };
|
|
576
582
|
case "Bash": {
|
|
577
583
|
// a PinDeck command only when the whole command is one: a script that ends in `cli.mjs layout`
|
|
@@ -1171,7 +1177,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1171
1177
|
const f = safeJoin(path.join(EDITOR_DIR, "public"), p.slice(8));
|
|
1172
1178
|
return f ? serveFile(res, f, null, req) : text(res, 403, "Forbidden");
|
|
1173
1179
|
}
|
|
1174
|
-
if ((p.startsWith("/deck/") || p.startsWith("/files/")) && !ok) return text(res, 403, "Not logged in");
|
|
1180
|
+
if ((p.startsWith("/deck/") || p.startsWith("/files/") || p.startsWith("/shots/")) && !ok) return text(res, 403, "Not logged in");
|
|
1181
|
+
// /shots/: the screenshots of this deck, for the chat; only PNG files directly in its shots folder
|
|
1182
|
+
if (p.startsWith("/shots/")) {
|
|
1183
|
+
const name = decodeURIComponent(p.slice(7));
|
|
1184
|
+
if (req.method !== "GET" || !SHOT_NAME.test(name)) return text(res, 404, "Not found");
|
|
1185
|
+
return serveFile(res, path.join(SHOT_DIR, name), null, req);
|
|
1186
|
+
}
|
|
1175
1187
|
let snap;
|
|
1176
1188
|
if (req.method === "GET" && (snap = p.match(/^\/(?:deck|files)\/(?:.*\/)?__pindeck-print-([\w-]+)\.html$/))) {
|
|
1177
1189
|
const html = snapshots.get(snap[1]);
|
|
@@ -1254,7 +1266,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1254
1266
|
// only files PinDeck itself stored can be attached
|
|
1255
1267
|
b.attachments = (b.attachments || []).filter((a) => a && state.uploads.has(a.path) && fs.existsSync(uploadFile(a.path)));
|
|
1256
1268
|
b.attachments.forEach((a) => { state.uploads.delete(a.path); if (a.waveform) state.uploads.delete(a.waveform); });
|
|
1257
|
-
|
|
1269
|
+
// the file's name, so the chat can show it (true in chats from before 2.11)
|
|
1270
|
+
const entry = addEntry({ kind: "user", text: b.text, screenshot: screenshot ? path.basename(screenshot.path) : false,
|
|
1258
1271
|
attachments: b.attachments.map((a) => ({ path: a.path, kind: a.kind, name: a.name })),
|
|
1259
1272
|
selection: b.selection ? { eid: b.selection.eid, label: b.selection.eid.replace(/^[^/]+\//, ""), slide: b.selection.slide.index + 1 } : null,
|
|
1260
1273
|
pins: (b.pins || []).map((p) => ({ n: p.n, eid: p.eid, label: label(p), slide: p.slide.index + 1 })) });
|