pi-studio 0.9.20 → 0.9.22
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 +18 -1
- package/README.md +2 -2
- package/client/studio-annotation-helpers.js +20 -1
- package/client/studio-client.js +451 -58
- package/client/studio.css +82 -0
- package/index.ts +168 -26
- package/package.json +1 -1
- package/shared/studio-markdown-fences.js +22 -0
- package/shared/studio-markdown-fences.ts +22 -0
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");
|
|
@@ -173,6 +177,7 @@
|
|
|
173
177
|
const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
|
|
174
178
|
|
|
175
179
|
const initialQueryParams = new URLSearchParams(window.location.search || "");
|
|
180
|
+
const skipInitialWorkspaceRestore = initialQueryParams.get("skipWorkspaceRestore") === "1";
|
|
176
181
|
const explicitDocumentIdentityFromUrl = initialQueryParams.has("docId")
|
|
177
182
|
|| initialQueryParams.has("docSource")
|
|
178
183
|
|| initialQueryParams.has("docLabel")
|
|
@@ -228,6 +233,10 @@
|
|
|
228
233
|
let pendingKind = null;
|
|
229
234
|
let stickyStudioKind = null;
|
|
230
235
|
const pendingCompanionWindows = new Map();
|
|
236
|
+
let sourceOriginSummaryEl = null;
|
|
237
|
+
let sourceResetOriginBtn = null;
|
|
238
|
+
let sourceOpenCurrentFileTabBtn = null;
|
|
239
|
+
let sourceOpenCurrentTextCopyTabBtn = null;
|
|
231
240
|
let initialDocumentApplied = false;
|
|
232
241
|
function normalizeRightViewValue(nextView) {
|
|
233
242
|
const raw = String(nextView || "").trim();
|
|
@@ -254,8 +263,8 @@
|
|
|
254
263
|
option.disabled = isEditorOnlyMode && !editorOnlyAllowed.has(option.value);
|
|
255
264
|
});
|
|
256
265
|
rightViewSelect.title = isEditorOnlyMode
|
|
257
|
-
? "Editor-only views: editor preview, Files, or REPL.
|
|
258
|
-
: "Right pane view mode.
|
|
266
|
+
? "Editor-only views: editor preview, Files, or REPL. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview."
|
|
267
|
+
: "Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview.";
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
function getInitialRightView(source) {
|
|
@@ -2044,6 +2053,9 @@
|
|
|
2044
2053
|
let scratchpadReturnFocusEl = null;
|
|
2045
2054
|
let scratchpadPersistTimer = null;
|
|
2046
2055
|
let scratchpadLoadNonce = 0;
|
|
2056
|
+
let scratchpadRecentEntries = [];
|
|
2057
|
+
let scratchpadRecentVisible = false;
|
|
2058
|
+
let scratchpadRecentLoading = false;
|
|
2047
2059
|
let reviewNotes = [];
|
|
2048
2060
|
let reviewNotesReturnFocusEl = null;
|
|
2049
2061
|
let reviewNotesPersistTimer = null;
|
|
@@ -2398,8 +2410,66 @@
|
|
|
2398
2410
|
suggestCompletionOptionsBtn.hidden = false;
|
|
2399
2411
|
if (completionContextSelect) completionContextSelect.hidden = true;
|
|
2400
2412
|
contextMenu = makeStudioUiRefreshMenu(suggestCompletionOptionsBtn, "context", "studio-refresh-context-anchor");
|
|
2401
|
-
|
|
2413
|
+
sourceOriginSummaryEl = makeStudioUiRefreshElement("div", "source-badge source-origin-summary", "Origin: blank");
|
|
2414
|
+
sourceOriginSummaryEl.setAttribute("aria-label", "Current editor origin");
|
|
2415
|
+
sourceResetOriginBtn = makeStudioUiRefreshElement("button", "source-reset-origin-btn", "Reset origin");
|
|
2416
|
+
sourceResetOriginBtn.type = "button";
|
|
2417
|
+
sourceResetOriginBtn.title = "Reset the editor origin and keep the current text in a new draft.";
|
|
2418
|
+
sourceResetOriginBtn.addEventListener("click", (event) => {
|
|
2419
|
+
event.preventDefault();
|
|
2420
|
+
event.stopPropagation();
|
|
2421
|
+
closeStudioUiRefreshMenus();
|
|
2422
|
+
resetEditorOrigin();
|
|
2423
|
+
});
|
|
2424
|
+
sourceOpenCurrentFileTabBtn = makeStudioUiRefreshElement("button", "source-open-file-tab-btn", "Open current file in new editor tab");
|
|
2425
|
+
sourceOpenCurrentFileTabBtn.type = "button";
|
|
2426
|
+
sourceOpenCurrentFileTabBtn.title = "Open this file-backed document in a new refreshable editor-only Studio tab.";
|
|
2427
|
+
sourceOpenCurrentFileTabBtn.addEventListener("click", (event) => {
|
|
2428
|
+
event.preventDefault();
|
|
2429
|
+
event.stopPropagation();
|
|
2430
|
+
const path = sourceState && sourceState.path ? String(sourceState.path) : "";
|
|
2431
|
+
if (!path) {
|
|
2432
|
+
setStatus("Open current file in new editor tab is only available for file-backed documents.", "warning");
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
closeStudioUiRefreshMenus();
|
|
2436
|
+
const targetUrl = buildAuthedStudioUrl("/", {
|
|
2437
|
+
mode: "editor-only",
|
|
2438
|
+
docSource: "file",
|
|
2439
|
+
docLabel: sourceState && sourceState.label ? sourceState.label : basenameForStudioPath(path),
|
|
2440
|
+
docPath: path,
|
|
2441
|
+
resourceDir: getCurrentResourceDirValue() || dirnameForDisplayPath(path),
|
|
2442
|
+
skipWorkspaceRestore: "1",
|
|
2443
|
+
});
|
|
2444
|
+
try {
|
|
2445
|
+
window.open(targetUrl, "_blank", "noopener");
|
|
2446
|
+
setStatus("Opening current file in a new editor tab.", "success");
|
|
2447
|
+
} catch (error) {
|
|
2448
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open file tab."), "warning");
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
sourceOpenCurrentTextCopyTabBtn = makeStudioUiRefreshElement("button", "source-open-text-copy-tab-btn", "Open current text as copy in new editor tab");
|
|
2452
|
+
sourceOpenCurrentTextCopyTabBtn.type = "button";
|
|
2453
|
+
sourceOpenCurrentTextCopyTabBtn.title = "Open a detached copy of the current editor text in a new editor-only Studio tab.";
|
|
2454
|
+
sourceOpenCurrentTextCopyTabBtn.addEventListener("click", (event) => {
|
|
2455
|
+
event.preventDefault();
|
|
2456
|
+
event.stopPropagation();
|
|
2457
|
+
const content = String(sourceTextEl.value || "");
|
|
2458
|
+
if (!content.trim()) {
|
|
2459
|
+
setStatus("Editor is empty. Use New editor tab for a blank editor.", "warning");
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
closeStudioUiRefreshMenus();
|
|
2463
|
+
requestOpenEditorOnlyDocument(content, {
|
|
2464
|
+
label: sourceState && sourceState.label ? sourceState.label : "current editor",
|
|
2465
|
+
path: sourceState && sourceState.path ? sourceState.path : undefined,
|
|
2466
|
+
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
2467
|
+
});
|
|
2468
|
+
});
|
|
2469
|
+
if (sendEditorBtn) sendEditorBtn.textContent = "Send current text to Pi editor";
|
|
2470
|
+
appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceOriginSummaryEl, sourceResetOriginBtn, sourceOpenCurrentFileTabBtn, sourceOpenCurrentTextCopyTabBtn]);
|
|
2402
2471
|
appendStudioUiRefreshMenuSection(contextMenu.menu, "Working directory", [resourceDirBtn, resourceDirLabel, resourceDirInputWrap]);
|
|
2472
|
+
if (!isEditorOnlyMode && sendEditorBtn) appendStudioUiRefreshMenuSection(contextMenu.menu, "Pi editor", [sendEditorBtn]);
|
|
2403
2473
|
const cursorContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor only");
|
|
2404
2474
|
cursorContextBtn.type = "button";
|
|
2405
2475
|
cursorContextBtn.setAttribute("data-completion-context-mode", "cursor");
|
|
@@ -2477,7 +2547,6 @@
|
|
|
2477
2547
|
actionLineTwoEl.appendChild(copyDraftBtn);
|
|
2478
2548
|
if (suggestCompletionBtn) actionLineTwoEl.appendChild(suggestCompletionBtn);
|
|
2479
2549
|
if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
|
|
2480
|
-
if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
|
|
2481
2550
|
const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
|
|
2482
2551
|
replActionLineEl.hidden = true;
|
|
2483
2552
|
if (sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
|
|
@@ -3206,13 +3275,26 @@
|
|
|
3206
3275
|
|
|
3207
3276
|
function updateSourceBadge() {
|
|
3208
3277
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
3209
|
-
|
|
3278
|
+
const originText = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : "");
|
|
3210
3279
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
3211
3280
|
if (sourceBadgeEl) {
|
|
3281
|
+
sourceBadgeEl.textContent = originText;
|
|
3212
3282
|
sourceBadgeEl.title = descriptor.fileBacked
|
|
3213
3283
|
? ("Editor origin: " + label + "\nClick to reset origin and detach the current editor text into a new draft. The file on disk will not be changed.")
|
|
3214
3284
|
: ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes.");
|
|
3215
3285
|
}
|
|
3286
|
+
if (sourceOriginSummaryEl) {
|
|
3287
|
+
sourceOriginSummaryEl.textContent = originText;
|
|
3288
|
+
sourceOriginSummaryEl.title = descriptor.fileBacked
|
|
3289
|
+
? ("File-backed editor: " + (descriptor.path || label))
|
|
3290
|
+
: ("Detached editor origin: " + label);
|
|
3291
|
+
}
|
|
3292
|
+
if (sourceResetOriginBtn) {
|
|
3293
|
+
sourceResetOriginBtn.textContent = descriptor.fileBacked ? "Detach from file" : "Reset origin";
|
|
3294
|
+
sourceResetOriginBtn.title = descriptor.fileBacked
|
|
3295
|
+
? "Detach the current editor text from this file and keep it in a new draft. The file on disk will not be changed."
|
|
3296
|
+
: "Reset the editor origin and keep the current text in a new draft.";
|
|
3297
|
+
}
|
|
3216
3298
|
// Show "Set working dir" button when not file-backed
|
|
3217
3299
|
var isFileBacked = hasRefreshableFilePath();
|
|
3218
3300
|
if (isFileBacked) {
|
|
@@ -3553,6 +3635,17 @@
|
|
|
3553
3635
|
setStatus("Right pane content focused.");
|
|
3554
3636
|
}
|
|
3555
3637
|
|
|
3638
|
+
function switchRightPaneToPrimaryPreview() {
|
|
3639
|
+
const targetView = isEditorOnlyMode ? "editor-preview" : "preview";
|
|
3640
|
+
const snapshot = snapshotStudioScrollablePositions();
|
|
3641
|
+
setRightView(targetView);
|
|
3642
|
+
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3643
|
+
const label = rightViewSelect && rightViewSelect.selectedOptions && rightViewSelect.selectedOptions[0]
|
|
3644
|
+
? rightViewSelect.selectedOptions[0].textContent
|
|
3645
|
+
: (isEditorOnlyMode ? "Editor (Preview)" : "Response (Preview)");
|
|
3646
|
+
setStatus("Right pane view: " + String(label || "Preview") + ".");
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3556
3649
|
function cycleActivePaneView(direction) {
|
|
3557
3650
|
if (activePane === "right") {
|
|
3558
3651
|
if (!rightViewSelect || rightViewSelect.disabled) {
|
|
@@ -3851,6 +3944,16 @@
|
|
|
3851
3944
|
return;
|
|
3852
3945
|
}
|
|
3853
3946
|
|
|
3947
|
+
const isPreviewShortcut = (key.toLowerCase() === "p" || code === "KeyP")
|
|
3948
|
+
&& (event.metaKey || event.ctrlKey)
|
|
3949
|
+
&& event.altKey
|
|
3950
|
+
&& !event.shiftKey;
|
|
3951
|
+
if (isPreviewShortcut) {
|
|
3952
|
+
event.preventDefault();
|
|
3953
|
+
switchRightPaneToPrimaryPreview();
|
|
3954
|
+
return;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3854
3957
|
const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3855
3958
|
if (isContentFocusShortcut) {
|
|
3856
3959
|
event.preventDefault();
|
|
@@ -7940,14 +8043,32 @@
|
|
|
7940
8043
|
}
|
|
7941
8044
|
}
|
|
7942
8045
|
|
|
7943
|
-
|
|
8046
|
+
function getStudioPdfViewerUrlForExportPayload(payload) {
|
|
8047
|
+
if (!payload || typeof payload !== "object") return "";
|
|
8048
|
+
const exportPath = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
8049
|
+
if (exportPath) {
|
|
8050
|
+
const resourceUrl = buildStudioPdfResourceUrl({ path: exportPath, resourceDir: exportPath.split(/[\\/]/).slice(0, -1).join("/") }, false);
|
|
8051
|
+
if (resourceUrl) return resourceUrl;
|
|
8052
|
+
}
|
|
8053
|
+
return typeof payload.downloadUrl === "string" ? payload.downloadUrl : "";
|
|
8054
|
+
}
|
|
8055
|
+
|
|
8056
|
+
async function exportRightPanePdf(options) {
|
|
8057
|
+
const exportOptions = options && typeof options === "object" ? options : {};
|
|
8058
|
+
const openTarget = exportOptions.openTarget === "studio" ? "studio" : "default";
|
|
8059
|
+
let studioPopup = null;
|
|
8060
|
+
if (openTarget === "studio") {
|
|
8061
|
+
studioPopup = openExportStudioPlaceholderWindow("PDF");
|
|
8062
|
+
}
|
|
7944
8063
|
if (uiBusy || previewExportInProgress) {
|
|
8064
|
+
closeExportStudioWindow(studioPopup);
|
|
7945
8065
|
setStatus("Studio is busy.", "warning");
|
|
7946
8066
|
return;
|
|
7947
8067
|
}
|
|
7948
8068
|
|
|
7949
8069
|
const token = getToken();
|
|
7950
8070
|
if (!token) {
|
|
8071
|
+
closeExportStudioWindow(studioPopup);
|
|
7951
8072
|
setStatus("Missing Studio token in URL. Re-run /studio.", "error");
|
|
7952
8073
|
return;
|
|
7953
8074
|
}
|
|
@@ -7955,17 +8076,20 @@
|
|
|
7955
8076
|
const exportingReplJournal = rightView === "repl";
|
|
7956
8077
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
7957
8078
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
8079
|
+
closeExportStudioWindow(studioPopup);
|
|
7958
8080
|
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
|
|
7959
8081
|
return;
|
|
7960
8082
|
}
|
|
7961
8083
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
7962
8084
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
8085
|
+
closeExportStudioWindow(studioPopup);
|
|
7963
8086
|
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
7964
8087
|
return;
|
|
7965
8088
|
}
|
|
7966
8089
|
|
|
7967
8090
|
const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
|
|
7968
8091
|
if (htmlArtifactSource) {
|
|
8092
|
+
closeExportStudioWindow(studioPopup);
|
|
7969
8093
|
setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
|
|
7970
8094
|
return;
|
|
7971
8095
|
}
|
|
@@ -7976,6 +8100,7 @@
|
|
|
7976
8100
|
? prepareEditorTextForPdfExport(sourceTextEl.value)
|
|
7977
8101
|
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
7978
8102
|
if (!markdown || !markdown.trim()) {
|
|
8103
|
+
closeExportStudioWindow(studioPopup);
|
|
7979
8104
|
setStatus("Nothing to export yet.", "warning");
|
|
7980
8105
|
return;
|
|
7981
8106
|
}
|
|
@@ -7998,7 +8123,7 @@
|
|
|
7998
8123
|
|
|
7999
8124
|
previewExportInProgress = true;
|
|
8000
8125
|
updateResultActionButtons();
|
|
8001
|
-
setStatus("Exporting PDF…", "warning");
|
|
8126
|
+
setStatus(openTarget === "studio" ? "Exporting PDF for Studio…" : "Exporting PDF…", "warning");
|
|
8002
8127
|
|
|
8003
8128
|
try {
|
|
8004
8129
|
const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
|
|
@@ -8013,6 +8138,7 @@
|
|
|
8013
8138
|
isLatex: isLatex,
|
|
8014
8139
|
editorPdfLanguage: editorPdfLanguage,
|
|
8015
8140
|
filenameHint: filenameHint,
|
|
8141
|
+
openTarget: openTarget,
|
|
8016
8142
|
}),
|
|
8017
8143
|
}, PDF_EXPORT_FETCH_TIMEOUT_MS, "PDF export");
|
|
8018
8144
|
|
|
@@ -8051,6 +8177,35 @@
|
|
|
8051
8177
|
downloadName += ".pdf";
|
|
8052
8178
|
}
|
|
8053
8179
|
|
|
8180
|
+
if (openTarget === "studio") {
|
|
8181
|
+
const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
|
|
8182
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
8183
|
+
: (typeof payload.url === "string" ? payload.url : "");
|
|
8184
|
+
const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
|
|
8185
|
+
if (!openedStudio) {
|
|
8186
|
+
closeExportStudioWindow(studioPopup);
|
|
8187
|
+
const viewerUrl = getStudioPdfViewerUrlForExportPayload(payload);
|
|
8188
|
+
if (viewerUrl) openStudioPdfFocusViewer(viewerUrl, downloadName);
|
|
8189
|
+
}
|
|
8190
|
+
if (writeError) {
|
|
8191
|
+
setStatus(openedStudio
|
|
8192
|
+
? "Opened exported PDF in a Studio preview tab, but could not write project file: " + writeError
|
|
8193
|
+
: "Exported PDF, but could not open a Studio preview tab and could not write project file: " + writeError,
|
|
8194
|
+
"warning");
|
|
8195
|
+
} else if (exportWarning) {
|
|
8196
|
+
setStatus(openedStudio
|
|
8197
|
+
? "Opened exported PDF in a Studio preview tab with warning: " + exportWarning
|
|
8198
|
+
: "Exported PDF, but could not open a Studio preview tab. Warning: " + exportWarning,
|
|
8199
|
+
"warning");
|
|
8200
|
+
} else {
|
|
8201
|
+
setStatus(openedStudio
|
|
8202
|
+
? "Opened exported PDF in a Studio preview tab: " + (exportPath || downloadName)
|
|
8203
|
+
: "Exported PDF, but could not open a Studio preview tab" + (targetUrl ? ": " + targetUrl : "."),
|
|
8204
|
+
openedStudio ? "success" : "warning");
|
|
8205
|
+
}
|
|
8206
|
+
return;
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8054
8209
|
if (openedExternal) {
|
|
8055
8210
|
if (writeError) {
|
|
8056
8211
|
setStatus("Opened PDF in default viewer, but could not write project file: " + writeError, "warning");
|
|
@@ -8112,6 +8267,7 @@
|
|
|
8112
8267
|
setStatus("Exported PDF: " + downloadName, "success");
|
|
8113
8268
|
}
|
|
8114
8269
|
} catch (error) {
|
|
8270
|
+
closeExportStudioWindow(studioPopup);
|
|
8115
8271
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
8116
8272
|
setStatus("PDF export failed: " + detail, "error");
|
|
8117
8273
|
} finally {
|
|
@@ -8120,14 +8276,58 @@
|
|
|
8120
8276
|
}
|
|
8121
8277
|
}
|
|
8122
8278
|
|
|
8123
|
-
|
|
8279
|
+
function openExportStudioPlaceholderWindow(formatLabel) {
|
|
8280
|
+
const label = String(formatLabel || "preview").trim() || "preview";
|
|
8281
|
+
let popup = null;
|
|
8282
|
+
try {
|
|
8283
|
+
popup = window.open("", "_blank");
|
|
8284
|
+
if (popup && popup.document && popup.document.body) {
|
|
8285
|
+
popup.document.title = "Opening " + label + " in Studio…";
|
|
8286
|
+
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>";
|
|
8287
|
+
}
|
|
8288
|
+
} catch {
|
|
8289
|
+
popup = null;
|
|
8290
|
+
}
|
|
8291
|
+
return popup;
|
|
8292
|
+
}
|
|
8293
|
+
|
|
8294
|
+
function navigateExportStudioWindow(popup, targetUrl) {
|
|
8295
|
+
if (!targetUrl) return false;
|
|
8296
|
+
if (popup && !popup.closed) {
|
|
8297
|
+
try {
|
|
8298
|
+
popup.opener = null;
|
|
8299
|
+
popup.location.href = targetUrl;
|
|
8300
|
+
return true;
|
|
8301
|
+
} catch {}
|
|
8302
|
+
}
|
|
8303
|
+
try {
|
|
8304
|
+
return Boolean(window.open(targetUrl, "_blank", "noopener"));
|
|
8305
|
+
} catch {
|
|
8306
|
+
return false;
|
|
8307
|
+
}
|
|
8308
|
+
}
|
|
8309
|
+
|
|
8310
|
+
function closeExportStudioWindow(popup) {
|
|
8311
|
+
if (!popup || popup.closed) return;
|
|
8312
|
+
try { popup.close(); } catch {}
|
|
8313
|
+
}
|
|
8314
|
+
|
|
8315
|
+
async function exportRightPaneHtml(options) {
|
|
8316
|
+
const exportOptions = options && typeof options === "object" ? options : {};
|
|
8317
|
+
const openTarget = exportOptions.openTarget === "studio" ? "studio" : "browser";
|
|
8318
|
+
let studioPopup = null;
|
|
8319
|
+
if (openTarget === "studio") {
|
|
8320
|
+
studioPopup = openExportStudioPlaceholderWindow("HTML");
|
|
8321
|
+
}
|
|
8124
8322
|
if (uiBusy || previewExportInProgress) {
|
|
8323
|
+
closeExportStudioWindow(studioPopup);
|
|
8125
8324
|
setStatus("Studio is busy.", "warning");
|
|
8126
8325
|
return;
|
|
8127
8326
|
}
|
|
8128
8327
|
|
|
8129
8328
|
const token = getToken();
|
|
8130
8329
|
if (!token) {
|
|
8330
|
+
closeExportStudioWindow(studioPopup);
|
|
8131
8331
|
setStatus("Missing Studio token in URL. Re-run /studio.", "error");
|
|
8132
8332
|
return;
|
|
8133
8333
|
}
|
|
@@ -8135,11 +8335,13 @@
|
|
|
8135
8335
|
const exportingReplJournal = rightView === "repl";
|
|
8136
8336
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
8137
8337
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
8338
|
+
closeExportStudioWindow(studioPopup);
|
|
8138
8339
|
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
|
|
8139
8340
|
return;
|
|
8140
8341
|
}
|
|
8141
8342
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
8142
8343
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
8344
|
+
closeExportStudioWindow(studioPopup);
|
|
8143
8345
|
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
8144
8346
|
return;
|
|
8145
8347
|
}
|
|
@@ -8149,6 +8351,7 @@
|
|
|
8149
8351
|
? prepareEditorTextForHtmlExport(sourceTextEl.value)
|
|
8150
8352
|
: prepareEditorTextForPreview(latestResponseMarkdown)));
|
|
8151
8353
|
if (!markdown || !markdown.trim()) {
|
|
8354
|
+
closeExportStudioWindow(studioPopup);
|
|
8152
8355
|
setStatus("Nothing to export yet.", "warning");
|
|
8153
8356
|
return;
|
|
8154
8357
|
}
|
|
@@ -8173,7 +8376,7 @@
|
|
|
8173
8376
|
|
|
8174
8377
|
previewExportInProgress = true;
|
|
8175
8378
|
updateResultActionButtons();
|
|
8176
|
-
setStatus("Exporting HTML…", "warning");
|
|
8379
|
+
setStatus(openTarget === "studio" ? "Exporting HTML for Studio…" : "Exporting HTML…", "warning");
|
|
8177
8380
|
|
|
8178
8381
|
try {
|
|
8179
8382
|
const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
|
|
@@ -8189,6 +8392,7 @@
|
|
|
8189
8392
|
editorHtmlLanguage: editorHtmlLanguage,
|
|
8190
8393
|
filenameHint: filenameHint,
|
|
8191
8394
|
title: titleHint,
|
|
8395
|
+
openTarget: openTarget,
|
|
8192
8396
|
}),
|
|
8193
8397
|
}, HTML_EXPORT_FETCH_TIMEOUT_MS, "HTML export");
|
|
8194
8398
|
|
|
@@ -8227,6 +8431,31 @@
|
|
|
8227
8431
|
downloadName += ".html";
|
|
8228
8432
|
}
|
|
8229
8433
|
|
|
8434
|
+
if (openTarget === "studio") {
|
|
8435
|
+
const targetUrl = typeof payload.relativeUrl === "string" && payload.relativeUrl
|
|
8436
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
8437
|
+
: (typeof payload.url === "string" ? payload.url : "");
|
|
8438
|
+
const openedStudio = navigateExportStudioWindow(studioPopup, targetUrl);
|
|
8439
|
+
if (!openedStudio) closeExportStudioWindow(studioPopup);
|
|
8440
|
+
if (writeError) {
|
|
8441
|
+
setStatus(openedStudio
|
|
8442
|
+
? "Opened exported HTML in Studio as an unsaved copy; could not write project file: " + writeError
|
|
8443
|
+
: "Exported HTML for Studio, but the popup was blocked and the project file could not be written: " + writeError,
|
|
8444
|
+
"warning");
|
|
8445
|
+
} else if (exportWarning) {
|
|
8446
|
+
setStatus(openedStudio
|
|
8447
|
+
? "Opened exported HTML in Studio with warning: " + exportWarning
|
|
8448
|
+
: "Exported HTML for Studio, but the popup was blocked. Warning: " + exportWarning,
|
|
8449
|
+
"warning");
|
|
8450
|
+
} else {
|
|
8451
|
+
setStatus(openedStudio
|
|
8452
|
+
? "Opened exported HTML in Studio: " + (exportPath || downloadName)
|
|
8453
|
+
: (targetUrl ? "Exported HTML for Studio: " + targetUrl : "Exported HTML, but Studio did not receive an editor URL."),
|
|
8454
|
+
openedStudio ? "success" : "warning");
|
|
8455
|
+
}
|
|
8456
|
+
return;
|
|
8457
|
+
}
|
|
8458
|
+
|
|
8230
8459
|
if (openedExternal) {
|
|
8231
8460
|
if (writeError) {
|
|
8232
8461
|
setStatus("Opened HTML in default browser, but could not write project file: " + writeError, "warning");
|
|
@@ -8262,6 +8491,7 @@
|
|
|
8262
8491
|
return;
|
|
8263
8492
|
}
|
|
8264
8493
|
|
|
8494
|
+
closeExportStudioWindow(studioPopup);
|
|
8265
8495
|
const exportWarning = String(response.headers.get("x-pi-studio-export-warning") || "").trim();
|
|
8266
8496
|
const blob = await response.blob();
|
|
8267
8497
|
const headerFilename = parseContentDispositionFilename(response.headers.get("content-disposition"));
|
|
@@ -8288,6 +8518,7 @@
|
|
|
8288
8518
|
setStatus("Exported HTML: " + downloadName, "success");
|
|
8289
8519
|
}
|
|
8290
8520
|
} catch (error) {
|
|
8521
|
+
closeExportStudioWindow(studioPopup);
|
|
8291
8522
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
8292
8523
|
setStatus("HTML export failed: " + detail, "error");
|
|
8293
8524
|
} finally {
|
|
@@ -8318,10 +8549,16 @@
|
|
|
8318
8549
|
|
|
8319
8550
|
function exportRightPaneFormat(format) {
|
|
8320
8551
|
closeExportPreviewMenu();
|
|
8321
|
-
if (format === "html") {
|
|
8322
|
-
return exportRightPaneHtml();
|
|
8552
|
+
if (format === "html-studio") {
|
|
8553
|
+
return exportRightPaneHtml({ openTarget: "studio" });
|
|
8554
|
+
}
|
|
8555
|
+
if (format === "html" || format === "html-browser") {
|
|
8556
|
+
return exportRightPaneHtml({ openTarget: "browser" });
|
|
8323
8557
|
}
|
|
8324
|
-
|
|
8558
|
+
if (format === "pdf-studio") {
|
|
8559
|
+
return exportRightPanePdf({ openTarget: "studio" });
|
|
8560
|
+
}
|
|
8561
|
+
return exportRightPanePdf({ openTarget: "default" });
|
|
8325
8562
|
}
|
|
8326
8563
|
|
|
8327
8564
|
function normalizeCopyableBlockText(text) {
|
|
@@ -9611,28 +9848,40 @@
|
|
|
9611
9848
|
} else if (isHtmlArtifactPreview) {
|
|
9612
9849
|
exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
|
|
9613
9850
|
} else if (exportingReplJournal) {
|
|
9614
|
-
exportPdfBtn.title = "Choose PDF or HTML
|
|
9851
|
+
exportPdfBtn.title = "Choose PDF export or an HTML export destination for the Studio REPL record.";
|
|
9615
9852
|
} else {
|
|
9616
|
-
exportPdfBtn.title = "Choose PDF or HTML
|
|
9853
|
+
exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
|
|
9617
9854
|
}
|
|
9618
9855
|
}
|
|
9856
|
+
if (exportPreviewPdfStudioBtn) {
|
|
9857
|
+
exportPreviewPdfStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
9858
|
+
exportPreviewPdfStudioBtn.title = isHtmlArtifactPreview
|
|
9859
|
+
? "Interactive HTML preview PDF export is not available yet."
|
|
9860
|
+
: (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.");
|
|
9861
|
+
}
|
|
9619
9862
|
if (exportPreviewPdfBtn) {
|
|
9620
9863
|
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
9621
9864
|
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
9622
9865
|
? "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.");
|
|
9866
|
+
: (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.");
|
|
9867
|
+
}
|
|
9868
|
+
if (exportPreviewHtmlStudioBtn) {
|
|
9869
|
+
exportPreviewHtmlStudioBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
9870
|
+
exportPreviewHtmlStudioBtn.title = isHtmlArtifactPreview
|
|
9871
|
+
? "Export the authored HTML preview and open it in a new Studio editor tab."
|
|
9872
|
+
: (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
9873
|
}
|
|
9625
9874
|
if (exportPreviewHtmlBtn) {
|
|
9626
9875
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
9627
9876
|
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.");
|
|
9877
|
+
? "Export the authored HTML preview and open it in the default browser."
|
|
9878
|
+
: (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
9879
|
}
|
|
9631
9880
|
if (exportPreviewControlsEl) {
|
|
9632
9881
|
exportPreviewControlsEl.title = canExportPreview
|
|
9633
9882
|
? (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."))
|
|
9883
|
+
? "Choose a format and export destination for the Studio REPL record."
|
|
9884
|
+
: (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview."))
|
|
9636
9885
|
: (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
|
|
9637
9886
|
}
|
|
9638
9887
|
if (!canExportPreview || previewExportInProgress) {
|
|
@@ -9755,6 +10004,14 @@
|
|
|
9755
10004
|
|
|
9756
10005
|
fileInput.disabled = uiBusy;
|
|
9757
10006
|
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
10007
|
+
if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy;
|
|
10008
|
+
if (sourceOpenCurrentFileTabBtn) {
|
|
10009
|
+
sourceOpenCurrentFileTabBtn.disabled = uiBusy || !hasRefreshableFilePath();
|
|
10010
|
+
sourceOpenCurrentFileTabBtn.title = hasRefreshableFilePath()
|
|
10011
|
+
? "Open this file-backed document in a new refreshable editor-only Studio tab."
|
|
10012
|
+
: "Available after opening a file-backed document.";
|
|
10013
|
+
}
|
|
10014
|
+
if (sourceOpenCurrentTextCopyTabBtn) sourceOpenCurrentTextCopyTabBtn.disabled = uiBusy || wsState !== "Ready" || !String(sourceTextEl.value || "").trim();
|
|
9758
10015
|
saveAsBtn.disabled = uiBusy;
|
|
9759
10016
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
9760
10017
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
@@ -9877,6 +10134,7 @@
|
|
|
9877
10134
|
}
|
|
9878
10135
|
|
|
9879
10136
|
function shouldRestorePersistedWorkspaceState(state) {
|
|
10137
|
+
if (skipInitialWorkspaceRestore) return false;
|
|
9880
10138
|
if (!state || typeof state.text !== "string") return false;
|
|
9881
10139
|
const storedSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
9882
10140
|
const initialIdentity = getWorkspaceStateIdentity(initialSourceState);
|
|
@@ -10798,6 +11056,7 @@
|
|
|
10798
11056
|
else params.delete("docPath");
|
|
10799
11057
|
if (nextDraftId) params.set("draftId", nextDraftId);
|
|
10800
11058
|
else params.delete("draftId");
|
|
11059
|
+
params.delete("skipWorkspaceRestore");
|
|
10801
11060
|
window.history.replaceState(null, "", currentUrl.toString());
|
|
10802
11061
|
} catch {
|
|
10803
11062
|
// Ignore URL-state update failures.
|
|
@@ -11013,10 +11272,10 @@
|
|
|
11013
11272
|
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
11014
11273
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
11015
11274
|
} else if (kind === "text") {
|
|
11016
|
-
appendPreviewLinkMenuButton(menu, "Open
|
|
11275
|
+
appendPreviewLinkMenuButton(menu, "Open file tab", "open-new");
|
|
11017
11276
|
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
11018
11277
|
} else if (kind === "office") {
|
|
11019
|
-
appendPreviewLinkMenuButton(menu, "Convert
|
|
11278
|
+
appendPreviewLinkMenuButton(menu, "Convert tab", "open-new");
|
|
11020
11279
|
appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
|
|
11021
11280
|
} else if (kind === "image") {
|
|
11022
11281
|
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
@@ -11153,10 +11412,11 @@
|
|
|
11153
11412
|
return;
|
|
11154
11413
|
}
|
|
11155
11414
|
const popup = pendingWindow || window.open("", "_blank");
|
|
11415
|
+
const openingLabel = getPreviewLocalLinkKind(href) === "office" ? "Opening converted document…" : "Opening file tab…";
|
|
11156
11416
|
try {
|
|
11157
11417
|
if (popup && popup.document && popup.document.body) {
|
|
11158
|
-
popup.document.title =
|
|
11159
|
-
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">
|
|
11418
|
+
popup.document.title = openingLabel;
|
|
11419
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">" + escapeHtml(openingLabel) + "</p>";
|
|
11160
11420
|
}
|
|
11161
11421
|
} catch {}
|
|
11162
11422
|
try {
|
|
@@ -11169,12 +11429,12 @@
|
|
|
11169
11429
|
try {
|
|
11170
11430
|
popup.opener = null;
|
|
11171
11431
|
popup.location.href = targetUrl;
|
|
11172
|
-
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening
|
|
11432
|
+
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.", "success");
|
|
11173
11433
|
return;
|
|
11174
11434
|
} catch {}
|
|
11175
11435
|
}
|
|
11176
11436
|
window.open(targetUrl, "_blank", "noopener");
|
|
11177
|
-
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening
|
|
11437
|
+
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.", "success");
|
|
11178
11438
|
} catch (error) {
|
|
11179
11439
|
if (popup && !popup.closed) {
|
|
11180
11440
|
try { popup.close(); } catch {}
|
|
@@ -12823,6 +13083,110 @@
|
|
|
12823
13083
|
return describeStudioDocument(sourceState);
|
|
12824
13084
|
}
|
|
12825
13085
|
|
|
13086
|
+
function formatScratchpadRecentTime(timestamp) {
|
|
13087
|
+
const value = Number(timestamp) || 0;
|
|
13088
|
+
if (!value) return "unknown time";
|
|
13089
|
+
try {
|
|
13090
|
+
return new Date(value).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
13091
|
+
} catch {
|
|
13092
|
+
return "unknown time";
|
|
13093
|
+
}
|
|
13094
|
+
}
|
|
13095
|
+
|
|
13096
|
+
function renderScratchpadRecentPanel() {
|
|
13097
|
+
if (!scratchpadRecentPanelEl) return;
|
|
13098
|
+
scratchpadRecentPanelEl.hidden = !scratchpadRecentVisible;
|
|
13099
|
+
if (!scratchpadRecentVisible) return;
|
|
13100
|
+
if (scratchpadRecentLoading) {
|
|
13101
|
+
scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-loading'>Loading recent scratchpads…</div>";
|
|
13102
|
+
return;
|
|
13103
|
+
}
|
|
13104
|
+
const currentKey = getCurrentStudioDocumentDescriptor().key;
|
|
13105
|
+
const entries = Array.isArray(scratchpadRecentEntries) ? scratchpadRecentEntries : [];
|
|
13106
|
+
if (!entries.length) {
|
|
13107
|
+
scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-empty'>No other saved scratchpads yet.</div>";
|
|
13108
|
+
return;
|
|
13109
|
+
}
|
|
13110
|
+
scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-list'>" + entries.map((entry) => {
|
|
13111
|
+
const key = String(entry && entry.documentKey ? entry.documentKey : "");
|
|
13112
|
+
const isCurrent = key === currentKey;
|
|
13113
|
+
const label = String(entry && entry.label ? entry.label : key || "scratchpad");
|
|
13114
|
+
const kind = String(entry && entry.kind ? entry.kind : "Scratchpad");
|
|
13115
|
+
const textLength = Math.max(0, Number(entry && entry.textLength) || 0);
|
|
13116
|
+
const preview = String(entry && entry.textPreview ? entry.textPreview : "");
|
|
13117
|
+
const meta = (isCurrent ? "Current · " : "") + kind + " · " + String(textLength) + " chars · " + formatScratchpadRecentTime(entry && entry.updatedAt);
|
|
13118
|
+
return "<div class='scratchpad-recent-item' data-scratchpad-key='" + escapeHtml(key) + "'>"
|
|
13119
|
+
+ "<div class='scratchpad-recent-main'>"
|
|
13120
|
+
+ "<div class='scratchpad-recent-title' title='" + escapeHtml(label) + "'>" + escapeHtml(label) + "</div>"
|
|
13121
|
+
+ "<div class='scratchpad-recent-meta'>" + escapeHtml(meta) + "</div>"
|
|
13122
|
+
+ (preview ? "<div class='scratchpad-recent-preview'>" + escapeHtml(preview) + "</div>" : "")
|
|
13123
|
+
+ "</div>"
|
|
13124
|
+
+ "<div class='scratchpad-recent-actions'>"
|
|
13125
|
+
+ "<button type='button' data-scratchpad-recent-action='load' data-scratchpad-key='" + escapeHtml(key) + "'" + (isCurrent ? " disabled" : "") + ">Load</button>"
|
|
13126
|
+
+ "<button type='button' data-scratchpad-recent-action='append' data-scratchpad-key='" + escapeHtml(key) + "'" + (isCurrent ? " disabled" : "") + ">Append</button>"
|
|
13127
|
+
+ "<button type='button' data-scratchpad-recent-action='copy' data-scratchpad-key='" + escapeHtml(key) + "'>Copy</button>"
|
|
13128
|
+
+ "</div>"
|
|
13129
|
+
+ "</div>";
|
|
13130
|
+
}).join("") + "</div>";
|
|
13131
|
+
}
|
|
13132
|
+
|
|
13133
|
+
async function loadScratchpadRecentEntries() {
|
|
13134
|
+
scratchpadRecentLoading = true;
|
|
13135
|
+
renderScratchpadRecentPanel();
|
|
13136
|
+
try {
|
|
13137
|
+
const payload = await fetchStudioJson("/scratchpad-state", { query: { action: "recent", limit: "20" } });
|
|
13138
|
+
scratchpadRecentEntries = Array.isArray(payload && payload.scratchpads) ? payload.scratchpads : [];
|
|
13139
|
+
} catch (error) {
|
|
13140
|
+
scratchpadRecentEntries = [];
|
|
13141
|
+
setStatus("Could not load recent scratchpads: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
13142
|
+
} finally {
|
|
13143
|
+
scratchpadRecentLoading = false;
|
|
13144
|
+
renderScratchpadRecentPanel();
|
|
13145
|
+
}
|
|
13146
|
+
}
|
|
13147
|
+
|
|
13148
|
+
function toggleScratchpadRecentPanel() {
|
|
13149
|
+
scratchpadRecentVisible = !scratchpadRecentVisible;
|
|
13150
|
+
if (scratchpadRecentVisible) {
|
|
13151
|
+
void loadScratchpadRecentEntries();
|
|
13152
|
+
} else {
|
|
13153
|
+
renderScratchpadRecentPanel();
|
|
13154
|
+
}
|
|
13155
|
+
updateScratchpadUi();
|
|
13156
|
+
}
|
|
13157
|
+
|
|
13158
|
+
async function applyScratchpadRecentAction(action, documentKey) {
|
|
13159
|
+
const key = String(documentKey || "").trim();
|
|
13160
|
+
if (!key) return;
|
|
13161
|
+
const mode = action === "append" ? "append" : (action === "copy" ? "copy" : "load");
|
|
13162
|
+
try {
|
|
13163
|
+
const text = await fetchScratchpadTextForDocumentKey(key);
|
|
13164
|
+
if (!String(text || "").trim()) {
|
|
13165
|
+
setStatus("That scratchpad is empty.", "warning");
|
|
13166
|
+
return;
|
|
13167
|
+
}
|
|
13168
|
+
if (mode === "copy") {
|
|
13169
|
+
const ok = await writeTextToClipboard(text);
|
|
13170
|
+
setStatus(ok ? "Copied recent scratchpad." : "Could not copy recent scratchpad.", ok ? "success" : "warning");
|
|
13171
|
+
return;
|
|
13172
|
+
}
|
|
13173
|
+
if (mode === "append") {
|
|
13174
|
+
const separator = scratchpadText && !scratchpadText.endsWith("\n") ? "\n\n" : (scratchpadText ? "\n" : "");
|
|
13175
|
+
setScratchpadText(String(scratchpadText || "") + separator + String(text || ""));
|
|
13176
|
+
setStatus("Appended recent scratchpad.", "success");
|
|
13177
|
+
return;
|
|
13178
|
+
}
|
|
13179
|
+
if (String(scratchpadText || "").trim() && String(scratchpadText || "") !== String(text || "")) {
|
|
13180
|
+
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.");
|
|
13181
|
+
if (!confirmed) return;
|
|
13182
|
+
}
|
|
13183
|
+
setScratchpadText(text);
|
|
13184
|
+
setStatus("Loaded recent scratchpad into current scratchpad.", "success");
|
|
13185
|
+
} catch (error) {
|
|
13186
|
+
setStatus("Could not use recent scratchpad: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
13187
|
+
}
|
|
13188
|
+
}
|
|
13189
|
+
|
|
12826
13190
|
async function fetchScratchpadTextForDocumentKey(documentKey) {
|
|
12827
13191
|
const payload = await fetchStudioJson("/scratchpad-state", {
|
|
12828
13192
|
query: { documentKey: documentKey },
|
|
@@ -12830,9 +13194,9 @@
|
|
|
12830
13194
|
return payload && typeof payload.text === "string" ? payload.text : "";
|
|
12831
13195
|
}
|
|
12832
13196
|
|
|
12833
|
-
function flushScratchpadPersistence(documentKeyOverride, textOverride) {
|
|
13197
|
+
function flushScratchpadPersistence(documentKeyOverride, textOverride, labelOverride) {
|
|
12834
13198
|
const descriptor = documentKeyOverride
|
|
12835
|
-
? { key: String(documentKeyOverride || "").trim() }
|
|
13199
|
+
? { key: String(documentKeyOverride || "").trim(), label: String(labelOverride || "").trim() }
|
|
12836
13200
|
: getCurrentStudioDocumentDescriptor();
|
|
12837
13201
|
const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
|
|
12838
13202
|
if (!key) return;
|
|
@@ -12841,27 +13205,29 @@
|
|
|
12841
13205
|
scratchpadPersistTimer = null;
|
|
12842
13206
|
}
|
|
12843
13207
|
const snapshot = String(arguments.length >= 2 ? textOverride : scratchpadText || "");
|
|
12844
|
-
|
|
13208
|
+
const label = String(descriptor && descriptor.label ? descriptor.label : "").trim();
|
|
13209
|
+
if (trySendStudioJsonBeacon("/scratchpad-state", { documentKey: key, text: snapshot, label })) {
|
|
12845
13210
|
return;
|
|
12846
13211
|
}
|
|
12847
13212
|
void fetchStudioJson("/scratchpad-state", {
|
|
12848
13213
|
method: "POST",
|
|
12849
|
-
body: JSON.stringify({ documentKey: key, text: snapshot }),
|
|
13214
|
+
body: JSON.stringify({ documentKey: key, text: snapshot, label }),
|
|
12850
13215
|
}).catch(() => {
|
|
12851
13216
|
// Ignore scratchpad persistence failures for now.
|
|
12852
13217
|
});
|
|
12853
13218
|
}
|
|
12854
13219
|
|
|
12855
|
-
function scheduleScratchpadPersistence(text, documentKey) {
|
|
13220
|
+
function scheduleScratchpadPersistence(text, documentKey, label) {
|
|
12856
13221
|
if (scratchpadPersistTimer !== null) {
|
|
12857
13222
|
window.clearTimeout(scratchpadPersistTimer);
|
|
12858
13223
|
}
|
|
12859
13224
|
const snapshot = String(text || "");
|
|
12860
13225
|
const key = String(documentKey || "").trim();
|
|
13226
|
+
const labelSnapshot = String(label || "").trim();
|
|
12861
13227
|
if (!key) return;
|
|
12862
13228
|
scratchpadPersistTimer = window.setTimeout(() => {
|
|
12863
13229
|
scratchpadPersistTimer = null;
|
|
12864
|
-
flushScratchpadPersistence(key, snapshot);
|
|
13230
|
+
flushScratchpadPersistence(key, snapshot, labelSnapshot);
|
|
12865
13231
|
}, 180);
|
|
12866
13232
|
}
|
|
12867
13233
|
|
|
@@ -12893,7 +13259,7 @@
|
|
|
12893
13259
|
if (String(existing || "").trim()) return;
|
|
12894
13260
|
await fetchStudioJson("/scratchpad-state", {
|
|
12895
13261
|
method: "POST",
|
|
12896
|
-
body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot }),
|
|
13262
|
+
body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot, label: nextDescriptor.label }),
|
|
12897
13263
|
});
|
|
12898
13264
|
} catch {
|
|
12899
13265
|
// Ignore carry-over failures and just fall back to normal scope loading.
|
|
@@ -12914,7 +13280,7 @@
|
|
|
12914
13280
|
|
|
12915
13281
|
function persistScratchpadText(value) {
|
|
12916
13282
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
12917
|
-
scheduleScratchpadPersistence(value, descriptor.key);
|
|
13283
|
+
scheduleScratchpadPersistence(value, descriptor.key, descriptor.label);
|
|
12918
13284
|
}
|
|
12919
13285
|
|
|
12920
13286
|
function normalizeReviewNoteAnchorKind(value) {
|
|
@@ -17111,6 +17477,10 @@
|
|
|
17111
17477
|
? ("Saved locally for this document/draft · " + normalized.length + " chars")
|
|
17112
17478
|
: "Empty · local to this document/draft";
|
|
17113
17479
|
}
|
|
17480
|
+
if (scratchpadRecentBtn) {
|
|
17481
|
+
scratchpadRecentBtn.textContent = scratchpadRecentVisible ? "Hide recent" : "Recent…";
|
|
17482
|
+
scratchpadRecentBtn.setAttribute("aria-expanded", scratchpadRecentVisible ? "true" : "false");
|
|
17483
|
+
}
|
|
17114
17484
|
if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
|
|
17115
17485
|
if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
|
|
17116
17486
|
if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
|
|
@@ -18239,11 +18609,11 @@
|
|
|
18239
18609
|
const opened = navigatePendingCompanionWindow(responseRequestId, targetUrl);
|
|
18240
18610
|
const readyMessage = typeof message.message === "string" && message.message.trim()
|
|
18241
18611
|
? message.message.trim()
|
|
18242
|
-
: "Opened
|
|
18612
|
+
: "Opened editor tab with a detached copy of the current editor text.";
|
|
18243
18613
|
setStatus(
|
|
18244
18614
|
opened
|
|
18245
18615
|
? readyMessage
|
|
18246
|
-
: (targetUrl ? "
|
|
18616
|
+
: (targetUrl ? "Editor tab ready: " + targetUrl : "Editor tab is ready, but Studio did not receive a URL."),
|
|
18247
18617
|
opened ? "success" : "warning",
|
|
18248
18618
|
);
|
|
18249
18619
|
return;
|
|
@@ -18563,8 +18933,8 @@
|
|
|
18563
18933
|
try {
|
|
18564
18934
|
companionWindow = window.open("", "_blank");
|
|
18565
18935
|
if (companionWindow && companionWindow.document && companionWindow.document.body) {
|
|
18566
|
-
companionWindow.document.title = "Opening
|
|
18567
|
-
companionWindow.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening
|
|
18936
|
+
companionWindow.document.title = "Opening editor tab…";
|
|
18937
|
+
companionWindow.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening editor tab…</p>";
|
|
18568
18938
|
}
|
|
18569
18939
|
} catch {
|
|
18570
18940
|
companionWindow = null;
|
|
@@ -18620,6 +18990,28 @@
|
|
|
18620
18990
|
}
|
|
18621
18991
|
}
|
|
18622
18992
|
|
|
18993
|
+
function requestOpenEditorOnlyDocument(content, options) {
|
|
18994
|
+
const requestId = beginUiAction("open_editor_only");
|
|
18995
|
+
if (!requestId) return false;
|
|
18996
|
+
openPendingCompanionWindow(requestId);
|
|
18997
|
+
const config = options && typeof options === "object" ? options : {};
|
|
18998
|
+
const sent = sendMessage({
|
|
18999
|
+
type: "open_editor_only_request",
|
|
19000
|
+
requestId,
|
|
19001
|
+
content: String(content || ""),
|
|
19002
|
+
label: config.label || "current editor",
|
|
19003
|
+
path: config.path || undefined,
|
|
19004
|
+
resourceDir: config.resourceDir || undefined,
|
|
19005
|
+
});
|
|
19006
|
+
if (!sent) {
|
|
19007
|
+
closePendingCompanionWindow(requestId);
|
|
19008
|
+
pendingRequestId = null;
|
|
19009
|
+
pendingKind = null;
|
|
19010
|
+
setBusy(false);
|
|
19011
|
+
}
|
|
19012
|
+
return sent;
|
|
19013
|
+
}
|
|
19014
|
+
|
|
18623
19015
|
function describeSourceForAnnotation() {
|
|
18624
19016
|
if (sourceState.source === "file" && sourceState.label) {
|
|
18625
19017
|
return "file " + sourceState.label;
|
|
@@ -19100,7 +19492,7 @@
|
|
|
19100
19492
|
event.stopPropagation();
|
|
19101
19493
|
if (actionBtn.disabled) return;
|
|
19102
19494
|
const format = String(actionBtn.getAttribute("data-export-preview-format") || "pdf").toLowerCase();
|
|
19103
|
-
void exportRightPaneFormat(format
|
|
19495
|
+
void exportRightPaneFormat(format);
|
|
19104
19496
|
});
|
|
19105
19497
|
}
|
|
19106
19498
|
|
|
@@ -19235,27 +19627,10 @@
|
|
|
19235
19627
|
|
|
19236
19628
|
if (openCompanionBtn) {
|
|
19237
19629
|
openCompanionBtn.addEventListener("click", () => {
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
const requestId = beginUiAction("open_editor_only");
|
|
19241
|
-
if (!requestId) return;
|
|
19242
|
-
openPendingCompanionWindow(requestId);
|
|
19243
|
-
|
|
19244
|
-
const sent = sendMessage({
|
|
19245
|
-
type: "open_editor_only_request",
|
|
19246
|
-
requestId,
|
|
19247
|
-
content,
|
|
19248
|
-
label: sourceState && sourceState.label ? sourceState.label : "current editor",
|
|
19249
|
-
path: sourceState && sourceState.path ? sourceState.path : undefined,
|
|
19630
|
+
requestOpenEditorOnlyDocument("", {
|
|
19631
|
+
label: "blank",
|
|
19250
19632
|
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
19251
19633
|
});
|
|
19252
|
-
|
|
19253
|
-
if (!sent) {
|
|
19254
|
-
closePendingCompanionWindow(requestId);
|
|
19255
|
-
pendingRequestId = null;
|
|
19256
|
-
pendingKind = null;
|
|
19257
|
-
setBusy(false);
|
|
19258
|
-
}
|
|
19259
19634
|
});
|
|
19260
19635
|
}
|
|
19261
19636
|
|
|
@@ -19651,6 +20026,24 @@
|
|
|
19651
20026
|
});
|
|
19652
20027
|
}
|
|
19653
20028
|
|
|
20029
|
+
if (scratchpadRecentBtn) {
|
|
20030
|
+
scratchpadRecentBtn.addEventListener("click", () => {
|
|
20031
|
+
toggleScratchpadRecentPanel();
|
|
20032
|
+
});
|
|
20033
|
+
}
|
|
20034
|
+
|
|
20035
|
+
if (scratchpadRecentPanelEl) {
|
|
20036
|
+
scratchpadRecentPanelEl.addEventListener("click", (event) => {
|
|
20037
|
+
const target = event.target;
|
|
20038
|
+
const actionEl = target instanceof Element ? target.closest("[data-scratchpad-recent-action]") : null;
|
|
20039
|
+
if (!actionEl) return;
|
|
20040
|
+
event.preventDefault();
|
|
20041
|
+
const action = String(actionEl.getAttribute("data-scratchpad-recent-action") || "load");
|
|
20042
|
+
const key = String(actionEl.getAttribute("data-scratchpad-key") || "");
|
|
20043
|
+
void applyScratchpadRecentAction(action, key);
|
|
20044
|
+
});
|
|
20045
|
+
}
|
|
20046
|
+
|
|
19654
20047
|
if (scratchpadInsertBtn) {
|
|
19655
20048
|
scratchpadInsertBtn.addEventListener("click", () => {
|
|
19656
20049
|
insertScratchpadIntoEditor();
|
|
@@ -19757,7 +20150,7 @@
|
|
|
19757
20150
|
}
|
|
19758
20151
|
if (sourceBadgeEl) {
|
|
19759
20152
|
sourceBadgeEl.addEventListener("click", () => {
|
|
19760
|
-
resetEditorOrigin();
|
|
20153
|
+
if (!studioUiRefreshEnabled) resetEditorOrigin();
|
|
19761
20154
|
});
|
|
19762
20155
|
}
|
|
19763
20156
|
if (resourceDirBtn) {
|