pi-studio 0.9.12 → 0.9.13
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 +12 -0
- package/README.md +2 -0
- package/client/studio-client.js +788 -49
- package/client/studio.css +47 -3
- package/index.ts +356 -12
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
const saveAsBtn = document.getElementById("saveAsBtn");
|
|
104
104
|
const saveOverBtn = document.getElementById("saveOverBtn");
|
|
105
105
|
const refreshFromDiskBtn = document.getElementById("refreshFromDiskBtn");
|
|
106
|
+
const clearWorkspaceBtn = document.getElementById("clearWorkspaceBtn");
|
|
106
107
|
const sendEditorBtn = document.getElementById("sendEditorBtn");
|
|
107
108
|
const openCompanionBtn = document.getElementById("openCompanionBtn");
|
|
108
109
|
const getEditorBtn = document.getElementById("getEditorBtn");
|
|
@@ -1880,6 +1881,8 @@
|
|
|
1880
1881
|
const PANE_SPLIT_MIN_PERCENT = 20;
|
|
1881
1882
|
const PANE_SPLIT_MAX_PERCENT = 80;
|
|
1882
1883
|
const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
|
|
1884
|
+
const STUDIO_WORKSPACE_STORAGE_KEY = "piStudio.workspaceState.v1";
|
|
1885
|
+
const STUDIO_WORKSPACE_MAX_TEXT_CHARS = 900_000;
|
|
1883
1886
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
|
|
1884
1887
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
1885
1888
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
@@ -1974,6 +1977,9 @@
|
|
|
1974
1977
|
let pendingReviewNoteInlineFocusId = null;
|
|
1975
1978
|
let activePreviewCommentSelection = null;
|
|
1976
1979
|
let suppressEditorSelectionComment = false;
|
|
1980
|
+
let workspacePersistenceReady = false;
|
|
1981
|
+
let workspacePersistTimer = null;
|
|
1982
|
+
let workspaceRestoredFromBrowser = false;
|
|
1977
1983
|
let suppressedEditorSelectionStart = null;
|
|
1978
1984
|
let suppressedEditorSelectionEnd = null;
|
|
1979
1985
|
const previewJumpHighlightState = new WeakMap();
|
|
@@ -2180,12 +2186,8 @@
|
|
|
2180
2186
|
if (!studioUiRefreshUi) return;
|
|
2181
2187
|
if (studioUiRefreshUi.annotationsButton) {
|
|
2182
2188
|
const inlineLabel = annotationsEnabled ? "Inline on" : "Inline hidden";
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
} else {
|
|
2186
|
-
const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
|
|
2187
|
-
setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
|
|
2188
|
-
}
|
|
2189
|
+
const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
|
|
2190
|
+
setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
|
|
2189
2191
|
}
|
|
2190
2192
|
if (studioUiRefreshUi.viewButton) {
|
|
2191
2193
|
const syntaxLabel = editorHighlightEnabled
|
|
@@ -2349,7 +2351,7 @@
|
|
|
2349
2351
|
const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
|
|
2350
2352
|
const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
|
|
2351
2353
|
const annotationsMenu = makeStudioUiRefreshMenu(annotationsButton, "annotations", "studio-refresh-annotations-anchor");
|
|
2352
|
-
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display",
|
|
2354
|
+
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display", [annotationModeSelect, insertHeaderBtn]);
|
|
2353
2355
|
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Actions", [stripAnnotationsBtn, saveAnnotatedBtn]);
|
|
2354
2356
|
const viewButton = makeStudioUiRefreshElement("button", "", "View");
|
|
2355
2357
|
const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
|
|
@@ -3071,14 +3073,16 @@
|
|
|
3071
3073
|
// Show "Set working dir" button when not file-backed
|
|
3072
3074
|
var isFileBacked = hasRefreshableFilePath();
|
|
3073
3075
|
if (isFileBacked) {
|
|
3074
|
-
|
|
3076
|
+
var fileBackedResourceDir = getCurrentResourceDirValue() || dirnameForDisplayPath(sourceState.path);
|
|
3077
|
+
if (resourceDirInput) resourceDirInput.value = fileBackedResourceDir;
|
|
3075
3078
|
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
3076
3079
|
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
3077
3080
|
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
3078
3081
|
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
3079
3082
|
} else {
|
|
3080
3083
|
// Restore to label if dir is set, otherwise show button
|
|
3081
|
-
var dir =
|
|
3084
|
+
var dir = getCurrentResourceDirValue();
|
|
3085
|
+
if (resourceDirInput) resourceDirInput.value = dir;
|
|
3082
3086
|
if (dir) {
|
|
3083
3087
|
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
3084
3088
|
if (resourceDirLabel) { resourceDirLabel.textContent = "Working dir: " + dir; resourceDirLabel.hidden = false; }
|
|
@@ -3144,7 +3148,6 @@
|
|
|
3144
3148
|
}
|
|
3145
3149
|
|
|
3146
3150
|
function loadPaneSplitPercent() {
|
|
3147
|
-
if (isEditorOnlyMode) return;
|
|
3148
3151
|
let stored = "";
|
|
3149
3152
|
try {
|
|
3150
3153
|
stored = window.localStorage ? String(window.localStorage.getItem(PANE_SPLIT_STORAGE_KEY) || "") : "";
|
|
@@ -3166,7 +3169,7 @@
|
|
|
3166
3169
|
}
|
|
3167
3170
|
|
|
3168
3171
|
function setupPaneResizeHandle() {
|
|
3169
|
-
if (!paneResizeHandleEl
|
|
3172
|
+
if (!paneResizeHandleEl) return;
|
|
3170
3173
|
loadPaneSplitPercent();
|
|
3171
3174
|
let dragging = false;
|
|
3172
3175
|
let movedDuringDrag = false;
|
|
@@ -3347,10 +3350,6 @@
|
|
|
3347
3350
|
|
|
3348
3351
|
function activatePaneFromShortcut(nextPane) {
|
|
3349
3352
|
const pane = nextPane === "right" ? "right" : "left";
|
|
3350
|
-
if (isEditorOnlyMode && pane === "right") {
|
|
3351
|
-
setStatus("Only the editor pane is available in editor-only Studio.", "warning");
|
|
3352
|
-
return;
|
|
3353
|
-
}
|
|
3354
3353
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3355
3354
|
setActivePane(pane);
|
|
3356
3355
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
@@ -3393,10 +3392,6 @@
|
|
|
3393
3392
|
}
|
|
3394
3393
|
|
|
3395
3394
|
function focusRightContentFromShortcut() {
|
|
3396
|
-
if (isEditorOnlyMode) {
|
|
3397
|
-
setStatus("Only the editor pane is available in editor-only Studio.", "warning");
|
|
3398
|
-
return;
|
|
3399
|
-
}
|
|
3400
3395
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3401
3396
|
setActivePane("right");
|
|
3402
3397
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
@@ -4316,6 +4311,36 @@
|
|
|
4316
4311
|
+ " if (node && node.nodeType === 3) node = node.parentElement;\n"
|
|
4317
4312
|
+ " return node && typeof node.closest === 'function' ? node.closest('a[href]') : null;\n"
|
|
4318
4313
|
+ " }\n"
|
|
4314
|
+
+ " function isLocalHtmlPreviewLinkHref(value) {\n"
|
|
4315
|
+
+ " const raw = String(value || '').trim();\n"
|
|
4316
|
+
+ " if (!raw || raw.charAt(0) === '#') return false;\n"
|
|
4317
|
+
+ " if (/^\\/\\//.test(raw)) return false;\n"
|
|
4318
|
+
+ " if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;\n"
|
|
4319
|
+
+ " return true;\n"
|
|
4320
|
+
+ " }\n"
|
|
4321
|
+
+ " function postHtmlPreviewLocalLink(action, anchor, event) {\n"
|
|
4322
|
+
+ " if (!anchor || typeof anchor.getAttribute !== 'function') return false;\n"
|
|
4323
|
+
+ " if (anchor.hasAttribute('download')) return false;\n"
|
|
4324
|
+
+ " const target = String(anchor.getAttribute('target') || '').trim().toLowerCase();\n"
|
|
4325
|
+
+ " if (target && target !== '_self') return false;\n"
|
|
4326
|
+
+ " const href = String(anchor.getAttribute('href') || '').trim();\n"
|
|
4327
|
+
+ " if (!isLocalHtmlPreviewLinkHref(href)) return false;\n"
|
|
4328
|
+
+ " try { parent.postMessage({ type: 'pi-studio-html-artifact-local-link', id: PREVIEW_ID, action, href, title: String(anchor.textContent || href).trim(), clientX: event && event.clientX || 0, clientY: event && event.clientY || 0 }, '*'); } catch {}\n"
|
|
4329
|
+
+ " return true;\n"
|
|
4330
|
+
+ " }\n"
|
|
4331
|
+
+ " function handleHtmlPreviewLocalLinkClick(event) {\n"
|
|
4332
|
+
+ " if (!event || event.defaultPrevented) return;\n"
|
|
4333
|
+
+ " if (typeof event.button === 'number' && event.button !== 0) return;\n"
|
|
4334
|
+
+ " const anchor = getAnchorFromClickTarget(event.target);\n"
|
|
4335
|
+
+ " if (!postHtmlPreviewLocalLink('open', anchor, event)) return;\n"
|
|
4336
|
+
+ " event.preventDefault();\n"
|
|
4337
|
+
+ " }\n"
|
|
4338
|
+
+ " function handleHtmlPreviewLocalLinkContextMenu(event) {\n"
|
|
4339
|
+
+ " if (!event || event.defaultPrevented) return;\n"
|
|
4340
|
+
+ " const anchor = getAnchorFromClickTarget(event.target);\n"
|
|
4341
|
+
+ " if (!postHtmlPreviewLocalLink('contextmenu', anchor, event)) return;\n"
|
|
4342
|
+
+ " event.preventDefault();\n"
|
|
4343
|
+
+ " }\n"
|
|
4319
4344
|
+ " function getSameDocumentFragment(anchor) {\n"
|
|
4320
4345
|
+ " if (!anchor || typeof anchor.getAttribute !== 'function') return null;\n"
|
|
4321
4346
|
+ " if (anchor.hasAttribute('download')) return null;\n"
|
|
@@ -4573,6 +4598,8 @@
|
|
|
4573
4598
|
+ " }\n"
|
|
4574
4599
|
+ " });\n"
|
|
4575
4600
|
+ " document.addEventListener('click', handleFragmentAnchorClick);\n"
|
|
4601
|
+
+ " document.addEventListener('click', handleHtmlPreviewLocalLinkClick);\n"
|
|
4602
|
+
+ " document.addEventListener('contextmenu', handleHtmlPreviewLocalLinkContextMenu);\n"
|
|
4576
4603
|
+ " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
4577
4604
|
+ " window.addEventListener('hashchange', () => {\n"
|
|
4578
4605
|
+ " const hash = String(window.location && window.location.hash || '');\n"
|
|
@@ -4840,7 +4867,8 @@
|
|
|
4840
4867
|
const params = new URLSearchParams({ token, path: String(resourceUrl || "") });
|
|
4841
4868
|
if (record && record.sourcePath) {
|
|
4842
4869
|
params.set("sourcePath", record.sourcePath);
|
|
4843
|
-
}
|
|
4870
|
+
}
|
|
4871
|
+
if (record && record.resourceDir) {
|
|
4844
4872
|
params.set("resourceDir", record.resourceDir);
|
|
4845
4873
|
}
|
|
4846
4874
|
return "/html-preview-resource?" + params.toString();
|
|
@@ -4905,10 +4933,71 @@
|
|
|
4905
4933
|
void resolveHtmlArtifactResources(record, items);
|
|
4906
4934
|
}
|
|
4907
4935
|
|
|
4936
|
+
function getHtmlArtifactLocalLinkContext(record, data) {
|
|
4937
|
+
return {
|
|
4938
|
+
href: typeof data.href === "string" ? data.href : "",
|
|
4939
|
+
title: typeof data.title === "string" && data.title.trim() ? data.title.trim() : (typeof data.href === "string" ? data.href : "local link"),
|
|
4940
|
+
sourcePath: record && record.sourcePath ? String(record.sourcePath) : "",
|
|
4941
|
+
resourceDir: record && record.resourceDir ? String(record.resourceDir) : "",
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
function getHtmlArtifactLocalLinkClientPoint(record, data) {
|
|
4946
|
+
const iframe = record && record.iframe;
|
|
4947
|
+
const rect = iframe && typeof iframe.getBoundingClientRect === "function"
|
|
4948
|
+
? iframe.getBoundingClientRect()
|
|
4949
|
+
: { left: 0, top: 0 };
|
|
4950
|
+
return {
|
|
4951
|
+
clientX: rect.left + (Number(data.clientX) || 0),
|
|
4952
|
+
clientY: rect.top + (Number(data.clientY) || 0),
|
|
4953
|
+
};
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
function handleHtmlArtifactFrameLocalLinkMessage(event) {
|
|
4957
|
+
const data = event && event.data;
|
|
4958
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-local-link") return;
|
|
4959
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
4960
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
4961
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
4962
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
4963
|
+
return;
|
|
4964
|
+
}
|
|
4965
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
4966
|
+
const context = getHtmlArtifactLocalLinkContext(record, data);
|
|
4967
|
+
if (!isStudioLocalPreviewHref(context.href)) return;
|
|
4968
|
+
const action = typeof data.action === "string" ? data.action : "open";
|
|
4969
|
+
if (action === "contextmenu") {
|
|
4970
|
+
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
4971
|
+
showPreviewLinkMenu(null, point, context);
|
|
4972
|
+
return;
|
|
4973
|
+
}
|
|
4974
|
+
const kind = getPreviewLocalLinkKind(context.href);
|
|
4975
|
+
if (kind === "pdf") {
|
|
4976
|
+
openPreviewPdfLink(context.href, context.title, context);
|
|
4977
|
+
return;
|
|
4978
|
+
}
|
|
4979
|
+
if (kind === "image") {
|
|
4980
|
+
const pendingWindow = window.open("", "_blank");
|
|
4981
|
+
void openPreviewImageLink(context.href, context.title, context, pendingWindow).catch((error) => {
|
|
4982
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
4983
|
+
});
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
if (kind === "text") {
|
|
4987
|
+
const pendingWindow = window.open("", "_blank");
|
|
4988
|
+
void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
|
|
4989
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
4990
|
+
});
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
setStatus("Right-click this local HTML preview link for file actions.", "warning");
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4908
4996
|
window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
|
|
4909
4997
|
window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
|
|
4910
4998
|
window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
|
|
4911
4999
|
window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
|
|
5000
|
+
window.addEventListener("message", handleHtmlArtifactFrameLocalLinkMessage);
|
|
4912
5001
|
|
|
4913
5002
|
function isStudioHtmlFocusOpen() {
|
|
4914
5003
|
return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
|
|
@@ -5461,13 +5550,16 @@
|
|
|
5461
5550
|
if (!token) return "";
|
|
5462
5551
|
const pdfPath = String(options && options.path ? options.path : "").trim();
|
|
5463
5552
|
if (!pdfPath) return "";
|
|
5553
|
+
const explicitSourcePath = options && typeof options.sourcePath === "string" ? options.sourcePath.trim() : "";
|
|
5554
|
+
const explicitResourceDir = options && typeof options.resourceDir === "string" ? normalizeStudioResourceDirValue(options.resourceDir) : "";
|
|
5464
5555
|
const effectivePath = getEffectiveSavePath();
|
|
5465
|
-
const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
|
|
5466
|
-
const resourceDir =
|
|
5556
|
+
const sourcePath = explicitSourcePath || (useEditorResourceContext ? (effectivePath || sourceState.path || "") : "");
|
|
5557
|
+
const resourceDir = explicitResourceDir || getCurrentResourceDirValue();
|
|
5467
5558
|
const params = new URLSearchParams({ token, path: pdfPath });
|
|
5468
5559
|
if (sourcePath) {
|
|
5469
5560
|
params.set("sourcePath", sourcePath);
|
|
5470
|
-
}
|
|
5561
|
+
}
|
|
5562
|
+
if (resourceDir) {
|
|
5471
5563
|
params.set("resourceDir", resourceDir);
|
|
5472
5564
|
}
|
|
5473
5565
|
return "/pdf-resource?" + params.toString();
|
|
@@ -6661,7 +6753,7 @@
|
|
|
6661
6753
|
const payload = {
|
|
6662
6754
|
markdown: String(markdown || ""),
|
|
6663
6755
|
sourcePath: sourcePath,
|
|
6664
|
-
resourceDir: (!sourcePath && resourceDirInput) ?
|
|
6756
|
+
resourceDir: (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "",
|
|
6665
6757
|
};
|
|
6666
6758
|
if (previewOptions.includeEditorLanguage) {
|
|
6667
6759
|
payload.editorLanguage = String(editorLanguage || "");
|
|
@@ -6789,7 +6881,7 @@
|
|
|
6789
6881
|
|
|
6790
6882
|
const effectivePath = getEffectiveSavePath();
|
|
6791
6883
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
6792
|
-
const resourceDir = (!sourcePath && resourceDirInput) ?
|
|
6884
|
+
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
6793
6885
|
const isEditorPreview = rightView === "editor-preview";
|
|
6794
6886
|
const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
|
|
6795
6887
|
const isLatex = isEditorPreview
|
|
@@ -6955,7 +7047,7 @@
|
|
|
6955
7047
|
|
|
6956
7048
|
const effectivePath = getEffectiveSavePath();
|
|
6957
7049
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
6958
|
-
const resourceDir = (!sourcePath && resourceDirInput) ?
|
|
7050
|
+
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
6959
7051
|
const isEditorPreview = rightView === "editor-preview";
|
|
6960
7052
|
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
|
|
6961
7053
|
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
@@ -7344,7 +7436,7 @@
|
|
|
7344
7436
|
decorateCopyablePreviewBlocks(targetEl);
|
|
7345
7437
|
|
|
7346
7438
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
7347
|
-
if (!sourceState.path && !(
|
|
7439
|
+
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
7348
7440
|
var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
|
|
7349
7441
|
var hasLatexImages = /\\includegraphics/.test(markdown || "");
|
|
7350
7442
|
if (hasRelativeImages || hasLatexImages) {
|
|
@@ -8131,23 +8223,54 @@
|
|
|
8131
8223
|
updateResultActionButtons();
|
|
8132
8224
|
}
|
|
8133
8225
|
|
|
8226
|
+
function normalizeStudioResourceDirValue(value) {
|
|
8227
|
+
let text = String(value || "").trim();
|
|
8228
|
+
if (text.length >= 2) {
|
|
8229
|
+
const first = text.charAt(0);
|
|
8230
|
+
const last = text.charAt(text.length - 1);
|
|
8231
|
+
if ((first === "\"" && last === "\"") || (first === "'" && last === "'")) {
|
|
8232
|
+
text = text.slice(1, -1).trim();
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
if (/^file:\/\//i.test(text)) {
|
|
8236
|
+
try {
|
|
8237
|
+
text = decodeURIComponent(new URL(text).pathname || text).trim();
|
|
8238
|
+
} catch {}
|
|
8239
|
+
}
|
|
8240
|
+
const markers = ["/Users/", "/home/", "/Volumes/", "/private/", "/tmp/", "/var/", "/opt/", "/Applications/"];
|
|
8241
|
+
let embeddedAbsoluteIndex = -1;
|
|
8242
|
+
for (const marker of markers) {
|
|
8243
|
+
const index = text.lastIndexOf(marker);
|
|
8244
|
+
if (index > 0) embeddedAbsoluteIndex = Math.max(embeddedAbsoluteIndex, index);
|
|
8245
|
+
}
|
|
8246
|
+
const windowsMatch = text.match(/.*([A-Za-z]:[\\/].*)$/);
|
|
8247
|
+
if (windowsMatch && windowsMatch[1]) return windowsMatch[1].trim();
|
|
8248
|
+
if (embeddedAbsoluteIndex > 0) text = text.slice(embeddedAbsoluteIndex).trim();
|
|
8249
|
+
return text;
|
|
8250
|
+
}
|
|
8251
|
+
|
|
8252
|
+
function getCurrentResourceDirValue() {
|
|
8253
|
+
return resourceDirInput ? normalizeStudioResourceDirValue(resourceDirInput.value) : "";
|
|
8254
|
+
}
|
|
8255
|
+
|
|
8134
8256
|
function getEffectiveSavePath() {
|
|
8135
8257
|
// File-backed: use the original path
|
|
8136
8258
|
if (sourceState.path) return sourceState.path;
|
|
8137
8259
|
// Upload with working dir + filename: derive path
|
|
8138
|
-
|
|
8260
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
8261
|
+
if (sourceState.source === "upload" && sourceState.label && resourceDir) {
|
|
8139
8262
|
var name = sourceState.label.replace(/^upload:\s*/i, "");
|
|
8140
|
-
if (name) return
|
|
8263
|
+
if (name) return resourceDir.replace(/\/$/, "") + "/" + name;
|
|
8141
8264
|
}
|
|
8142
8265
|
return null;
|
|
8143
8266
|
}
|
|
8144
8267
|
|
|
8145
8268
|
function getHtmlPreviewResourceContextOptions() {
|
|
8146
8269
|
const sourcePath = getEffectiveSavePath() || sourceState.path || "";
|
|
8147
|
-
const resourceDir =
|
|
8270
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
8148
8271
|
return {
|
|
8149
8272
|
sourcePath,
|
|
8150
|
-
resourceDir
|
|
8273
|
+
resourceDir,
|
|
8151
8274
|
};
|
|
8152
8275
|
}
|
|
8153
8276
|
|
|
@@ -8163,8 +8286,8 @@
|
|
|
8163
8286
|
|
|
8164
8287
|
const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
|
|
8165
8288
|
const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
|
|
8166
|
-
const suggestedDir =
|
|
8167
|
-
?
|
|
8289
|
+
const suggestedDir = getCurrentResourceDirValue()
|
|
8290
|
+
? getCurrentResourceDirValue().replace(/\/$/, "") + "/"
|
|
8168
8291
|
: "./";
|
|
8169
8292
|
return suggestedDir + stem + ".annotated.md";
|
|
8170
8293
|
}
|
|
@@ -8201,6 +8324,7 @@
|
|
|
8201
8324
|
saveAsBtn.disabled = uiBusy;
|
|
8202
8325
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
8203
8326
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
8327
|
+
if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy;
|
|
8204
8328
|
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
8205
8329
|
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
8206
8330
|
if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
|
|
@@ -8217,7 +8341,7 @@
|
|
|
8217
8341
|
rightViewSelect.disabled = isEditorOnlyMode;
|
|
8218
8342
|
followSelect.disabled = isEditorOnlyMode || uiBusy;
|
|
8219
8343
|
if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
|
|
8220
|
-
insertHeaderBtn.disabled = uiBusy
|
|
8344
|
+
insertHeaderBtn.disabled = uiBusy;
|
|
8221
8345
|
lensSelect.disabled = uiBusy || isEditorOnlyMode;
|
|
8222
8346
|
updateSaveFileTooltip();
|
|
8223
8347
|
updateRefreshFromDiskTooltip();
|
|
@@ -8259,6 +8383,197 @@
|
|
|
8259
8383
|
previousDescriptor: previousDescriptor,
|
|
8260
8384
|
carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
|
|
8261
8385
|
});
|
|
8386
|
+
scheduleWorkspacePersistence();
|
|
8387
|
+
}
|
|
8388
|
+
|
|
8389
|
+
function normalizeWorkspaceSourceState(value) {
|
|
8390
|
+
const raw = value && typeof value === "object" ? value : {};
|
|
8391
|
+
const path = typeof raw.path === "string" && raw.path.trim() ? raw.path.trim() : null;
|
|
8392
|
+
return {
|
|
8393
|
+
source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "blank",
|
|
8394
|
+
label: typeof raw.label === "string" && raw.label.trim() ? raw.label.trim() : "blank",
|
|
8395
|
+
path,
|
|
8396
|
+
draftId: path ? null : (typeof raw.draftId === "string" && raw.draftId.trim() ? raw.draftId.trim() : null),
|
|
8397
|
+
};
|
|
8398
|
+
}
|
|
8399
|
+
|
|
8400
|
+
function getWorkspaceStateIdentity(state) {
|
|
8401
|
+
const normalized = normalizeWorkspaceSourceState(state);
|
|
8402
|
+
if (normalized.path) return "file:" + normalized.path;
|
|
8403
|
+
if (normalized.draftId) return "draft:" + normalized.draftId;
|
|
8404
|
+
return "source:" + normalized.source + ":" + normalized.label;
|
|
8405
|
+
}
|
|
8406
|
+
|
|
8407
|
+
function readPersistedWorkspaceState() {
|
|
8408
|
+
try {
|
|
8409
|
+
const raw = window.localStorage ? window.localStorage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
|
|
8410
|
+
if (!raw) return null;
|
|
8411
|
+
const parsed = JSON.parse(raw);
|
|
8412
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return null;
|
|
8413
|
+
if (typeof parsed.text !== "string") return null;
|
|
8414
|
+
return parsed;
|
|
8415
|
+
} catch {
|
|
8416
|
+
return null;
|
|
8417
|
+
}
|
|
8418
|
+
}
|
|
8419
|
+
|
|
8420
|
+
function shouldRestorePersistedWorkspaceState(state) {
|
|
8421
|
+
if (!state || typeof state.text !== "string") return false;
|
|
8422
|
+
const storedSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
8423
|
+
const initialIdentity = getWorkspaceStateIdentity(initialSourceState);
|
|
8424
|
+
const storedIdentity = getWorkspaceStateIdentity(storedSourceState);
|
|
8425
|
+
if (storedIdentity === initialIdentity) return true;
|
|
8426
|
+
if (!explicitDocumentIdentityFromUrl && initialSourceState.source === "blank" && !initialSourceState.path) return true;
|
|
8427
|
+
return false;
|
|
8428
|
+
}
|
|
8429
|
+
|
|
8430
|
+
function buildWorkspacePersistencePayload() {
|
|
8431
|
+
return {
|
|
8432
|
+
version: 1,
|
|
8433
|
+
savedAt: Date.now(),
|
|
8434
|
+
sourceState: normalizeWorkspaceSourceState(sourceState),
|
|
8435
|
+
resourceDir: getCurrentResourceDirValue(),
|
|
8436
|
+
editorView,
|
|
8437
|
+
rightView,
|
|
8438
|
+
editorLanguage,
|
|
8439
|
+
followLatest,
|
|
8440
|
+
responseHistoryIndex,
|
|
8441
|
+
selectionStart: typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0,
|
|
8442
|
+
selectionEnd: typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : 0,
|
|
8443
|
+
scrollTop: typeof sourceTextEl.scrollTop === "number" ? sourceTextEl.scrollTop : 0,
|
|
8444
|
+
text: String(sourceTextEl.value || ""),
|
|
8445
|
+
};
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
function persistWorkspaceStateNow() {
|
|
8449
|
+
if (!workspacePersistenceReady) return;
|
|
8450
|
+
try {
|
|
8451
|
+
if (!window.localStorage) return;
|
|
8452
|
+
const payload = buildWorkspacePersistencePayload();
|
|
8453
|
+
if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
|
|
8454
|
+
window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8455
|
+
return;
|
|
8456
|
+
}
|
|
8457
|
+
window.localStorage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
|
|
8458
|
+
} catch {
|
|
8459
|
+
// Ignore browser storage failures and quota limits.
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
|
|
8463
|
+
function scheduleWorkspacePersistence() {
|
|
8464
|
+
if (!workspacePersistenceReady) return;
|
|
8465
|
+
if (workspacePersistTimer !== null) window.clearTimeout(workspacePersistTimer);
|
|
8466
|
+
workspacePersistTimer = window.setTimeout(() => {
|
|
8467
|
+
workspacePersistTimer = null;
|
|
8468
|
+
persistWorkspaceStateNow();
|
|
8469
|
+
}, 160);
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8472
|
+
function flushWorkspacePersistence() {
|
|
8473
|
+
if (workspacePersistTimer !== null) {
|
|
8474
|
+
window.clearTimeout(workspacePersistTimer);
|
|
8475
|
+
workspacePersistTimer = null;
|
|
8476
|
+
}
|
|
8477
|
+
persistWorkspaceStateNow();
|
|
8478
|
+
}
|
|
8479
|
+
|
|
8480
|
+
function clearPersistedWorkspaceState() {
|
|
8481
|
+
if (workspacePersistTimer !== null) {
|
|
8482
|
+
window.clearTimeout(workspacePersistTimer);
|
|
8483
|
+
workspacePersistTimer = null;
|
|
8484
|
+
}
|
|
8485
|
+
try {
|
|
8486
|
+
if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8487
|
+
} catch {}
|
|
8488
|
+
}
|
|
8489
|
+
|
|
8490
|
+
function applyPersistedWorkspaceState(state) {
|
|
8491
|
+
if (!shouldRestorePersistedWorkspaceState(state)) return false;
|
|
8492
|
+
const nextSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
8493
|
+
const nextResourceDir = normalizeStudioResourceDirValue(typeof state.resourceDir === "string" ? state.resourceDir : "");
|
|
8494
|
+
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
8495
|
+
setEditorText(state.text, { preserveScroll: false, preserveSelection: false });
|
|
8496
|
+
setSourceState(nextSourceState);
|
|
8497
|
+
if (resourceDirInput && nextResourceDir) {
|
|
8498
|
+
resourceDirInput.value = nextResourceDir;
|
|
8499
|
+
updateSourceBadge();
|
|
8500
|
+
}
|
|
8501
|
+
if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
|
|
8502
|
+
setEditorLanguage(state.editorLanguage.trim());
|
|
8503
|
+
}
|
|
8504
|
+
editorView = state.editorView === "preview" ? "preview" : "markdown";
|
|
8505
|
+
rightView = state.rightView === "preview"
|
|
8506
|
+
? "preview"
|
|
8507
|
+
: (state.rightView === "editor-preview"
|
|
8508
|
+
? "editor-preview"
|
|
8509
|
+
: (state.rightView === "repl" ? "repl" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")));
|
|
8510
|
+
if (typeof state.followLatest === "boolean") {
|
|
8511
|
+
followLatest = state.followLatest;
|
|
8512
|
+
}
|
|
8513
|
+
if (followSelect) followSelect.value = followLatest ? "on" : "off";
|
|
8514
|
+
if (typeof state.responseHistoryIndex === "number" && Number.isFinite(state.responseHistoryIndex)) {
|
|
8515
|
+
responseHistoryIndex = Math.max(-1, Math.floor(state.responseHistoryIndex));
|
|
8516
|
+
}
|
|
8517
|
+
const maxIndex = String(sourceTextEl.value || "").length;
|
|
8518
|
+
const start = Math.max(0, Math.min(Math.floor(Number(state.selectionStart) || 0), maxIndex));
|
|
8519
|
+
const end = Math.max(start, Math.min(Math.floor(Number(state.selectionEnd) || start), maxIndex));
|
|
8520
|
+
try { sourceTextEl.setSelectionRange(start, end); } catch {}
|
|
8521
|
+
if (typeof state.scrollTop === "number" && Number.isFinite(state.scrollTop)) {
|
|
8522
|
+
sourceTextEl.scrollTop = Math.max(0, state.scrollTop);
|
|
8523
|
+
}
|
|
8524
|
+
workspaceRestoredFromBrowser = true;
|
|
8525
|
+
initialDocumentApplied = true;
|
|
8526
|
+
return true;
|
|
8527
|
+
}
|
|
8528
|
+
|
|
8529
|
+
function clearStudioWorkspace() {
|
|
8530
|
+
if (uiBusy) {
|
|
8531
|
+
setStatus("Studio is busy.", "warning");
|
|
8532
|
+
return;
|
|
8533
|
+
}
|
|
8534
|
+
const confirmed = window.confirm("Clear the current editor draft in this browser tab? Saved files and responses are not changed.");
|
|
8535
|
+
if (!confirmed) return;
|
|
8536
|
+
const preservedResponseState = {
|
|
8537
|
+
responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
|
|
8538
|
+
responseHistoryIndex,
|
|
8539
|
+
queuedLatestResponse,
|
|
8540
|
+
followLatest,
|
|
8541
|
+
latestResponseMarkdown,
|
|
8542
|
+
latestResponseThinking,
|
|
8543
|
+
latestResponseTimestamp,
|
|
8544
|
+
latestResponseKind,
|
|
8545
|
+
latestResponseIsStructuredCritique,
|
|
8546
|
+
latestResponseHasContent,
|
|
8547
|
+
latestResponseNormalized,
|
|
8548
|
+
latestResponseThinkingNormalized,
|
|
8549
|
+
latestCritiqueNotes,
|
|
8550
|
+
latestCritiqueNotesNormalized,
|
|
8551
|
+
};
|
|
8552
|
+
clearPersistedWorkspaceState();
|
|
8553
|
+
if (resourceDirInput) resourceDirInput.value = "";
|
|
8554
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
8555
|
+
setEditorText("", { preserveScroll: false, preserveSelection: false });
|
|
8556
|
+
setSourceState({ source: "blank", label: "blank", path: null, draftId: makeStudioDraftId() });
|
|
8557
|
+
setEditorLanguage("markdown");
|
|
8558
|
+
setEditorView("markdown");
|
|
8559
|
+
responseHistory = preservedResponseState.responseHistory;
|
|
8560
|
+
responseHistoryIndex = preservedResponseState.responseHistoryIndex;
|
|
8561
|
+
queuedLatestResponse = preservedResponseState.queuedLatestResponse;
|
|
8562
|
+
followLatest = preservedResponseState.followLatest;
|
|
8563
|
+
latestResponseMarkdown = preservedResponseState.latestResponseMarkdown;
|
|
8564
|
+
latestResponseThinking = preservedResponseState.latestResponseThinking;
|
|
8565
|
+
latestResponseTimestamp = preservedResponseState.latestResponseTimestamp;
|
|
8566
|
+
latestResponseKind = preservedResponseState.latestResponseKind;
|
|
8567
|
+
latestResponseIsStructuredCritique = preservedResponseState.latestResponseIsStructuredCritique;
|
|
8568
|
+
latestResponseHasContent = preservedResponseState.latestResponseHasContent;
|
|
8569
|
+
latestResponseNormalized = preservedResponseState.latestResponseNormalized;
|
|
8570
|
+
latestResponseThinkingNormalized = preservedResponseState.latestResponseThinkingNormalized;
|
|
8571
|
+
latestCritiqueNotes = preservedResponseState.latestCritiqueNotes;
|
|
8572
|
+
latestCritiqueNotesNormalized = preservedResponseState.latestCritiqueNotesNormalized;
|
|
8573
|
+
if (followSelect) followSelect.value = followLatest ? "on" : "off";
|
|
8574
|
+
refreshResponseUi();
|
|
8575
|
+
persistWorkspaceStateNow();
|
|
8576
|
+
setStatus("Editor cleared. Saved files and responses were not changed.", "success");
|
|
8262
8577
|
}
|
|
8263
8578
|
|
|
8264
8579
|
function setEditorText(nextText, options) {
|
|
@@ -8308,6 +8623,7 @@
|
|
|
8308
8623
|
}
|
|
8309
8624
|
updateEditorSelectionCommentUi();
|
|
8310
8625
|
updateOutlineUi();
|
|
8626
|
+
scheduleWorkspacePersistence();
|
|
8311
8627
|
}
|
|
8312
8628
|
|
|
8313
8629
|
function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
|
|
@@ -8445,6 +8761,7 @@
|
|
|
8445
8761
|
updateReviewNotesUi();
|
|
8446
8762
|
updateEditorSelectionCommentUi();
|
|
8447
8763
|
updateOutlineUi();
|
|
8764
|
+
scheduleWorkspacePersistence();
|
|
8448
8765
|
}
|
|
8449
8766
|
|
|
8450
8767
|
function setRightView(nextView) {
|
|
@@ -8477,6 +8794,7 @@
|
|
|
8477
8794
|
|
|
8478
8795
|
refreshResponseUi();
|
|
8479
8796
|
syncActionButtons();
|
|
8797
|
+
scheduleWorkspacePersistence();
|
|
8480
8798
|
}
|
|
8481
8799
|
|
|
8482
8800
|
function lineNumbersShouldBeVisible() {
|
|
@@ -8785,6 +9103,383 @@
|
|
|
8785
9103
|
}
|
|
8786
9104
|
}
|
|
8787
9105
|
|
|
9106
|
+
const PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS = new Set([
|
|
9107
|
+
".md", ".markdown", ".mdx", ".qmd", ".txt", ".tex", ".latex", ".rst", ".adoc",
|
|
9108
|
+
".html", ".htm", ".css", ".xml", ".yaml", ".yml", ".toml", ".json", ".jsonc", ".json5", ".csv", ".tsv", ".log",
|
|
9109
|
+
".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx",
|
|
9110
|
+
".py", ".pyw", ".sh", ".bash", ".zsh", ".rs", ".c", ".h", ".cpp", ".cxx", ".cc", ".hpp", ".hxx",
|
|
9111
|
+
".jl", ".f90", ".f95", ".f03", ".f", ".for", ".r", ".m", ".java", ".go", ".rb", ".swift", ".lua",
|
|
9112
|
+
".diff", ".patch",
|
|
9113
|
+
]);
|
|
9114
|
+
const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
9115
|
+
let previewLinkMenuEl = null;
|
|
9116
|
+
let activePreviewLinkContext = null;
|
|
9117
|
+
|
|
9118
|
+
function stripPreviewLocalLinkUrlSuffix(href) {
|
|
9119
|
+
const raw = String(href || "").trim();
|
|
9120
|
+
const hashIndex = raw.indexOf("#");
|
|
9121
|
+
const queryIndex = raw.indexOf("?");
|
|
9122
|
+
let end = raw.length;
|
|
9123
|
+
if (queryIndex >= 0) end = Math.min(end, queryIndex);
|
|
9124
|
+
if (hashIndex >= 0) end = Math.min(end, hashIndex);
|
|
9125
|
+
return raw.slice(0, end);
|
|
9126
|
+
}
|
|
9127
|
+
|
|
9128
|
+
function parsePreviewLocalLinkPage(href) {
|
|
9129
|
+
const raw = String(href || "");
|
|
9130
|
+
const parts = [];
|
|
9131
|
+
const queryIndex = raw.indexOf("?");
|
|
9132
|
+
if (queryIndex >= 0) {
|
|
9133
|
+
const queryEnd = raw.indexOf("#", queryIndex);
|
|
9134
|
+
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
9135
|
+
}
|
|
9136
|
+
const hashIndex = raw.indexOf("#");
|
|
9137
|
+
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
9138
|
+
for (const part of parts) {
|
|
9139
|
+
try {
|
|
9140
|
+
const params = new URLSearchParams(part);
|
|
9141
|
+
const value = params.get("page") || params.get("p");
|
|
9142
|
+
if (value) {
|
|
9143
|
+
const page = Number.parseInt(value, 10);
|
|
9144
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
9145
|
+
}
|
|
9146
|
+
} catch {}
|
|
9147
|
+
const match = String(part || "").match(/(?:^|[&;])page=(\d+)/i) || String(part || "").match(/^page=(\d+)$/i);
|
|
9148
|
+
if (match && match[1]) {
|
|
9149
|
+
const page = Number.parseInt(match[1], 10);
|
|
9150
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
return 0;
|
|
9154
|
+
}
|
|
9155
|
+
|
|
9156
|
+
function getPreviewLocalLinkExtension(href) {
|
|
9157
|
+
const path = stripPreviewLocalLinkUrlSuffix(href);
|
|
9158
|
+
const match = path.match(/\.([A-Za-z0-9_+-]+)$/);
|
|
9159
|
+
return match ? ("." + match[1].toLowerCase()) : "";
|
|
9160
|
+
}
|
|
9161
|
+
|
|
9162
|
+
function getPreviewLocalLinkKind(href) {
|
|
9163
|
+
const ext = getPreviewLocalLinkExtension(href);
|
|
9164
|
+
if (ext === ".pdf") return "pdf";
|
|
9165
|
+
if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext)) return "text";
|
|
9166
|
+
if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
|
|
9167
|
+
return "other";
|
|
9168
|
+
}
|
|
9169
|
+
|
|
9170
|
+
function isStudioLocalPreviewHref(href) {
|
|
9171
|
+
const raw = String(href || "").trim();
|
|
9172
|
+
if (!raw || raw.charAt(0) === "#") return false;
|
|
9173
|
+
if (/^\/\//.test(raw)) return false;
|
|
9174
|
+
if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
|
|
9175
|
+
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|local-preview-link|reveal-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
9176
|
+
return true;
|
|
9177
|
+
}
|
|
9178
|
+
|
|
9179
|
+
function getEffectivePreviewLinkContext(contextOverride) {
|
|
9180
|
+
const fallback = getHtmlPreviewResourceContextOptions();
|
|
9181
|
+
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : null;
|
|
9182
|
+
return {
|
|
9183
|
+
sourcePath: context && context.sourcePath ? String(context.sourcePath) : (fallback.sourcePath || ""),
|
|
9184
|
+
resourceDir: context && context.resourceDir ? String(context.resourceDir) : (fallback.resourceDir || ""),
|
|
9185
|
+
};
|
|
9186
|
+
}
|
|
9187
|
+
|
|
9188
|
+
function getPreviewLinkResourceQuery(path, contextOverride) {
|
|
9189
|
+
const context = getEffectivePreviewLinkContext(contextOverride);
|
|
9190
|
+
const query = { path: String(path || "") };
|
|
9191
|
+
if (context.sourcePath) query.sourcePath = String(context.sourcePath);
|
|
9192
|
+
if (context.resourceDir) query.resourceDir = String(context.resourceDir);
|
|
9193
|
+
return query;
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9196
|
+
function getPreviewLinkAnchorFromEvent(event) {
|
|
9197
|
+
const target = event && event.target;
|
|
9198
|
+
const anchor = target instanceof Element ? target.closest("#sourcePreview a[href], #critiqueView a[href]") : null;
|
|
9199
|
+
if (!anchor) return null;
|
|
9200
|
+
if (anchor.closest(".studio-pdf-card, .studio-html-artifact-toolbar, .studio-copy-block-btn")) return null;
|
|
9201
|
+
const href = String(anchor.getAttribute("href") || "").trim();
|
|
9202
|
+
if (!isStudioLocalPreviewHref(href)) return null;
|
|
9203
|
+
return anchor;
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9206
|
+
function closePreviewLinkMenu() {
|
|
9207
|
+
activePreviewLinkContext = null;
|
|
9208
|
+
if (previewLinkMenuEl) previewLinkMenuEl.hidden = true;
|
|
9209
|
+
}
|
|
9210
|
+
|
|
9211
|
+
function ensurePreviewLinkMenu() {
|
|
9212
|
+
if (previewLinkMenuEl) return previewLinkMenuEl;
|
|
9213
|
+
const menu = document.createElement("div");
|
|
9214
|
+
menu.className = "studio-preview-link-menu";
|
|
9215
|
+
menu.hidden = true;
|
|
9216
|
+
menu.setAttribute("role", "menu");
|
|
9217
|
+
document.body.appendChild(menu);
|
|
9218
|
+
previewLinkMenuEl = menu;
|
|
9219
|
+
return menu;
|
|
9220
|
+
}
|
|
9221
|
+
|
|
9222
|
+
function appendPreviewLinkMenuButton(menu, label, action) {
|
|
9223
|
+
const button = document.createElement("button");
|
|
9224
|
+
button.type = "button";
|
|
9225
|
+
button.setAttribute("role", "menuitem");
|
|
9226
|
+
button.dataset.previewLinkAction = action;
|
|
9227
|
+
button.textContent = label;
|
|
9228
|
+
menu.appendChild(button);
|
|
9229
|
+
}
|
|
9230
|
+
|
|
9231
|
+
function positionPreviewLinkMenu(menu, clientX, clientY) {
|
|
9232
|
+
const margin = 8;
|
|
9233
|
+
menu.style.left = "0px";
|
|
9234
|
+
menu.style.top = "0px";
|
|
9235
|
+
menu.hidden = false;
|
|
9236
|
+
const rect = menu.getBoundingClientRect();
|
|
9237
|
+
const x = Math.max(margin, Math.min(window.innerWidth - rect.width - margin, Number(clientX) || margin));
|
|
9238
|
+
const y = Math.max(margin, Math.min(window.innerHeight - rect.height - margin, Number(clientY) || margin));
|
|
9239
|
+
menu.style.left = x + "px";
|
|
9240
|
+
menu.style.top = y + "px";
|
|
9241
|
+
}
|
|
9242
|
+
|
|
9243
|
+
function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
9244
|
+
const href = String(anchor && anchor.getAttribute ? anchor.getAttribute("href") || "" : (contextOverride && contextOverride.href ? contextOverride.href : "")).trim();
|
|
9245
|
+
if (!isStudioLocalPreviewHref(href)) return false;
|
|
9246
|
+
const kind = getPreviewLocalLinkKind(href);
|
|
9247
|
+
const menu = ensurePreviewLinkMenu();
|
|
9248
|
+
menu.innerHTML = "";
|
|
9249
|
+
const linkContext = getEffectivePreviewLinkContext(contextOverride);
|
|
9250
|
+
activePreviewLinkContext = {
|
|
9251
|
+
href,
|
|
9252
|
+
title: String((contextOverride && contextOverride.title) || (anchor && anchor.textContent) || href || "local link").trim() || href,
|
|
9253
|
+
sourcePath: linkContext.sourcePath,
|
|
9254
|
+
resourceDir: linkContext.resourceDir,
|
|
9255
|
+
};
|
|
9256
|
+
if (kind === "pdf") {
|
|
9257
|
+
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
9258
|
+
} else if (kind === "text") {
|
|
9259
|
+
appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
|
|
9260
|
+
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
9261
|
+
} else if (kind === "image") {
|
|
9262
|
+
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
9263
|
+
}
|
|
9264
|
+
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
9265
|
+
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
9266
|
+
positionPreviewLinkMenu(menu, event && event.clientX, event && event.clientY);
|
|
9267
|
+
const firstButton = menu.querySelector("button");
|
|
9268
|
+
if (firstButton && typeof firstButton.focus === "function") {
|
|
9269
|
+
window.setTimeout(() => firstButton.focus({ preventScroll: true }), 0);
|
|
9270
|
+
}
|
|
9271
|
+
return true;
|
|
9272
|
+
}
|
|
9273
|
+
|
|
9274
|
+
async function fetchPreviewLocalLink(action, href, contextOverride) {
|
|
9275
|
+
return fetchStudioJson("/local-preview-link", {
|
|
9276
|
+
query: { ...getPreviewLinkResourceQuery(href, contextOverride), action },
|
|
9277
|
+
});
|
|
9278
|
+
}
|
|
9279
|
+
|
|
9280
|
+
function getPreviewPdfViewerUrl(href, contextOverride) {
|
|
9281
|
+
const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
|
|
9282
|
+
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
|
|
9283
|
+
const resourceUrl = buildStudioPdfResourceUrl({ path: cleanPath, sourcePath: context.sourcePath || "", resourceDir: context.resourceDir || "" }, true);
|
|
9284
|
+
const page = parsePreviewLocalLinkPage(href);
|
|
9285
|
+
return resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
9286
|
+
}
|
|
9287
|
+
|
|
9288
|
+
function openPreviewPdfLink(href, title, contextOverride) {
|
|
9289
|
+
const viewerUrl = getPreviewPdfViewerUrl(href, contextOverride);
|
|
9290
|
+
if (!viewerUrl) {
|
|
9291
|
+
setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
|
|
9292
|
+
return false;
|
|
9293
|
+
}
|
|
9294
|
+
openStudioPdfFocusViewer(viewerUrl, title || href);
|
|
9295
|
+
return true;
|
|
9296
|
+
}
|
|
9297
|
+
|
|
9298
|
+
async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
|
|
9299
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
9300
|
+
try {
|
|
9301
|
+
if (popup && popup.document && popup.document.body) {
|
|
9302
|
+
popup.document.title = "Opening image…";
|
|
9303
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening image…</p>";
|
|
9304
|
+
}
|
|
9305
|
+
} catch {}
|
|
9306
|
+
try {
|
|
9307
|
+
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
9308
|
+
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
9309
|
+
});
|
|
9310
|
+
const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
|
|
9311
|
+
if (!dataUrl) throw new Error("Studio did not return image data.");
|
|
9312
|
+
const safeTitle = escapeHtml(String(title || href || "Local image"));
|
|
9313
|
+
const safeSrc = escapeHtml(dataUrl);
|
|
9314
|
+
const html = "<!doctype html><html><head><meta charset='utf-8'><title>" + safeTitle + "</title>"
|
|
9315
|
+
+ "<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee;font:13px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}img{max-width:100vw;max-height:100vh;object-fit:contain;}header{position:fixed;left:0;right:0;top:0;padding:8px 10px;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);}</style>"
|
|
9316
|
+
+ "</head><body><header>" + safeTitle + "</header><img src='" + safeSrc + "' alt='" + safeTitle + "'></body></html>";
|
|
9317
|
+
if (popup && !popup.closed && popup.document) {
|
|
9318
|
+
popup.document.open();
|
|
9319
|
+
popup.document.write(html);
|
|
9320
|
+
popup.document.close();
|
|
9321
|
+
setStatus("Opened local image preview.", "success");
|
|
9322
|
+
return;
|
|
9323
|
+
}
|
|
9324
|
+
const opened = window.open(dataUrl, "_blank");
|
|
9325
|
+
if (!opened) throw new Error("Popup blocked while opening image preview.");
|
|
9326
|
+
setStatus("Opened local image preview.", "success");
|
|
9327
|
+
} catch (error) {
|
|
9328
|
+
if (popup && !popup.closed) {
|
|
9329
|
+
try { popup.close(); } catch {}
|
|
9330
|
+
}
|
|
9331
|
+
throw error;
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
|
|
9335
|
+
function editorHasPotentialUnsavedContent() {
|
|
9336
|
+
const text = String(sourceTextEl.value || "");
|
|
9337
|
+
if (!text.trim()) return false;
|
|
9338
|
+
if (hasRefreshableFilePath()) return editorDiffersFromFileBackedBaseline();
|
|
9339
|
+
return true;
|
|
9340
|
+
}
|
|
9341
|
+
|
|
9342
|
+
async function openPreviewDocumentHere(href, contextOverride) {
|
|
9343
|
+
if (editorHasPotentialUnsavedContent()) {
|
|
9344
|
+
const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
|
|
9345
|
+
if (!confirmed) return;
|
|
9346
|
+
}
|
|
9347
|
+
const payload = await fetchPreviewLocalLink("document", href, contextOverride);
|
|
9348
|
+
if (typeof payload.text !== "string") throw new Error("Studio did not return document text.");
|
|
9349
|
+
const path = typeof payload.path === "string" ? payload.path : "";
|
|
9350
|
+
const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
|
|
9351
|
+
const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
|
|
9352
|
+
if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
|
|
9353
|
+
setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
|
|
9354
|
+
setSourceState({ source: "file", label, path });
|
|
9355
|
+
markFileBackedBaseline(payload.text);
|
|
9356
|
+
const detected = detectLanguageFromName(path || label);
|
|
9357
|
+
if (detected) setEditorLanguage(detected);
|
|
9358
|
+
setEditorView("markdown");
|
|
9359
|
+
setActivePane("left");
|
|
9360
|
+
setStatus("Opened linked file in editor: " + label, "success");
|
|
9361
|
+
}
|
|
9362
|
+
|
|
9363
|
+
async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
|
|
9364
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
9365
|
+
try {
|
|
9366
|
+
if (popup && popup.document && popup.document.body) {
|
|
9367
|
+
popup.document.title = "Opening linked file…";
|
|
9368
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening linked file…</p>";
|
|
9369
|
+
}
|
|
9370
|
+
} catch {}
|
|
9371
|
+
try {
|
|
9372
|
+
const payload = await fetchPreviewLocalLink("editor-url", href, contextOverride);
|
|
9373
|
+
const targetUrl = payload && typeof payload.relativeUrl === "string"
|
|
9374
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
9375
|
+
: (payload && typeof payload.url === "string" ? payload.url : "");
|
|
9376
|
+
if (!targetUrl) throw new Error("Studio did not return an editor URL.");
|
|
9377
|
+
if (popup && !popup.closed) {
|
|
9378
|
+
try {
|
|
9379
|
+
popup.opener = null;
|
|
9380
|
+
popup.location.href = targetUrl;
|
|
9381
|
+
setStatus("Opening linked file in a new editor.", "success");
|
|
9382
|
+
return;
|
|
9383
|
+
} catch {}
|
|
9384
|
+
}
|
|
9385
|
+
window.open(targetUrl, "_blank", "noopener");
|
|
9386
|
+
setStatus("Opening linked file in a new editor.", "success");
|
|
9387
|
+
} catch (error) {
|
|
9388
|
+
if (popup && !popup.closed) {
|
|
9389
|
+
try { popup.close(); } catch {}
|
|
9390
|
+
}
|
|
9391
|
+
throw error;
|
|
9392
|
+
}
|
|
9393
|
+
}
|
|
9394
|
+
|
|
9395
|
+
async function copyPreviewLocalLinkPath(href, contextOverride) {
|
|
9396
|
+
const payload = await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
9397
|
+
const path = typeof payload.path === "string" ? payload.path : "";
|
|
9398
|
+
if (!path) throw new Error("Studio did not return a file path.");
|
|
9399
|
+
const ok = await writeTextToClipboard(path);
|
|
9400
|
+
if (!ok) throw new Error("Clipboard write failed.");
|
|
9401
|
+
setStatus("Copied local path.", "success");
|
|
9402
|
+
}
|
|
9403
|
+
|
|
9404
|
+
async function revealPreviewLocalLink(href, contextOverride) {
|
|
9405
|
+
const query = getPreviewLinkResourceQuery(href, contextOverride);
|
|
9406
|
+
const payload = await fetchStudioJson("/reveal-local-resource", {
|
|
9407
|
+
method: "POST",
|
|
9408
|
+
body: JSON.stringify(query),
|
|
9409
|
+
});
|
|
9410
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Opened file manager.", "success");
|
|
9411
|
+
}
|
|
9412
|
+
|
|
9413
|
+
async function runPreviewLinkAction(action, context) {
|
|
9414
|
+
const href = context && context.href ? context.href : "";
|
|
9415
|
+
if (!href) return;
|
|
9416
|
+
try {
|
|
9417
|
+
if (action === "open-pdf") {
|
|
9418
|
+
openPreviewPdfLink(href, context.title || href, context);
|
|
9419
|
+
return;
|
|
9420
|
+
}
|
|
9421
|
+
if (action === "open-new") {
|
|
9422
|
+
await openPreviewDocumentInNewEditor(href, null, context);
|
|
9423
|
+
return;
|
|
9424
|
+
}
|
|
9425
|
+
if (action === "open-here") {
|
|
9426
|
+
await openPreviewDocumentHere(href, context);
|
|
9427
|
+
return;
|
|
9428
|
+
}
|
|
9429
|
+
if (action === "open-image") {
|
|
9430
|
+
await openPreviewImageLink(href, context.title || href, context);
|
|
9431
|
+
return;
|
|
9432
|
+
}
|
|
9433
|
+
if (action === "copy-path") {
|
|
9434
|
+
await copyPreviewLocalLinkPath(href, context);
|
|
9435
|
+
return;
|
|
9436
|
+
}
|
|
9437
|
+
if (action === "reveal") {
|
|
9438
|
+
await revealPreviewLocalLink(href, context);
|
|
9439
|
+
}
|
|
9440
|
+
} catch (error) {
|
|
9441
|
+
setStatus((error && error.message) ? error.message : String(error || "Local link action failed."), "warning");
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
|
|
9445
|
+
function handlePreviewLocalLinkClick(event) {
|
|
9446
|
+
const anchor = getPreviewLinkAnchorFromEvent(event);
|
|
9447
|
+
if (!anchor) return;
|
|
9448
|
+
const href = String(anchor.getAttribute("href") || "").trim();
|
|
9449
|
+
const kind = getPreviewLocalLinkKind(href);
|
|
9450
|
+
event.preventDefault();
|
|
9451
|
+
event.stopPropagation();
|
|
9452
|
+
closePreviewLinkMenu();
|
|
9453
|
+
const title = String(anchor.textContent || href).trim() || href;
|
|
9454
|
+
if (kind === "pdf") {
|
|
9455
|
+
openPreviewPdfLink(href, title);
|
|
9456
|
+
return;
|
|
9457
|
+
}
|
|
9458
|
+
if (kind === "image") {
|
|
9459
|
+
const pendingWindow = window.open("", "_blank");
|
|
9460
|
+
void openPreviewImageLink(href, title, null, pendingWindow).catch((error) => {
|
|
9461
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
9462
|
+
});
|
|
9463
|
+
return;
|
|
9464
|
+
}
|
|
9465
|
+
if (kind === "text") {
|
|
9466
|
+
const pendingWindow = window.open("", "_blank");
|
|
9467
|
+
void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
|
|
9468
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
9469
|
+
});
|
|
9470
|
+
return;
|
|
9471
|
+
}
|
|
9472
|
+
setStatus("Right-click this local link for file actions.", "warning");
|
|
9473
|
+
}
|
|
9474
|
+
|
|
9475
|
+
function handlePreviewLocalLinkContextMenu(event) {
|
|
9476
|
+
const anchor = getPreviewLinkAnchorFromEvent(event);
|
|
9477
|
+
if (!anchor) return;
|
|
9478
|
+
event.preventDefault();
|
|
9479
|
+
event.stopPropagation();
|
|
9480
|
+
showPreviewLinkMenu(anchor, event);
|
|
9481
|
+
}
|
|
9482
|
+
|
|
8788
9483
|
function makeRequestId() {
|
|
8789
9484
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
8790
9485
|
return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -9002,10 +9697,6 @@
|
|
|
9002
9697
|
return index > 0 ? value.slice(0, index) : "";
|
|
9003
9698
|
}
|
|
9004
9699
|
|
|
9005
|
-
function getCurrentResourceDirValue() {
|
|
9006
|
-
return resourceDirInput ? String(resourceDirInput.value || "").trim() : "";
|
|
9007
|
-
}
|
|
9008
|
-
|
|
9009
9700
|
function getDefaultQuizContextPath(scope) {
|
|
9010
9701
|
const normalizedScope = normalizeQuizScope(scope);
|
|
9011
9702
|
const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
|
|
@@ -14656,6 +15347,7 @@
|
|
|
14656
15347
|
scheduleSourcePreviewRender(0);
|
|
14657
15348
|
}
|
|
14658
15349
|
updateOutlineUi();
|
|
15350
|
+
scheduleWorkspacePersistence();
|
|
14659
15351
|
}
|
|
14660
15352
|
|
|
14661
15353
|
function setEditorHighlightMode(mode) {
|
|
@@ -15384,6 +16076,10 @@
|
|
|
15384
16076
|
stickyStudioKind = null;
|
|
15385
16077
|
}
|
|
15386
16078
|
if (message.path) {
|
|
16079
|
+
const savedResourceDir = typeof message.resourceDir === "string" && message.resourceDir.trim()
|
|
16080
|
+
? normalizeStudioResourceDirValue(message.resourceDir)
|
|
16081
|
+
: dirnameForDisplayPath(message.path);
|
|
16082
|
+
if (resourceDirInput) resourceDirInput.value = savedResourceDir;
|
|
15387
16083
|
setSourceState({
|
|
15388
16084
|
source: "file",
|
|
15389
16085
|
label: message.label || message.path,
|
|
@@ -15459,6 +16155,10 @@
|
|
|
15459
16155
|
? nextDoc.path
|
|
15460
16156
|
: null;
|
|
15461
16157
|
|
|
16158
|
+
const nextResourceDir = typeof nextDoc.resourceDir === "string" && nextDoc.resourceDir.trim()
|
|
16159
|
+
? normalizeStudioResourceDirValue(nextDoc.resourceDir)
|
|
16160
|
+
: (nextPath ? dirnameForDisplayPath(nextPath) : "");
|
|
16161
|
+
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
15462
16162
|
setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
|
|
15463
16163
|
setSourceState({
|
|
15464
16164
|
source: nextSource,
|
|
@@ -16027,6 +16727,7 @@
|
|
|
16027
16727
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
16028
16728
|
window.addEventListener("beforeunload", () => {
|
|
16029
16729
|
stopFooterSpinner();
|
|
16730
|
+
flushWorkspacePersistence();
|
|
16030
16731
|
flushScratchpadPersistence();
|
|
16031
16732
|
flushReviewNotesPersistence();
|
|
16032
16733
|
});
|
|
@@ -16043,6 +16744,7 @@
|
|
|
16043
16744
|
|
|
16044
16745
|
followSelect.addEventListener("change", () => {
|
|
16045
16746
|
followLatest = followSelect.value !== "off";
|
|
16747
|
+
scheduleWorkspacePersistence();
|
|
16046
16748
|
if (followLatest && queuedLatestResponse) {
|
|
16047
16749
|
if (responseHistory.length > 0) {
|
|
16048
16750
|
selectHistoryIndex(responseHistory.length - 1, { silent: true });
|
|
@@ -16206,6 +16908,7 @@
|
|
|
16206
16908
|
renderReviewNotesList();
|
|
16207
16909
|
updateReviewNotesUi();
|
|
16208
16910
|
}
|
|
16911
|
+
scheduleWorkspacePersistence();
|
|
16209
16912
|
});
|
|
16210
16913
|
|
|
16211
16914
|
sourceTextEl.addEventListener("select", () => {
|
|
@@ -16382,7 +17085,10 @@
|
|
|
16382
17085
|
closeExportPreviewMenu();
|
|
16383
17086
|
});
|
|
16384
17087
|
document.addEventListener("keydown", (event) => {
|
|
16385
|
-
if (event.key === "Escape")
|
|
17088
|
+
if (event.key === "Escape") {
|
|
17089
|
+
closeExportPreviewMenu();
|
|
17090
|
+
closePreviewLinkMenu();
|
|
17091
|
+
}
|
|
16386
17092
|
});
|
|
16387
17093
|
|
|
16388
17094
|
saveAsBtn.addEventListener("click", () => {
|
|
@@ -16393,7 +17099,7 @@
|
|
|
16393
17099
|
}
|
|
16394
17100
|
|
|
16395
17101
|
var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
|
|
16396
|
-
var suggestedDir =
|
|
17102
|
+
var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
|
|
16397
17103
|
const suggested = sourceState.path || (suggestedDir + suggestedName);
|
|
16398
17104
|
const path = window.prompt("Save editor content as:", suggested);
|
|
16399
17105
|
if (!path) return;
|
|
@@ -16472,6 +17178,12 @@
|
|
|
16472
17178
|
});
|
|
16473
17179
|
}
|
|
16474
17180
|
|
|
17181
|
+
if (clearWorkspaceBtn) {
|
|
17182
|
+
clearWorkspaceBtn.addEventListener("click", () => {
|
|
17183
|
+
clearStudioWorkspace();
|
|
17184
|
+
});
|
|
17185
|
+
}
|
|
17186
|
+
|
|
16475
17187
|
sendEditorBtn.addEventListener("click", () => {
|
|
16476
17188
|
const content = sourceTextEl.value;
|
|
16477
17189
|
if (!content.trim()) {
|
|
@@ -16509,9 +17221,7 @@
|
|
|
16509
17221
|
content,
|
|
16510
17222
|
label: sourceState && sourceState.label ? sourceState.label : "current editor",
|
|
16511
17223
|
path: sourceState && sourceState.path ? sourceState.path : undefined,
|
|
16512
|
-
resourceDir:
|
|
16513
|
-
? resourceDirInput.value.trim()
|
|
16514
|
-
: undefined,
|
|
17224
|
+
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
16515
17225
|
});
|
|
16516
17226
|
|
|
16517
17227
|
if (!sent) {
|
|
@@ -16551,9 +17261,7 @@
|
|
|
16551
17261
|
type: "load_git_diff_request",
|
|
16552
17262
|
requestId,
|
|
16553
17263
|
sourcePath: effectivePath || sourceState.path || undefined,
|
|
16554
|
-
resourceDir:
|
|
16555
|
-
? resourceDirInput.value.trim()
|
|
16556
|
-
: undefined,
|
|
17264
|
+
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
16557
17265
|
});
|
|
16558
17266
|
|
|
16559
17267
|
if (!sent) {
|
|
@@ -16772,6 +17480,27 @@
|
|
|
16772
17480
|
void handleCopyPreviewBlockButtonClick(event);
|
|
16773
17481
|
}, true);
|
|
16774
17482
|
|
|
17483
|
+
document.addEventListener("click", (event) => {
|
|
17484
|
+
const target = event.target;
|
|
17485
|
+
const menuButton = target instanceof Element ? target.closest(".studio-preview-link-menu [data-preview-link-action]") : null;
|
|
17486
|
+
if (menuButton) {
|
|
17487
|
+
event.preventDefault();
|
|
17488
|
+
event.stopPropagation();
|
|
17489
|
+
const action = String(menuButton.getAttribute("data-preview-link-action") || "");
|
|
17490
|
+
const context = activePreviewLinkContext;
|
|
17491
|
+
closePreviewLinkMenu();
|
|
17492
|
+
void runPreviewLinkAction(action, context);
|
|
17493
|
+
return;
|
|
17494
|
+
}
|
|
17495
|
+
if (target instanceof Element && target.closest(".studio-preview-link-menu")) return;
|
|
17496
|
+
closePreviewLinkMenu();
|
|
17497
|
+
handlePreviewLocalLinkClick(event);
|
|
17498
|
+
}, true);
|
|
17499
|
+
|
|
17500
|
+
document.addEventListener("contextmenu", (event) => {
|
|
17501
|
+
handlePreviewLocalLinkContextMenu(event);
|
|
17502
|
+
}, true);
|
|
17503
|
+
|
|
16775
17504
|
document.addEventListener("pointerup", (event) => {
|
|
16776
17505
|
const target = event.target;
|
|
16777
17506
|
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|
|
@@ -16962,7 +17691,8 @@
|
|
|
16962
17691
|
if (resourceDirLabel) resourceDirLabel.hidden = state !== "label";
|
|
16963
17692
|
}
|
|
16964
17693
|
function applyResourceDir() {
|
|
16965
|
-
var dir =
|
|
17694
|
+
var dir = getCurrentResourceDirValue();
|
|
17695
|
+
if (resourceDirInput) resourceDirInput.value = dir;
|
|
16966
17696
|
if (dir) {
|
|
16967
17697
|
if (resourceDirLabel) resourceDirLabel.textContent = "Working dir: " + dir;
|
|
16968
17698
|
showResourceDirState("label");
|
|
@@ -16972,6 +17702,7 @@
|
|
|
16972
17702
|
updateSaveFileTooltip();
|
|
16973
17703
|
syncActionButtons();
|
|
16974
17704
|
renderSourcePreview();
|
|
17705
|
+
scheduleWorkspacePersistence();
|
|
16975
17706
|
}
|
|
16976
17707
|
if (sourceBadgeEl) {
|
|
16977
17708
|
sourceBadgeEl.addEventListener("click", () => {
|
|
@@ -16997,7 +17728,7 @@
|
|
|
16997
17728
|
applyResourceDir();
|
|
16998
17729
|
} else if (e.key === "Escape") {
|
|
16999
17730
|
e.preventDefault();
|
|
17000
|
-
var dir =
|
|
17731
|
+
var dir = getCurrentResourceDirValue();
|
|
17001
17732
|
if (dir) {
|
|
17002
17733
|
showResourceDirState("label");
|
|
17003
17734
|
} else {
|
|
@@ -17014,6 +17745,7 @@
|
|
|
17014
17745
|
updateSaveFileTooltip();
|
|
17015
17746
|
syncActionButtons();
|
|
17016
17747
|
renderSourcePreview();
|
|
17748
|
+
scheduleWorkspacePersistence();
|
|
17017
17749
|
});
|
|
17018
17750
|
}
|
|
17019
17751
|
|
|
@@ -17062,7 +17794,7 @@
|
|
|
17062
17794
|
setResponseFontSize(initialResponseFontSize, { persist: false });
|
|
17063
17795
|
|
|
17064
17796
|
if (resourceDirInput && initialResourceDir) {
|
|
17065
|
-
resourceDirInput.value = initialResourceDir;
|
|
17797
|
+
resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
|
|
17066
17798
|
}
|
|
17067
17799
|
setSourceState(initialSourceState);
|
|
17068
17800
|
refreshResponseUi();
|
|
@@ -17090,9 +17822,16 @@
|
|
|
17090
17822
|
setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
|
|
17091
17823
|
setReplSendMode(replSendMode);
|
|
17092
17824
|
|
|
17825
|
+
const persistedWorkspaceState = readPersistedWorkspaceState();
|
|
17826
|
+
applyPersistedWorkspaceState(persistedWorkspaceState);
|
|
17827
|
+
|
|
17093
17828
|
setEditorView(editorView);
|
|
17094
17829
|
setRightView(rightView);
|
|
17095
17830
|
renderSourcePreview();
|
|
17831
|
+
workspacePersistenceReady = true;
|
|
17832
|
+
if (workspaceRestoredFromBrowser) {
|
|
17833
|
+
setStatus("Restored editor workspace from this browser tab. Use Clear editor to discard it.", "success");
|
|
17834
|
+
}
|
|
17096
17835
|
connect();
|
|
17097
17836
|
} catch (error) {
|
|
17098
17837
|
hardFail("Studio UI init failed", error);
|