pi-studio 0.9.11 → 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.
@@ -61,6 +61,7 @@
61
61
  const editorSelectionJumpBtn = document.getElementById("editorSelectionJumpBtn");
62
62
  const leftPaneEl = document.getElementById("leftPane");
63
63
  const rightPaneEl = document.getElementById("rightPane");
64
+ const paneResizeHandleEl = document.getElementById("paneResizeHandle");
64
65
  const sourceBadgeEl = document.getElementById("sourceBadge");
65
66
  const syncBadgeEl = document.getElementById("syncBadge");
66
67
  let critiqueViewEl = document.getElementById("critiqueView");
@@ -102,6 +103,7 @@
102
103
  const saveAsBtn = document.getElementById("saveAsBtn");
103
104
  const saveOverBtn = document.getElementById("saveOverBtn");
104
105
  const refreshFromDiskBtn = document.getElementById("refreshFromDiskBtn");
106
+ const clearWorkspaceBtn = document.getElementById("clearWorkspaceBtn");
105
107
  const sendEditorBtn = document.getElementById("sendEditorBtn");
106
108
  const openCompanionBtn = document.getElementById("openCompanionBtn");
107
109
  const getEditorBtn = document.getElementById("getEditorBtn");
@@ -246,6 +248,7 @@
246
248
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
247
249
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
248
250
  const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
251
+ const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
249
252
  const EDITOR_TAB_TEXT = " ";
250
253
  const QUIZ_DEFAULT_COUNT = 5;
251
254
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
@@ -288,6 +291,15 @@
288
291
  return "python";
289
292
  }
290
293
  })();
294
+ let replCommandOverrides = (() => {
295
+ try {
296
+ const raw = window.localStorage && window.localStorage.getItem("piStudio.replCommandOverrides");
297
+ const parsed = raw ? JSON.parse(raw) : {};
298
+ return parsed && typeof parsed === "object" ? parsed : {};
299
+ } catch {
300
+ return {};
301
+ }
302
+ })();
291
303
  let replTranscript = "";
292
304
  let replError = "";
293
305
  let replMessage = "";
@@ -952,11 +964,26 @@
952
964
  setStatus("Loaded visible working into editor.", "success");
953
965
  }
954
966
 
955
- function normalizeReplRuntime(value) {
967
+ function getKnownReplRuntime(value) {
956
968
  const runtime = String(value || "").trim().toLowerCase();
957
969
  return runtime === "shell" || runtime === "python" || runtime === "ipython" || runtime === "julia" || runtime === "r" || runtime === "ghci" || runtime === "clojure"
958
970
  ? runtime
959
- : "python";
971
+ : "";
972
+ }
973
+
974
+ function normalizeReplRuntime(value) {
975
+ return getKnownReplRuntime(value) || "python";
976
+ }
977
+
978
+ function getReplRuntimeLabel(value) {
979
+ const runtime = normalizeReplRuntime(value);
980
+ if (runtime === "shell") return "Shell";
981
+ if (runtime === "python") return "Python";
982
+ if (runtime === "ipython") return "IPython";
983
+ if (runtime === "julia") return "Julia";
984
+ if (runtime === "r") return "R";
985
+ if (runtime === "ghci") return "GHCi";
986
+ return "Clojure";
960
987
  }
961
988
 
962
989
  function normalizeReplSession(session) {
@@ -981,6 +1008,47 @@
981
1008
  }
982
1009
  }
983
1010
 
1011
+ function normalizeReplCommandOverride(value) {
1012
+ return String(value || "").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 240);
1013
+ }
1014
+
1015
+ function getReplCommandOverride(runtime) {
1016
+ const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
1017
+ const value = replCommandOverrides && typeof replCommandOverrides === "object"
1018
+ ? replCommandOverrides[normalizedRuntime]
1019
+ : "";
1020
+ return normalizeReplCommandOverride(value);
1021
+ }
1022
+
1023
+ function persistReplCommandOverrides() {
1024
+ try {
1025
+ if (window.localStorage) window.localStorage.setItem("piStudio.replCommandOverrides", JSON.stringify(replCommandOverrides || {}));
1026
+ } catch {
1027
+ // Ignore storage failures.
1028
+ }
1029
+ }
1030
+
1031
+ function setReplCommandOverride(runtime, command) {
1032
+ const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
1033
+ const normalizedCommand = normalizeReplCommandOverride(command);
1034
+ replCommandOverrides = replCommandOverrides && typeof replCommandOverrides === "object" ? replCommandOverrides : {};
1035
+ if (normalizedCommand) {
1036
+ replCommandOverrides[normalizedRuntime] = normalizedCommand;
1037
+ } else {
1038
+ delete replCommandOverrides[normalizedRuntime];
1039
+ }
1040
+ persistReplCommandOverrides();
1041
+ }
1042
+
1043
+ function getCurrentReplStartCommandFromDom() {
1044
+ if (!critiqueViewEl || typeof critiqueViewEl.querySelector !== "function") return getReplCommandOverride(replRuntime);
1045
+ const inputEl = critiqueViewEl.querySelector("[data-repl-command]");
1046
+ if (inputEl && "value" in inputEl) {
1047
+ return normalizeReplCommandOverride(inputEl.value);
1048
+ }
1049
+ return getReplCommandOverride(replRuntime);
1050
+ }
1051
+
984
1052
  function normalizeReplSendMode(value) {
985
1053
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
986
1054
  }
@@ -1056,7 +1124,10 @@
1056
1124
  ? sessions.map(normalizeReplSession).filter(Boolean)
1057
1125
  : [];
1058
1126
  if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
1059
- replActiveSessionName = replSessions[0] ? replSessions[0].sessionName : "";
1127
+ setActiveReplSession("");
1128
+ }
1129
+ if (!getActiveReplSessionForCurrentRuntime()) {
1130
+ selectReplSessionForRuntime(replRuntime, replActiveSessionName);
1060
1131
  }
1061
1132
  return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
1062
1133
  }
@@ -1065,9 +1136,43 @@
1065
1136
  return replSessions.find((session) => session.sessionName === replActiveSessionName) || null;
1066
1137
  }
1067
1138
 
