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.
@@ -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
- if (isEditorOnlyMode) {
2184
- setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel);
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", isEditorOnlyMode ? [annotationModeSelect] : [annotationModeSelect, insertHeaderBtn]);
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
- if (resourceDirInput) resourceDirInput.value = "";
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 = resourceDirInput ? resourceDirInput.value.trim() : "";
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 || isEditorOnlyMode) return;
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
- } else if (record && record.resourceDir) {
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 = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
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
- } else if (resourceDir) {
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) ? resourceDirInput.value.trim() : "",
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) ? resourceDirInput.value.trim() : "";
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) ? resourceDirInput.value.trim() : "";
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 && !(resourceDirInput && resourceDirInput.value.trim())) {
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
- if (sourceState.source === "upload" && sourceState.label && resourceDirInput && resourceDirInput.value.trim()) {
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 resourceDirInput.value.trim().replace(/\/$/, "") + "/" + name;
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 = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
8270
+ const resourceDir = getCurrentResourceDirValue();
8148
8271
  return {
8149
8272
  sourcePath,
8150
- resourceDir: sourcePath ? "" : 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 = resourceDirInput && resourceDirInput.value.trim()
8167
- ? resourceDirInput.value.trim().replace(/\/$/, "") + "/"
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 || isEditorOnlyMode;
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") closeExportPreviewMenu();
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 = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim().replace(/\/$/, "") + "/" : "./";
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: resourceDirInput && resourceDirInput.value.trim()
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: resourceDirInput && resourceDirInput.value.trim()
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 = resourceDirInput ? resourceDirInput.value.trim() : "";
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 = resourceDirInput.value.trim();
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);