pi-studio 0.9.20 → 0.9.21
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/CHANGELOG.md +6 -1
- package/README.md +2 -2
- package/client/studio-annotation-helpers.js +20 -1
- package/client/studio-client.js +304 -23
- package/client/studio.css +73 -0
- package/index.ts +156 -17
- package/package.json +1 -1
- package/shared/studio-markdown-fences.js +22 -0
- package/shared/studio-markdown-fences.ts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `pi-studio` are documented here.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.9.21] — 2026-05-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added **Recent…** to the Scratchpad so scratchpads saved under previous file/draft identities can be loaded, appended, or copied after reopening Studio or continuing a Pi session.
|
|
9
|
+
- Preview export now distinguishes **Export PDF and Open in Studio preview tab** from **Export PDF and Open in default PDF viewer**; the Studio path opens a Files-view-style editor-only preview tab with a `studio-pdf` block.
|
|
10
|
+
- HTML export now distinguishes **Export HTML and Open in Studio editor** from **Export HTML and Open in browser**; the Studio path opens the exported HTML in an editor-only Studio tab for inspection, editing, previewing, and comment mode.
|
|
6
11
|
|
|
7
12
|
## [0.9.20] — 2026-05-27
|
|
8
13
|
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
25
25
|
- Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
|
|
26
26
|
- Includes a right-pane **Files** view for browsing the current Pi session/resource directory, opening folders, loading text/code/CSV/TSV documents into the editor, previewing PDFs/images, opening PDF/image previews in a new Studio tab, converting DOCX/ODT documents to editable Markdown when Pandoc is available after confirmation, copying paths, setting the current folder as the Studio working directory, and revealing files in the file manager
|
|
27
27
|
- Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/stop/interrupt controls, a compact refresh-persistent **Studio REPL Record** of user and Pi-sent code, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
|
|
28
|
-
- Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
|
|
28
|
+
- Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them, with a **Recent…** picker for recovering scratchpads saved under earlier file/draft identities
|
|
29
29
|
- Includes a docked **Outline** rail for navigating document structure in the current editor text, with clickable entries that jump in the raw editor and reveal matching preview locations when available
|
|
30
30
|
- Restores the current browser-tab editor workspace after refresh and provides an explicit **Reset editor** action when you want to discard the restored draft and return the tab to a fresh blank draft without changing responses or saved files
|
|
31
31
|
- Turns local preview links, including links inside sandboxed HTML previews, into Studio actions: PDFs open in the embedded viewer, images open in a zoomable focus viewer, PDF/image links can open in a new Studio preview tab, text/code/CSV/TSV document links can open in a new editor tab, DOCX/ODT links can be converted to editable Markdown, and right-click menus provide **Open here**, **Reveal in file manager**, and **Copy path** for local resources
|
|
@@ -43,7 +43,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
43
43
|
- Renders straight, unfenced interactive HTML in preview via a sandboxed browser iframe with zoom controls, while fenced `html` blocks remain source code
|
|
44
44
|
- Embeds local PDFs in Studio Markdown previews via explicit `studio-pdf` fenced blocks, with a Focus action for temporarily enlarging the embedded viewer
|
|
45
45
|
- Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
|
|
46
|
-
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML into the source file directory, Studio working directory, or Pi session directory, preserving authored HTML previews as HTML and rendering CSV/TSV editor previews as tables
|
|
46
|
+
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML into the source file directory, Studio working directory, or Pi session directory; PDF export can open in a Studio preview tab or the default PDF viewer, and HTML export can open in the default browser or in a new Studio editor tab for inspection/commenting, while preserving authored HTML previews as HTML and rendering CSV/TSV editor previews as tables
|
|
47
47
|
- Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf` or `/studio-html <path>` to `<name>.studio.html`; without a path, those commands export the last model response to a timestamped file
|
|
48
48
|
- Shows model/session/context usage in the footer, plus a compact-context action
|
|
49
49
|
|
|
@@ -500,6 +500,23 @@
|
|
|
500
500
|
return out;
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
const SMART_MARKDOWN_FENCE_CHARS = "`´‘’‚‛“”„‟′‵";
|
|
504
|
+
const SMART_MARKDOWN_FENCE_RE = new RegExp(`^([ \\t]{0,3})([${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\\\]^-]/g, "\\\\$&")}]{3,})([^${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\\\]^-]/g, "\\\\$&")}\\r\\n]*)$`);
|
|
505
|
+
|
|
506
|
+
function normalizeMarkdownSmartFences(markdown) {
|
|
507
|
+
return String(markdown || "")
|
|
508
|
+
.replace(/\r\n/g, "\n")
|
|
509
|
+
.split("\n")
|
|
510
|
+
.map(function(line) {
|
|
511
|
+
const match = line.match(SMART_MARKDOWN_FENCE_RE);
|
|
512
|
+
if (!match) return line;
|
|
513
|
+
const run = match[2] || "";
|
|
514
|
+
if (!/[´‘’‚‛“”„‟′‵]/u.test(run)) return line;
|
|
515
|
+
return (match[1] || "") + "`".repeat(Math.max(3, Array.from(run).length)) + (match[3] || "");
|
|
516
|
+
})
|
|
517
|
+
.join("\n");
|
|
518
|
+
}
|
|
519
|
+
|
|
503
520
|
function transformMarkdownOutsideFences(text, plainTransformer) {
|
|
504
521
|
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
505
522
|
if (!source) return source;
|
|
@@ -642,11 +659,12 @@
|
|
|
642
659
|
}
|
|
643
660
|
|
|
644
661
|
function prepareMarkdownForPandocPreview(markdown, placeholderPrefix) {
|
|
662
|
+
const normalizedMarkdown = normalizeMarkdownSmartFences(markdown);
|
|
645
663
|
const placeholders = [];
|
|
646
664
|
const prefix = typeof placeholderPrefix === "string" && placeholderPrefix
|
|
647
665
|
? placeholderPrefix
|
|
648
666
|
: "PISTUDIOANNOT";
|
|
649
|
-
const prepared = transformMarkdownOutsideFences(
|
|
667
|
+
const prepared = transformMarkdownOutsideFences(normalizedMarkdown, function(segment) {
|
|
650
668
|
return replaceInlineAnnotationMarkers(segment, function(marker) {
|
|
651
669
|
const label = normalizePreviewAnnotationLabel(marker.body);
|
|
652
670
|
if (!label) return "";
|
|
@@ -663,6 +681,7 @@
|
|
|
663
681
|
hasAnnotationMarkers: hasAnnotationMarkers,
|
|
664
682
|
normalizePreviewAnnotationLabel: normalizePreviewAnnotationLabel,
|
|
665
683
|
extractStandaloneMarkdownImageCaptionText: extractStandaloneMarkdownImageCaptionText,
|
|
684
|
+
normalizeMarkdownSmartFences: normalizeMarkdownSmartFences,
|
|
666
685
|
prepareMarkdownForPandocPreview: prepareMarkdownForPandocPreview,
|
|
667
686
|
readInlineAnnotationMarkerAt: readInlineAnnotationMarkerAt,
|
|
668
687
|
renderPreviewAnnotationHtml: renderPreviewAnnotationHtml,
|
package/client/studio-client.js
CHANGED
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
const copyResponseBtn = document.getElementById("copyResponseBtn");
|
|
93
93
|
const exportPreviewControlsEl = document.getElementById("exportPreviewControls");
|
|
94
94
|
const exportPreviewMenuEl = document.getElementById("exportPreviewMenu");
|
|
95
|
+
const exportPreviewPdfStudioBtn = document.getElementById("exportPreviewPdfStudioBtn");
|
|
95
96
|
const exportPreviewPdfBtn = document.getElementById("exportPreviewPdfBtn");
|
|
97
|
+
const exportPreviewHtmlStudioBtn = document.getElementById("exportPreviewHtmlStudioBtn");
|
|
96
98
|
const exportPreviewHtmlBtn = document.getElementById("exportPreviewHtmlBtn");
|
|
97
99
|
const exportPdfBtn = document.getElementById("exportPdfBtn");
|
|
98
100
|
const historyPrevBtn = document.getElementById("historyPrevBtn");
|
|
@@ -142,6 +144,8 @@
|
|
|
142
144
|
const scratchpadDialogEl = document.getElementById("scratchpadDialog");
|
|
143
145
|
const scratchpadTextEl = document.getElementById("scratchpadText");
|
|
144
146
|
const scratchpadMetaEl = document.getElementById("scratchpadMeta");
|
|
147
|
+
const scratchpadRecentBtn = document.getElementById("scratchpadRecentBtn");
|
|
148
|
+
const scratchpadRecentPanelEl = document.getElementById("scratchpadRecentPanel");
|
|
145
149
|
const scratchpadInsertBtn = document.getElementById("scratchpadInsertBtn");
|
|
146
150
|
const scratchpadCopyBtn = document.getElementById("scratchpadCopyBtn");
|
|
147
151
|
const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
|
|
@@ -2044,6 +2048,9 @@
|
|
|
2044
2048
|
let scratchpadReturnFocusEl = null;
|
|
2045
2049
|
let scratchpadPersistTimer = null;
|
|
2046
2050
|
let scratchpadLoadNonce = 0;
|
|
2051
|
+
let scratchpadRecentEntries = [];
|
|
2052
|
+
let scratchpadRecentVisible = false;
|
|
2053
|
+
let scratchpadRecentLoading = false;
|
|
2047
2054
|
let reviewNotes = [];
|
|
2048
2055
|
let reviewNotesReturnFocusEl = null;
|
|
2049
2056
|
let reviewNotesPersistTimer = null;
|
|
@@ -7940,14 +7947,32 @@
|
|
|
7940
7947
|
}
|
|
7941
7948
|
}
|
|
7942
7949
|
|
|
7943
|
-
|
|
7950
|
+
function getStudioPdfViewerUrlForExportPayload(payload) {
|
|
7951
|
+
if (!payload || typeof payload !== "object") return "";
|
|
7952
|
+
const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
7953
|
+
if (exportPath) {
|
|
7954
|
+
const resourceUrl = buildStudioPdfResourceUrl({ path: exportPath, resourceDir: exportPath.split(/[\\/]/).slice(0, -1).join("/") }, false);
|
|
7955
|
+
if (resourceUrl) return resourceUrl;
|
|
7956
|
+
}
|
|
7957
|
+
return typeof payload.downloadUrl === "string" ? payload.downloadUrl : "";
|
|
7958
|
+
}
|
|
7959
|
+
|
|
7960
|
+
async function exportRightPanePdf(options) {
|
|
7961
|
+
const exportOptions = options && typeof options === "object" ? options : {};
|
|
7962
|
+
const openTarget = exportOptions.openTarget === "studio" ? "studio" : "default";
|
|
7963
|
+
let studioPopup = null;
|
|
7964
|
+
if (openTarget === "studio") {
|
|
7965
|
+
studioPopup = openExportStudioPlaceholderWindow("PDF");
|
|
7966
|
+
}
|
|
7944
7967
|
if (uiBusy || previewExportInProgress) {
|
|
7968
|
+
closeExportStudioWindow(studioPopup);
|
|
7945
7969
|
setStatus("Studio is busy.", "warning");
|
|
7946
7970
|
return;
|
|
7947
7971
|
}
|
|
7948
7972
|
|
|
7949
7973
|
const token = getToken();
|
|
7950
7974
|
if (!token) {
|
|
7975
|
+
closeExportStudioWindow(studioPopup);
|
|
7951
7976
|
setStatus("Missing Studio token in URL. Re-run /studio.", "error");
|
|
7952
7977
|
return;
|
|
7953
7978
|
}
|
|
@@ -7955,17 +7980,20 @@
|
|
|
7955
7980
|
const exportingReplJournal = rightView === "repl";
|
|
7956
7981
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
7957
7982
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
7983
|
+
closeExportStudioWindow(studioPopup);
|
|
7958
7984
|
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
|
|
7959
7985
|
return;
|
|
7960
7986
|
}
|
|
7961
7987
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
7962
7988
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
7989
|
+
closeExportStudioWindow(studioPopup);
|
|
7963
7990
|
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
7964
7991
|
return;
|
|
7965
7992
|
}
|
|
7966
7993
|
|
|
7967
7994
|
const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
|
|
7968
7995
|
if (htmlArtifactSource) {
|
|
7996
|
+
closeExportStudioWindow(studioPopup);
|
|
7969
7997
|
setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
|
|
7970
7998
|
return;
|
|
7971
7999
|
}
|
|
@@ -7976,6 +8004,7 @@
|
|
|
7976
8004
|
? prepareEditorTextForPdfExport(sourceTextEl.value)
|
|
7977
8005
|
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
7978
8006
|
if (!markdown || !markdown.trim()) {
|
|
8007
|
+
closeExportStudioWindow(studioPopup);
|
|
7979
8008
|
setStatus("Nothing to export yet.", "warning");
|
|
7980
8009
|
return;
|
|
7981
8010
|
}
|
|
@@ -7998,7 +8027,7 @@
|
|
|
7998
8027
|
|
|
7999
8028
|
previewExportInProgress = true;
|
|
8000
8029
|
updateResultActionButtons();
|
|
8001
|
-
setStatus("Exporting PDF…", "warning");
|
|
8030
|
+
setStatus(openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…", "warning");
|
|
8002
8031
|
|
|
8003
8032
|
try {
|
|
8004
8033
|
const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
|
|
@@ -8013,6 +8042,7 @@
|
|
|
8013
8042
|
isLatex: isLatex,
|
|
8014
8043
|
editorPdfLanguage: editorPdfLanguage,
|
|
8015
8044
|
filenameHint: filenameHint,
|
|
8045
|
+
openTarget: openTarget,
|
|
8016
8046
|
}),
|
|
8017
8047
|
}, PDF_EXPORT_FETCH_TIMEOUT_MS, "PDF export");
|
|
8018
8048
|
|
|
@@ -8051,6 +8081,35 @@
|
|
|
8051
8081
|
downloadName += ".pdf";
|
|
8052
8082
|
}
|
|
8053
8083
|
|
|
8084
|
+
if (openTarget === "studio") {
|
|
8085
|
+
const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
|
|
8086
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
8087
|
+
: (typeof payload.url === "string" ? payload.url : "");
|
|
8088
|
+
const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
|
|
8089
|
+
if (!openedStudio) {
|
|
8090
|
+
closeExportStudioWindow(studioPopup);
|
|
8091
|
+
const viewerUrl = getStudioPdfViewerUrlForExportPayload(payload);
|
|
8092
|
+
if (viewerUrl) openStudioPdfFocusViewer(viewerUrl, downloadName);
|
|
8093
|
+
}
|
|
8094
|
+
if (writeError) {
|
|
8095
|
+
setStatus(openedStudio
|
|
8096
|
+
? "Opened exported PDF in a Studio preview tab, but could not write project file: " + writeError
|
|
8097
|
+
: "Exported PDF, but could not open a Studio preview tab and could not write project file: " + writeError,
|
|
8098
|
+
"warning");
|
|
8099
|
+
} else if (exportWarning) {
|
|
8100
|
+
setStatus(openedStudio
|
|
8101
|
+
? "Opened exported PDF in a Studio preview tab with warning: " + exportWarning
|
|
8102
|
+
: "Exported PDF, but could not open a Studio preview tab. Warning: " + exportWarning,
|
|
8103
|
+
"warning");
|
|
8104
|
+
} else {
|
|
8105
|
+
setStatus(openedStudio
|
|
8106
|
+
? "Opened exported PDF in a Studio preview tab: " + (exportPath || downloadName)
|
|
8107
|
+
: "Exported PDF, but could not open a Studio preview tab" + (targetUrl ? ": " + targetUrl : "."),
|
|
8108
|
+
openedStudio ? "success" : "warning");
|
|
8109
|
+
}
|
|
8110
|
+
return;
|
|
8111
|
+
}
|
|
8112
|
+
|
|
8054
8113
|
if (openedExternal) {
|
|
8055
8114
|
if (writeError) {
|
|
8056
8115
|
setStatus("Opened PDF in default viewer, but could not write project file: " + writeError, "warning");
|
|
@@ -8112,6 +8171,7 @@
|
|
|
8112
8171
|
setStatus("Exported PDF: " + downloadName, "success");
|
|
8113
8172
|
}
|
|
8114
8173
|
} catch (error) {
|
|
8174
|
+
closeExportStudioWindow(studioPopup);
|
|
8115
8175
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
8116
8176
|
setStatus("PDF export failed: " + detail, "error");
|
|
8117
8177
|
} finally {
|
|
@@ -8120,14 +8180,58 @@
|
|
|
8120
8180
|
}
|
|
8121
8181
|
}
|
|
8122
8182
|
|
|
8123
|
-
|
|
8183
|
+
function openExportStudioPlaceholderWindow(formatLabel) {
|
|
8184
|
+
const label = String(formatLabel || "preview").trim() || "preview";
|
|
8185
|
+
let popup = null;
|
|
8186
|
+
try {
|
|
8187
|
+
popup = window.open("", "_blank");
|
|
8188
|
+
if (popup && popup.document && popup.document.body) {
|
|
8189
|
+
popup.document.title = "Opening " + label + " in Studio…";
|
|
8190
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Exporting " + escapeHtml(label) + " and opening it in Studio…</p>";
|
|
8191
|
+
}
|
|
8192
|
+
} catch {
|
|
8193
|
+
popup = null;
|
|
8194
|
+
}
|
|
8195
|
+
return popup;
|
|
8196
|
+
}
|
|
8197
|
+
|
|
8198
|
+
function navigateExportStudioWindow(popup, targetUrl) {
|
|
8199
|
+
if (!targetUrl) return false;
|
|
8200
|
+
if (popup && !popup.closed) {
|
|
8201
|
+
try {
|
|
8202
|
+
popup.opener = null;
|
|
8203
|
+
popup.location.href = targetUrl;
|
|
8204
|
+
return true;
|
|
8205
|
+
} catch {}
|
|
8206
|
+
}
|
|
8207
|
+
try {
|
|
8208
|
+
return Boolean(window.open(targetUrl, "_blank", "noopener"));
|
|
8209
|
+
} catch {
|
|
8210
|
+
return false;
|
|
8211
|
+
}
|
|
8212
|
+
}
|
|
8213
|
+
|
|
8214
|
+
function closeExportStudioWindow(popup) {
|
|
8215
|
+
if (!popup || popup.closed) return;
|
|
8216
|
+
try { popup.close(); } catch {}
|
|
8217
|
+
}
|
|
8218
|
+
|
|
8219
|
+
async function exportRightPaneHtml(options) {
|
|
8220
|
+
const exportOptions = options && typeof options === "object" ? options : {};
|
|
8221
|
+
const openTarget = exportOptions.openTarget === "studio" ? "studio" : "browser";
|
|
8222
|
+
let studioPopup = null;
|
|
8223
|
+
if (openTarget === "studio") {
|
|
8224
|
+
studioPopup = openExportStudioPlaceholderWindow("HTML");
|
|
8225
|
+
}
|
|
8124
8226
|
if (uiBusy || previewExportInProgress) {
|
|
8227
|
+
closeExportStudioWindow(studioPopup);
|
|
8125
8228
|
setStatus("Studio is busy.", "warning");
|
|
8126
8229
|
return;
|
|
8127
8230
|
}
|
|
8128
8231
|
|
|
8129
8232
|
const token = getToken();
|
|
8130
8233
|
if (!token) {
|
|
8234
|
+
closeExportStudioWindow(studioPopup);
|
|
8131
8235
|
setStatus("Missing Studio token in URL. Re-run /studio.", "error");
|
|
8132
8236
|
return;
|
|
8133
8237
|
}
|
|
@@ -8135,11 +8239,13 @@
|
|
|
8135
8239
|
const exportingReplJournal = rightView === "repl";
|
|
8136
8240
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
8137
8241
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
8242
|
+
closeExportStudioWindow(studioPopup);
|
|
8138
8243
|
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
|
|
8139
8244
|
return;
|
|
8140
8245
|
}
|
|
8141
8246
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
8142
8247
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
8248
|
+
closeExportStudioWindow(studioPopup);
|
|
8143
8249
|
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
8144
8250
|
return;
|
|
8145
8251
|
}
|
|
@@ -8149,6 +8255,7 @@
|
|
|
8149
8255
|
? prepareEditorTextForHtmlExport(sourceTextEl.value)
|
|
8150
8256
|
: prepareEditorTextForPreview(latestResponseMarkdown)));
|
|
8151
8257
|
if (!markdown || !markdown.trim()) {
|
|
8258
|
+
closeExportStudioWindow(studioPopup);
|
|
8152
8259
|
setStatus("Nothing to export yet.", "warning");
|
|
8153
8260
|
return;
|
|
8154
8261
|
}
|
|
@@ -8173,7 +8280,7 @@
|
|
|
8173
8280
|
|
|
8174
8281
|
previewExportInProgress = true;
|
|
8175
8282
|
updateResultActionButtons();
|
|
8176
|
-
setStatus("Exporting HTML…", "warning");
|
|
8283
|
+
setStatus(openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…", "warning");
|
|
8177
8284
|
|
|
8178
8285
|
try {
|
|
8179
8286
|
const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
|
|
@@ -8189,6 +8296,7 @@
|
|
|
8189
8296
|
editorHtmlLanguage: editorHtmlLanguage,
|
|
8190
8297
|
filenameHint: filenameHint,
|
|
8191
8298
|
title: titleHint,
|
|
8299
|
+
openTarget: openTarget,
|
|
8192
8300
|
}),
|
|
8193
8301
|
}, HTML_EXPORT_FETCH_TIMEOUT_MS, "HTML export");
|
|
8194
8302
|
|
|
@@ -8227,6 +8335,31 @@
|
|
|
8227
8335
|
downloadName += ".html";
|
|
8228
8336
|
}
|
|
8229
8337
|
|
|
8338
|
+
if (openTarget === "studio") {
|
|
8339
|
+
const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
|
|
8340
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
8341
|
+
: (typeof payload.url === "string" ? payload.url : "");
|
|
8342
|
+
const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
|
|
8343
|
+
if (!openedStudio) closeExportStudioWindow(studioPopup);
|
|
8344
|
+
if (writeError) {
|
|
8345
|
+
setStatus(openedStudio
|
|
8346
|
+
? "Opened exported HTML in Studio as an unsaved copy; could not write project file: " + writeError
|
|
8347
|
+
: "Exported HTML for Studio, but the popup was blocked and the project file could not be written: " + writeError,
|
|
8348
|
+
"warning");
|
|
8349
|
+
} else if (exportWarning) {
|
|
8350
|
+
setStatus(openedStudio
|
|
8351
|
+
? "Opened exported HTML in Studio with warning: " + exportWarning
|
|
8352
|
+
: "Exported HTML for Studio, but the popup was blocked. Warning: " + exportWarning,
|
|
8353
|
+
"warning");
|
|
8354
|
+
} else {
|
|
8355
|
+
setStatus(openedStudio
|
|
8356
|
+
? "Opened exported HTML in Studio: " + (exportPath || downloadName)
|
|
8357
|
+
: (targetUrl ? "Exported HTML for Studio: " + targetUrl : "Exported HTML, but Studio did not receive an editor URL."),
|
|
8358
|
+
openedStudio ? "success" : "warning");
|
|
8359
|
+
}
|
|
8360
|
+
return;
|
|
8361
|
+
}
|
|
8362
|
+
|
|
8230
8363
|
if (openedExternal) {
|
|
8231
8364
|
if (writeError) {
|
|
8232
8365
|
setStatus("Opened HTML in default browser, but could not write project file: " + writeError, "warning");
|
|
@@ -8262,6 +8395,7 @@
|
|
|
8262
8395
|
return;
|
|
8263
8396
|
}
|
|
8264
8397
|
|
|
8398
|
+
closeExportStudioWindow(studioPopup);
|
|
8265
8399
|
const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
|
|
8266
8400
|
const blob = await response.blob();
|
|
8267
8401
|
const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
|
|
@@ -8288,6 +8422,7 @@
|
|
|
8288
8422
|
setStatus("Exported HTML: " + downloadName, "success");
|
|
8289
8423
|
}
|
|
8290
8424
|
} catch (error) {
|
|
8425
|
+
closeExportStudioWindow(studioPopup);
|
|
8291
8426
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
8292
8427
|
setStatus("HTML export failed: " + detail, "error");
|
|
8293
8428
|
} finally {
|
|
@@ -8318,10 +8453,16 @@
|
|
|
8318
8453
|
|
|
8319
8454
|
function exportRightPaneFormat(format) {
|
|
8320
8455
|
closeExportPreviewMenu();
|
|
8321
|
-
if (format === "html") {
|
|
8322
|
-
return exportRightPaneHtml();
|
|
8456
|
+
if (format === "html-studio") {
|
|
8457
|
+
return exportRightPaneHtml({ openTarget: "studio" });
|
|
8458
|
+
}
|
|
8459
|
+
if (format === "html" || format === "html-browser") {
|
|
8460
|
+
return exportRightPaneHtml({ openTarget: "browser" });
|
|
8461
|
+
}
|
|
8462
|
+
if (format === "pdf-studio") {
|
|
8463
|
+
return exportRightPanePdf({ openTarget: "studio" });
|
|
8323
8464
|
}
|
|
8324
|
-
return exportRightPanePdf();
|
|
8465
|
+
return exportRightPanePdf({ openTarget: "default" });
|
|
8325
8466
|
}
|
|
8326
8467
|
|
|
8327
8468
|
function normalizeCopyableBlockText(text) {
|
|
@@ -9611,28 +9752,40 @@
|
|
|
9611
9752
|
} else if (isHtmlArtifactPreview) {
|
|
9612
9753
|
exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
|
|
9613
9754
|
} else if (exportingReplJournal) {
|
|
9614
|
-
exportPdfBtn.title = "Choose PDF or HTML
|
|
9755
|
+
exportPdfBtn.title = "Choose PDF export or an HTML export destination for the Studio REPL record.";
|
|
9615
9756
|
} else {
|
|
9616
|
-
exportPdfBtn.title = "Choose PDF or HTML
|
|
9757
|
+
exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
|
|
9617
9758
|
}
|
|
9618
9759
|
}
|
|
9760
|
+
if (exportPreviewPdfStudioBtn) {
|
|
9761
|
+
exportPreviewPdfStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
9762
|
+
exportPreviewPdfStudioBtn.title = isHtmlArtifactPreview
|
|
9763
|
+
? "Interactive HTML preview PDF export is not available yet."
|
|
9764
|
+
: (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in Studio." : "Export the current right-pane preview as PDF and open it in Studio.");
|
|
9765
|
+
}
|
|
9619
9766
|
if (exportPreviewPdfBtn) {
|
|
9620
9767
|
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
9621
9768
|
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
9622
9769
|
? "Interactive HTML preview PDF export is not available yet."
|
|
9623
|
-
: (exportingReplJournal ? "Export the Studio REPL record as PDF." : "Export the current right-pane preview as PDF.");
|
|
9770
|
+
: (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in the default PDF viewer." : "Export the current right-pane preview as PDF and open it in the default PDF viewer.");
|
|
9771
|
+
}
|
|
9772
|
+
if (exportPreviewHtmlStudioBtn) {
|
|
9773
|
+
exportPreviewHtmlStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
9774
|
+
exportPreviewHtmlStudioBtn.title = isHtmlArtifactPreview
|
|
9775
|
+
? "Export the authored HTML preview and open it in a new Studio editor tab."
|
|
9776
|
+
: (exportingReplJournal ? "Export the Studio REPL record as standalone HTML and open it in a new Studio editor tab." : "Export the current right-pane preview as standalone HTML and open it in a new Studio editor tab.");
|
|
9624
9777
|
}
|
|
9625
9778
|
if (exportPreviewHtmlBtn) {
|
|
9626
9779
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
9627
9780
|
exportPreviewHtmlBtn.title = isHtmlArtifactPreview
|
|
9628
|
-
? "Export the authored HTML preview."
|
|
9629
|
-
: (exportingReplJournal ? "Export the Studio REPL record as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
|
|
9781
|
+
? "Export the authored HTML preview and open it in the default browser."
|
|
9782
|
+
: (exportingReplJournal ? "Export the Studio REPL record as standalone HTML and open it in the default browser." : "Export the current right-pane preview as standalone HTML and open it in the default browser.");
|
|
9630
9783
|
}
|
|
9631
9784
|
if (exportPreviewControlsEl) {
|
|
9632
9785
|
exportPreviewControlsEl.title = canExportPreview
|
|
9633
9786
|
? (exportingReplJournal
|
|
9634
|
-
? "Choose a format and export the Studio REPL record."
|
|
9635
|
-
: (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
|
|
9787
|
+
? "Choose a format and export destination for the Studio REPL record."
|
|
9788
|
+
: (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview."))
|
|
9636
9789
|
: (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
|
|
9637
9790
|
}
|
|
9638
9791
|
if (!canExportPreview || previewExportInProgress) {
|
|
@@ -12823,6 +12976,110 @@
|
|
|
12823
12976
|
return describeStudioDocument(sourceState);
|
|
12824
12977
|
}
|
|
12825
12978
|
|
|
12979
|
+
function formatScratchpadRecentTime(timestamp) {
|
|
12980
|
+
const value = Number(timestamp) || 0;
|
|
12981
|
+
if (!value) return "unknown time";
|
|
12982
|
+
try {
|
|
12983
|
+
return new Date(value).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
12984
|
+
} catch {
|
|
12985
|
+
return "unknown time";
|
|
12986
|
+
}
|
|
12987
|
+
}
|
|
12988
|
+
|
|
12989
|
+
function renderScratchpadRecentPanel() {
|
|
12990
|
+
if (!scratchpadRecentPanelEl) return;
|
|
12991
|
+
scratchpadRecentPanelEl.hidden = !scratchpadRecentVisible;
|
|
12992
|
+
if (!scratchpadRecentVisible) return;
|
|
12993
|
+
if (scratchpadRecentLoading) {
|
|
12994
|
+
scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-loading'>Loading recent scratchpads…</div>";
|
|
12995
|
+
return;
|
|
12996
|
+
}
|
|
12997
|
+
const currentKey = getCurrentStudioDocumentDescriptor().key;
|
|
12998
|
+
const entries = Array.isArray(scratchpadRecentEntries) ? scratchpadRecentEntries : [];
|
|
12999
|
+
if (!entries.length) {
|
|
13000
|
+
scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-empty'>No other saved scratchpads yet.</div>";
|
|
13001
|
+
return;
|
|
13002
|
+
}
|
|
13003
|
+
scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-list'>" + entries.map((entry) => {
|
|
13004
|
+
const key = String(entry && entry.documentKey ? entry.documentKey : "");
|
|
13005
|
+
const isCurrent = key === currentKey;
|
|
13006
|
+
const label = String(entry && entry.label ? entry.label : key || "scratchpad");
|
|
13007
|
+
const kind = String(entry && entry.kind ? entry.kind : "Scratchpad");
|
|
13008
|
+
const textLength = Math.max(0, Number(entry && entry.textLength) || 0);
|
|
13009
|
+
const preview = String(entry && entry.textPreview ? entry.textPreview : "");
|
|
13010
|
+
const meta = (isCurrent ? "Current · " : "") + kind + " · " + String(textLength) + " chars · " + formatScratchpadRecentTime(entry && entry.updatedAt);
|
|
13011
|
+
return "<div class='scratchpad-recent-item' data-scratchpad-key='" + escapeHtml(key) + "'>"
|
|
13012
|
+
+ "<div class='scratchpad-recent-main'>"
|
|
13013
|
+
+ "<div class='scratchpad-recent-title' title='" + escapeHtml(label) + "'>" + escapeHtml(label) + "</div>"
|
|
13014
|
+
+ "<div class='scratchpad-recent-meta'>" + escapeHtml(meta) + "</div>"
|
|
13015
|
+
+ (preview ? "<div class='scratchpad-recent-preview'>" + escapeHtml(preview) + "</div>" : "")
|
|
13016
|
+
+ "</div>"
|
|
13017
|
+
+ "<div class='scratchpad-recent-actions'>"
|
|
13018
|
+
+ "<button type='button' data-scratchpad-recent-action='load' data-scratchpad-key='" + escapeHtml(key) + "'" + (isCurrent ? " disabled" : "") + ">Load</button>"
|
|
13019
|
+
+ "<button type='button' data-scratchpad-recent-action='append' data-scratchpad-key='" + escapeHtml(key) + "'" + (isCurrent ? " disabled" : "") + ">Append</button>"
|
|
13020
|
+
+ "<button type='button' data-scratchpad-recent-action='copy' data-scratchpad-key='" + escapeHtml(key) + "'>Copy</button>"
|
|
13021
|
+
+ "</div>"
|
|
13022
|
+
+ "</div>";
|
|
13023
|
+
}).join("") + "</div>";
|
|
13024
|
+
}
|
|
13025
|
+
|
|
13026
|
+
async function loadScratchpadRecentEntries() {
|
|
13027
|
+
scratchpadRecentLoading = true;
|
|
13028
|
+
renderScratchpadRecentPanel();
|
|
13029
|
+
try {
|
|
13030
|
+
const payload = await fetchStudioJson("/scratchpad-state", { query: { action: "recent", limit: "20" } });
|
|
13031
|
+
scratchpadRecentEntries = Array.isArray(payload && payload.scratchpads) ? payload.scratchpads : [];
|
|
13032
|
+
} catch (error) {
|
|
13033
|
+
scratchpadRecentEntries = [];
|
|
13034
|
+
setStatus("Could not load recent scratchpads: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
13035
|
+
} finally {
|
|
13036
|
+
scratchpadRecentLoading = false;
|
|
13037
|
+
renderScratchpadRecentPanel();
|
|
13038
|
+
}
|
|
13039
|
+
}
|
|
13040
|
+
|
|
13041
|
+
function toggleScratchpadRecentPanel() {
|
|
13042
|
+
scratchpadRecentVisible = !scratchpadRecentVisible;
|
|
13043
|
+
if (scratchpadRecentVisible) {
|
|
13044
|
+
void loadScratchpadRecentEntries();
|
|
13045
|
+
} else {
|
|
13046
|
+
renderScratchpadRecentPanel();
|
|
13047
|
+
}
|
|
13048
|
+
updateScratchpadUi();
|
|
13049
|
+
}
|
|
13050
|
+
|
|
13051
|
+
async function applyScratchpadRecentAction(action, documentKey) {
|
|
13052
|
+
const key = String(documentKey || "").trim();
|
|
13053
|
+
if (!key) return;
|
|
13054
|
+
const mode = action === "append" ? "append" : (action === "copy" ? "copy" : "load");
|
|
13055
|
+
try {
|
|
13056
|
+
const text = await fetchScratchpadTextForDocumentKey(key);
|
|
13057
|
+
if (!String(text || "").trim()) {
|
|
13058
|
+
setStatus("That scratchpad is empty.", "warning");
|
|
13059
|
+
return;
|
|
13060
|
+
}
|
|
13061
|
+
if (mode === "copy") {
|
|
13062
|
+
const ok = await writeTextToClipboard(text);
|
|
13063
|
+
setStatus(ok ? "Copied recent scratchpad." : "Could not copy recent scratchpad.", ok ? "success" : "warning");
|
|
13064
|
+
return;
|
|
13065
|
+
}
|
|
13066
|
+
if (mode === "append") {
|
|
13067
|
+
const separator = scratchpadText && !scratchpadText.endsWith("\n") ? "\n\n" : (scratchpadText ? "\n" : "");
|
|
13068
|
+
setScratchpadText(String(scratchpadText || "") + separator + String(text || ""));
|
|
13069
|
+
setStatus("Appended recent scratchpad.", "success");
|
|
13070
|
+
return;
|
|
13071
|
+
}
|
|
13072
|
+
if (String(scratchpadText || "").trim() && String(scratchpadText || "") !== String(text || "")) {
|
|
13073
|
+
const confirmed = window.confirm("Replace the current scratchpad with this recent scratchpad? Current scratchpad text will remain saved under its current document/draft identity, but this panel will show the loaded text for the current document.");
|
|
13074
|
+
if (!confirmed) return;
|
|
13075
|
+
}
|
|
13076
|
+
setScratchpadText(text);
|
|
13077
|
+
setStatus("Loaded recent scratchpad into current scratchpad.", "success");
|
|
13078
|
+
} catch (error) {
|
|
13079
|
+
setStatus("Could not use recent scratchpad: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
13080
|
+
}
|
|
13081
|
+
}
|
|
13082
|
+
|
|
12826
13083
|
async function fetchScratchpadTextForDocumentKey(documentKey) {
|
|
12827
13084
|
const payload = await fetchStudioJson("/scratchpad-state", {
|
|
12828
13085
|
query: { documentKey: documentKey },
|
|
@@ -12830,9 +13087,9 @@
|
|
|
12830
13087
|
return payload && typeof payload.text === "string" ? payload.text : "";
|
|
12831
13088
|
}
|
|
12832
13089
|
|
|
12833
|
-
function flushScratchpadPersistence(documentKeyOverride, textOverride) {
|
|
13090
|
+
function flushScratchpadPersistence(documentKeyOverride, textOverride, labelOverride) {
|
|
12834
13091
|
const descriptor = documentKeyOverride
|
|
12835
|
-
? { key: String(documentKeyOverride || "").trim() }
|
|
13092
|
+
? { key: String(documentKeyOverride || "").trim(), label: String(labelOverride || "").trim() }
|
|
12836
13093
|
: getCurrentStudioDocumentDescriptor();
|
|
12837
13094
|
const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
|
|
12838
13095
|
if (!key) return;
|
|
@@ -12841,27 +13098,29 @@
|
|
|
12841
13098
|
scratchpadPersistTimer = null;
|
|
12842
13099
|
}
|
|
12843
13100
|
const snapshot = String(arguments.length >= 2 ? textOverride : scratchpadText || "");
|
|
12844
|
-
|
|
13101
|
+
const label = String(descriptor && descriptor.label ? descriptor.label : "").trim();
|
|
13102
|
+
if (trySendStudioJsonBeacon("/scratchpad-state", { documentKey: key, text: snapshot, label })) {
|
|
12845
13103
|
return;
|
|
12846
13104
|
}
|
|
12847
13105
|
void fetchStudioJson("/scratchpad-state", {
|
|
12848
13106
|
method: "POST",
|
|
12849
|
-
body: JSON.stringify({ documentKey: key, text: snapshot }),
|
|
13107
|
+
body: JSON.stringify({ documentKey: key, text: snapshot, label }),
|
|
12850
13108
|
}).catch(() => {
|
|
12851
13109
|
// Ignore scratchpad persistence failures for now.
|
|
12852
13110
|
});
|
|
12853
13111
|
}
|
|
12854
13112
|
|
|
12855
|
-
function scheduleScratchpadPersistence(text, documentKey) {
|
|
13113
|
+
function scheduleScratchpadPersistence(text, documentKey, label) {
|
|
12856
13114
|
if (scratchpadPersistTimer !== null) {
|
|
12857
13115
|
window.clearTimeout(scratchpadPersistTimer);
|
|
12858
13116
|
}
|
|
12859
13117
|
const snapshot = String(text || "");
|
|
12860
13118
|
const key = String(documentKey || "").trim();
|
|
13119
|
+
const labelSnapshot = String(label || "").trim();
|
|
12861
13120
|
if (!key) return;
|
|
12862
13121
|
scratchpadPersistTimer = window.setTimeout(() => {
|
|
12863
13122
|
scratchpadPersistTimer = null;
|
|
12864
|
-
flushScratchpadPersistence(key, snapshot);
|
|
13123
|
+
flushScratchpadPersistence(key, snapshot, labelSnapshot);
|
|
12865
13124
|
}, 180);
|
|
12866
13125
|
}
|
|
12867
13126
|
|
|
@@ -12893,7 +13152,7 @@
|
|
|
12893
13152
|
if (String(existing || "").trim()) return;
|
|
12894
13153
|
await fetchStudioJson("/scratchpad-state", {
|
|
12895
13154
|
method: "POST",
|
|
12896
|
-
body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot }),
|
|
13155
|
+
body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot, label: nextDescriptor.label }),
|
|
12897
13156
|
});
|
|
12898
13157
|
} catch {
|
|
12899
13158
|
// Ignore carry-over failures and just fall back to normal scope loading.
|
|
@@ -12914,7 +13173,7 @@
|
|
|
12914
13173
|
|
|
12915
13174
|
function persistScratchpadText(value) {
|
|
12916
13175
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
12917
|
-
scheduleScratchpadPersistence(value, descriptor.key);
|
|
13176
|
+
scheduleScratchpadPersistence(value, descriptor.key, descriptor.label);
|
|
12918
13177
|
}
|
|
12919
13178
|
|
|
12920
13179
|
function normalizeReviewNoteAnchorKind(value) {
|
|
@@ -17111,6 +17370,10 @@
|
|
|
17111
17370
|
? ("Saved locally for this document/draft · " + normalized.length + " chars")
|
|
17112
17371
|
: "Empty · local to this document/draft";
|
|
17113
17372
|
}
|
|
17373
|
+
if (scratchpadRecentBtn) {
|
|
17374
|
+
scratchpadRecentBtn.textContent = scratchpadRecentVisible ? "Hide recent" : "Recent…";
|
|
17375
|
+
scratchpadRecentBtn.setAttribute("aria-expanded", scratchpadRecentVisible ? "true" : "false");
|
|
17376
|
+
}
|
|
17114
17377
|
if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
|
|
17115
17378
|
if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
|
|
17116
17379
|
if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
|
|
@@ -19100,7 +19363,7 @@
|
|
|
19100
19363
|
event.stopPropagation();
|
|
19101
19364
|
if (actionBtn.disabled) return;
|
|
19102
19365
|
const format = String(actionBtn.getAttribute("data-export-preview-format") || "pdf").toLowerCase();
|
|
19103
|
-
void exportRightPaneFormat(format
|
|
19366
|
+
void exportRightPaneFormat(format);
|
|
19104
19367
|
});
|
|
19105
19368
|
}
|
|
19106
19369
|
|
|
@@ -19651,6 +19914,24 @@
|
|
|
19651
19914
|
});
|
|
19652
19915
|
}
|
|
19653
19916
|
|
|
19917
|
+
if (scratchpadRecentBtn) {
|
|
19918
|
+
scratchpadRecentBtn.addEventListener("click", () => {
|
|
19919
|
+
toggleScratchpadRecentPanel();
|
|
19920
|
+
});
|
|
19921
|
+
}
|
|
19922
|
+
|
|
19923
|
+
if (scratchpadRecentPanelEl) {
|
|
19924
|
+
scratchpadRecentPanelEl.addEventListener("click", (event) => {
|
|
19925
|
+
const target = event.target;
|
|
19926
|
+
const actionEl = target instanceof Element ? target.closest("[data-scratchpad-recent-action]") : null;
|
|
19927
|
+
if (!actionEl) return;
|
|
19928
|
+
event.preventDefault();
|
|
19929
|
+
const action = String(actionEl.getAttribute("data-scratchpad-recent-action") || "load");
|
|
19930
|
+
const key = String(actionEl.getAttribute("data-scratchpad-key") || "");
|
|
19931
|
+
void applyScratchpadRecentAction(action, key);
|
|
19932
|
+
});
|
|
19933
|
+
}
|
|
19934
|
+
|
|
19654
19935
|
if (scratchpadInsertBtn) {
|
|
19655
19936
|
scratchpadInsertBtn.addEventListener("click", () => {
|
|
19656
19937
|
insertScratchpadIntoEditor();
|
package/client/studio.css
CHANGED
|
@@ -4068,6 +4068,79 @@
|
|
|
4068
4068
|
display: none !important;
|
|
4069
4069
|
}
|
|
4070
4070
|
|
|
4071
|
+
.scratchpad-recent-panel {
|
|
4072
|
+
flex: 0 0 auto;
|
|
4073
|
+
max-height: 260px;
|
|
4074
|
+
overflow: auto;
|
|
4075
|
+
border-bottom: 1px solid var(--panel-border);
|
|
4076
|
+
background: var(--scratchpad-body-bg, var(--panel));
|
|
4077
|
+
padding: 10px 12px;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
.scratchpad-recent-panel[hidden] {
|
|
4081
|
+
display: none !important;
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
.scratchpad-recent-empty,
|
|
4085
|
+
.scratchpad-recent-loading {
|
|
4086
|
+
color: var(--studio-info-text, var(--muted));
|
|
4087
|
+
font-size: 12px;
|
|
4088
|
+
padding: 8px 6px;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
.scratchpad-recent-list {
|
|
4092
|
+
display: grid;
|
|
4093
|
+
gap: 8px;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
.scratchpad-recent-item {
|
|
4097
|
+
display: grid;
|
|
4098
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
4099
|
+
gap: 8px 12px;
|
|
4100
|
+
align-items: start;
|
|
4101
|
+
padding: 9px 10px;
|
|
4102
|
+
border: 1px solid var(--border-subtle);
|
|
4103
|
+
border-radius: 10px;
|
|
4104
|
+
background: var(--panel-2);
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
.scratchpad-recent-main {
|
|
4108
|
+
min-width: 0;
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
.scratchpad-recent-title {
|
|
4112
|
+
font-size: 12px;
|
|
4113
|
+
font-weight: 650;
|
|
4114
|
+
color: var(--text);
|
|
4115
|
+
overflow: hidden;
|
|
4116
|
+
text-overflow: ellipsis;
|
|
4117
|
+
white-space: nowrap;
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
.scratchpad-recent-meta,
|
|
4121
|
+
.scratchpad-recent-preview {
|
|
4122
|
+
font-size: 11px;
|
|
4123
|
+
color: var(--studio-info-text, var(--muted));
|
|
4124
|
+
line-height: 1.35;
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
.scratchpad-recent-preview {
|
|
4128
|
+
margin-top: 3px;
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
.scratchpad-recent-actions {
|
|
4132
|
+
display: inline-flex;
|
|
4133
|
+
gap: 6px;
|
|
4134
|
+
flex-wrap: wrap;
|
|
4135
|
+
justify-content: flex-end;
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
.scratchpad-recent-actions button {
|
|
4139
|
+
padding: 4px 7px;
|
|
4140
|
+
font-size: 11px;
|
|
4141
|
+
line-height: 1.1;
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4071
4144
|
.scratchpad-textarea {
|
|
4072
4145
|
width: 100%;
|
|
4073
4146
|
min-height: 280px;
|
package/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
transformStudioMarkdownOutsideFences,
|
|
23
23
|
} from "./shared/studio-annotation-scanner.js";
|
|
24
24
|
import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
|
|
25
|
+
import { normalizeStudioMarkdownSmartFences } from "./shared/studio-markdown-fences.js";
|
|
25
26
|
import {
|
|
26
27
|
extractStandaloneLatexDefinitionsFromMarkdown,
|
|
27
28
|
preserveLiteralLatexCommandsInMarkdown,
|
|
@@ -229,9 +230,15 @@ interface PersistedStudioReviewNote {
|
|
|
229
230
|
htmlPreviewTitle?: string;
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
interface PersistedStudioScratchpadMetadata {
|
|
234
|
+
label?: string;
|
|
235
|
+
updatedAt?: number;
|
|
236
|
+
}
|
|
237
|
+
|
|
232
238
|
interface StudioPersistentState {
|
|
233
239
|
version: 2;
|
|
234
240
|
scratchpadsByDocument: Record<string, string>;
|
|
241
|
+
scratchpadMetadataByDocument: Record<string, PersistedStudioScratchpadMetadata>;
|
|
235
242
|
reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]>;
|
|
236
243
|
}
|
|
237
244
|
|
|
@@ -728,6 +735,7 @@ function createEmptyStudioPersistentState(): StudioPersistentState {
|
|
|
728
735
|
return {
|
|
729
736
|
version: 2,
|
|
730
737
|
scratchpadsByDocument: {},
|
|
738
|
+
scratchpadMetadataByDocument: {},
|
|
731
739
|
reviewNotesByDocument: {},
|
|
732
740
|
};
|
|
733
741
|
}
|
|
@@ -784,6 +792,7 @@ function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
|
784
792
|
const candidate = value as Partial<StudioPersistentState> & {
|
|
785
793
|
reviewNotesByDocument?: unknown;
|
|
786
794
|
scratchpadsByDocument?: unknown;
|
|
795
|
+
scratchpadMetadataByDocument?: unknown;
|
|
787
796
|
scratchpadText?: unknown;
|
|
788
797
|
};
|
|
789
798
|
const reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]> = {};
|
|
@@ -807,9 +816,21 @@ function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
|
807
816
|
} else if (typeof candidate.scratchpadText === "string" && candidate.scratchpadText.length > 0) {
|
|
808
817
|
scratchpadsByDocument[STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY] = candidate.scratchpadText;
|
|
809
818
|
}
|
|
819
|
+
const scratchpadMetadataByDocument: Record<string, PersistedStudioScratchpadMetadata> = {};
|
|
820
|
+
if (candidate.scratchpadMetadataByDocument && typeof candidate.scratchpadMetadataByDocument === "object") {
|
|
821
|
+
for (const [documentKey, rawMeta] of Object.entries(candidate.scratchpadMetadataByDocument as Record<string, unknown>)) {
|
|
822
|
+
if (typeof documentKey !== "string" || !documentKey.trim() || !rawMeta || typeof rawMeta !== "object") continue;
|
|
823
|
+
const meta = rawMeta as { label?: unknown; updatedAt?: unknown };
|
|
824
|
+
scratchpadMetadataByDocument[documentKey] = {
|
|
825
|
+
label: typeof meta.label === "string" ? meta.label : undefined,
|
|
826
|
+
updatedAt: typeof meta.updatedAt === "number" && Number.isFinite(meta.updatedAt) ? meta.updatedAt : undefined,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
810
830
|
return {
|
|
811
831
|
version: 2,
|
|
812
832
|
scratchpadsByDocument,
|
|
833
|
+
scratchpadMetadataByDocument,
|
|
813
834
|
reviewNotesByDocument,
|
|
814
835
|
};
|
|
815
836
|
}
|
|
@@ -852,16 +873,50 @@ async function readPersistedStudioScratchpadText(documentKey: string): Promise<s
|
|
|
852
873
|
return typeof value === "string" ? value : "";
|
|
853
874
|
}
|
|
854
875
|
|
|
855
|
-
|
|
876
|
+
function describePersistedScratchpadKey(documentKey: string): { label: string; kind: string } {
|
|
877
|
+
const key = String(documentKey || "").trim();
|
|
878
|
+
if (key.startsWith("file:")) return { label: key.slice(5) || "file", kind: "File" };
|
|
879
|
+
if (key.startsWith("draft:")) return { label: key.slice(6) || "draft", kind: "Draft" };
|
|
880
|
+
if (key.startsWith("doc:")) return { label: key.slice(4).replace(/^blank:/, "") || "document", kind: "Document" };
|
|
881
|
+
return { label: key || "scratchpad", kind: "Scratchpad" };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function summarizeScratchpadText(text: string): string {
|
|
885
|
+
const normalized = String(text || "").replace(/\s+/g, " ").trim();
|
|
886
|
+
return normalized.length > 160 ? `${normalized.slice(0, 157)}…` : normalized;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function listRecentPersistedStudioScratchpads(limit = 20): Promise<Array<{ documentKey: string; label: string; kind: string; updatedAt: number; textPreview: string; textLength: number }>> {
|
|
890
|
+
const state = await loadStudioPersistentState();
|
|
891
|
+
return Object.entries(state.scratchpadsByDocument)
|
|
892
|
+
.filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string" && entry[1].trim().length > 0)
|
|
893
|
+
.map(([documentKey, text]) => {
|
|
894
|
+
const fallback = describePersistedScratchpadKey(documentKey);
|
|
895
|
+
const meta = state.scratchpadMetadataByDocument[documentKey] ?? {};
|
|
896
|
+
const label = typeof meta.label === "string" && meta.label.trim() ? meta.label.trim() : fallback.label;
|
|
897
|
+
const updatedAt = typeof meta.updatedAt === "number" && Number.isFinite(meta.updatedAt) ? meta.updatedAt : 0;
|
|
898
|
+
return { documentKey, label, kind: fallback.kind, updatedAt, textPreview: summarizeScratchpadText(text), textLength: text.length };
|
|
899
|
+
})
|
|
900
|
+
.sort((left, right) => (right.updatedAt - left.updatedAt) || left.label.localeCompare(right.label) || left.documentKey.localeCompare(right.documentKey))
|
|
901
|
+
.slice(0, Math.max(1, Math.min(100, Math.floor(limit) || 20)));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async function writePersistedStudioScratchpadText(documentKey: string, text: string, label?: string): Promise<void> {
|
|
856
905
|
const key = String(documentKey ?? "").trim();
|
|
857
906
|
if (!key) return;
|
|
858
907
|
await mutateStudioPersistentState((state) => {
|
|
859
908
|
const normalized = String(text ?? "");
|
|
860
909
|
if (normalized.length === 0) {
|
|
861
910
|
delete state.scratchpadsByDocument[key];
|
|
911
|
+
delete state.scratchpadMetadataByDocument[key];
|
|
862
912
|
return;
|
|
863
913
|
}
|
|
864
914
|
state.scratchpadsByDocument[key] = normalized;
|
|
915
|
+
state.scratchpadMetadataByDocument[key] = {
|
|
916
|
+
...(state.scratchpadMetadataByDocument[key] ?? {}),
|
|
917
|
+
label: typeof label === "string" && label.trim() ? label.trim() : state.scratchpadMetadataByDocument[key]?.label,
|
|
918
|
+
updatedAt: Date.now(),
|
|
919
|
+
};
|
|
865
920
|
});
|
|
866
921
|
}
|
|
867
922
|
|
|
@@ -4582,7 +4637,8 @@ function hasStudioYamlHeaderIncludes(markdown: string): boolean {
|
|
|
4582
4637
|
function prepareStudioMarkdownForPandoc(markdown: string, options?: { preserveLiteralLatexCommands?: boolean }): string {
|
|
4583
4638
|
const shouldPreserveLiteralLatexCommands = options?.preserveLiteralLatexCommands !== false;
|
|
4584
4639
|
return mapStudioMarkdownBodyPreservingYamlFrontMatter(markdown, (body) => {
|
|
4585
|
-
const
|
|
4640
|
+
const normalizedFences = normalizeStudioMarkdownSmartFences(body);
|
|
4641
|
+
const normalizedMath = normalizeMathDelimiters(normalizedFences);
|
|
4586
4642
|
const latexReady = shouldPreserveLiteralLatexCommands
|
|
4587
4643
|
? preserveLiteralLatexCommandsInMarkdown(normalizedMath)
|
|
4588
4644
|
: normalizedMath;
|
|
@@ -5432,9 +5488,10 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
5432
5488
|
&& !isStudioSingleFencedCodeBlock(input)
|
|
5433
5489
|
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
5434
5490
|
: input;
|
|
5491
|
+
const fenceNormalizedSource = effectiveEditorLanguage === "latex" ? source : normalizeStudioMarkdownSmartFences(source);
|
|
5435
5492
|
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
5436
|
-
? replaceStudioAnnotationMarkersForPdf(
|
|
5437
|
-
:
|
|
5493
|
+
? replaceStudioAnnotationMarkersForPdf(fenceNormalizedSource)
|
|
5494
|
+
: fenceNormalizedSource;
|
|
5438
5495
|
const commentStrippedSource = stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(annotationReadySource);
|
|
5439
5496
|
return prepareStudioMarkdownForPandoc(commentStrippedSource, {
|
|
5440
5497
|
preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(annotationReadySource),
|
|
@@ -5733,7 +5790,8 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
|
|
|
5733
5790
|
|
|
5734
5791
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
5735
5792
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
5736
|
-
const
|
|
5793
|
+
const markdownWithNormalizedFences = isLatex ? markdown : normalizeStudioMarkdownSmartFences(markdown);
|
|
5794
|
+
const markdownWithoutHtmlComments = isLatex ? markdownWithNormalizedFences : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdownWithNormalizedFences);
|
|
5737
5795
|
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
5738
5796
|
const latexSubfigurePreviewTransform = isLatex
|
|
5739
5797
|
? preprocessStudioLatexSubfiguresForPreview(markdownWithPreviewPageBreaks)
|
|
@@ -10101,8 +10159,10 @@ ${cssVarsBlock}
|
|
|
10101
10159
|
<span id="exportPreviewControls" class="export-preview-controls">
|
|
10102
10160
|
<button id="exportPdfBtn" class="export-preview-trigger" type="button" aria-haspopup="menu" aria-expanded="false" title="Choose a format and export the current right-pane preview.">Export right preview</button>
|
|
10103
10161
|
<div id="exportPreviewMenu" class="export-preview-menu" role="menu" hidden>
|
|
10104
|
-
<button id="
|
|
10105
|
-
<button id="
|
|
10162
|
+
<button id="exportPreviewPdfStudioBtn" type="button" role="menuitem" data-export-preview-format="pdf-studio">Export PDF and Open in Studio preview tab</button>
|
|
10163
|
+
<button id="exportPreviewPdfBtn" type="button" role="menuitem" data-export-preview-format="pdf-default">Export PDF and Open in default PDF viewer</button>
|
|
10164
|
+
<button id="exportPreviewHtmlStudioBtn" type="button" role="menuitem" data-export-preview-format="html-studio">Export HTML and Open in Studio editor</button>
|
|
10165
|
+
<button id="exportPreviewHtmlBtn" type="button" role="menuitem" data-export-preview-format="html-browser">Export HTML and Open in browser</button>
|
|
10106
10166
|
</div>
|
|
10107
10167
|
</span>
|
|
10108
10168
|
</div>
|
|
@@ -10227,14 +10287,16 @@ ${cssVarsBlock}
|
|
|
10227
10287
|
<div class="scratchpad-header">
|
|
10228
10288
|
<div>
|
|
10229
10289
|
<h2 id="scratchpadTitle">Scratchpad</h2>
|
|
10230
|
-
<p class="scratchpad-description">Local persistent notes for thoughts you want to park while working on the current Studio document or draft. Closing the scratchpad does not clear it: notes persist locally for this document identity until you edit or clear them. File-backed documents reliably come back across Pi restarts; unsaved drafts stay with their own draft instance until you save them or discard them. Scratchpad text is not run, critiqued, sent, or exported unless you explicitly insert it into the editor.</p>
|
|
10290
|
+
<p class="scratchpad-description">Local persistent notes for thoughts you want to park while working on the current Studio document or draft. Closing the scratchpad does not clear it: notes persist locally for this document identity until you edit or clear them. File-backed documents reliably come back across Pi restarts; unsaved drafts stay with their own draft instance until you save them or discard them. Use Recent… to recover scratchpads from other draft identities after a Studio/Pi restart. Scratchpad text is not run, critiqued, sent, or exported unless you explicitly insert it into the editor.</p>
|
|
10231
10291
|
</div>
|
|
10232
10292
|
<button id="scratchpadCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Keep current scratchpad text and close scratchpad" title="Keep current scratchpad text and close scratchpad">✕</button>
|
|
10233
10293
|
</div>
|
|
10294
|
+
<div id="scratchpadRecentPanel" class="scratchpad-recent-panel" hidden></div>
|
|
10234
10295
|
<textarea id="scratchpadText" class="scratchpad-textarea" placeholder="Jot quick thoughts, TODOs, or prompt ideas here..."></textarea>
|
|
10235
10296
|
<div class="scratchpad-footer">
|
|
10236
10297
|
<span id="scratchpadMeta" class="scratchpad-meta">Empty · local only</span>
|
|
10237
10298
|
<div class="scratchpad-actions">
|
|
10299
|
+
<button id="scratchpadRecentBtn" type="button" title="Show recent non-empty scratchpads saved for other files and drafts.">Recent…</button>
|
|
10238
10300
|
<button id="scratchpadInsertBtn" type="button" title="Insert the scratchpad text into the editor at the current selection, or append it if no editor selection is available.">Insert into editor</button>
|
|
10239
10301
|
<button id="scratchpadCopyBtn" type="button" title="Copy scratchpad text to the clipboard.">Copy</button>
|
|
10240
10302
|
<button id="scratchpadClearBtn" type="button" title="Clear scratchpad text.">Clear</button>
|
|
@@ -12607,6 +12669,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12607
12669
|
const handleScratchpadStateRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
12608
12670
|
const method = (req.method ?? "GET").toUpperCase();
|
|
12609
12671
|
if (method === "GET") {
|
|
12672
|
+
const action = (requestUrl.searchParams.get("action") ?? "").trim().toLowerCase();
|
|
12673
|
+
if (action === "recent") {
|
|
12674
|
+
const limit = Number.parseInt(requestUrl.searchParams.get("limit") ?? "20", 10);
|
|
12675
|
+
respondJson(res, 200, { ok: true, scratchpads: await listRecentPersistedStudioScratchpads(limit) });
|
|
12676
|
+
return;
|
|
12677
|
+
}
|
|
12610
12678
|
const documentKey = (requestUrl.searchParams.get("documentKey") ?? "").trim();
|
|
12611
12679
|
if (!documentKey) {
|
|
12612
12680
|
respondJson(res, 400, { ok: false, error: "Missing documentKey query parameter." });
|
|
@@ -12656,8 +12724,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
12656
12724
|
respondJson(res, 400, { ok: false, error: "Missing scratchpad text in request body." });
|
|
12657
12725
|
return;
|
|
12658
12726
|
}
|
|
12727
|
+
const label =
|
|
12728
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { label?: unknown }).label === "string"
|
|
12729
|
+
? (parsedBody as { label: string }).label
|
|
12730
|
+
: undefined;
|
|
12659
12731
|
|
|
12660
|
-
await writePersistedStudioScratchpadText(documentKey, text);
|
|
12732
|
+
await writePersistedStudioScratchpadText(documentKey, text, label);
|
|
12661
12733
|
respondJson(res, 200, { ok: true });
|
|
12662
12734
|
};
|
|
12663
12735
|
|
|
@@ -12966,6 +13038,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
12966
13038
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorPdfLanguage?: unknown }).editorPdfLanguage === "string"
|
|
12967
13039
|
? (parsedBody as { editorPdfLanguage: string }).editorPdfLanguage
|
|
12968
13040
|
: "";
|
|
13041
|
+
const requestedOpenTarget =
|
|
13042
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { openTarget?: unknown }).openTarget === "string"
|
|
13043
|
+
? (parsedBody as { openTarget: string }).openTarget.trim().toLowerCase()
|
|
13044
|
+
: "default";
|
|
13045
|
+
const openTarget = requestedOpenTarget === "studio" ? "studio" : "default";
|
|
12969
13046
|
const editorPdfLanguage = inferStudioPdfLanguage(markdown, requestedEditorPdfLanguage);
|
|
12970
13047
|
const isLatex = editorPdfLanguage === "latex"
|
|
12971
13048
|
|| (
|
|
@@ -12979,17 +13056,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
12979
13056
|
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), pdf);
|
|
12980
13057
|
const exportId = storePreparedPdfExport(pdf, filename, warning, writeResult.filePath ?? undefined);
|
|
12981
13058
|
const token = serverState?.token ?? "";
|
|
13059
|
+
if (openTarget === "studio" && serverState && writeResult.filePath) {
|
|
13060
|
+
const exportedPath = writeResult.filePath;
|
|
13061
|
+
const title = sanitizeStudioPreviewBlockLine(filename || basename(exportedPath) || "PDF preview");
|
|
13062
|
+
const document: InitialStudioDocument = {
|
|
13063
|
+
text: "```studio-pdf\n"
|
|
13064
|
+
+ `path: ${sanitizeStudioPreviewBlockLine(basename(exportedPath))}\n`
|
|
13065
|
+
+ `title: ${title || "PDF preview"}\n`
|
|
13066
|
+
+ "height: 820\n"
|
|
13067
|
+
+ "```\n",
|
|
13068
|
+
label: `${filename || basename(exportedPath) || "PDF"} preview`,
|
|
13069
|
+
source: "blank",
|
|
13070
|
+
resourceDir: dirname(exportedPath),
|
|
13071
|
+
};
|
|
13072
|
+
const docId = storeTransientStudioDocument(document);
|
|
13073
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
13074
|
+
const parsedUrl = new URL(url);
|
|
13075
|
+
respondJson(res, 200, {
|
|
13076
|
+
ok: true,
|
|
13077
|
+
filename,
|
|
13078
|
+
path: writeResult.filePath,
|
|
13079
|
+
writeError: writeResult.error,
|
|
13080
|
+
warning: warning ?? null,
|
|
13081
|
+
openedStudio: true,
|
|
13082
|
+
url,
|
|
13083
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
13084
|
+
downloadUrl: `/export-pdf?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
13085
|
+
});
|
|
13086
|
+
return;
|
|
13087
|
+
}
|
|
12982
13088
|
let openedExternal = false;
|
|
12983
13089
|
let openError: string | null = null;
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
13090
|
+
if (openTarget !== "studio") {
|
|
13091
|
+
try {
|
|
13092
|
+
const prepared = await ensurePreparedPdfExportFile(exportId);
|
|
13093
|
+
if (!prepared?.filePath) {
|
|
13094
|
+
throw new Error("Prepared PDF file was not available for external open.");
|
|
13095
|
+
}
|
|
13096
|
+
await openPathInDefaultViewer(prepared.filePath);
|
|
13097
|
+
openedExternal = true;
|
|
13098
|
+
} catch (viewerError) {
|
|
13099
|
+
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
12988
13100
|
}
|
|
12989
|
-
await openPathInDefaultViewer(prepared.filePath);
|
|
12990
|
-
openedExternal = true;
|
|
12991
|
-
} catch (viewerError) {
|
|
12992
|
-
openError = viewerError instanceof Error ? viewerError.message : String(viewerError);
|
|
12993
13101
|
}
|
|
12994
13102
|
respondJson(res, 200, {
|
|
12995
13103
|
ok: true,
|
|
@@ -13068,6 +13176,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
13068
13176
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorHtmlLanguage?: unknown }).editorHtmlLanguage === "string"
|
|
13069
13177
|
? (parsedBody as { editorHtmlLanguage: string }).editorHtmlLanguage
|
|
13070
13178
|
: "";
|
|
13179
|
+
const requestedOpenTarget =
|
|
13180
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { openTarget?: unknown }).openTarget === "string"
|
|
13181
|
+
? (parsedBody as { openTarget: string }).openTarget.trim().toLowerCase()
|
|
13182
|
+
: "browser";
|
|
13183
|
+
const openTarget = requestedOpenTarget === "studio" ? "studio" : "browser";
|
|
13071
13184
|
const editorHtmlLanguage = inferStudioPdfLanguage(markdown, requestedEditorHtmlLanguage);
|
|
13072
13185
|
const isLatex = editorHtmlLanguage === "latex"
|
|
13073
13186
|
|| (
|
|
@@ -13093,6 +13206,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
13093
13206
|
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), html);
|
|
13094
13207
|
const exportId = storePreparedHtmlExport(html, filename, warning, writeResult.filePath ?? undefined);
|
|
13095
13208
|
const token = serverState?.token ?? "";
|
|
13209
|
+
if (openTarget === "studio" && serverState) {
|
|
13210
|
+
const exportedPath = writeResult.filePath ?? "";
|
|
13211
|
+
const document: InitialStudioDocument = {
|
|
13212
|
+
text: html.toString("utf-8"),
|
|
13213
|
+
label: filename,
|
|
13214
|
+
source: exportedPath ? "file" : "blank",
|
|
13215
|
+
path: exportedPath || undefined,
|
|
13216
|
+
resourceDir: exportedPath ? dirname(exportedPath) : (userResourceDir || resourcePath || studioCwd),
|
|
13217
|
+
draftId: exportedPath ? undefined : createStudioDraftId(),
|
|
13218
|
+
};
|
|
13219
|
+
const docId = storeTransientStudioDocument(document);
|
|
13220
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
13221
|
+
const parsedUrl = new URL(url);
|
|
13222
|
+
respondJson(res, 200, {
|
|
13223
|
+
ok: true,
|
|
13224
|
+
filename,
|
|
13225
|
+
path: writeResult.filePath,
|
|
13226
|
+
writeError: writeResult.error,
|
|
13227
|
+
warning: warning ?? null,
|
|
13228
|
+
openedStudio: true,
|
|
13229
|
+
url,
|
|
13230
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
13231
|
+
downloadUrl: `/export-html?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
13232
|
+
});
|
|
13233
|
+
return;
|
|
13234
|
+
}
|
|
13096
13235
|
let openedExternal = false;
|
|
13097
13236
|
let openError: string | null = null;
|
|
13098
13237
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.21",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const SMART_MARKDOWN_FENCE_CHARS = "`´‘’‚‛“”„‟′‵";
|
|
2
|
+
const SMART_MARKDOWN_FENCE_RE = new RegExp(`^([ \\t]{0,3})([${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}]{3,})([^${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}\\r\\n]*)$`);
|
|
3
|
+
|
|
4
|
+
function normalizeSmartFenceRun(run) {
|
|
5
|
+
return "`".repeat(Math.max(3, Array.from(String(run || "")).length));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeStudioMarkdownSmartFences(markdown) {
|
|
9
|
+
return String(markdown ?? "")
|
|
10
|
+
.replace(/\r\n/g, "\n")
|
|
11
|
+
.split("\n")
|
|
12
|
+
.map((line) => {
|
|
13
|
+
const match = line.match(SMART_MARKDOWN_FENCE_RE);
|
|
14
|
+
if (!match) return line;
|
|
15
|
+
const indent = match[1] ?? "";
|
|
16
|
+
const run = match[2] ?? "";
|
|
17
|
+
const suffix = match[3] ?? "";
|
|
18
|
+
if (!/[´‘’‚‛“”„‟′‵]/u.test(run)) return line;
|
|
19
|
+
return `${indent}${normalizeSmartFenceRun(run)}${suffix}`;
|
|
20
|
+
})
|
|
21
|
+
.join("\n");
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const SMART_MARKDOWN_FENCE_CHARS = "`´‘’‚‛“”„‟′‵";
|
|
2
|
+
const SMART_MARKDOWN_FENCE_RE = new RegExp(`^([ \\t]{0,3})([${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}]{3,})([^${SMART_MARKDOWN_FENCE_CHARS.replace(/[\\\]^-]/g, "\\$&")}\\r\\n]*)$`);
|
|
3
|
+
|
|
4
|
+
function normalizeSmartFenceRun(run: string): string {
|
|
5
|
+
return "`".repeat(Math.max(3, Array.from(String(run || "")).length));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeStudioMarkdownSmartFences(markdown: string): string {
|
|
9
|
+
return String(markdown ?? "")
|
|
10
|
+
.replace(/\r\n/g, "\n")
|
|
11
|
+
.split("\n")
|
|
12
|
+
.map((line) => {
|
|
13
|
+
const match = line.match(SMART_MARKDOWN_FENCE_RE);
|
|
14
|
+
if (!match) return line;
|
|
15
|
+
const indent = match[1] ?? "";
|
|
16
|
+
const run = match[2] ?? "";
|
|
17
|
+
const suffix = match[3] ?? "";
|
|
18
|
+
if (!/[´‘’‚‛“”„‟′‵]/u.test(run)) return line;
|
|
19
|
+
return `${indent}${normalizeSmartFenceRun(run)}${suffix}`;
|
|
20
|
+
})
|
|
21
|
+
.join("\n");
|
|
22
|
+
}
|