pi-studio 0.9.12 → 0.9.14
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 +24 -0
- package/README.md +2 -0
- package/client/studio-client.js +849 -49
- package/client/studio.css +68 -7
- package/index.ts +369 -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();
|
|
@@ -2168,6 +2174,49 @@
|
|
|
2168
2174
|
scheduleResponsePaneRepaintNudge();
|
|
2169
2175
|
}
|
|
2170
2176
|
|
|
2177
|
+
function getActivePaneTextSizeConfig() {
|
|
2178
|
+
if (activePane === "right") {
|
|
2179
|
+
return {
|
|
2180
|
+
label: "Right pane text size",
|
|
2181
|
+
value: responseFontSize,
|
|
2182
|
+
defaultValue: DEFAULT_RESPONSE_FONT_SIZE,
|
|
2183
|
+
options: RESPONSE_FONT_SIZE_OPTIONS,
|
|
2184
|
+
setValue: setResponseFontSize,
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
return {
|
|
2188
|
+
label: "Editor text size",
|
|
2189
|
+
value: editorFontSize,
|
|
2190
|
+
defaultValue: DEFAULT_EDITOR_FONT_SIZE,
|
|
2191
|
+
options: EDITOR_FONT_SIZE_OPTIONS,
|
|
2192
|
+
setValue: setEditorFontSize,
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
function getNextStudioFontSizeOption(currentValue, options, defaultValue, direction) {
|
|
2197
|
+
const normalized = normalizeStudioFontSize(currentValue, options, defaultValue);
|
|
2198
|
+
const currentIndex = Math.max(0, options.findIndex((option) => Math.abs(option - normalized) < 0.001));
|
|
2199
|
+
const nextIndex = Math.max(0, Math.min(options.length - 1, currentIndex + direction));
|
|
2200
|
+
return options[nextIndex];
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function adjustActivePaneTextSize(direction) {
|
|
2204
|
+
const config = getActivePaneTextSizeConfig();
|
|
2205
|
+
const nextSize = getNextStudioFontSizeOption(config.value, config.options, config.defaultValue, direction);
|
|
2206
|
+
if (Math.abs(nextSize - config.value) < 0.001) {
|
|
2207
|
+
setStatus(config.label + " already at " + formatStudioFontSizeLabel(nextSize) + ".", "warning");
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
config.setValue(nextSize);
|
|
2211
|
+
setStatus(config.label + ": " + formatStudioFontSizeLabel(nextSize) + ".");
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function resetActivePaneTextSize() {
|
|
2215
|
+
const config = getActivePaneTextSizeConfig();
|
|
2216
|
+
config.setValue(config.defaultValue);
|
|
2217
|
+
setStatus(config.label + " reset to " + formatStudioFontSizeLabel(config.defaultValue) + ".");
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2171
2220
|
function getStudioUiRefreshAnnotationHeaderEnabled() {
|
|
2172
2221
|
try {
|
|
2173
2222
|
return Boolean(stripAnnotationHeader(sourceTextEl.value).hadHeader);
|
|
@@ -2180,12 +2229,8 @@
|
|
|
2180
2229
|
if (!studioUiRefreshUi) return;
|
|
2181
2230
|
if (studioUiRefreshUi.annotationsButton) {
|
|
2182
2231
|
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
|
-
}
|
|
2232
|
+
const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
|
|
2233
|
+
setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
|
|
2189
2234
|
}
|
|
2190
2235
|
if (studioUiRefreshUi.viewButton) {
|
|
2191
2236
|
const syntaxLabel = editorHighlightEnabled
|
|
@@ -2349,7 +2394,7 @@
|
|
|
2349
2394
|
const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
|
|
2350
2395
|
const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
|
|
2351
2396
|
const annotationsMenu = makeStudioUiRefreshMenu(annotationsButton, "annotations", "studio-refresh-annotations-anchor");
|
|
2352
|
-
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display",
|
|
2397
|
+
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display", [annotationModeSelect, insertHeaderBtn]);
|
|
2353
2398
|
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Actions", [stripAnnotationsBtn, saveAnnotatedBtn]);
|
|
2354
2399
|
const viewButton = makeStudioUiRefreshElement("button", "", "View");
|
|
2355
2400
|
const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
|
|
@@ -3071,14 +3116,16 @@
|
|
|
3071
3116
|
// Show "Set working dir" button when not file-backed
|
|
3072
3117
|
var isFileBacked = hasRefreshableFilePath();
|
|
3073
3118
|
if (isFileBacked) {
|
|
3074
|
-
|
|
3119
|
+
var fileBackedResourceDir = getCurrentResourceDirValue() || dirnameForDisplayPath(sourceState.path);
|
|
3120
|
+
if (resourceDirInput) resourceDirInput.value = fileBackedResourceDir;
|
|
3075
3121
|
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
3076
3122
|
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
3077
3123
|
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
3078
3124
|
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
3079
3125
|
} else {
|
|
3080
3126
|
// Restore to label if dir is set, otherwise show button
|
|
3081
|
-
var dir =
|
|
3127
|
+
var dir = getCurrentResourceDirValue();
|
|
3128
|
+
if (resourceDirInput) resourceDirInput.value = dir;
|
|
3082
3129
|
if (dir) {
|
|
3083
3130
|
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
3084
3131
|
if (resourceDirLabel) { resourceDirLabel.textContent = "Working dir: " + dir; resourceDirLabel.hidden = false; }
|
|
@@ -3144,7 +3191,6 @@
|
|
|
3144
3191
|
}
|
|
3145
3192
|
|
|
3146
3193
|
function loadPaneSplitPercent() {
|
|
3147
|
-
if (isEditorOnlyMode) return;
|
|
3148
3194
|
let stored = "";
|
|
3149
3195
|
try {
|
|
3150
3196
|
stored = window.localStorage ? String(window.localStorage.getItem(PANE_SPLIT_STORAGE_KEY) || "") : "";
|
|
@@ -3166,7 +3212,7 @@
|
|
|
3166
3212
|
}
|
|
3167
3213
|
|
|
3168
3214
|
function setupPaneResizeHandle() {
|
|
3169
|
-
if (!paneResizeHandleEl
|
|
3215
|
+
if (!paneResizeHandleEl) return;
|
|
3170
3216
|
loadPaneSplitPercent();
|
|
3171
3217
|
let dragging = false;
|
|
3172
3218
|
let movedDuringDrag = false;
|
|
@@ -3347,10 +3393,6 @@
|
|
|
3347
3393
|
|
|
3348
3394
|
function activatePaneFromShortcut(nextPane) {
|
|
3349
3395
|
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
3396
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3355
3397
|
setActivePane(pane);
|
|
3356
3398
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
@@ -3393,10 +3435,6 @@
|
|
|
3393
3435
|
}
|
|
3394
3436
|
|
|
3395
3437
|
function focusRightContentFromShortcut() {
|
|
3396
|
-
if (isEditorOnlyMode) {
|
|
3397
|
-
setStatus("Only the editor pane is available in editor-only Studio.", "warning");
|
|
3398
|
-
return;
|
|
3399
|
-
}
|
|
3400
3438
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3401
3439
|
setActivePane("right");
|
|
3402
3440
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
@@ -3653,6 +3691,24 @@
|
|
|
3653
3691
|
}
|
|
3654
3692
|
}
|
|
3655
3693
|
|
|
3694
|
+
if (!isTextEntryShortcutTarget(event.target) && !event.metaKey && !event.ctrlKey && event.altKey) {
|
|
3695
|
+
if (code === "Equal" || code === "NumpadAdd" || key === "=" || key === "+") {
|
|
3696
|
+
event.preventDefault();
|
|
3697
|
+
adjustActivePaneTextSize(1);
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
if (code === "Minus" || code === "NumpadSubtract" || key === "-" || key === "_") {
|
|
3701
|
+
event.preventDefault();
|
|
3702
|
+
adjustActivePaneTextSize(-1);
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (code === "Digit0" || code === "Numpad0" || key === "0") {
|
|
3706
|
+
event.preventDefault();
|
|
3707
|
+
resetActivePaneTextSize();
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3656
3712
|
const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3657
3713
|
if (isPaneSwitchShortcut) {
|
|
3658
3714
|
event.preventDefault();
|
|
@@ -4316,6 +4372,36 @@
|
|
|
4316
4372
|
+ " if (node && node.nodeType === 3) node = node.parentElement;\n"
|
|
4317
4373
|
+ " return node && typeof node.closest === 'function' ? node.closest('a[href]') : null;\n"
|
|
4318
4374
|
+ " }\n"
|
|
4375
|
+
+ " function isLocalHtmlPreviewLinkHref(value) {\n"
|
|
4376
|
+
+ " const raw = String(value || '').trim();\n"
|
|
4377
|
+
+ " if (!raw || raw.charAt(0) === '#') return false;\n"
|
|
4378
|
+
+ " if (/^\\/\\//.test(raw)) return false;\n"
|
|
4379
|
+
+ " if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;\n"
|
|
4380
|
+
+ " return true;\n"
|
|
4381
|
+
+ " }\n"
|
|
4382
|
+
+ " function postHtmlPreviewLocalLink(action, anchor, event) {\n"
|
|
4383
|
+
+ " if (!anchor || typeof anchor.getAttribute !== 'function') return false;\n"
|
|
4384
|
+
+ " if (anchor.hasAttribute('download')) return false;\n"
|
|
4385
|
+
+ " const target = String(anchor.getAttribute('target') || '').trim().toLowerCase();\n"
|
|
4386
|
+
+ " if (target && target !== '_self') return false;\n"
|
|
4387
|
+
+ " const href = String(anchor.getAttribute('href') || '').trim();\n"
|
|
4388
|
+
+ " if (!isLocalHtmlPreviewLinkHref(href)) return false;\n"
|
|
4389
|
+
+ " 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"
|
|
4390
|
+
+ " return true;\n"
|
|
4391
|
+
+ " }\n"
|
|
4392
|
+
+ " function handleHtmlPreviewLocalLinkClick(event) {\n"
|
|
4393
|
+
+ " if (!event || event.defaultPrevented) return;\n"
|
|
4394
|
+
+ " if (typeof event.button === 'number' && event.button !== 0) return;\n"
|
|
4395
|
+
+ " const anchor = getAnchorFromClickTarget(event.target);\n"
|
|
4396
|
+
+ " if (!postHtmlPreviewLocalLink('open', anchor, event)) return;\n"
|
|
4397
|
+
+ " event.preventDefault();\n"
|
|
4398
|
+
+ " }\n"
|
|
4399
|
+
+ " function handleHtmlPreviewLocalLinkContextMenu(event) {\n"
|
|
4400
|
+
+ " if (!event || event.defaultPrevented) return;\n"
|
|
4401
|
+
+ " const anchor = getAnchorFromClickTarget(event.target);\n"
|
|
4402
|
+
+ " if (!postHtmlPreviewLocalLink('contextmenu', anchor, event)) return;\n"
|
|
4403
|
+
+ " event.preventDefault();\n"
|
|
4404
|
+
+ " }\n"
|
|
4319
4405
|
+ " function getSameDocumentFragment(anchor) {\n"
|
|
4320
4406
|
+ " if (!anchor || typeof anchor.getAttribute !== 'function') return null;\n"
|
|
4321
4407
|
+ " if (anchor.hasAttribute('download')) return null;\n"
|
|
@@ -4573,6 +4659,8 @@
|
|
|
4573
4659
|
+ " }\n"
|
|
4574
4660
|
+ " });\n"
|
|
4575
4661
|
+ " document.addEventListener('click', handleFragmentAnchorClick);\n"
|
|
4662
|
+
+ " document.addEventListener('click', handleHtmlPreviewLocalLinkClick);\n"
|
|
4663
|
+
+ " document.addEventListener('contextmenu', handleHtmlPreviewLocalLinkContextMenu);\n"
|
|
4576
4664
|
+ " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
4577
4665
|
+ " window.addEventListener('hashchange', () => {\n"
|
|
4578
4666
|
+ " const hash = String(window.location && window.location.hash || '');\n"
|
|
@@ -4840,7 +4928,8 @@
|
|
|
4840
4928
|
const params = new URLSearchParams({ token, path: String(resourceUrl || "") });
|
|
4841
4929
|
if (record && record.sourcePath) {
|
|
4842
4930
|
params.set("sourcePath", record.sourcePath);
|
|
4843
|
-
}
|
|
4931
|
+
}
|
|
4932
|
+
if (record && record.resourceDir) {
|
|
4844
4933
|
params.set("resourceDir", record.resourceDir);
|
|
4845
4934
|
}
|
|
4846
4935
|
return "/html-preview-resource?" + params.toString();
|
|
@@ -4905,10 +4994,71 @@
|
|
|
4905
4994
|
void resolveHtmlArtifactResources(record, items);
|
|
4906
4995
|
}
|
|
4907
4996
|
|
|
4997
|
+
function getHtmlArtifactLocalLinkContext(record, data) {
|
|
4998
|
+
return {
|
|
4999
|
+
href: typeof data.href === "string" ? data.href : "",
|
|
5000
|
+
title: typeof data.title === "string" && data.title.trim() ? data.title.trim() : (typeof data.href === "string" ? data.href : "local link"),
|
|
5001
|
+
sourcePath: record && record.sourcePath ? String(record.sourcePath) : "",
|
|
5002
|
+
resourceDir: record && record.resourceDir ? String(record.resourceDir) : "",
|
|
5003
|
+
};
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
function getHtmlArtifactLocalLinkClientPoint(record, data) {
|
|
5007
|
+
const iframe = record && record.iframe;
|
|
5008
|
+
const rect = iframe && typeof iframe.getBoundingClientRect === "function"
|
|
5009
|
+
? iframe.getBoundingClientRect()
|
|
5010
|
+
: { left: 0, top: 0 };
|
|
5011
|
+
return {
|
|
5012
|
+
clientX: rect.left + (Number(data.clientX) || 0),
|
|
5013
|
+
clientY: rect.top + (Number(data.clientY) || 0),
|
|
5014
|
+
};
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
function handleHtmlArtifactFrameLocalLinkMessage(event) {
|
|
5018
|
+
const data = event && event.data;
|
|
5019
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-local-link") return;
|
|
5020
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
5021
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
5022
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
5023
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
5024
|
+
return;
|
|
5025
|
+
}
|
|
5026
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
5027
|
+
const context = getHtmlArtifactLocalLinkContext(record, data);
|
|
5028
|
+
if (!isStudioLocalPreviewHref(context.href)) return;
|
|
5029
|
+
const action = typeof data.action === "string" ? data.action : "open";
|
|
5030
|
+
if (action === "contextmenu") {
|
|
5031
|
+
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
5032
|
+
showPreviewLinkMenu(null, point, context);
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
const kind = getPreviewLocalLinkKind(context.href);
|
|
5036
|
+
if (kind === "pdf") {
|
|
5037
|
+
openPreviewPdfLink(context.href, context.title, context);
|
|
5038
|
+
return;
|
|
5039
|
+
}
|
|
5040
|
+
if (kind === "image") {
|
|
5041
|
+
const pendingWindow = window.open("", "_blank");
|
|
5042
|
+
void openPreviewImageLink(context.href, context.title, context, pendingWindow).catch((error) => {
|
|
5043
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
5044
|
+
});
|
|
5045
|
+
return;
|
|
5046
|
+
}
|
|
5047
|
+
if (kind === "text") {
|
|
5048
|
+
const pendingWindow = window.open("", "_blank");
|
|
5049
|
+
void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
|
|
5050
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
5051
|
+
});
|
|
5052
|
+
return;
|
|
5053
|
+
}
|
|
5054
|
+
setStatus("Right-click this local HTML preview link for file actions.", "warning");
|
|
5055
|
+
}
|
|
5056
|
+
|
|
4908
5057
|
window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
|
|
4909
5058
|
window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
|
|
4910
5059
|
window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
|
|
4911
5060
|
window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
|
|
5061
|
+
window.addEventListener("message", handleHtmlArtifactFrameLocalLinkMessage);
|
|
4912
5062
|
|
|
4913
5063
|
function isStudioHtmlFocusOpen() {
|
|
4914
5064
|
return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
|
|
@@ -5461,13 +5611,16 @@
|
|
|
5461
5611
|
if (!token) return "";
|
|
5462
5612
|
const pdfPath = String(options && options.path ? options.path : "").trim();
|
|
5463
5613
|
if (!pdfPath) return "";
|
|
5614
|
+
const explicitSourcePath = options && typeof options.sourcePath === "string" ? options.sourcePath.trim() : "";
|
|
5615
|
+
const explicitResourceDir = options && typeof options.resourceDir === "string" ? normalizeStudioResourceDirValue(options.resourceDir) : "";
|
|
5464
5616
|
const effectivePath = getEffectiveSavePath();
|
|
5465
|
-
const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
|
|
5466
|
-
const resourceDir =
|
|
5617
|
+
const sourcePath = explicitSourcePath || (useEditorResourceContext ? (effectivePath || sourceState.path || "") : "");
|
|
5618
|
+
const resourceDir = explicitResourceDir || getCurrentResourceDirValue();
|
|
5467
5619
|
const params = new URLSearchParams({ token, path: pdfPath });
|
|
5468
5620
|
if (sourcePath) {
|
|
5469
5621
|
params.set("sourcePath", sourcePath);
|
|
5470
|
-
}
|
|
5622
|
+
}
|
|
5623
|
+
if (resourceDir) {
|
|
5471
5624
|
params.set("resourceDir", resourceDir);
|
|
5472
5625
|
}
|
|
5473
5626
|
return "/pdf-resource?" + params.toString();
|
|
@@ -6661,7 +6814,7 @@
|
|
|
6661
6814
|
const payload = {
|
|
6662
6815
|
markdown: String(markdown || ""),
|
|
6663
6816
|
sourcePath: sourcePath,
|
|
6664
|
-
resourceDir: (!sourcePath && resourceDirInput) ?
|
|
6817
|
+
resourceDir: (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "",
|
|
6665
6818
|
};
|
|
6666
6819
|
if (previewOptions.includeEditorLanguage) {
|
|
6667
6820
|
payload.editorLanguage = String(editorLanguage || "");
|
|
@@ -6789,7 +6942,7 @@
|
|
|
6789
6942
|
|
|
6790
6943
|
const effectivePath = getEffectiveSavePath();
|
|
6791
6944
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
6792
|
-
const resourceDir = (!sourcePath && resourceDirInput) ?
|
|
6945
|
+
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
6793
6946
|
const isEditorPreview = rightView === "editor-preview";
|
|
6794
6947
|
const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
|
|
6795
6948
|
const isLatex = isEditorPreview
|
|
@@ -6955,7 +7108,7 @@
|
|
|
6955
7108
|
|
|
6956
7109
|
const effectivePath = getEffectiveSavePath();
|
|
6957
7110
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
6958
|
-
const resourceDir = (!sourcePath && resourceDirInput) ?
|
|
7111
|
+
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
6959
7112
|
const isEditorPreview = rightView === "editor-preview";
|
|
6960
7113
|
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
|
|
6961
7114
|
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
@@ -7344,7 +7497,7 @@
|
|
|
7344
7497
|
decorateCopyablePreviewBlocks(targetEl);
|
|
7345
7498
|
|
|
7346
7499
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
7347
|
-
if (!sourceState.path && !(
|
|
7500
|
+
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
7348
7501
|
var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
|
|
7349
7502
|
var hasLatexImages = /\\includegraphics/.test(markdown || "");
|
|
7350
7503
|
if (hasRelativeImages || hasLatexImages) {
|
|
@@ -8131,23 +8284,54 @@
|
|
|
8131
8284
|
updateResultActionButtons();
|
|
8132
8285
|
}
|
|
8133
8286
|
|
|
8287
|
+
function normalizeStudioResourceDirValue(value) {
|
|
8288
|
+
let text = String(value || "").trim();
|
|
8289
|
+
if (text.length >= 2) {
|
|
8290
|
+
const first = text.charAt(0);
|
|
8291
|
+
const last = text.charAt(text.length - 1);
|
|
8292
|
+
if ((first === "\"" && last === "\"") || (first === "'" && last === "'")) {
|
|
8293
|
+
text = text.slice(1, -1).trim();
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
if (/^file:\/\//i.test(text)) {
|
|
8297
|
+
try {
|
|
8298
|
+
text = decodeURIComponent(new URL(text).pathname || text).trim();
|
|
8299
|
+
} catch {}
|
|
8300
|
+
}
|
|
8301
|
+
const markers = ["/Users/", "/home/", "/Volumes/", "/private/", "/tmp/", "/var/", "/opt/", "/Applications/"];
|
|
8302
|
+
let embeddedAbsoluteIndex = -1;
|
|
8303
|
+
for (const marker of markers) {
|
|
8304
|
+
const index = text.lastIndexOf(marker);
|
|
8305
|
+
if (index > 0) embeddedAbsoluteIndex = Math.max(embeddedAbsoluteIndex, index);
|
|
8306
|
+
}
|
|
8307
|
+
const windowsMatch = text.match(/.*([A-Za-z]:[\\/].*)$/);
|
|
8308
|
+
if (windowsMatch && windowsMatch[1]) return windowsMatch[1].trim();
|
|
8309
|
+
if (embeddedAbsoluteIndex > 0) text = text.slice(embeddedAbsoluteIndex).trim();
|
|
8310
|
+
return text;
|
|
8311
|
+
}
|
|
8312
|
+
|
|
8313
|
+
function getCurrentResourceDirValue() {
|
|
8314
|
+
return resourceDirInput ? normalizeStudioResourceDirValue(resourceDirInput.value) : "";
|
|
8315
|
+
}
|
|
8316
|
+
|
|
8134
8317
|
function getEffectiveSavePath() {
|
|
8135
8318
|
// File-backed: use the original path
|
|
8136
8319
|
if (sourceState.path) return sourceState.path;
|
|
8137
8320
|
// Upload with working dir + filename: derive path
|
|
8138
|
-
|
|
8321
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
8322
|
+
if (sourceState.source === "upload" && sourceState.label && resourceDir) {
|
|
8139
8323
|
var name = sourceState.label.replace(/^upload:\s*/i, "");
|
|
8140
|
-
if (name) return
|
|
8324
|
+
if (name) return resourceDir.replace(/\/$/, "") + "/" + name;
|
|
8141
8325
|
}
|
|
8142
8326
|
return null;
|
|
8143
8327
|
}
|
|
8144
8328
|
|
|
8145
8329
|
function getHtmlPreviewResourceContextOptions() {
|
|
8146
8330
|
const sourcePath = getEffectiveSavePath() || sourceState.path || "";
|
|
8147
|
-
const resourceDir =
|
|
8331
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
8148
8332
|
return {
|
|
8149
8333
|
sourcePath,
|
|
8150
|
-
resourceDir
|
|
8334
|
+
resourceDir,
|
|
8151
8335
|
};
|
|
8152
8336
|
}
|
|
8153
8337
|
|
|
@@ -8163,8 +8347,8 @@
|
|
|
8163
8347
|
|
|
8164
8348
|
const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
|
|
8165
8349
|
const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
|
|
8166
|
-
const suggestedDir =
|
|
8167
|
-
?
|
|
8350
|
+
const suggestedDir = getCurrentResourceDirValue()
|
|
8351
|
+
? getCurrentResourceDirValue().replace(/\/$/, "") + "/"
|
|
8168
8352
|
: "./";
|
|
8169
8353
|
return suggestedDir + stem + ".annotated.md";
|
|
8170
8354
|
}
|
|
@@ -8201,6 +8385,7 @@
|
|
|
8201
8385
|
saveAsBtn.disabled = uiBusy;
|
|
8202
8386
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
8203
8387
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
8388
|
+
if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy;
|
|
8204
8389
|
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
8205
8390
|
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
8206
8391
|
if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
|
|
@@ -8217,7 +8402,7 @@
|
|
|
8217
8402
|
rightViewSelect.disabled = isEditorOnlyMode;
|
|
8218
8403
|
followSelect.disabled = isEditorOnlyMode || uiBusy;
|
|
8219
8404
|
if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
|
|
8220
|
-
insertHeaderBtn.disabled = uiBusy
|
|
8405
|
+
insertHeaderBtn.disabled = uiBusy;
|
|
8221
8406
|
lensSelect.disabled = uiBusy || isEditorOnlyMode;
|
|
8222
8407
|
updateSaveFileTooltip();
|
|
8223
8408
|
updateRefreshFromDiskTooltip();
|
|
@@ -8259,6 +8444,197 @@
|
|
|
8259
8444
|
previousDescriptor: previousDescriptor,
|
|
8260
8445
|
carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
|
|
8261
8446
|
});
|
|
8447
|
+
scheduleWorkspacePersistence();
|
|
8448
|
+
}
|
|
8449
|
+
|
|
8450
|
+
function normalizeWorkspaceSourceState(value) {
|
|
8451
|
+
const raw = value && typeof value === "object" ? value : {};
|
|
8452
|
+
const path = typeof raw.path === "string" && raw.path.trim() ? raw.path.trim() : null;
|
|
8453
|
+
return {
|
|
8454
|
+
source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "blank",
|
|
8455
|
+
label: typeof raw.label === "string" && raw.label.trim() ? raw.label.trim() : "blank",
|
|
8456
|
+
path,
|
|
8457
|
+
draftId: path ? null : (typeof raw.draftId === "string" && raw.draftId.trim() ? raw.draftId.trim() : null),
|
|
8458
|
+
};
|
|
8459
|
+
}
|
|
8460
|
+
|
|
8461
|
+
function getWorkspaceStateIdentity(state) {
|
|
8462
|
+
const normalized = normalizeWorkspaceSourceState(state);
|
|
8463
|
+
if (normalized.path) return "file:" + normalized.path;
|
|
8464
|
+
if (normalized.draftId) return "draft:" + normalized.draftId;
|
|
8465
|
+
return "source:" + normalized.source + ":" + normalized.label;
|
|
8466
|
+
}
|
|
8467
|
+
|
|
8468
|
+
function readPersistedWorkspaceState() {
|
|
8469
|
+
try {
|
|
8470
|
+
const raw = window.localStorage ? window.localStorage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
|
|
8471
|
+
if (!raw) return null;
|
|
8472
|
+
const parsed = JSON.parse(raw);
|
|
8473
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return null;
|
|
8474
|
+
if (typeof parsed.text !== "string") return null;
|
|
8475
|
+
return parsed;
|
|
8476
|
+
} catch {
|
|
8477
|
+
return null;
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
|
|
8481
|
+
function shouldRestorePersistedWorkspaceState(state) {
|
|
8482
|
+
if (!state || typeof state.text !== "string") return false;
|
|
8483
|
+
const storedSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
8484
|
+
const initialIdentity = getWorkspaceStateIdentity(initialSourceState);
|
|
8485
|
+
const storedIdentity = getWorkspaceStateIdentity(storedSourceState);
|
|
8486
|
+
if (storedIdentity === initialIdentity) return true;
|
|
8487
|
+
if (!explicitDocumentIdentityFromUrl && initialSourceState.source === "blank" && !initialSourceState.path) return true;
|
|
8488
|
+
return false;
|
|
8489
|
+
}
|
|
8490
|
+
|
|
8491
|
+
function buildWorkspacePersistencePayload() {
|
|
8492
|
+
return {
|
|
8493
|
+
version: 1,
|
|
8494
|
+
savedAt: Date.now(),
|
|
8495
|
+
sourceState: normalizeWorkspaceSourceState(sourceState),
|
|
8496
|
+
resourceDir: getCurrentResourceDirValue(),
|
|
8497
|
+
editorView,
|
|
8498
|
+
rightView,
|
|
8499
|
+
editorLanguage,
|
|
8500
|
+
followLatest,
|
|
8501
|
+
responseHistoryIndex,
|
|
8502
|
+
selectionStart: typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0,
|
|
8503
|
+
selectionEnd: typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : 0,
|
|
8504
|
+
scrollTop: typeof sourceTextEl.scrollTop === "number" ? sourceTextEl.scrollTop : 0,
|
|
8505
|
+
text: String(sourceTextEl.value || ""),
|
|
8506
|
+
};
|
|
8507
|
+
}
|
|
8508
|
+
|
|
8509
|
+
function persistWorkspaceStateNow() {
|
|
8510
|
+
if (!workspacePersistenceReady) return;
|
|
8511
|
+
try {
|
|
8512
|
+
if (!window.localStorage) return;
|
|
8513
|
+
const payload = buildWorkspacePersistencePayload();
|
|
8514
|
+
if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
|
|
8515
|
+
window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8516
|
+
return;
|
|
8517
|
+
}
|
|
8518
|
+
window.localStorage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
|
|
8519
|
+
} catch {
|
|
8520
|
+
// Ignore browser storage failures and quota limits.
|
|
8521
|
+
}
|
|
8522
|
+
}
|
|
8523
|
+
|
|
8524
|
+
function scheduleWorkspacePersistence() {
|
|
8525
|
+
if (!workspacePersistenceReady) return;
|
|
8526
|
+
if (workspacePersistTimer !== null) window.clearTimeout(workspacePersistTimer);
|
|
8527
|
+
workspacePersistTimer = window.setTimeout(() => {
|
|
8528
|
+
workspacePersistTimer = null;
|
|
8529
|
+
persistWorkspaceStateNow();
|
|
8530
|
+
}, 160);
|
|
8531
|
+
}
|
|
8532
|
+
|
|
8533
|
+
function flushWorkspacePersistence() {
|
|
8534
|
+
if (workspacePersistTimer !== null) {
|
|
8535
|
+
window.clearTimeout(workspacePersistTimer);
|
|
8536
|
+
workspacePersistTimer = null;
|
|
8537
|
+
}
|
|
8538
|
+
persistWorkspaceStateNow();
|
|
8539
|
+
}
|
|
8540
|
+
|
|
8541
|
+
function clearPersistedWorkspaceState() {
|
|
8542
|
+
if (workspacePersistTimer !== null) {
|
|
8543
|
+
window.clearTimeout(workspacePersistTimer);
|
|
8544
|
+
workspacePersistTimer = null;
|
|
8545
|
+
}
|
|
8546
|
+
try {
|
|
8547
|
+
if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8548
|
+
} catch {}
|
|
8549
|
+
}
|
|
8550
|
+
|
|
8551
|
+
function applyPersistedWorkspaceState(state) {
|
|
8552
|
+
if (!shouldRestorePersistedWorkspaceState(state)) return false;
|
|
8553
|
+
const nextSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
8554
|
+
const nextResourceDir = normalizeStudioResourceDirValue(typeof state.resourceDir === "string" ? state.resourceDir : "");
|
|
8555
|
+
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
8556
|
+
setEditorText(state.text, { preserveScroll: false, preserveSelection: false });
|
|
8557
|
+
setSourceState(nextSourceState);
|
|
8558
|
+
if (resourceDirInput && nextResourceDir) {
|
|
8559
|
+
resourceDirInput.value = nextResourceDir;
|
|
8560
|
+
updateSourceBadge();
|
|
8561
|
+
}
|
|
8562
|
+
if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
|
|
8563
|
+
setEditorLanguage(state.editorLanguage.trim());
|
|
8564
|
+
}
|
|
8565
|
+
editorView = state.editorView === "preview" ? "preview" : "markdown";
|
|
8566
|
+
rightView = state.rightView === "preview"
|
|
8567
|
+
? "preview"
|
|
8568
|
+
: (state.rightView === "editor-preview"
|
|
8569
|
+
? "editor-preview"
|
|
8570
|
+
: (state.rightView === "repl" ? "repl" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")));
|
|
8571
|
+
if (typeof state.followLatest === "boolean") {
|
|
8572
|
+
followLatest = state.followLatest;
|
|
8573
|
+
}
|
|
8574
|
+
if (followSelect) followSelect.value = followLatest ? "on" : "off";
|
|
8575
|
+
if (typeof state.responseHistoryIndex === "number" && Number.isFinite(state.responseHistoryIndex)) {
|
|
8576
|
+
responseHistoryIndex = Math.max(-1, Math.floor(state.responseHistoryIndex));
|
|
8577
|
+
}
|
|
8578
|
+
const maxIndex = String(sourceTextEl.value || "").length;
|
|
8579
|
+
const start = Math.max(0, Math.min(Math.floor(Number(state.selectionStart) || 0), maxIndex));
|
|
8580
|
+
const end = Math.max(start, Math.min(Math.floor(Number(state.selectionEnd) || start), maxIndex));
|
|
8581
|
+
try { sourceTextEl.setSelectionRange(start, end); } catch {}
|
|
8582
|
+
if (typeof state.scrollTop === "number" && Number.isFinite(state.scrollTop)) {
|
|
8583
|
+
sourceTextEl.scrollTop = Math.max(0, state.scrollTop);
|
|
8584
|
+
}
|
|
8585
|
+
workspaceRestoredFromBrowser = true;
|
|
8586
|
+
initialDocumentApplied = true;
|
|
8587
|
+
return true;
|
|
8588
|
+
}
|
|
8589
|
+
|
|
8590
|
+
function clearStudioWorkspace() {
|
|
8591
|
+
if (uiBusy) {
|
|
8592
|
+
setStatus("Studio is busy.", "warning");
|
|
8593
|
+
return;
|
|
8594
|
+
}
|
|
8595
|
+
const confirmed = window.confirm("Clear the current editor draft in this browser tab? Saved files and responses are not changed.");
|
|
8596
|
+
if (!confirmed) return;
|
|
8597
|
+
const preservedResponseState = {
|
|
8598
|
+
responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
|
|
8599
|
+
responseHistoryIndex,
|
|
8600
|
+
queuedLatestResponse,
|
|
8601
|
+
followLatest,
|
|
8602
|
+
latestResponseMarkdown,
|
|
8603
|
+
latestResponseThinking,
|
|
8604
|
+
latestResponseTimestamp,
|
|
8605
|
+
latestResponseKind,
|
|
8606
|
+
latestResponseIsStructuredCritique,
|
|
8607
|
+
latestResponseHasContent,
|
|
8608
|
+
latestResponseNormalized,
|
|
8609
|
+
latestResponseThinkingNormalized,
|
|
8610
|
+
latestCritiqueNotes,
|
|
8611
|
+
latestCritiqueNotesNormalized,
|
|
8612
|
+
};
|
|
8613
|
+
clearPersistedWorkspaceState();
|
|
8614
|
+
if (resourceDirInput) resourceDirInput.value = "";
|
|
8615
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
8616
|
+
setEditorText("", { preserveScroll: false, preserveSelection: false });
|
|
8617
|
+
setSourceState({ source: "blank", label: "blank", path: null, draftId: makeStudioDraftId() });
|
|
8618
|
+
setEditorLanguage("markdown");
|
|
8619
|
+
setEditorView("markdown");
|
|
8620
|
+
responseHistory = preservedResponseState.responseHistory;
|
|
8621
|
+
responseHistoryIndex = preservedResponseState.responseHistoryIndex;
|
|
8622
|
+
queuedLatestResponse = preservedResponseState.queuedLatestResponse;
|
|
8623
|
+
followLatest = preservedResponseState.followLatest;
|
|
8624
|
+
latestResponseMarkdown = preservedResponseState.latestResponseMarkdown;
|
|
8625
|
+
latestResponseThinking = preservedResponseState.latestResponseThinking;
|
|
8626
|
+
latestResponseTimestamp = preservedResponseState.latestResponseTimestamp;
|
|
8627
|
+
latestResponseKind = preservedResponseState.latestResponseKind;
|
|
8628
|
+
latestResponseIsStructuredCritique = preservedResponseState.latestResponseIsStructuredCritique;
|
|
8629
|
+
latestResponseHasContent = preservedResponseState.latestResponseHasContent;
|
|
8630
|
+
latestResponseNormalized = preservedResponseState.latestResponseNormalized;
|
|
8631
|
+
latestResponseThinkingNormalized = preservedResponseState.latestResponseThinkingNormalized;
|
|
8632
|
+
latestCritiqueNotes = preservedResponseState.latestCritiqueNotes;
|
|
8633
|
+
latestCritiqueNotesNormalized = preservedResponseState.latestCritiqueNotesNormalized;
|
|
8634
|
+
if (followSelect) followSelect.value = followLatest ? "on" : "off";
|
|
8635
|
+
refreshResponseUi();
|
|
8636
|
+
persistWorkspaceStateNow();
|
|
8637
|
+
setStatus("Editor cleared. Saved files and responses were not changed.", "success");
|
|
8262
8638
|
}
|
|
8263
8639
|
|
|
8264
8640
|
function setEditorText(nextText, options) {
|
|
@@ -8308,6 +8684,7 @@
|
|
|
8308
8684
|
}
|
|
8309
8685
|
updateEditorSelectionCommentUi();
|
|
8310
8686
|
updateOutlineUi();
|
|
8687
|
+
scheduleWorkspacePersistence();
|
|
8311
8688
|
}
|
|
8312
8689
|
|
|
8313
8690
|
function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
|
|
@@ -8445,6 +8822,7 @@
|
|
|
8445
8822
|
updateReviewNotesUi();
|
|
8446
8823
|
updateEditorSelectionCommentUi();
|
|
8447
8824
|
updateOutlineUi();
|
|
8825
|
+
scheduleWorkspacePersistence();
|
|
8448
8826
|
}
|
|
8449
8827
|
|
|
8450
8828
|
function setRightView(nextView) {
|
|
@@ -8477,6 +8855,7 @@
|
|
|
8477
8855
|
|
|
8478
8856
|
refreshResponseUi();
|
|
8479
8857
|
syncActionButtons();
|
|
8858
|
+
scheduleWorkspacePersistence();
|
|
8480
8859
|
}
|
|
8481
8860
|
|
|
8482
8861
|
function lineNumbersShouldBeVisible() {
|
|
@@ -8785,6 +9164,383 @@
|
|
|
8785
9164
|
}
|
|
8786
9165
|
}
|
|
8787
9166
|
|
|
9167
|
+
const PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS = new Set([
|
|
9168
|
+
".md", ".markdown", ".mdx", ".qmd", ".txt", ".tex", ".latex", ".rst", ".adoc",
|
|
9169
|
+
".html", ".htm", ".css", ".xml", ".yaml", ".yml", ".toml", ".json", ".jsonc", ".json5", ".csv", ".tsv", ".log",
|
|
9170
|
+
".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx",
|
|
9171
|
+
".py", ".pyw", ".sh", ".bash", ".zsh", ".rs", ".c", ".h", ".cpp", ".cxx", ".cc", ".hpp", ".hxx",
|
|
9172
|
+
".jl", ".f90", ".f95", ".f03", ".f", ".for", ".r", ".m", ".java", ".go", ".rb", ".swift", ".lua",
|
|
9173
|
+
".diff", ".patch",
|
|
9174
|
+
]);
|
|
9175
|
+
const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
9176
|
+
let previewLinkMenuEl = null;
|
|
9177
|
+
let activePreviewLinkContext = null;
|
|
9178
|
+
|
|
9179
|
+
function stripPreviewLocalLinkUrlSuffix(href) {
|
|
9180
|
+
const raw = String(href || "").trim();
|
|
9181
|
+
const hashIndex = raw.indexOf("#");
|
|
9182
|
+
const queryIndex = raw.indexOf("?");
|
|
9183
|
+
let end = raw.length;
|
|
9184
|
+
if (queryIndex >= 0) end = Math.min(end, queryIndex);
|
|
9185
|
+
if (hashIndex >= 0) end = Math.min(end, hashIndex);
|
|
9186
|
+
return raw.slice(0, end);
|
|
9187
|
+
}
|
|
9188
|
+
|
|
9189
|
+
function parsePreviewLocalLinkPage(href) {
|
|
9190
|
+
const raw = String(href || "");
|
|
9191
|
+
const parts = [];
|
|
9192
|
+
const queryIndex = raw.indexOf("?");
|
|
9193
|
+
if (queryIndex >= 0) {
|
|
9194
|
+
const queryEnd = raw.indexOf("#", queryIndex);
|
|
9195
|
+
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
9196
|
+
}
|
|
9197
|
+
const hashIndex = raw.indexOf("#");
|
|
9198
|
+
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
9199
|
+
for (const part of parts) {
|
|
9200
|
+
try {
|
|
9201
|
+
const params = new URLSearchParams(part);
|
|
9202
|
+
const value = params.get("page") || params.get("p");
|
|
9203
|
+
if (value) {
|
|
9204
|
+
const page = Number.parseInt(value, 10);
|
|
9205
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
9206
|
+
}
|
|
9207
|
+
} catch {}
|
|
9208
|
+
const match = String(part || "").match(/(?:^|[&;])page=(\d+)/i) || String(part || "").match(/^page=(\d+)$/i);
|
|
9209
|
+
if (match && match[1]) {
|
|
9210
|
+
const page = Number.parseInt(match[1], 10);
|
|
9211
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
9212
|
+
}
|
|
9213
|
+
}
|
|
9214
|
+
return 0;
|
|
9215
|
+
}
|
|
9216
|
+
|
|
9217
|
+
function getPreviewLocalLinkExtension(href) {
|
|
9218
|
+
const path = stripPreviewLocalLinkUrlSuffix(href);
|
|
9219
|
+
const match = path.match(/\.([A-Za-z0-9_+-]+)$/);
|
|
9220
|
+
return match ? ("." + match[1].toLowerCase()) : "";
|
|
9221
|
+
}
|
|
9222
|
+
|
|
9223
|
+
function getPreviewLocalLinkKind(href) {
|
|
9224
|
+
const ext = getPreviewLocalLinkExtension(href);
|
|
9225
|
+
if (ext === ".pdf") return "pdf";
|
|
9226
|
+
if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext)) return "text";
|
|
9227
|
+
if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
|
|
9228
|
+
return "other";
|
|
9229
|
+
}
|
|
9230
|
+
|
|
9231
|
+
function isStudioLocalPreviewHref(href) {
|
|
9232
|
+
const raw = String(href || "").trim();
|
|
9233
|
+
if (!raw || raw.charAt(0) === "#") return false;
|
|
9234
|
+
if (/^\/\//.test(raw)) return false;
|
|
9235
|
+
if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
|
|
9236
|
+
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;
|
|
9237
|
+
return true;
|
|
9238
|
+
}
|
|
9239
|
+
|
|
9240
|
+
function getEffectivePreviewLinkContext(contextOverride) {
|
|
9241
|
+
const fallback = getHtmlPreviewResourceContextOptions();
|
|
9242
|
+
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : null;
|
|
9243
|
+
return {
|
|
9244
|
+
sourcePath: context && context.sourcePath ? String(context.sourcePath) : (fallback.sourcePath || ""),
|
|
9245
|
+
resourceDir: context && context.resourceDir ? String(context.resourceDir) : (fallback.resourceDir || ""),
|
|
9246
|
+
};
|
|
9247
|
+
}
|
|
9248
|
+
|
|
9249
|
+
function getPreviewLinkResourceQuery(path, contextOverride) {
|
|
9250
|
+
const context = getEffectivePreviewLinkContext(contextOverride);
|
|
9251
|
+
const query = { path: String(path || "") };
|
|
9252
|
+
if (context.sourcePath) query.sourcePath = String(context.sourcePath);
|
|
9253
|
+
if (context.resourceDir) query.resourceDir = String(context.resourceDir);
|
|
9254
|
+
return query;
|
|
9255
|
+
}
|
|
9256
|
+
|
|
9257
|
+
function getPreviewLinkAnchorFromEvent(event) {
|
|
9258
|
+
const target = event && event.target;
|
|
9259
|
+
const anchor = target instanceof Element ? target.closest("#sourcePreview a[href], #critiqueView a[href]") : null;
|
|
9260
|
+
if (!anchor) return null;
|
|
9261
|
+
if (anchor.closest(".studio-pdf-card, .studio-html-artifact-toolbar, .studio-copy-block-btn")) return null;
|
|
9262
|
+
const href = String(anchor.getAttribute("href") || "").trim();
|
|
9263
|
+
if (!isStudioLocalPreviewHref(href)) return null;
|
|
9264
|
+
return anchor;
|
|
9265
|
+
}
|
|
9266
|
+
|
|
9267
|
+
function closePreviewLinkMenu() {
|
|
9268
|
+
activePreviewLinkContext = null;
|
|
9269
|
+
if (previewLinkMenuEl) previewLinkMenuEl.hidden = true;
|
|
9270
|
+
}
|
|
9271
|
+
|
|
9272
|
+
function ensurePreviewLinkMenu() {
|
|
9273
|
+
if (previewLinkMenuEl) return previewLinkMenuEl;
|
|
9274
|
+
const menu = document.createElement("div");
|
|
9275
|
+
menu.className = "studio-preview-link-menu";
|
|
9276
|
+
menu.hidden = true;
|
|
9277
|
+
menu.setAttribute("role", "menu");
|
|
9278
|
+
document.body.appendChild(menu);
|
|
9279
|
+
previewLinkMenuEl = menu;
|
|
9280
|
+
return menu;
|
|
9281
|
+
}
|
|
9282
|
+
|
|
9283
|
+
function appendPreviewLinkMenuButton(menu, label, action) {
|
|
9284
|
+
const button = document.createElement("button");
|
|
9285
|
+
button.type = "button";
|
|
9286
|
+
button.setAttribute("role", "menuitem");
|
|
9287
|
+
button.dataset.previewLinkAction = action;
|
|
9288
|
+
button.textContent = label;
|
|
9289
|
+
menu.appendChild(button);
|
|
9290
|
+
}
|
|
9291
|
+
|
|
9292
|
+
function positionPreviewLinkMenu(menu, clientX, clientY) {
|
|
9293
|
+
const margin = 8;
|
|
9294
|
+
menu.style.left = "0px";
|
|
9295
|
+
menu.style.top = "0px";
|
|
9296
|
+
menu.hidden = false;
|
|
9297
|
+
const rect = menu.getBoundingClientRect();
|
|
9298
|
+
const x = Math.max(margin, Math.min(window.innerWidth - rect.width - margin, Number(clientX) || margin));
|
|
9299
|
+
const y = Math.max(margin, Math.min(window.innerHeight - rect.height - margin, Number(clientY) || margin));
|
|
9300
|
+
menu.style.left = x + "px";
|
|
9301
|
+
menu.style.top = y + "px";
|
|
9302
|
+
}
|
|
9303
|
+
|
|
9304
|
+
function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
9305
|
+
const href = String(anchor && anchor.getAttribute ? anchor.getAttribute("href") || "" : (contextOverride && contextOverride.href ? contextOverride.href : "")).trim();
|
|
9306
|
+
if (!isStudioLocalPreviewHref(href)) return false;
|
|
9307
|
+
const kind = getPreviewLocalLinkKind(href);
|
|
9308
|
+
const menu = ensurePreviewLinkMenu();
|
|
9309
|
+
menu.innerHTML = "";
|
|
9310
|
+
const linkContext = getEffectivePreviewLinkContext(contextOverride);
|
|
9311
|
+
activePreviewLinkContext = {
|
|
9312
|
+
href,
|
|
9313
|
+
title: String((contextOverride && contextOverride.title) || (anchor && anchor.textContent) || href || "local link").trim() || href,
|
|
9314
|
+
sourcePath: linkContext.sourcePath,
|
|
9315
|
+
resourceDir: linkContext.resourceDir,
|
|
9316
|
+
};
|
|
9317
|
+
if (kind === "pdf") {
|
|
9318
|
+
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
9319
|
+
} else if (kind === "text") {
|
|
9320
|
+
appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
|
|
9321
|
+
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
9322
|
+
} else if (kind === "image") {
|
|
9323
|
+
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
9324
|
+
}
|
|
9325
|
+
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
9326
|
+
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
9327
|
+
positionPreviewLinkMenu(menu, event && event.clientX, event && event.clientY);
|
|
9328
|
+
const firstButton = menu.querySelector("button");
|
|
9329
|
+
if (firstButton && typeof firstButton.focus === "function") {
|
|
9330
|
+
window.setTimeout(() => firstButton.focus({ preventScroll: true }), 0);
|
|
9331
|
+
}
|
|
9332
|
+
return true;
|
|
9333
|
+
}
|
|
9334
|
+
|
|
9335
|
+
async function fetchPreviewLocalLink(action, href, contextOverride) {
|
|
9336
|
+
return fetchStudioJson("/local-preview-link", {
|
|
9337
|
+
query: { ...getPreviewLinkResourceQuery(href, contextOverride), action },
|
|
9338
|
+
});
|
|
9339
|
+
}
|
|
9340
|
+
|
|
9341
|
+
function getPreviewPdfViewerUrl(href, contextOverride) {
|
|
9342
|
+
const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
|
|
9343
|
+
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
|
|
9344
|
+
const resourceUrl = buildStudioPdfResourceUrl({ path: cleanPath, sourcePath: context.sourcePath || "", resourceDir: context.resourceDir || "" }, true);
|
|
9345
|
+
const page = parsePreviewLocalLinkPage(href);
|
|
9346
|
+
return resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
9347
|
+
}
|
|
9348
|
+
|
|
9349
|
+
function openPreviewPdfLink(href, title, contextOverride) {
|
|
9350
|
+
const viewerUrl = getPreviewPdfViewerUrl(href, contextOverride);
|
|
9351
|
+
if (!viewerUrl) {
|
|
9352
|
+
setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
|
|
9353
|
+
return false;
|
|
9354
|
+
}
|
|
9355
|
+
openStudioPdfFocusViewer(viewerUrl, title || href);
|
|
9356
|
+
return true;
|
|
9357
|
+
}
|
|
9358
|
+
|
|
9359
|
+
async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
|
|
9360
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
9361
|
+
try {
|
|
9362
|
+
if (popup && popup.document && popup.document.body) {
|
|
9363
|
+
popup.document.title = "Opening image…";
|
|
9364
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening image…</p>";
|
|
9365
|
+
}
|
|
9366
|
+
} catch {}
|
|
9367
|
+
try {
|
|
9368
|
+
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
9369
|
+
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
9370
|
+
});
|
|
9371
|
+
const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
|
|
9372
|
+
if (!dataUrl) throw new Error("Studio did not return image data.");
|
|
9373
|
+
const safeTitle = escapeHtml(String(title || href || "Local image"));
|
|
9374
|
+
const safeSrc = escapeHtml(dataUrl);
|
|
9375
|
+
const html = "<!doctype html><html><head><meta charset='utf-8'><title>" + safeTitle + "</title>"
|
|
9376
|
+
+ "<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>"
|
|
9377
|
+
+ "</head><body><header>" + safeTitle + "</header><img src='" + safeSrc + "' alt='" + safeTitle + "'></body></html>";
|
|
9378
|
+
if (popup && !popup.closed && popup.document) {
|
|
9379
|
+
popup.document.open();
|
|
9380
|
+
popup.document.write(html);
|
|
9381
|
+
popup.document.close();
|
|
9382
|
+
setStatus("Opened local image preview.", "success");
|
|
9383
|
+
return;
|
|
9384
|
+
}
|
|
9385
|
+
const opened = window.open(dataUrl, "_blank");
|
|
9386
|
+
if (!opened) throw new Error("Popup blocked while opening image preview.");
|
|
9387
|
+
setStatus("Opened local image preview.", "success");
|
|
9388
|
+
} catch (error) {
|
|
9389
|
+
if (popup && !popup.closed) {
|
|
9390
|
+
try { popup.close(); } catch {}
|
|
9391
|
+
}
|
|
9392
|
+
throw error;
|
|
9393
|
+
}
|
|
9394
|
+
}
|
|
9395
|
+
|
|
9396
|
+
function editorHasPotentialUnsavedContent() {
|
|
9397
|
+
const text = String(sourceTextEl.value || "");
|
|
9398
|
+
if (!text.trim()) return false;
|
|
9399
|
+
if (hasRefreshableFilePath()) return editorDiffersFromFileBackedBaseline();
|
|
9400
|
+
return true;
|
|
9401
|
+
}
|
|
9402
|
+
|
|
9403
|
+
async function openPreviewDocumentHere(href, contextOverride) {
|
|
9404
|
+
if (editorHasPotentialUnsavedContent()) {
|
|
9405
|
+
const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
|
|
9406
|
+
if (!confirmed) return;
|
|
9407
|
+
}
|
|
9408
|
+
const payload = await fetchPreviewLocalLink("document", href, contextOverride);
|
|
9409
|
+
if (typeof payload.text !== "string") throw new Error("Studio did not return document text.");
|
|
9410
|
+
const path = typeof payload.path === "string" ? payload.path : "";
|
|
9411
|
+
const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
|
|
9412
|
+
const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
|
|
9413
|
+
if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
|
|
9414
|
+
setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
|
|
9415
|
+
setSourceState({ source: "file", label, path });
|
|
9416
|
+
markFileBackedBaseline(payload.text);
|
|
9417
|
+
const detected = detectLanguageFromName(path || label);
|
|
9418
|
+
if (detected) setEditorLanguage(detected);
|
|
9419
|
+
setEditorView("markdown");
|
|
9420
|
+
setActivePane("left");
|
|
9421
|
+
setStatus("Opened linked file in editor: " + label, "success");
|
|
9422
|
+
}
|
|
9423
|
+
|
|
9424
|
+
async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
|
|
9425
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
9426
|
+
try {
|
|
9427
|
+
if (popup && popup.document && popup.document.body) {
|
|
9428
|
+
popup.document.title = "Opening linked file…";
|
|
9429
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening linked file…</p>";
|
|
9430
|
+
}
|
|
9431
|
+
} catch {}
|
|
9432
|
+
try {
|
|
9433
|
+
const payload = await fetchPreviewLocalLink("editor-url", href, contextOverride);
|
|
9434
|
+
const targetUrl = payload && typeof payload.relativeUrl === "string"
|
|
9435
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
9436
|
+
: (payload && typeof payload.url === "string" ? payload.url : "");
|
|
9437
|
+
if (!targetUrl) throw new Error("Studio did not return an editor URL.");
|
|
9438
|
+
if (popup && !popup.closed) {
|
|
9439
|
+
try {
|
|
9440
|
+
popup.opener = null;
|
|
9441
|
+
popup.location.href = targetUrl;
|
|
9442
|
+
setStatus("Opening linked file in a new editor.", "success");
|
|
9443
|
+
return;
|
|
9444
|
+
} catch {}
|
|
9445
|
+
}
|
|
9446
|
+
window.open(targetUrl, "_blank", "noopener");
|
|
9447
|
+
setStatus("Opening linked file in a new editor.", "success");
|
|
9448
|
+
} catch (error) {
|
|
9449
|
+
if (popup && !popup.closed) {
|
|
9450
|
+
try { popup.close(); } catch {}
|
|
9451
|
+
}
|
|
9452
|
+
throw error;
|
|
9453
|
+
}
|
|
9454
|
+
}
|
|
9455
|
+
|
|
9456
|
+
async function copyPreviewLocalLinkPath(href, contextOverride) {
|
|
9457
|
+
const payload = await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
9458
|
+
const path = typeof payload.path === "string" ? payload.path : "";
|
|
9459
|
+
if (!path) throw new Error("Studio did not return a file path.");
|
|
9460
|
+
const ok = await writeTextToClipboard(path);
|
|
9461
|
+
if (!ok) throw new Error("Clipboard write failed.");
|
|
9462
|
+
setStatus("Copied local path.", "success");
|
|
9463
|
+
}
|
|
9464
|
+
|
|
9465
|
+
async function revealPreviewLocalLink(href, contextOverride) {
|
|
9466
|
+
const query = getPreviewLinkResourceQuery(href, contextOverride);
|
|
9467
|
+
const payload = await fetchStudioJson("/reveal-local-resource", {
|
|
9468
|
+
method: "POST",
|
|
9469
|
+
body: JSON.stringify(query),
|
|
9470
|
+
});
|
|
9471
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Opened file manager.", "success");
|
|
9472
|
+
}
|
|
9473
|
+
|
|
9474
|
+
async function runPreviewLinkAction(action, context) {
|
|
9475
|
+
const href = context && context.href ? context.href : "";
|
|
9476
|
+
if (!href) return;
|
|
9477
|
+
try {
|
|
9478
|
+
if (action === "open-pdf") {
|
|
9479
|
+
openPreviewPdfLink(href, context.title || href, context);
|
|
9480
|
+
return;
|
|
9481
|
+
}
|
|
9482
|
+
if (action === "open-new") {
|
|
9483
|
+
await openPreviewDocumentInNewEditor(href, null, context);
|
|
9484
|
+
return;
|
|
9485
|
+
}
|
|
9486
|
+
if (action === "open-here") {
|
|
9487
|
+
await openPreviewDocumentHere(href, context);
|
|
9488
|
+
return;
|
|
9489
|
+
}
|
|
9490
|
+
if (action === "open-image") {
|
|
9491
|
+
await openPreviewImageLink(href, context.title || href, context);
|
|
9492
|
+
return;
|
|
9493
|
+
}
|
|
9494
|
+
if (action === "copy-path") {
|
|
9495
|
+
await copyPreviewLocalLinkPath(href, context);
|
|
9496
|
+
return;
|
|
9497
|
+
}
|
|
9498
|
+
if (action === "reveal") {
|
|
9499
|
+
await revealPreviewLocalLink(href, context);
|
|
9500
|
+
}
|
|
9501
|
+
} catch (error) {
|
|
9502
|
+
setStatus((error && error.message) ? error.message : String(error || "Local link action failed."), "warning");
|
|
9503
|
+
}
|
|
9504
|
+
}
|
|
9505
|
+
|
|
9506
|
+
function handlePreviewLocalLinkClick(event) {
|
|
9507
|
+
const anchor = getPreviewLinkAnchorFromEvent(event);
|
|
9508
|
+
if (!anchor) return;
|
|
9509
|
+
const href = String(anchor.getAttribute("href") || "").trim();
|
|
9510
|
+
const kind = getPreviewLocalLinkKind(href);
|
|
9511
|
+
event.preventDefault();
|
|
9512
|
+
event.stopPropagation();
|
|
9513
|
+
closePreviewLinkMenu();
|
|
9514
|
+
const title = String(anchor.textContent || href).trim() || href;
|
|
9515
|
+
if (kind === "pdf") {
|
|
9516
|
+
openPreviewPdfLink(href, title);
|
|
9517
|
+
return;
|
|
9518
|
+
}
|
|
9519
|
+
if (kind === "image") {
|
|
9520
|
+
const pendingWindow = window.open("", "_blank");
|
|
9521
|
+
void openPreviewImageLink(href, title, null, pendingWindow).catch((error) => {
|
|
9522
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
9523
|
+
});
|
|
9524
|
+
return;
|
|
9525
|
+
}
|
|
9526
|
+
if (kind === "text") {
|
|
9527
|
+
const pendingWindow = window.open("", "_blank");
|
|
9528
|
+
void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
|
|
9529
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
9530
|
+
});
|
|
9531
|
+
return;
|
|
9532
|
+
}
|
|
9533
|
+
setStatus("Right-click this local link for file actions.", "warning");
|
|
9534
|
+
}
|
|
9535
|
+
|
|
9536
|
+
function handlePreviewLocalLinkContextMenu(event) {
|
|
9537
|
+
const anchor = getPreviewLinkAnchorFromEvent(event);
|
|
9538
|
+
if (!anchor) return;
|
|
9539
|
+
event.preventDefault();
|
|
9540
|
+
event.stopPropagation();
|
|
9541
|
+
showPreviewLinkMenu(anchor, event);
|
|
9542
|
+
}
|
|
9543
|
+
|
|
8788
9544
|
function makeRequestId() {
|
|
8789
9545
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
8790
9546
|
return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -9002,10 +9758,6 @@
|
|
|
9002
9758
|
return index > 0 ? value.slice(0, index) : "";
|
|
9003
9759
|
}
|
|
9004
9760
|
|
|
9005
|
-
function getCurrentResourceDirValue() {
|
|
9006
|
-
return resourceDirInput ? String(resourceDirInput.value || "").trim() : "";
|
|
9007
|
-
}
|
|
9008
|
-
|
|
9009
9761
|
function getDefaultQuizContextPath(scope) {
|
|
9010
9762
|
const normalizedScope = normalizeQuizScope(scope);
|
|
9011
9763
|
const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
|
|
@@ -14656,6 +15408,7 @@
|
|
|
14656
15408
|
scheduleSourcePreviewRender(0);
|
|
14657
15409
|
}
|
|
14658
15410
|
updateOutlineUi();
|
|
15411
|
+
scheduleWorkspacePersistence();
|
|
14659
15412
|
}
|
|
14660
15413
|
|
|
14661
15414
|
function setEditorHighlightMode(mode) {
|
|
@@ -15384,6 +16137,10 @@
|
|
|
15384
16137
|
stickyStudioKind = null;
|
|
15385
16138
|
}
|
|
15386
16139
|
if (message.path) {
|
|
16140
|
+
const savedResourceDir = typeof message.resourceDir === "string" && message.resourceDir.trim()
|
|
16141
|
+
? normalizeStudioResourceDirValue(message.resourceDir)
|
|
16142
|
+
: dirnameForDisplayPath(message.path);
|
|
16143
|
+
if (resourceDirInput) resourceDirInput.value = savedResourceDir;
|
|
15387
16144
|
setSourceState({
|
|
15388
16145
|
source: "file",
|
|
15389
16146
|
label: message.label || message.path,
|
|
@@ -15459,6 +16216,10 @@
|
|
|
15459
16216
|
? nextDoc.path
|
|
15460
16217
|
: null;
|
|
15461
16218
|
|
|
16219
|
+
const nextResourceDir = typeof nextDoc.resourceDir === "string" && nextDoc.resourceDir.trim()
|
|
16220
|
+
? normalizeStudioResourceDirValue(nextDoc.resourceDir)
|
|
16221
|
+
: (nextPath ? dirnameForDisplayPath(nextPath) : "");
|
|
16222
|
+
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
15462
16223
|
setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
|
|
15463
16224
|
setSourceState({
|
|
15464
16225
|
source: nextSource,
|
|
@@ -16027,6 +16788,7 @@
|
|
|
16027
16788
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
16028
16789
|
window.addEventListener("beforeunload", () => {
|
|
16029
16790
|
stopFooterSpinner();
|
|
16791
|
+
flushWorkspacePersistence();
|
|
16030
16792
|
flushScratchpadPersistence();
|
|
16031
16793
|
flushReviewNotesPersistence();
|
|
16032
16794
|
});
|
|
@@ -16043,6 +16805,7 @@
|
|
|
16043
16805
|
|
|
16044
16806
|
followSelect.addEventListener("change", () => {
|
|
16045
16807
|
followLatest = followSelect.value !== "off";
|
|
16808
|
+
scheduleWorkspacePersistence();
|
|
16046
16809
|
if (followLatest && queuedLatestResponse) {
|
|
16047
16810
|
if (responseHistory.length > 0) {
|
|
16048
16811
|
selectHistoryIndex(responseHistory.length - 1, { silent: true });
|
|
@@ -16206,6 +16969,7 @@
|
|
|
16206
16969
|
renderReviewNotesList();
|
|
16207
16970
|
updateReviewNotesUi();
|
|
16208
16971
|
}
|
|
16972
|
+
scheduleWorkspacePersistence();
|
|
16209
16973
|
});
|
|
16210
16974
|
|
|
16211
16975
|
sourceTextEl.addEventListener("select", () => {
|
|
@@ -16382,7 +17146,10 @@
|
|
|
16382
17146
|
closeExportPreviewMenu();
|
|
16383
17147
|
});
|
|
16384
17148
|
document.addEventListener("keydown", (event) => {
|
|
16385
|
-
if (event.key === "Escape")
|
|
17149
|
+
if (event.key === "Escape") {
|
|
17150
|
+
closeExportPreviewMenu();
|
|
17151
|
+
closePreviewLinkMenu();
|
|
17152
|
+
}
|
|
16386
17153
|
});
|
|
16387
17154
|
|
|
16388
17155
|
saveAsBtn.addEventListener("click", () => {
|
|
@@ -16393,7 +17160,7 @@
|
|
|
16393
17160
|
}
|
|
16394
17161
|
|
|
16395
17162
|
var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
|
|
16396
|
-
var suggestedDir =
|
|
17163
|
+
var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
|
|
16397
17164
|
const suggested = sourceState.path || (suggestedDir + suggestedName);
|
|
16398
17165
|
const path = window.prompt("Save editor content as:", suggested);
|
|
16399
17166
|
if (!path) return;
|
|
@@ -16472,6 +17239,12 @@
|
|
|
16472
17239
|
});
|
|
16473
17240
|
}
|
|
16474
17241
|
|
|
17242
|
+
if (clearWorkspaceBtn) {
|
|
17243
|
+
clearWorkspaceBtn.addEventListener("click", () => {
|
|
17244
|
+
clearStudioWorkspace();
|
|
17245
|
+
});
|
|
17246
|
+
}
|
|
17247
|
+
|
|
16475
17248
|
sendEditorBtn.addEventListener("click", () => {
|
|
16476
17249
|
const content = sourceTextEl.value;
|
|
16477
17250
|
if (!content.trim()) {
|
|
@@ -16509,9 +17282,7 @@
|
|
|
16509
17282
|
content,
|
|
16510
17283
|
label: sourceState && sourceState.label ? sourceState.label : "current editor",
|
|
16511
17284
|
path: sourceState && sourceState.path ? sourceState.path : undefined,
|
|
16512
|
-
resourceDir:
|
|
16513
|
-
? resourceDirInput.value.trim()
|
|
16514
|
-
: undefined,
|
|
17285
|
+
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
16515
17286
|
});
|
|
16516
17287
|
|
|
16517
17288
|
if (!sent) {
|
|
@@ -16551,9 +17322,7 @@
|
|
|
16551
17322
|
type: "load_git_diff_request",
|
|
16552
17323
|
requestId,
|
|
16553
17324
|
sourcePath: effectivePath || sourceState.path || undefined,
|
|
16554
|
-
resourceDir:
|
|
16555
|
-
? resourceDirInput.value.trim()
|
|
16556
|
-
: undefined,
|
|
17325
|
+
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
16557
17326
|
});
|
|
16558
17327
|
|
|
16559
17328
|
if (!sent) {
|
|
@@ -16772,6 +17541,27 @@
|
|
|
16772
17541
|
void handleCopyPreviewBlockButtonClick(event);
|
|
16773
17542
|
}, true);
|
|
16774
17543
|
|
|
17544
|
+
document.addEventListener("click", (event) => {
|
|
17545
|
+
const target = event.target;
|
|
17546
|
+
const menuButton = target instanceof Element ? target.closest(".studio-preview-link-menu [data-preview-link-action]") : null;
|
|
17547
|
+
if (menuButton) {
|
|
17548
|
+
event.preventDefault();
|
|
17549
|
+
event.stopPropagation();
|
|
17550
|
+
const action = String(menuButton.getAttribute("data-preview-link-action") || "");
|
|
17551
|
+
const context = activePreviewLinkContext;
|
|
17552
|
+
closePreviewLinkMenu();
|
|
17553
|
+
void runPreviewLinkAction(action, context);
|
|
17554
|
+
return;
|
|
17555
|
+
}
|
|
17556
|
+
if (target instanceof Element && target.closest(".studio-preview-link-menu")) return;
|
|
17557
|
+
closePreviewLinkMenu();
|
|
17558
|
+
handlePreviewLocalLinkClick(event);
|
|
17559
|
+
}, true);
|
|
17560
|
+
|
|
17561
|
+
document.addEventListener("contextmenu", (event) => {
|
|
17562
|
+
handlePreviewLocalLinkContextMenu(event);
|
|
17563
|
+
}, true);
|
|
17564
|
+
|
|
16775
17565
|
document.addEventListener("pointerup", (event) => {
|
|
16776
17566
|
const target = event.target;
|
|
16777
17567
|
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|
|
@@ -16962,7 +17752,8 @@
|
|
|
16962
17752
|
if (resourceDirLabel) resourceDirLabel.hidden = state !== "label";
|
|
16963
17753
|
}
|
|
16964
17754
|
function applyResourceDir() {
|
|
16965
|
-
var dir =
|
|
17755
|
+
var dir = getCurrentResourceDirValue();
|
|
17756
|
+
if (resourceDirInput) resourceDirInput.value = dir;
|
|
16966
17757
|
if (dir) {
|
|
16967
17758
|
if (resourceDirLabel) resourceDirLabel.textContent = "Working dir: " + dir;
|
|
16968
17759
|
showResourceDirState("label");
|
|
@@ -16972,6 +17763,7 @@
|
|
|
16972
17763
|
updateSaveFileTooltip();
|
|
16973
17764
|
syncActionButtons();
|
|
16974
17765
|
renderSourcePreview();
|
|
17766
|
+
scheduleWorkspacePersistence();
|
|
16975
17767
|
}
|
|
16976
17768
|
if (sourceBadgeEl) {
|
|
16977
17769
|
sourceBadgeEl.addEventListener("click", () => {
|
|
@@ -16997,7 +17789,7 @@
|
|
|
16997
17789
|
applyResourceDir();
|
|
16998
17790
|
} else if (e.key === "Escape") {
|
|
16999
17791
|
e.preventDefault();
|
|
17000
|
-
var dir =
|
|
17792
|
+
var dir = getCurrentResourceDirValue();
|
|
17001
17793
|
if (dir) {
|
|
17002
17794
|
showResourceDirState("label");
|
|
17003
17795
|
} else {
|
|
@@ -17014,6 +17806,7 @@
|
|
|
17014
17806
|
updateSaveFileTooltip();
|
|
17015
17807
|
syncActionButtons();
|
|
17016
17808
|
renderSourcePreview();
|
|
17809
|
+
scheduleWorkspacePersistence();
|
|
17017
17810
|
});
|
|
17018
17811
|
}
|
|
17019
17812
|
|
|
@@ -17062,7 +17855,7 @@
|
|
|
17062
17855
|
setResponseFontSize(initialResponseFontSize, { persist: false });
|
|
17063
17856
|
|
|
17064
17857
|
if (resourceDirInput && initialResourceDir) {
|
|
17065
|
-
resourceDirInput.value = initialResourceDir;
|
|
17858
|
+
resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
|
|
17066
17859
|
}
|
|
17067
17860
|
setSourceState(initialSourceState);
|
|
17068
17861
|
refreshResponseUi();
|
|
@@ -17090,9 +17883,16 @@
|
|
|
17090
17883
|
setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
|
|
17091
17884
|
setReplSendMode(replSendMode);
|
|
17092
17885
|
|
|
17886
|
+
const persistedWorkspaceState = readPersistedWorkspaceState();
|
|
17887
|
+
applyPersistedWorkspaceState(persistedWorkspaceState);
|
|
17888
|
+
|
|
17093
17889
|
setEditorView(editorView);
|
|
17094
17890
|
setRightView(rightView);
|
|
17095
17891
|
renderSourcePreview();
|
|
17892
|
+
workspacePersistenceReady = true;
|
|
17893
|
+
if (workspaceRestoredFromBrowser) {
|
|
17894
|
+
setStatus("Restored editor workspace from this browser tab. Use Clear editor to discard it.", "success");
|
|
17895
|
+
}
|
|
17096
17896
|
connect();
|
|
17097
17897
|
} catch (error) {
|
|
17098
17898
|
hardFail("Studio UI init failed", error);
|