1139
+ function isReplSessionRuntimeCompatible(session, runtime) {
1140
+ if (!session) return false;
1141
+ const sessionRuntime = getKnownReplRuntime(session.runtime);
1142
+ return Boolean(sessionRuntime) && sessionRuntime === normalizeReplRuntime(runtime);
1143
+ }
1144
+
1145
+ function getReplSessionsForRuntime(runtime) {
1146
+ const normalizedRuntime = normalizeReplRuntime(runtime);
1147
+ return replSessions.filter((session) => isReplSessionRuntimeCompatible(session, normalizedRuntime));
1148
+ }
1149
+
1150
+ function selectReplSessionForRuntime(runtime, preferredSessionName) {
1151
+ const sessions = getReplSessionsForRuntime(runtime);
1152
+ const preferred = String(preferredSessionName || "").trim();
1153
+ const selected = sessions.find((session) => session.sessionName === preferred) || sessions[0] || null;
1154
+ setActiveReplSession(selected ? selected.sessionName : "");
1155
+ return selected;
1156
+ }
1157
+
1158
+ function setActiveReplSessionForCurrentRuntime(sessionName) {
1159
+ const name = String(sessionName || "").trim();
1160
+ const candidate = name ? replSessions.find((session) => session.sessionName === name) : null;
1161
+ if (candidate && isReplSessionRuntimeCompatible(candidate, replRuntime)) {
1162
+ setActiveReplSession(candidate.sessionName);
1163
+ return candidate;
1164
+ }
1165
+ return selectReplSessionForRuntime(replRuntime, replActiveSessionName);
1166
+ }
1167
+
1168
+ function getActiveReplSessionForCurrentRuntime() {
1169
+ const session = getActiveReplSession();
1170
+ return isReplSessionRuntimeCompatible(session, replRuntime) ? session : null;
1171
+ }
1172
+
1068
1173
  function buildActiveReplPromptContext() {
1069
1174
  if (rightView !== "repl") return "";
1070
- const session = getActiveReplSession();
1175
+ const session = getActiveReplSessionForCurrentRuntime();
1071
1176
  if (!session) return "";
1072
1177
  const runtime = session.runtime && session.runtime !== "unknown" ? session.runtime : "unknown";
1073
1178
  return [
@@ -1162,7 +1267,7 @@
1162
1267
  }
1163
1268
 
1164
1269
  function getActiveReplRuntime() {
1165
- const session = getActiveReplSession();
1270
+ const session = getActiveReplSessionForCurrentRuntime();
1166
1271
  if (session && session.runtime && session.runtime !== "unknown") return normalizeReplRuntime(session.runtime);
1167
1272
  return normalizeReplRuntime(replRuntime);
1168
1273
  }
@@ -1502,7 +1607,7 @@
1502
1607
 
1503
1608
  function stripStudioReplSubmissionEcho(delta) {
1504
1609
  let value = String(delta || "").replace(/^\s+/, "");
1505
- // The raw mirror below remains raw; REPL Studio cards hide only the
1610
+ // The raw mirror below remains raw; Studio record cards hide only the
1506
1611
  // temp-file wrapper used to submit multiline snippets safely. The
1507
1612
  // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
1508
1613
  const submissionEchoPatterns = [
@@ -1566,11 +1671,11 @@
1566
1671
  function buildReplJournalMarkdown(entries) {
1567
1672
  const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1568
1673
  const sessionName = getActiveReplJournalSessionName();
1569
- const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString()];
1674
+ const lines = ["# Studio REPL Record", "", "Generated: " + new Date().toLocaleString()];
1570
1675
  if (sessionName) lines.push("Session: `" + sessionName + "`");
1571
1676
  lines.push("");
1572
1677
  if (!visibleEntries.length) {
1573
- lines.push(sessionName ? ("_No REPL Studio entries for `" + sessionName + "` yet._") : "_No REPL Studio entries yet._");
1678
+ lines.push(sessionName ? ("_No Studio REPL record entries for `" + sessionName + "` yet._") : "_No Studio REPL record entries yet._");
1574
1679
  return lines.join("\n");
1575
1680
  }
1576
1681
  visibleEntries.forEach((entry, index) => {
@@ -1602,11 +1707,11 @@
1602
1707
  async function copyReplJournalToClipboard() {
1603
1708
  const entries = getVisibleReplJournalEntries();
1604
1709
  if (!entries.length) {
1605
- setStatus("No REPL Studio entries to copy for this session yet.", "warning");
1710
+ setStatus("No Studio REPL record entries to copy for this session yet.", "warning");
1606
1711
  return;
1607
1712
  }
1608
1713
  if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
1609
- setStatus("Copied REPL Studio session as Markdown.", "success");
1714
+ setStatus("Copied Studio REPL record as Markdown.", "success");
1610
1715
  } else {
1611
1716
  setStatus("Clipboard write failed.", "warning");
1612
1717
  }
@@ -1615,7 +1720,7 @@
1615
1720
  function exportReplJournalMarkdown() {
1616
1721
  const entries = getVisibleReplJournalEntries();
1617
1722
  if (!entries.length) {
1618
- setStatus("No REPL Studio entries to export for this session yet.", "warning");
1723
+ setStatus("No Studio REPL record entries to export for this session yet.", "warning");
1619
1724
  return;
1620
1725
  }
1621
1726
  const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
@@ -1629,7 +1734,7 @@
1629
1734
  link.click();
1630
1735
  link.remove();
1631
1736
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1632
- setStatus("Exported REPL Studio session Markdown.", "success");
1737
+ setStatus("Exported Studio REPL record Markdown.", "success");
1633
1738
  }
1634
1739
 
1635
1740
  function clearReplJournal() {
@@ -1641,27 +1746,27 @@
1641
1746
  }
1642
1747
  activeReplJournalEntryId = "";
1643
1748
  persistReplJournalEntries();
1644
- setStatus(sessionName ? "Cleared REPL Studio for this session." : "Cleared REPL Studio.", "success");
1749
+ setStatus(sessionName ? "Cleared Studio REPL record for this session." : "Cleared Studio REPL record.", "success");
1645
1750
  renderReplViewIfActive({ force: true });
1646
1751
  }
1647
1752
 
1648
1753
  function loadReplJournalIntoEditor() {
1649
1754
  const entries = getVisibleReplJournalEntries();
1650
1755
  if (!entries.length) {
1651
- setStatus("No REPL Studio entries to load for this session yet.", "warning");
1756
+ setStatus("No Studio REPL record entries to load for this session yet.", "warning");
1652
1757
  return;
1653
1758
  }
1654
1759
  const markdown = buildReplJournalMarkdown(entries);
1655
1760
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1656
- setSourceState({ source: "blank", label: "REPL Studio", path: null });
1761
+ setSourceState({ source: "blank", label: "Studio REPL Record", path: null });
1657
1762
  setEditorLanguage("markdown");
1658
- setStatus("Loaded REPL Studio session into editor.", "success");
1763
+ setStatus("Loaded Studio REPL record into editor.", "success");
1659
1764
  }
1660
1765
 
1661
1766
  function addSelectedReplJournalNote() {
1662
1767
  const note = getSelectedOrCurrentParagraphForReplNote();
1663
1768
  if (!note.trim()) {
1664
- setStatus("Select prose or place the cursor in a paragraph to add a REPL Studio note.", "warning");
1769
+ setStatus("Select prose or place the cursor in a paragraph to add a Studio REPL record note.", "warning");
1665
1770
  return;
1666
1771
  }
1667
1772
  addReplJournalEntry({
@@ -1672,14 +1777,14 @@
1672
1777
  sessionName: replActiveSessionName,
1673
1778
  runtime: getActiveReplRuntime(),
1674
1779
  });
1675
- setStatus("Added note to REPL Studio.", "success");
1780
+ setStatus("Added note to Studio REPL record.", "success");
1676
1781
  renderReplViewIfActive({ force: true });
1677
1782
  }
1678
1783
 
1679
1784
  function sendReplPayload(payload) {
1680
- const session = getActiveReplSession();
1785
+ const session = getActiveReplSessionForCurrentRuntime();
1681
1786
  if (!session) {
1682
- setStatus("Start or select a REPL session first.", "warning");
1787
+ setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
1683
1788
  return;
1684
1789
  }
1685
1790
  if (!payload || payload.error) {
@@ -1697,7 +1802,7 @@
1697
1802
  runtime: getActiveReplRuntime(),
1698
1803
  skippedChunks: payload.skippedChunks,
1699
1804
  });
1700
- setStatus("Added prose to REPL Studio.", "success");
1805
+ setStatus("Added prose to Studio REPL record.", "success");
1701
1806
  renderReplViewIfActive({ force: true });
1702
1807
  } else {
1703
1808
  setStatus("No code or prose found to send.", "warning");
@@ -1771,6 +1876,13 @@
1771
1876
  let fileBackedBaselineText = null;
1772
1877
  let activePane = "left";
1773
1878
  let paneFocusTarget = "off";
1879
+ let paneSplitPercent = 50;
1880
+ const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
1881
+ const PANE_SPLIT_MIN_PERCENT = 20;
1882
+ const PANE_SPLIT_MAX_PERCENT = 80;
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;
1774
1886
  const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
1775
1887
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
1776
1888
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
@@ -1865,6 +1977,9 @@
1865
1977
  let pendingReviewNoteInlineFocusId = null;
1866
1978
  let activePreviewCommentSelection = null;
1867
1979
  let suppressEditorSelectionComment = false;
1980
+ let workspacePersistenceReady = false;
1981
+ let workspacePersistTimer = null;
1982
+ let workspaceRestoredFromBrowser = false;
1868
1983
  let suppressedEditorSelectionStart = null;
1869
1984
  let suppressedEditorSelectionEnd = null;
1870
1985
  const previewJumpHighlightState = new WeakMap();
@@ -2071,12 +2186,8 @@
2071
2186
  if (!studioUiRefreshUi) return;
2072
2187
  if (studioUiRefreshUi.annotationsButton) {
2073
2188
  const inlineLabel = annotationsEnabled ? "Inline on" : "Inline hidden";
2074
- if (isEditorOnlyMode) {
2075
- setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel);
2076
- } else {
2077
- const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
2078
- setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
2079
- }
2189
+ const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
2190
+ setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
2080
2191
  }
2081
2192
  if (studioUiRefreshUi.viewButton) {
2082
2193
  const syntaxLabel = editorHighlightEnabled
@@ -2240,7 +2351,7 @@
2240
2351
  const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
2241
2352
  const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
2242
2353
  const annotationsMenu = makeStudioUiRefreshMenu(annotationsButton, "annotations", "studio-refresh-annotations-anchor");
2243
- appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display", isEditorOnlyMode ? [annotationModeSelect] : [annotationModeSelect, insertHeaderBtn]);
2354
+ appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display", [annotationModeSelect, insertHeaderBtn]);
2244
2355
  appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Actions", [stripAnnotationsBtn, saveAnnotatedBtn]);
2245
2356
  const viewButton = makeStudioUiRefreshElement("button", "", "View");
2246
2357
  const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
@@ -2431,7 +2542,7 @@
2431
2542
 
2432
2543
  function getTerminalBusyStatus() {
2433
2544
  if (terminalActivityPhase === "tool") {
2434
- if (terminalActivityLabel) {
2545
+ if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
2435
2546
  return "Terminal: " + withEllipsis(terminalActivityLabel);
2436
2547
  }
2437
2548
  return terminalActivityToolName
@@ -2475,7 +2586,7 @@
2475
2586
  const action = getStudioActionLabel(kind);
2476
2587
  const queueSuffix = studioRunChainActive ? formatQueuedSteeringSuffix() : "";
2477
2588
  if (terminalActivityPhase === "tool") {
2478
- if (terminalActivityLabel) {
2589
+ if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
2479
2590
  return "Studio: " + withEllipsis(terminalActivityLabel) + queueSuffix;
2480
2591
  }
2481
2592
  return terminalActivityToolName
@@ -2962,14 +3073,16 @@
2962
3073
  // Show "Set working dir" button when not file-backed
2963
3074
  var isFileBacked = hasRefreshableFilePath();
2964
3075
  if (isFileBacked) {
2965
- if (resourceDirInput) resourceDirInput.value = "";
3076
+ var fileBackedResourceDir = getCurrentResourceDirValue() || dirnameForDisplayPath(sourceState.path);
3077
+ if (resourceDirInput) resourceDirInput.value = fileBackedResourceDir;
2966
3078
  if (resourceDirLabel) resourceDirLabel.textContent = "";
2967
3079
  if (resourceDirBtn) resourceDirBtn.hidden = true;
2968
3080
  if (resourceDirLabel) resourceDirLabel.hidden = true;
2969
3081
  if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
2970
3082
  } else {
2971
3083
  // Restore to label if dir is set, otherwise show button
2972
- var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
3084
+ var dir = getCurrentResourceDirValue();
3085
+ if (resourceDirInput) resourceDirInput.value = dir;
2973
3086
  if (dir) {
2974
3087
  if (resourceDirBtn) resourceDirBtn.hidden = true;
2975
3088
  if (resourceDirLabel) { resourceDirLabel.textContent = "Working dir: " + dir; resourceDirLabel.hidden = false; }
@@ -3002,6 +3115,141 @@
3002
3115
  setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
3003
3116
  }
3004
3117
 
3118
+ function clampPaneSplitPercent(value) {
3119
+ const numeric = Number(value);
3120
+ if (!Number.isFinite(numeric)) return 50;
3121
+ const clamped = Math.max(PANE_SPLIT_MIN_PERCENT, Math.min(PANE_SPLIT_MAX_PERCENT, Math.round(numeric * 10) / 10));
3122
+ return Math.abs(clamped - 50) <= PANE_SPLIT_SNAP_TO_CENTER_PERCENT ? 50 : clamped;
3123
+ }
3124
+
3125
+ function applyPaneSplitPercent(percent, options) {
3126
+ paneSplitPercent = clampPaneSplitPercent(percent);
3127
+ const rightPercent = Math.round((100 - paneSplitPercent) * 10) / 10;
3128
+ document.documentElement.style.setProperty("--studio-left-pane-fr", paneSplitPercent + "fr");
3129
+ document.documentElement.style.setProperty("--studio-right-pane-fr", rightPercent + "fr");
3130
+ if (paneResizeHandleEl) {
3131
+ paneResizeHandleEl.setAttribute("aria-valuemin", String(PANE_SPLIT_MIN_PERCENT));
3132
+ paneResizeHandleEl.setAttribute("aria-valuemax", String(PANE_SPLIT_MAX_PERCENT));
3133
+ paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(paneSplitPercent)));
3134
+ paneResizeHandleEl.setAttribute("aria-valuetext", "Editor " + Math.round(paneSplitPercent) + " percent, response " + Math.round(rightPercent) + " percent");
3135
+ }
3136
+ if (!options || options.persist !== false) {
3137
+ try {
3138
+ if (window.localStorage) window.localStorage.setItem(PANE_SPLIT_STORAGE_KEY, String(paneSplitPercent));
3139
+ } catch {
3140
+ // Ignore localStorage failures.
3141
+ }
3142
+ }
3143
+ }
3144
+
3145
+ function resetPaneSplitPercent() {
3146
+ applyPaneSplitPercent(50);
3147
+ setStatus("Pane split reset to 50/50.");
3148
+ }
3149
+
3150
+ function loadPaneSplitPercent() {
3151
+ let stored = "";
3152
+ try {
3153
+ stored = window.localStorage ? String(window.localStorage.getItem(PANE_SPLIT_STORAGE_KEY) || "") : "";
3154
+ } catch {
3155
+ stored = "";
3156
+ }
3157
+ applyPaneSplitPercent(stored ? Number(stored) : 50, { persist: false });
3158
+ }
3159
+
3160
+ function getPaneSplitPercentFromPointerEvent(event) {
3161
+ const mainEl = paneResizeHandleEl && typeof paneResizeHandleEl.closest === "function"
3162
+ ? paneResizeHandleEl.closest("main")
3163
+ : null;
3164
+ if (!mainEl || typeof mainEl.getBoundingClientRect !== "function") return paneSplitPercent;
3165
+ const rect = mainEl.getBoundingClientRect();
3166
+ if (!rect.width) return paneSplitPercent;
3167
+ const x = typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2);
3168
+ return ((x - rect.left) / rect.width) * 100;
3169
+ }
3170
+
3171
+ function setupPaneResizeHandle() {
3172
+ if (!paneResizeHandleEl) return;
3173
+ loadPaneSplitPercent();
3174
+ let dragging = false;
3175
+ let movedDuringDrag = false;
3176
+ let pointerStartX = 0;
3177
+ let activePaneResizePointerId = null;
3178
+ const finishDrag = () => {
3179
+ if (!dragging) return;
3180
+ dragging = false;
3181
+ if (document.body && document.body.classList) document.body.classList.remove("pane-resizing");
3182
+ try {
3183
+ if (typeof paneResizeHandleEl.releasePointerCapture === "function" && activePaneResizePointerId != null) {
3184
+ paneResizeHandleEl.releasePointerCapture(activePaneResizePointerId);
3185
+ }
3186
+ } catch {
3187
+ // Ignore pointer-capture cleanup failures.
3188
+ }
3189
+ activePaneResizePointerId = null;
3190
+ if (movedDuringDrag) {
3191
+ setStatus("Pane split: editor " + Math.round(paneSplitPercent) + "%, response " + Math.round(100 - paneSplitPercent) + "%.");
3192
+ }
3193
+ movedDuringDrag = false;
3194
+ };
3195
+ paneResizeHandleEl.addEventListener("pointerdown", (event) => {
3196
+ if (event.button != null && event.button !== 0) return;
3197
+ event.preventDefault();
3198
+ event.stopPropagation();
3199
+ dragging = true;
3200
+ movedDuringDrag = false;
3201
+ pointerStartX = typeof event.clientX === "number" ? event.clientX : 0;
3202
+ activePaneResizePointerId = event.pointerId;
3203
+ if (document.body && document.body.classList) document.body.classList.add("pane-resizing");
3204
+ try {
3205
+ if (typeof paneResizeHandleEl.focus === "function") paneResizeHandleEl.focus({ preventScroll: true });
3206
+ } catch {
3207
+ try { paneResizeHandleEl.focus(); } catch {}
3208
+ }
3209
+ try {
3210
+ if (typeof paneResizeHandleEl.setPointerCapture === "function") paneResizeHandleEl.setPointerCapture(event.pointerId);
3211
+ } catch {
3212
+ // Ignore pointer-capture failures.
3213
+ }
3214
+ });
3215
+ paneResizeHandleEl.addEventListener("pointermove", (event) => {
3216
+ if (!dragging) return;
3217
+ const movement = typeof event.clientX === "number" ? Math.abs(event.clientX - pointerStartX) : 0;
3218
+ if (!movedDuringDrag && movement < 3) return;
3219
+ movedDuringDrag = true;
3220
+ event.preventDefault();
3221
+ applyPaneSplitPercent(getPaneSplitPercentFromPointerEvent(event));
3222
+ });
3223
+ paneResizeHandleEl.addEventListener("pointerup", finishDrag);
3224
+ paneResizeHandleEl.addEventListener("pointercancel", finishDrag);
3225
+ paneResizeHandleEl.addEventListener("dblclick", (event) => {
3226
+ event.preventDefault();
3227
+ event.stopPropagation();
3228
+ resetPaneSplitPercent();
3229
+ });
3230
+ paneResizeHandleEl.addEventListener("keydown", (event) => {
3231
+ if (event.key === "Home") {
3232
+ event.preventDefault();
3233
+ applyPaneSplitPercent(PANE_SPLIT_MIN_PERCENT);
3234
+ return;
3235
+ }
3236
+ if (event.key === "End") {
3237
+ event.preventDefault();
3238
+ applyPaneSplitPercent(PANE_SPLIT_MAX_PERCENT);
3239
+ return;
3240
+ }
3241
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
3242
+ event.preventDefault();
3243
+ resetPaneSplitPercent();
3244
+ return;
3245
+ }
3246
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
3247
+ event.preventDefault();
3248
+ const step = event.shiftKey ? 10 : 5;
3249
+ applyPaneSplitPercent(paneSplitPercent + (event.key === "ArrowLeft" ? -step : step));
3250
+ });
3251
+ }
3252
+
3005
3253
  function updatePaneFocusButtons() {
3006
3254
  [
3007
3255
  [leftFocusBtn, "left"],
@@ -3102,10 +3350,6 @@
3102
3350
 
3103
3351
  function activatePaneFromShortcut(nextPane) {
3104
3352
  const pane = nextPane === "right" ? "right" : "left";
3105
- if (isEditorOnlyMode && pane === "right") {
3106
- setStatus("Only the editor pane is available in editor-only Studio.", "warning");
3107
- return;
3108
- }
3109
3353
  const snapshot = snapshotStudioScrollablePositions();
3110
3354
  setActivePane(pane);
3111
3355
  scheduleStudioScrollablePositionRestore(snapshot);
@@ -3148,10 +3392,6 @@
3148
3392
  }
3149
3393
 
3150
3394
  function focusRightContentFromShortcut() {
3151
- if (isEditorOnlyMode) {
3152
- setStatus("Only the editor pane is available in editor-only Studio.", "warning");
3153
- return;
3154
- }
3155
3395
  const snapshot = snapshotStudioScrollablePositions();
3156
3396
  setActivePane("right");
3157
3397
  scheduleStudioScrollablePositionRestore(snapshot);
@@ -3240,6 +3480,40 @@
3240
3480
  return false;
3241
3481
  }
3242
3482
 
3483
+ function triggerResponseHistoryShortcut(action) {
3484
+ if (isEditorOnlyMode) {
3485
+ setStatus("Response history is unavailable in editor-only Studio.", "warning");
3486
+ return false;
3487
+ }
3488
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
3489
+ if (total <= 0) {
3490
+ setStatus("No response history available yet.", "warning");
3491
+ return false;
3492
+ }
3493
+ if (action === "previous") {
3494
+ if (responseHistoryIndex <= 0) {
3495
+ setStatus("Already at the first response.", "warning");
3496
+ return false;
3497
+ }
3498
+ return selectHistoryIndex(responseHistoryIndex - 1);
3499
+ }
3500
+ if (action === "next") {
3501
+ if (responseHistoryIndex >= total - 1) {
3502
+ setStatus("Already at the latest response.", "warning");
3503
+ return false;
3504
+ }
3505
+ return selectHistoryIndex(responseHistoryIndex + 1);
3506
+ }
3507
+ if (action === "latest") {
3508
+ if (responseHistoryIndex >= total - 1) {
3509
+ setStatus("Already viewing the latest response.");
3510
+ return false;
3511
+ }
3512
+ return selectHistoryIndex(total - 1);
3513
+ }
3514
+ return false;
3515
+ }
3516
+
3243
3517
  function isTextEntryShortcutTarget(target) {
3244
3518
  if (!(target instanceof Element)) return false;
3245
3519
  const editable = target.closest("input, textarea, select, [contenteditable]");
@@ -3255,6 +3529,7 @@
3255
3529
  if (!event || event.defaultPrevented) return;
3256
3530
 
3257
3531
  const key = typeof event.key === "string" ? event.key : "";
3532
+ const code = typeof event.code === "string" ? event.code : "";
3258
3533
  const plainEscape = key === "Escape"
3259
3534
  && !event.metaKey
3260
3535
  && !event.ctrlKey
@@ -3355,6 +3630,24 @@
3355
3630
  return;
3356
3631
  }
3357
3632
 
3633
+ if (!isTextEntryShortcutTarget(event.target) && !event.metaKey && !event.ctrlKey && event.altKey && !event.shiftKey) {
3634
+ if (key === "ArrowLeft") {
3635
+ event.preventDefault();
3636
+ triggerResponseHistoryShortcut("previous");
3637
+ return;
3638
+ }
3639
+ if (key === "ArrowRight") {
3640
+ event.preventDefault();
3641
+ triggerResponseHistoryShortcut("next");
3642
+ return;
3643
+ }
3644
+ if (key.toLowerCase() === "l" || code === "KeyL") {
3645
+ event.preventDefault();
3646
+ triggerResponseHistoryShortcut("latest");
3647
+ return;
3648
+ }
3649
+ }
3650
+
3358
3651
  const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
3359
3652
  if (isPaneSwitchShortcut) {
3360
3653
  event.preventDefault();
@@ -4018,6 +4311,36 @@
4018
4311
  + " if (node && node.nodeType === 3) node = node.parentElement;\n"
4019
4312
  + " return node && typeof node.closest === 'function' ? node.closest('a[href]') : null;\n"
4020
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"
4021
4344
  + " function getSameDocumentFragment(anchor) {\n"
4022
4345
  + " if (!anchor || typeof anchor.getAttribute !== 'function') return null;\n"
4023
4346
  + " if (anchor.hasAttribute('download')) return null;\n"
@@ -4205,6 +4528,60 @@
4205
4528
  + " htmlMathScanScheduled = true;\n"
4206
4529
  + " requestAnimationFrame(runHtmlMathRenderScan);\n"
4207
4530
  + " }\n"
4531
+ + " const htmlResourcePlaceholders = new Map();\n"
4532
+ + " let htmlResourceSerial = 0;\n"
4533
+ + " let htmlResourceScanScheduled = false;\n"
4534
+ + " function shouldResolveHtmlPreviewResourceUrl(value) {\n"
4535
+ + " const raw = String(value || '').trim();\n"
4536
+ + " if (!raw || raw.charAt(0) === '#') return false;\n"
4537
+ + " if (/^(?:data|blob|http|https|about|javascript|mailto):/i.test(raw)) return false;\n"
4538
+ + " if (/^\\/\\//.test(raw)) return false;\n"
4539
+ + " return /\\.(?:png|jpe?g|gif|webp)(?:[?#].*)?$/i.test(raw);\n"
4540
+ + " }\n"
4541
+ + " function scanHtmlPreviewResources() {\n"
4542
+ + " htmlResourceScanScheduled = false;\n"
4543
+ + " if (!document.body) return;\n"
4544
+ + " const items = [];\n"
4545
+ + " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
4546
+ + " images.forEach((image) => {\n"
4547
+ + " if (!image || !image.getAttribute) return;\n"
4548
+ + " if (image.getAttribute('data-pi-studio-html-resource-resolved') === 'true') return;\n"
4549
+ + " const raw = String(image.getAttribute('src') || '').trim();\n"
4550
+ + " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
4551
+ + " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
4552
+ + " if (!resourceId) {\n"
4553
+ + " resourceId = PREVIEW_ID + '_resource_' + (++htmlResourceSerial).toString(36);\n"
4554
+ + " image.setAttribute('data-pi-studio-html-resource-id', resourceId);\n"
4555
+ + " }\n"
4556
+ + " htmlResourcePlaceholders.set(resourceId, image);\n"
4557
+ + " items.push({ resourceId, url: raw });\n"
4558
+ + " });\n"
4559
+ + " if (items.length > 0) {\n"
4560
+ + " try { parent.postMessage({ type: 'pi-studio-html-artifact-resolve-resources', id: PREVIEW_ID, resources: items.slice(0, 100) }, '*'); } catch {}\n"
4561
+ + " }\n"
4562
+ + " }\n"
4563
+ + " function scheduleHtmlPreviewResourceScan() {\n"
4564
+ + " if (htmlResourceScanScheduled) return;\n"
4565
+ + " htmlResourceScanScheduled = true;\n"
4566
+ + " requestAnimationFrame(scanHtmlPreviewResources);\n"
4567
+ + " }\n"
4568
+ + " function applyResolvedHtmlPreviewResources(results) {\n"
4569
+ + " if (!Array.isArray(results)) return;\n"
4570
+ + " results.forEach((result) => {\n"
4571
+ + " if (!result || typeof result !== 'object') return;\n"
4572
+ + " const resourceId = typeof result.resourceId === 'string' ? result.resourceId : '';\n"
4573
+ + " const image = resourceId ? htmlResourcePlaceholders.get(resourceId) : null;\n"
4574
+ + " if (!image || !image.isConnected) return;\n"
4575
+ + " if (result.ok === true && typeof result.dataUrl === 'string' && result.dataUrl) {\n"
4576
+ + " image.setAttribute('src', result.dataUrl);\n"
4577
+ + " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
4578
+ + " } else if (typeof result.error === 'string' && result.error) {\n"
4579
+ + " image.setAttribute('title', result.error);\n"
4580
+ + " }\n"
4581
+ + " htmlResourcePlaceholders.delete(resourceId);\n"
4582
+ + " });\n"
4583
+ + " scheduleHeight();\n"
4584
+ + " }\n"
4208
4585
  + " window.addEventListener('message', (event) => {\n"
4209
4586
  + " const data = event && event.data;\n"
4210
4587
  + " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
@@ -4214,15 +4591,21 @@
4214
4591
  + " }\n"
4215
4592
  + " if (data.type === 'pi-studio-html-artifact-math-rendered') {\n"
4216
4593
  + " applyRenderedHtmlMath(data.results);\n"
4594
+ + " return;\n"
4595
+ + " }\n"
4596
+ + " if (data.type === 'pi-studio-html-artifact-resources-resolved') {\n"
4597
+ + " applyResolvedHtmlPreviewResources(data.results);\n"
4217
4598
  + " }\n"
4218
4599
  + " });\n"
4219
4600
  + " document.addEventListener('click', handleFragmentAnchorClick);\n"
4220
- + " document.addEventListener('DOMContentLoaded', scheduleHtmlMathRenderScan);\n"
4601
+ + " document.addEventListener('click', handleHtmlPreviewLocalLinkClick);\n"
4602
+ + " document.addEventListener('contextmenu', handleHtmlPreviewLocalLinkContextMenu);\n"
4603
+ + " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4221
4604
  + " window.addEventListener('hashchange', () => {\n"
4222
4605
  + " const hash = String(window.location && window.location.hash || '');\n"
4223
4606
  + " if (hash) scrollFragmentIntoView(hash.slice(1), { smooth: false });\n"
4224
4607
  + " });\n"
4225
- + " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
4608
+ + " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4226
4609
  + " window.addEventListener('resize', scheduleHeight);\n"
4227
4610
  + " if (typeof ResizeObserver === 'function') {\n"
4228
4611
  + " const observer = new ResizeObserver(scheduleHeight);\n"
@@ -4230,15 +4613,15 @@
4230
4613
  + " if (document.body) observer.observe(document.body);\n"
4231
4614
  + " }\n"
4232
4615
  + " if (typeof MutationObserver === 'function') {\n"
4233
- + " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
4616
+ + " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4234
4617
  + " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
4235
4618
  + " }\n"
4236
4619
  + " scheduleHeight();\n"
4237
4620
  + " setTimeout(scheduleHeight, 80);\n"
4238
4621
  + " setTimeout(scheduleHeight, 350);\n"
4239
- + " setTimeout(scheduleHtmlMathRenderScan, 0);\n"
4240
- + " setTimeout(scheduleHtmlMathRenderScan, 120);\n"
4241
- + " setTimeout(scheduleHtmlMathRenderScan, 500);\n"
4622
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 0);\n"
4623
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 120);\n"
4624
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 500);\n"
4242
4625
  + "})();\n"
4243
4626
  + "<\/script>";
4244
4627
  }
@@ -4467,9 +4850,154 @@
4467
4850
  void renderHtmlArtifactMathItems(record, items);
4468
4851
  }
4469
4852
 
4853
+ function normalizeHtmlArtifactResourceItems(rawItems) {
4854
+ if (!Array.isArray(rawItems)) return [];
4855
+ return rawItems.slice(0, 100).map((item) => {
4856
+ const raw = item && typeof item === "object" ? item : null;
4857
+ const resourceId = raw && typeof raw.resourceId === "string" ? raw.resourceId : "";
4858
+ const url = raw && typeof raw.url === "string" ? raw.url : "";
4859
+ if (!resourceId || !url.trim()) return null;
4860
+ return { resourceId, url };
4861
+ }).filter(Boolean);
4862
+ }
4863
+
4864
+ function buildHtmlArtifactResourceFetchUrl(record, resourceUrl) {
4865
+ const token = getToken();
4866
+ if (!token) return "";
4867
+ const params = new URLSearchParams({ token, path: String(resourceUrl || "") });
4868
+ if (record && record.sourcePath) {
4869
+ params.set("sourcePath", record.sourcePath);
4870
+ }
4871
+ if (record && record.resourceDir) {
4872
+ params.set("resourceDir", record.resourceDir);
4873
+ }
4874
+ return "/html-preview-resource?" + params.toString();
4875
+ }
4876
+
4877
+ function postHtmlArtifactResourceResults(record, results) {
4878
+ if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
4879
+ try {
4880
+ record.iframe.contentWindow.postMessage({
4881
+ type: "pi-studio-html-artifact-resources-resolved",
4882
+ id: record.id || "",
4883
+ results: Array.isArray(results) ? results : [],
4884
+ }, "*");
4885
+ } catch {
4886
+ // Ignore iframe postMessage failures.
4887
+ }
4888
+ }
4889
+
4890
+ async function fetchHtmlArtifactResource(record, item) {
4891
+ const resourceId = item && item.resourceId ? item.resourceId : "";
4892
+ try {
4893
+ const fetchUrl = buildHtmlArtifactResourceFetchUrl(record, item.url);
4894
+ if (!fetchUrl) throw new Error("Missing Studio token in URL.");
4895
+ const response = await fetchWithTimeout(fetchUrl, { method: "GET" }, HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS, "HTML preview resource load");
4896
+ const payload = await response.json().catch(() => null);
4897
+ if (!response.ok || !payload || payload.ok !== true || typeof payload.dataUrl !== "string") {
4898
+ const message = payload && typeof payload.error === "string" ? payload.error : "HTML preview resource load failed with HTTP " + response.status + ".";
4899
+ throw new Error(message);
4900
+ }
4901
+ return { resourceId, ok: true, dataUrl: payload.dataUrl };
4902
+ } catch (error) {
4903
+ return { resourceId, ok: false, error: error && error.message ? error.message : String(error || "HTML preview resource load failed.") };
4904
+ }
4905
+ }
4906
+
4907
+ async function resolveHtmlArtifactResources(record, items) {
4908
+ if (!record || !Array.isArray(items) || items.length === 0) return;
4909
+ if (record.detail) record.detail.textContent = "HTML preview · loading local images";
4910
+ const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
4911
+ postHtmlArtifactResourceResults(record, results);
4912
+ if (record.detail) record.detail.textContent = "HTML preview";
4913
+ }
4914
+
4915
+ function handleHtmlArtifactFrameResourceMessage(event) {
4916
+ const data = event && event.data;
4917
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-resolve-resources") return;
4918
+ const id = typeof data.id === "string" ? data.id : "";
4919
+ const record = id ? htmlArtifactFramesById.get(id) : null;
4920
+ if (!record || !record.iframe || !record.iframe.isConnected) {
4921
+ if (id) htmlArtifactFramesById.delete(id);
4922
+ return;
4923
+ }
4924
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
4925
+ const items = normalizeHtmlArtifactResourceItems(data.resources);
4926
+ if (items.length === 0) return;
4927
+ record.resourceResolveBatchCount = Math.max(0, Number(record.resourceResolveBatchCount) || 0) + 1;
4928
+ record.resourceResolveItemCount = Math.max(0, Number(record.resourceResolveItemCount) || 0) + items.length;
4929
+ if (record.resourceResolveBatchCount > 12 || record.resourceResolveItemCount > 300) {
4930
+ postHtmlArtifactResourceResults(record, items.map((item) => ({ resourceId: item.resourceId, ok: false, error: "HTML preview local image load limit reached." })));
4931
+ return;
4932
+ }
4933
+ void resolveHtmlArtifactResources(record, items);
4934
+ }
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
+
4470
4996
  window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
4471
4997
  window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
4472
4998
  window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
4999
+ window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
5000
+ window.addEventListener("message", handleHtmlArtifactFrameLocalLinkMessage);
4473
5001
 
4474
5002
  function isStudioHtmlFocusOpen() {
4475
5003
  return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
@@ -4777,7 +5305,19 @@
4777
5305
  iframe.addEventListener("load", () => { postArtifactZoom(); });
4778
5306
  iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
4779
5307
  shell.appendChild(iframe);
4780
- htmlArtifactFramesById.set(previewId, { id: previewId, iframe, shell, detail, zoomControls, mathRenderBatchCount: 0, mathRenderItemCount: 0 });
5308
+ htmlArtifactFramesById.set(previewId, {
5309
+ id: previewId,
5310
+ iframe,
5311
+ shell,
5312
+ detail,
5313
+ zoomControls,
5314
+ sourcePath: options && options.sourcePath ? String(options.sourcePath) : "",
5315
+ resourceDir: options && options.resourceDir ? String(options.resourceDir) : "",
5316
+ mathRenderBatchCount: 0,
5317
+ mathRenderItemCount: 0,
5318
+ resourceResolveBatchCount: 0,
5319
+ resourceResolveItemCount: 0,
5320
+ });
4781
5321
 
4782
5322
  targetEl.appendChild(shell);
4783
5323
 
@@ -5010,13 +5550,16 @@
5010
5550
  if (!token) return "";
5011
5551
  const pdfPath = String(options && options.path ? options.path : "").trim();
5012
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) : "";
5013
5555
  const effectivePath = getEffectiveSavePath();
5014
- const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
5015
- const resourceDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
5556
+ const sourcePath = explicitSourcePath || (useEditorResourceContext ? (effectivePath || sourceState.path || "") : "");
5557
+ const resourceDir = explicitResourceDir || getCurrentResourceDirValue();
5016
5558
  const params = new URLSearchParams({ token, path: pdfPath });
5017
5559
  if (sourcePath) {
5018
5560
  params.set("sourcePath", sourcePath);
5019
- } else if (resourceDir) {
5561
+ }
5562
+ if (resourceDir) {
5020
5563
  params.set("resourceDir", resourceDir);
5021
5564
  }
5022
5565
  return "/pdf-resource?" + params.toString();
@@ -5892,12 +6435,16 @@
5892
6435
  const action = actionBtn.getAttribute("data-repl-action") || "";
5893
6436
  if (action === "start" || action === "new-session") {
5894
6437
  const requestId = makeRequestId();
6438
+ const command = getCurrentReplStartCommandFromDom();
6439
+ setReplCommandOverride(replRuntime, command);
5895
6440
  replBusy = true;
5896
6441
  replError = "";
5897
- replMessage = (action === "new-session" ? "Starting new " : "Starting ") + replRuntime + " REPL…";
6442
+ replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
5898
6443
  syncActionButtons();
5899
6444
  renderReplViewIfActive({ force: true });
5900
- if (!sendMessage({ type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" })) {
6445
+ const message = { type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" };
6446
+ if (command) message.command = command;
6447
+ if (!sendMessage(message)) {
5901
6448
  replBusy = false;
5902
6449
  syncActionButtons();
5903
6450
  }
@@ -6001,13 +6548,29 @@
6001
6548
  if (!(target instanceof Element)) return;
6002
6549
  const runtimeSelect = target.closest("[data-repl-runtime]");
6003
6550
  if (runtimeSelect && "value" in runtimeSelect) {
6551
+ const previousActive = replActiveSessionName;
6004
6552
  setReplRuntime(runtimeSelect.value);
6553
+ selectReplSessionForRuntime(replRuntime, previousActive);
6554
+ replError = "";
6555
+ replMessage = "";
6556
+ if (replActiveSessionName) {
6557
+ requestReplCapture();
6558
+ } else {
6559
+ replTranscript = "";
6560
+ replCapturedAt = 0;
6561
+ syncActionButtons();
6562
+ }
6005
6563
  renderReplViewIfActive({ force: true });
6006
6564
  return;
6007
6565
  }
6566
+ const commandInput = target.closest("[data-repl-command]");
6567
+ if (commandInput && "value" in commandInput) {
6568
+ setReplCommandOverride(replRuntime, commandInput.value);
6569
+ return;
6570
+ }
6008
6571
  const sessionSelect = target.closest("[data-repl-session]");
6009
6572
  if (sessionSelect && "value" in sessionSelect) {
6010
- setActiveReplSession(sessionSelect.value);
6573
+ setActiveReplSessionForCurrentRuntime(sessionSelect.value);
6011
6574
  replError = "";
6012
6575
  replMessage = "";
6013
6576
  replFollow = true;
@@ -6190,7 +6753,7 @@
6190
6753
  const payload = {
6191
6754
  markdown: String(markdown || ""),
6192
6755
  sourcePath: sourcePath,
6193
- resourceDir: (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "",
6756
+ resourceDir: (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "",
6194
6757
  };
6195
6758
  if (previewOptions.includeEditorLanguage) {
6196
6759
  payload.editorLanguage = String(editorLanguage || "");
@@ -6291,12 +6854,12 @@
6291
6854
  const exportingReplJournal = rightView === "repl";
6292
6855
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6293
6856
  if (!rightPaneShowsPreview && !exportingReplJournal) {
6294
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
6857
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
6295
6858
  return;
6296
6859
  }
6297
6860
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6298
6861
  if (exportingReplJournal && !replJournalExportEntries.length) {
6299
- setStatus("No REPL Studio entries to export for this session yet.", "warning");
6862
+ setStatus("No Studio REPL record entries to export for this session yet.", "warning");
6300
6863
  return;
6301
6864
  }
6302
6865
 
@@ -6318,7 +6881,7 @@
6318
6881
 
6319
6882
  const effectivePath = getEffectiveSavePath();
6320
6883
  const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
6321
- const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
6884
+ const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
6322
6885
  const isEditorPreview = rightView === "editor-preview";
6323
6886
  const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
6324
6887
  const isLatex = isEditorPreview
@@ -6464,12 +7027,12 @@
6464
7027
  const exportingReplJournal = rightView === "repl";
6465
7028
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6466
7029
  if (!rightPaneShowsPreview && !exportingReplJournal) {
6467
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
7030
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
6468
7031
  return;
6469
7032
  }
6470
7033
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6471
7034
  if (exportingReplJournal && !replJournalExportEntries.length) {
6472
- setStatus("No REPL Studio entries to export for this session yet.", "warning");
7035
+ setStatus("No Studio REPL record entries to export for this session yet.", "warning");
6473
7036
  return;
6474
7037
  }
6475
7038
 
@@ -6484,14 +7047,14 @@
6484
7047
 
6485
7048
  const effectivePath = getEffectiveSavePath();
6486
7049
  const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
6487
- const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
7050
+ const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
6488
7051
  const isEditorPreview = rightView === "editor-preview";
6489
7052
  const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
6490
7053
  const isLatex = htmlArtifactSource ? false : (isEditorPreview
6491
7054
  ? editorHtmlLanguage === "latex"
6492
7055
  : /\\documentclass\b|\\begin\{document\}/.test(markdown));
6493
7056
  let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
6494
- let titleHint = exportingReplJournal ? "REPL Studio" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
7057
+ let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
6495
7058
  if (sourcePath) {
6496
7059
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
6497
7060
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -6873,7 +7436,7 @@
6873
7436
  decorateCopyablePreviewBlocks(targetEl);
6874
7437
 
6875
7438
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
6876
- if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
7439
+ if (!sourceState.path && !getCurrentResourceDirValue()) {
6877
7440
  var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
6878
7441
  var hasLatexImages = /\\includegraphics/.test(markdown || "");
6879
7442
  if (hasRelativeImages || hasLatexImages) {
@@ -6907,7 +7470,7 @@
6907
7470
  if (editorView !== "preview") return;
6908
7471
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
6909
7472
  if (isHtmlArtifactPreviewText(text, editorLanguage)) {
6910
- renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview" });
7473
+ renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
6911
7474
  return;
6912
7475
  }
6913
7476
  if (supportsCodePreviewCommentsForCurrentEditor()) {
@@ -7190,10 +7753,9 @@
7190
7753
  const visibleEntries = getVisibleReplJournalEntries();
7191
7754
  const hasEntries = visibleEntries.length > 0;
7192
7755
  const entryCount = visibleEntries.length;
7193
- const hiddenEntryCount = getHiddenReplJournalEntryCount();
7194
7756
  const sessionName = getActiveReplJournalSessionName();
7195
7757
  const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
7196
- const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show REPL Studio" : "Hide REPL Studio") + "</button>";
7758
+ const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
7197
7759
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
7198
7760
  const summaryText = hasEntries
7199
7761
  ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
@@ -7201,7 +7763,7 @@
7201
7763
  if (replJournalCollapsed) {
7202
7764
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
7203
7765
  + "<div class='repl-journal-compact-row'>"
7204
- + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>REPL Studio</span><span>" + escapeHtml(summaryText) + "</span></div>"
7766
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Studio REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
7205
7767
  + "<div class='repl-journal-actions'>" + toggleButton + "</div>"
7206
7768
  + "</div>"
7207
7769
  + "</section>";
@@ -7239,14 +7801,14 @@
7239
7801
  }).join("");
7240
7802
  const emptyText = sessionName
7241
7803
  ? (String(transcript || "").trim()
7242
- ? "No REPL Studio entries for this tmux session yet. The raw tmux mirror below still has this session's history; send code from Studio to build a clean record."
7243
- : "No REPL Studio entries for this tmux session yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
7244
- : "No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
7804
+ ? "No Studio REPL record entries yet. The raw tmux mirror below still has this session's history; send code from Studio to build a clean record."
7805
+ : "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
7806
+ : "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
7245
7807
  const terminalContent = banner
7246
7808
  + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
7247
7809
  return "<section class='repl-journal'>"
7248
- + "<div class='repl-journal-header'><div><h3>REPL Studio</h3><p>Clean collaborative Studio REPL record for the selected tmux session. The raw tmux mirror is available below.</p></div>" + toggleActions + "</div>"
7249
- + (hiddenEntryCount ? "<div class='repl-journal-omitted'>" + escapeHtml(String(hiddenEntryCount)) + " entr" + (hiddenEntryCount === 1 ? "y" : "ies") + " from other REPL sessions hidden.</div>" : "")
7810
+ + "<div class='repl-journal-header'><h3>Studio REPL Record</h3>" + toggleActions + "</div>"
7811
+ + "<p class='repl-journal-description'>Clean record for the selected tmux session. Raw tmux mirror below.</p>"
7250
7812
  + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
7251
7813
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
7252
7814
  + "</section>";
@@ -7270,7 +7832,7 @@
7270
7832
  + "</section>";
7271
7833
  }
7272
7834
  return "<section class='repl-mirror'>"
7273
- + "<div class='repl-journal-header'><div><h3>Raw REPL mirror</h3><p>Best-effort tmux pane mirror. Useful for directly typed commands and debugging; REPL Studio above is the cleaner record.</p></div>" + actions + "</div>"
7835
+ + "<div class='repl-journal-header'><div><h3>Raw REPL Mirror</h3><p>Best-effort tmux pane mirror. Useful for directly typed commands and debugging; the Studio record above is cleaner.</p></div>" + actions + "</div>"
7274
7836
  + body
7275
7837
  + "</section>";
7276
7838
  }
@@ -7290,10 +7852,13 @@
7290
7852
  ["ghci", "GHCi"],
7291
7853
  ["clojure", "Clojure"],
7292
7854
  ].map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (replRuntime === value ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
7293
- const sessionOptions = replSessions.length
7294
- ? replSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
7295
- : "<option value=''>No REPL sessions</option>";
7296
- const activeSession = getActiveReplSession();
7855
+ const runtimeLabel = getReplRuntimeLabel(replRuntime);
7856
+ const visibleSessions = getReplSessionsForRuntime(replRuntime);
7857
+ const sessionOptions = visibleSessions.length
7858
+ ? visibleSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
7859
+ : "<option value=''>None</option>";
7860
+ const replCommand = getReplCommandOverride(replRuntime);
7861
+ const activeSession = getActiveReplSessionForCurrentRuntime();
7297
7862
  const transcript = trimReplTranscript(replTranscript);
7298
7863
  const emptyMessage = replTmuxAvailable === false
7299
7864
  ? "tmux is not available. Install tmux to use Studio REPL sessions."
@@ -7307,17 +7872,17 @@
7307
7872
  + "<div class='repl-toolbar'>"
7308
7873
  + "<div class='repl-controls'>"
7309
7874
  + "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
7310
- + "<button type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or attach to the default session for this runtime.'>Start</button>"
7311
- + "<label class='repl-control-label'>Session <select data-repl-session aria-label='REPL session'" + (replSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
7875
+ + "<label class='repl-control-label repl-command-label'>Start cmd <input data-repl-command type='text' value='" + escapeHtml(replCommand) + "' placeholder='default' aria-label='REPL start command' title='Command used by Start for this runtime. Leave blank for the default; use this for envs, e.g. .venv/bin/python, uv run python, or conda run --no-capture-output -n env python.'></label>"
7876
+ + "<button class='repl-start-btn' type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or switch to a " + escapeHtml(runtimeLabel) + " session using the selected runtime and start command.'>Start</button>"
7877
+ + "<label class='repl-control-label repl-session-label'>Session <select data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
7312
7878
  + "<details class='repl-more-controls'>"
7313
7879
  + "<summary title='More REPL actions'>More</summary>"
7314
7880
  + "<div class='repl-more-menu'>"
7315
- + "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
7316
7881
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
7317
7882
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
7318
7883
  + "<button type='button' data-repl-action='copy-attach-command'" + (activeSession ? "" : " disabled") + " title='Copy command for attaching to this tmux session in a terminal.'>Copy attach command</button>"
7319
7884
  + "<button type='button' data-repl-action='run-all-chunks'" + (canSendToActiveSession ? "" : " disabled") + " title='Literate send: send all fenced code chunks matching the active REPL runtime.'>Run all chunks</button>"
7320
- + "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to REPL Studio (Literate send) without sending it to the runtime.'>Add note</button>"
7885
+ + "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to the Studio REPL record (Literate send) without sending it to the runtime.'>Add note</button>"
7321
7886
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
7322
7887
  + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
7323
7888
  + "</div>"
@@ -7501,7 +8066,7 @@
7501
8066
  return;
7502
8067
  }
7503
8068
  if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
7504
- renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview" });
8069
+ renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
7505
8070
  return;
7506
8071
  }
7507
8072
  if (supportsCodePreviewCommentsForCurrentEditor()) {
@@ -7525,7 +8090,7 @@
7525
8090
 
7526
8091
  if (rightView === "preview") {
7527
8092
  if (isHtmlArtifactPreviewText(markdown, "")) {
7528
- renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview" });
8093
+ renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview", ...getHtmlPreviewResourceContextOptions() });
7529
8094
  return;
7530
8095
  }
7531
8096
  const nonce = ++responsePreviewRenderNonce;
@@ -7604,19 +8169,19 @@
7604
8169
  exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
7605
8170
  exportPdfBtn.textContent = previewExportInProgress
7606
8171
  ? "Exporting…"
7607
- : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
8172
+ : (exportingReplJournal ? "Export record" : "Export right preview");
7608
8173
  if (rightView === "trace") {
7609
8174
  exportPdfBtn.title = "Working view does not support preview export.";
7610
8175
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
7611
- exportPdfBtn.title = "No REPL Studio entries to export for this session yet.";
8176
+ exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
7612
8177
  } else if (rightView === "markdown") {
7613
- exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
8178
+ exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL to export.";
7614
8179
  } else if (!canExportPreview) {
7615
8180
  exportPdfBtn.title = "Nothing to export yet.";
7616
8181
  } else if (isHtmlArtifactPreview) {
7617
8182
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
7618
8183
  } else if (exportingReplJournal) {
7619
- exportPdfBtn.title = "Choose PDF or HTML and export REPL Studio.";
8184
+ exportPdfBtn.title = "Choose PDF or HTML and export the Studio REPL record.";
7620
8185
  } else {
7621
8186
  exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
7622
8187
  }
@@ -7625,20 +8190,20 @@
7625
8190
  exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
7626
8191
  exportPreviewPdfBtn.title = isHtmlArtifactPreview
7627
8192
  ? "Interactive HTML preview PDF export is not available yet."
7628
- : (exportingReplJournal ? "Export REPL Studio as PDF." : "Export the current right-pane preview as PDF.");
8193
+ : (exportingReplJournal ? "Export the Studio REPL record as PDF." : "Export the current right-pane preview as PDF.");
7629
8194
  }
7630
8195
  if (exportPreviewHtmlBtn) {
7631
8196
  exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
7632
8197
  exportPreviewHtmlBtn.title = isHtmlArtifactPreview
7633
8198
  ? "Export the authored HTML preview."
7634
- : (exportingReplJournal ? "Export REPL Studio as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
8199
+ : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
7635
8200
  }
7636
8201
  if (exportPreviewControlsEl) {
7637
8202
  exportPreviewControlsEl.title = canExportPreview
7638
8203
  ? (exportingReplJournal
7639
- ? "Choose a format and export REPL Studio."
8204
+ ? "Choose a format and export the Studio REPL record."
7640
8205
  : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
7641
- : (exportingReplJournal ? "No REPL Studio entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
8206
+ : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
7642
8207
  }
7643
8208
  if (!canExportPreview || previewExportInProgress) {
7644
8209
  closeExportPreviewMenu();
@@ -7658,17 +8223,57 @@
7658
8223
  updateResultActionButtons();
7659
8224
  }
7660
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
+
7661
8256
  function getEffectiveSavePath() {
7662
8257
  // File-backed: use the original path
7663
8258
  if (sourceState.path) return sourceState.path;
7664
8259
  // Upload with working dir + filename: derive path
7665
- if (sourceState.source === "upload" && sourceState.label && resourceDirInput && resourceDirInput.value.trim()) {
8260
+ const resourceDir = getCurrentResourceDirValue();
8261
+ if (sourceState.source === "upload" && sourceState.label && resourceDir) {
7666
8262
  var name = sourceState.label.replace(/^upload:\s*/i, "");
7667
- if (name) return resourceDirInput.value.trim().replace(/\/$/, "") + "/" + name;
8263
+ if (name) return resourceDir.replace(/\/$/, "") + "/" + name;
7668
8264
  }
7669
8265
  return null;
7670
8266
  }
7671
8267
 
8268
+ function getHtmlPreviewResourceContextOptions() {
8269
+ const sourcePath = getEffectiveSavePath() || sourceState.path || "";
8270
+ const resourceDir = getCurrentResourceDirValue();
8271
+ return {
8272
+ sourcePath,
8273
+ resourceDir,
8274
+ };
8275
+ }
8276
+
7672
8277
  function buildAnnotatedSaveSuggestion() {
7673
8278
  const effectivePath = getEffectiveSavePath() || sourceState.path || "";
7674
8279
  if (effectivePath) {
@@ -7681,8 +8286,8 @@
7681
8286
 
7682
8287
  const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
7683
8288
  const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
7684
- const suggestedDir = resourceDirInput && resourceDirInput.value.trim()
7685
- ? resourceDirInput.value.trim().replace(/\/$/, "") + "/"
8289
+ const suggestedDir = getCurrentResourceDirValue()
8290
+ ? getCurrentResourceDirValue().replace(/\/$/, "") + "/"
7686
8291
  : "./";
7687
8292
  return suggestedDir + stem + ".annotated.md";
7688
8293
  }
@@ -7719,6 +8324,7 @@
7719
8324
  saveAsBtn.disabled = uiBusy;
7720
8325
  saveOverBtn.disabled = uiBusy || !canSaveOver;
7721
8326
  if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
8327
+ if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy;
7722
8328
  sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
7723
8329
  if (getEditorBtn) getEditorBtn.disabled = uiBusy;
7724
8330
  if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
@@ -7735,7 +8341,7 @@
7735
8341
  rightViewSelect.disabled = isEditorOnlyMode;
7736
8342
  followSelect.disabled = isEditorOnlyMode || uiBusy;
7737
8343
  if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
7738
- insertHeaderBtn.disabled = uiBusy || isEditorOnlyMode;
8344
+ insertHeaderBtn.disabled = uiBusy;
7739
8345
  lensSelect.disabled = uiBusy || isEditorOnlyMode;
7740
8346
  updateSaveFileTooltip();
7741
8347
  updateRefreshFromDiskTooltip();
@@ -7777,6 +8383,197 @@
7777
8383
  previousDescriptor: previousDescriptor,
7778
8384
  carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
7779
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");
7780
8577
  }
7781
8578
 
7782
8579
  function setEditorText(nextText, options) {
@@ -7826,6 +8623,7 @@
7826
8623
  }
7827
8624
  updateEditorSelectionCommentUi();
7828
8625
  updateOutlineUi();
8626
+ scheduleWorkspacePersistence();
7829
8627
  }
7830
8628
 
7831
8629
  function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
@@ -7963,6 +8761,7 @@
7963
8761
  updateReviewNotesUi();
7964
8762
  updateEditorSelectionCommentUi();
7965
8763
  updateOutlineUi();
8764
+ scheduleWorkspacePersistence();
7966
8765
  }
7967
8766
 
7968
8767
  function setRightView(nextView) {
@@ -7995,6 +8794,7 @@
7995
8794
 
7996
8795
  refreshResponseUi();
7997
8796
  syncActionButtons();
8797
+ scheduleWorkspacePersistence();
7998
8798
  }
7999
8799
 
8000
8800
  function lineNumbersShouldBeVisible() {
@@ -8303,6 +9103,383 @@
8303
9103
  }
8304
9104
  }
8305
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
+
8306
9483
  function makeRequestId() {
8307
9484
  if (window.crypto && typeof window.crypto.randomUUID === "function") {
8308
9485
  return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -8520,10 +9697,6 @@
8520
9697
  return index > 0 ? value.slice(0, index) : "";
8521
9698
  }
8522
9699
 
8523
- function getCurrentResourceDirValue() {
8524
- return resourceDirInput ? String(resourceDirInput.value || "").trim() : "";
8525
- }
8526
-
8527
9700
  function getDefaultQuizContextPath(scope) {
8528
9701
  const normalizedScope = normalizeQuizScope(scope);
8529
9702
  const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
@@ -14174,6 +15347,7 @@
14174
15347
  scheduleSourcePreviewRender(0);
14175
15348
  }
14176
15349
  updateOutlineUi();
15350
+ scheduleWorkspacePersistence();
14177
15351
  }
14178
15352
 
14179
15353
  function setEditorHighlightMode(mode) {
@@ -14262,7 +15436,7 @@
14262
15436
  sendRunBtn.classList.toggle("request-stop-active", directIsStop);
14263
15437
  sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
14264
15438
  sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
14265
- const replHint = rightView === "repl" && getActiveReplSession()
15439
+ const replHint = rightView === "repl" && getActiveReplSessionForCurrentRuntime()
14266
15440
  ? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
14267
15441
  : "";
14268
15442
  sendRunBtn.title = directIsStop
@@ -14283,7 +15457,7 @@
14283
15457
  : "Queue steering is available while Run editor text is active.";
14284
15458
  }
14285
15459
 
14286
- const hasReplSession = Boolean(getActiveReplSession());
15460
+ const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
14287
15461
  if (sendReplBtn) {
14288
15462
  const showReplSend = rightView === "repl";
14289
15463
  sendReplBtn.hidden = !showReplSend;
@@ -14651,7 +15825,7 @@
14651
15825
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
14652
15826
  const sessionsChanged = setReplSessions(message.sessions);
14653
15827
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
14654
- setActiveReplSession(message.activeSessionName);
15828
+ setActiveReplSessionForCurrentRuntime(message.activeSessionName);
14655
15829
  }
14656
15830
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
14657
15831
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
@@ -14704,7 +15878,7 @@
14704
15878
  }
14705
15879
  }
14706
15880
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
14707
- setActiveReplSession(message.activeSessionName);
15881
+ setActiveReplSessionForCurrentRuntime(message.activeSessionName);
14708
15882
  }
14709
15883
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
14710
15884
  if (typeof message.transcript === "string") {
@@ -14902,6 +16076,10 @@
14902
16076
  stickyStudioKind = null;
14903
16077
  }
14904
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;
14905
16083
  setSourceState({
14906
16084
  source: "file",
14907
16085
  label: message.label || message.path,
@@ -14977,6 +16155,10 @@
14977
16155
  ? nextDoc.path
14978
16156
  : null;
14979
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;
14980
16162
  setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
14981
16163
  setSourceState({
14982
16164
  source: nextSource,
@@ -15519,6 +16701,8 @@
15519
16701
  rightPaneEl.addEventListener("focusin", (event) => activatePaneFromInteraction("right", event));
15520
16702
  }
15521
16703
 
16704
+ setupPaneResizeHandle();
16705
+
15522
16706
  if (leftFocusBtn) {
15523
16707
  leftFocusBtn.addEventListener("click", () => {
15524
16708
  if (paneFocusTarget === "left") {
@@ -15543,6 +16727,7 @@
15543
16727
  window.addEventListener("keydown", handlePaneShortcut);
15544
16728
  window.addEventListener("beforeunload", () => {
15545
16729
  stopFooterSpinner();
16730
+ flushWorkspacePersistence();
15546
16731
  flushScratchpadPersistence();
15547
16732
  flushReviewNotesPersistence();
15548
16733
  });
@@ -15559,6 +16744,7 @@
15559
16744
 
15560
16745
  followSelect.addEventListener("change", () => {
15561
16746
  followLatest = followSelect.value !== "off";
16747
+ scheduleWorkspacePersistence();
15562
16748
  if (followLatest && queuedLatestResponse) {
15563
16749
  if (responseHistory.length > 0) {
15564
16750
  selectHistoryIndex(responseHistory.length - 1, { silent: true });
@@ -15722,6 +16908,7 @@
15722
16908
  renderReviewNotesList();
15723
16909
  updateReviewNotesUi();
15724
16910
  }
16911
+ scheduleWorkspacePersistence();
15725
16912
  });
15726
16913
 
15727
16914
  sourceTextEl.addEventListener("select", () => {
@@ -15898,7 +17085,10 @@
15898
17085
  closeExportPreviewMenu();
15899
17086
  });
15900
17087
  document.addEventListener("keydown", (event) => {
15901
- if (event.key === "Escape") closeExportPreviewMenu();
17088
+ if (event.key === "Escape") {
17089
+ closeExportPreviewMenu();
17090
+ closePreviewLinkMenu();
17091
+ }
15902
17092
  });
15903
17093
 
15904
17094
  saveAsBtn.addEventListener("click", () => {
@@ -15909,7 +17099,7 @@
15909
17099
  }
15910
17100
 
15911
17101
  var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
15912
- var suggestedDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim().replace(/\/$/, "") + "/" : "./";
17102
+ var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
15913
17103
  const suggested = sourceState.path || (suggestedDir + suggestedName);
15914
17104
  const path = window.prompt("Save editor content as:", suggested);
15915
17105
  if (!path) return;
@@ -15988,6 +17178,12 @@
15988
17178
  });
15989
17179
  }
15990
17180
 
17181
+ if (clearWorkspaceBtn) {
17182
+ clearWorkspaceBtn.addEventListener("click", () => {
17183
+ clearStudioWorkspace();
17184
+ });
17185
+ }
17186
+
15991
17187
  sendEditorBtn.addEventListener("click", () => {
15992
17188
  const content = sourceTextEl.value;
15993
17189
  if (!content.trim()) {
@@ -16025,9 +17221,7 @@
16025
17221
  content,
16026
17222
  label: sourceState && sourceState.label ? sourceState.label : "current editor",
16027
17223
  path: sourceState && sourceState.path ? sourceState.path : undefined,
16028
- resourceDir: resourceDirInput && resourceDirInput.value.trim()
16029
- ? resourceDirInput.value.trim()
16030
- : undefined,
17224
+ resourceDir: getCurrentResourceDirValue() || undefined,
16031
17225
  });
16032
17226
 
16033
17227
  if (!sent) {
@@ -16067,9 +17261,7 @@
16067
17261
  type: "load_git_diff_request",
16068
17262
  requestId,
16069
17263
  sourcePath: effectivePath || sourceState.path || undefined,
16070
- resourceDir: resourceDirInput && resourceDirInput.value.trim()
16071
- ? resourceDirInput.value.trim()
16072
- : undefined,
17264
+ resourceDir: getCurrentResourceDirValue() || undefined,
16073
17265
  });
16074
17266
 
16075
17267
  if (!sent) {
@@ -16288,6 +17480,27 @@
16288
17480
  void handleCopyPreviewBlockButtonClick(event);
16289
17481
  }, true);
16290
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
+
16291
17504
  document.addEventListener("pointerup", (event) => {
16292
17505
  const target = event.target;
16293
17506
  const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
@@ -16478,7 +17691,8 @@
16478
17691
  if (resourceDirLabel) resourceDirLabel.hidden = state !== "label";
16479
17692
  }
16480
17693
  function applyResourceDir() {
16481
- var dir = resourceDirInput ? resourceDirInput.value.trim() : "";
17694
+ var dir = getCurrentResourceDirValue();
17695
+ if (resourceDirInput) resourceDirInput.value = dir;
16482
17696
  if (dir) {
16483
17697
  if (resourceDirLabel) resourceDirLabel.textContent = "Working dir: " + dir;
16484
17698
  showResourceDirState("label");
@@ -16488,6 +17702,7 @@
16488
17702
  updateSaveFileTooltip();
16489
17703
  syncActionButtons();
16490
17704
  renderSourcePreview();
17705
+ scheduleWorkspacePersistence();
16491
17706
  }
16492
17707
  if (sourceBadgeEl) {
16493
17708
  sourceBadgeEl.addEventListener("click", () => {
@@ -16513,7 +17728,7 @@
16513
17728
  applyResourceDir();
16514
17729
  } else if (e.key === "Escape") {
16515
17730
  e.preventDefault();
16516
- var dir = resourceDirInput.value.trim();
17731
+ var dir = getCurrentResourceDirValue();
16517
17732
  if (dir) {
16518
17733
  showResourceDirState("label");
16519
17734
  } else {
@@ -16530,6 +17745,7 @@
16530
17745
  updateSaveFileTooltip();
16531
17746
  syncActionButtons();
16532
17747
  renderSourcePreview();
17748
+ scheduleWorkspacePersistence();
16533
17749
  });
16534
17750
  }
16535
17751
 
@@ -16578,7 +17794,7 @@
16578
17794
  setResponseFontSize(initialResponseFontSize, { persist: false });
16579
17795
 
16580
17796
  if (resourceDirInput && initialResourceDir) {
16581
- resourceDirInput.value = initialResourceDir;
17797
+ resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
16582
17798
  }
16583
17799
  setSourceState(initialSourceState);
16584
17800
  refreshResponseUi();
@@ -16606,9 +17822,16 @@
16606
17822
  setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
16607
17823
  setReplSendMode(replSendMode);
16608
17824
 
17825
+ const persistedWorkspaceState = readPersistedWorkspaceState();
17826
+ applyPersistedWorkspaceState(persistedWorkspaceState);
17827
+
16609
17828
  setEditorView(editorView);
16610
17829
  setRightView(rightView);
16611
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
+ }
16612
17835
  connect();
16613
17836
  } catch (error) {
16614
17837
  hardFail("Studio UI init failed", error);