pi-studio 0.9.10 → 0.9.12

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");
@@ -245,6 +246,8 @@
245
246
  const REPL_JOURNAL_MAX_ENTRIES = 80;
246
247
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
247
248
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
249
+ const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
250
+ const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
248
251
  const EDITOR_TAB_TEXT = " ";
249
252
  const QUIZ_DEFAULT_COUNT = 5;
250
253
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
@@ -287,6 +290,15 @@
287
290
  return "python";
288
291
  }
289
292
  })();
293
+ let replCommandOverrides = (() => {
294
+ try {
295
+ const raw = window.localStorage && window.localStorage.getItem("piStudio.replCommandOverrides");
296
+ const parsed = raw ? JSON.parse(raw) : {};
297
+ return parsed && typeof parsed === "object" ? parsed : {};
298
+ } catch {
299
+ return {};
300
+ }
301
+ })();
290
302
  let replTranscript = "";
291
303
  let replError = "";
292
304
  let replMessage = "";
@@ -951,11 +963,26 @@
951
963
  setStatus("Loaded visible working into editor.", "success");
952
964
  }
953
965
 
954
- function normalizeReplRuntime(value) {
966
+ function getKnownReplRuntime(value) {
955
967
  const runtime = String(value || "").trim().toLowerCase();
956
968
  return runtime === "shell" || runtime === "python" || runtime === "ipython" || runtime === "julia" || runtime === "r" || runtime === "ghci" || runtime === "clojure"
957
969
  ? runtime
958
- : "python";
970
+ : "";
971
+ }
972
+
973
+ function normalizeReplRuntime(value) {
974
+ return getKnownReplRuntime(value) || "python";
975
+ }
976
+
977
+ function getReplRuntimeLabel(value) {
978
+ const runtime = normalizeReplRuntime(value);
979
+ if (runtime === "shell") return "Shell";
980
+ if (runtime === "python") return "Python";
981
+ if (runtime === "ipython") return "IPython";
982
+ if (runtime === "julia") return "Julia";
983
+ if (runtime === "r") return "R";
984
+ if (runtime === "ghci") return "GHCi";
985
+ return "Clojure";
959
986
  }
960
987
 
961
988
  function normalizeReplSession(session) {
@@ -980,6 +1007,47 @@
980
1007
  }
981
1008
  }
982
1009
 
1010
+ function normalizeReplCommandOverride(value) {
1011
+ return String(value || "").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 240);
1012
+ }
1013
+
1014
+ function getReplCommandOverride(runtime) {
1015
+ const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
1016
+ const value = replCommandOverrides && typeof replCommandOverrides === "object"
1017
+ ? replCommandOverrides[normalizedRuntime]
1018
+ : "";
1019
+ return normalizeReplCommandOverride(value);
1020
+ }
1021
+
1022
+ function persistReplCommandOverrides() {
1023
+ try {
1024
+ if (window.localStorage) window.localStorage.setItem("piStudio.replCommandOverrides", JSON.stringify(replCommandOverrides || {}));
1025
+ } catch {
1026
+ // Ignore storage failures.
1027
+ }
1028
+ }
1029
+
1030
+ function setReplCommandOverride(runtime, command) {
1031
+ const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
1032
+ const normalizedCommand = normalizeReplCommandOverride(command);
1033
+ replCommandOverrides = replCommandOverrides && typeof replCommandOverrides === "object" ? replCommandOverrides : {};
1034
+ if (normalizedCommand) {
1035
+ replCommandOverrides[normalizedRuntime] = normalizedCommand;
1036
+ } else {
1037
+ delete replCommandOverrides[normalizedRuntime];
1038
+ }
1039
+ persistReplCommandOverrides();
1040
+ }
1041
+
1042
+ function getCurrentReplStartCommandFromDom() {
1043
+ if (!critiqueViewEl || typeof critiqueViewEl.querySelector !== "function") return getReplCommandOverride(replRuntime);
1044
+ const inputEl = critiqueViewEl.querySelector("[data-repl-command]");
1045
+ if (inputEl && "value" in inputEl) {
1046
+ return normalizeReplCommandOverride(inputEl.value);
1047
+ }
1048
+ return getReplCommandOverride(replRuntime);
1049
+ }
1050
+
983
1051
  function normalizeReplSendMode(value) {
984
1052
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
985
1053
  }
@@ -1055,7 +1123,10 @@
1055
1123
  ? sessions.map(normalizeReplSession).filter(Boolean)
1056
1124
  : [];
1057
1125
  if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
1058
- replActiveSessionName = replSessions[0] ? replSessions[0].sessionName : "";
1126
+ setActiveReplSession("");
1127
+ }
1128
+ if (!getActiveReplSessionForCurrentRuntime()) {
1129
+ selectReplSessionForRuntime(replRuntime, replActiveSessionName);
1059
1130
  }
1060
1131
  return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
1061
1132
  }
@@ -1064,9 +1135,43 @@
1064
1135
  return replSessions.find((session) => session.sessionName === replActiveSessionName) || null;
1065
1136
  }
1066
1137
 
1138
+ function isReplSessionRuntimeCompatible(session, runtime) {
1139
+ if (!session) return false;
1140
+ const sessionRuntime = getKnownReplRuntime(session.runtime);
1141
+ return Boolean(sessionRuntime) && sessionRuntime === normalizeReplRuntime(runtime);
1142
+ }
1143
+
1144
+ function getReplSessionsForRuntime(runtime) {
1145
+ const normalizedRuntime = normalizeReplRuntime(runtime);
1146
+ return replSessions.filter((session) => isReplSessionRuntimeCompatible(session, normalizedRuntime));
1147
+ }
1148
+
1149
+ function selectReplSessionForRuntime(runtime, preferredSessionName) {
1150
+ const sessions = getReplSessionsForRuntime(runtime);
1151
+ const preferred = String(preferredSessionName || "").trim();
1152
+ const selected = sessions.find((session) => session.sessionName === preferred) || sessions[0] || null;
1153
+ setActiveReplSession(selected ? selected.sessionName : "");
1154
+ return selected;
1155
+ }
1156
+
1157
+ function setActiveReplSessionForCurrentRuntime(sessionName) {
1158
+ const name = String(sessionName || "").trim();
1159
+ const candidate = name ? replSessions.find((session) => session.sessionName === name) : null;
1160
+ if (candidate && isReplSessionRuntimeCompatible(candidate, replRuntime)) {
1161
+ setActiveReplSession(candidate.sessionName);
1162
+ return candidate;
1163
+ }
1164
+ return selectReplSessionForRuntime(replRuntime, replActiveSessionName);
1165
+ }
1166
+
1167
+ function getActiveReplSessionForCurrentRuntime() {
1168
+ const session = getActiveReplSession();
1169
+ return isReplSessionRuntimeCompatible(session, replRuntime) ? session : null;
1170
+ }
1171
+
1067
1172
  function buildActiveReplPromptContext() {
1068
1173
  if (rightView !== "repl") return "";
1069
- const session = getActiveReplSession();
1174
+ const session = getActiveReplSessionForCurrentRuntime();
1070
1175
  if (!session) return "";
1071
1176
  const runtime = session.runtime && session.runtime !== "unknown" ? session.runtime : "unknown";
1072
1177
  return [
@@ -1161,7 +1266,7 @@
1161
1266
  }
1162
1267
 
1163
1268
  function getActiveReplRuntime() {
1164
- const session = getActiveReplSession();
1269
+ const session = getActiveReplSessionForCurrentRuntime();
1165
1270
  if (session && session.runtime && session.runtime !== "unknown") return normalizeReplRuntime(session.runtime);
1166
1271
  return normalizeReplRuntime(replRuntime);
1167
1272
  }
@@ -1501,7 +1606,7 @@
1501
1606
 
1502
1607
  function stripStudioReplSubmissionEcho(delta) {
1503
1608
  let value = String(delta || "").replace(/^\s+/, "");
1504
- // The raw mirror below remains raw; REPL Studio cards hide only the
1609
+ // The raw mirror below remains raw; Studio record cards hide only the
1505
1610
  // temp-file wrapper used to submit multiline snippets safely. The
1506
1611
  // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
1507
1612
  const submissionEchoPatterns = [
@@ -1565,11 +1670,11 @@
1565
1670
  function buildReplJournalMarkdown(entries) {
1566
1671
  const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1567
1672
  const sessionName = getActiveReplJournalSessionName();
1568
- const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString()];
1673
+ const lines = ["# Studio REPL Record", "", "Generated: " + new Date().toLocaleString()];
1569
1674
  if (sessionName) lines.push("Session: `" + sessionName + "`");
1570
1675
  lines.push("");
1571
1676
  if (!visibleEntries.length) {
1572
- lines.push(sessionName ? ("_No REPL Studio entries for `" + sessionName + "` yet._") : "_No REPL Studio entries yet._");
1677
+ lines.push(sessionName ? ("_No Studio REPL record entries for `" + sessionName + "` yet._") : "_No Studio REPL record entries yet._");
1573
1678
  return lines.join("\n");
1574
1679
  }
1575
1680
  visibleEntries.forEach((entry, index) => {
@@ -1601,11 +1706,11 @@
1601
1706
  async function copyReplJournalToClipboard() {
1602
1707
  const entries = getVisibleReplJournalEntries();
1603
1708
  if (!entries.length) {
1604
- setStatus("No REPL Studio entries to copy for this session yet.", "warning");
1709
+ setStatus("No Studio REPL record entries to copy for this session yet.", "warning");
1605
1710
  return;
1606
1711
  }
1607
1712
  if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
1608
- setStatus("Copied REPL Studio session as Markdown.", "success");
1713
+ setStatus("Copied Studio REPL record as Markdown.", "success");
1609
1714
  } else {
1610
1715
  setStatus("Clipboard write failed.", "warning");
1611
1716
  }
@@ -1614,7 +1719,7 @@
1614
1719
  function exportReplJournalMarkdown() {
1615
1720
  const entries = getVisibleReplJournalEntries();
1616
1721
  if (!entries.length) {
1617
- setStatus("No REPL Studio entries to export for this session yet.", "warning");
1722
+ setStatus("No Studio REPL record entries to export for this session yet.", "warning");
1618
1723
  return;
1619
1724
  }
1620
1725
  const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
@@ -1628,7 +1733,7 @@
1628
1733
  link.click();
1629
1734
  link.remove();
1630
1735
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1631
- setStatus("Exported REPL Studio session Markdown.", "success");
1736
+ setStatus("Exported Studio REPL record Markdown.", "success");
1632
1737
  }
1633
1738
 
1634
1739
  function clearReplJournal() {
@@ -1640,27 +1745,27 @@
1640
1745
  }
1641
1746
  activeReplJournalEntryId = "";
1642
1747
  persistReplJournalEntries();
1643
- setStatus(sessionName ? "Cleared REPL Studio for this session." : "Cleared REPL Studio.", "success");
1748
+ setStatus(sessionName ? "Cleared Studio REPL record for this session." : "Cleared Studio REPL record.", "success");
1644
1749
  renderReplViewIfActive({ force: true });
1645
1750
  }
1646
1751
 
1647
1752
  function loadReplJournalIntoEditor() {
1648
1753
  const entries = getVisibleReplJournalEntries();
1649
1754
  if (!entries.length) {
1650
- setStatus("No REPL Studio entries to load for this session yet.", "warning");
1755
+ setStatus("No Studio REPL record entries to load for this session yet.", "warning");
1651
1756
  return;
1652
1757
  }
1653
1758
  const markdown = buildReplJournalMarkdown(entries);
1654
1759
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1655
- setSourceState({ source: "blank", label: "REPL Studio", path: null });
1760
+ setSourceState({ source: "blank", label: "Studio REPL Record", path: null });
1656
1761
  setEditorLanguage("markdown");
1657
- setStatus("Loaded REPL Studio session into editor.", "success");
1762
+ setStatus("Loaded Studio REPL record into editor.", "success");
1658
1763
  }
1659
1764
 
1660
1765
  function addSelectedReplJournalNote() {
1661
1766
  const note = getSelectedOrCurrentParagraphForReplNote();
1662
1767
  if (!note.trim()) {
1663
- setStatus("Select prose or place the cursor in a paragraph to add a REPL Studio note.", "warning");
1768
+ setStatus("Select prose or place the cursor in a paragraph to add a Studio REPL record note.", "warning");
1664
1769
  return;
1665
1770
  }
1666
1771
  addReplJournalEntry({
@@ -1671,14 +1776,14 @@
1671
1776
  sessionName: replActiveSessionName,
1672
1777
  runtime: getActiveReplRuntime(),
1673
1778
  });
1674
- setStatus("Added note to REPL Studio.", "success");
1779
+ setStatus("Added note to Studio REPL record.", "success");
1675
1780
  renderReplViewIfActive({ force: true });
1676
1781
  }
1677
1782
 
1678
1783
  function sendReplPayload(payload) {
1679
- const session = getActiveReplSession();
1784
+ const session = getActiveReplSessionForCurrentRuntime();
1680
1785
  if (!session) {
1681
- setStatus("Start or select a REPL session first.", "warning");
1786
+ setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
1682
1787
  return;
1683
1788
  }
1684
1789
  if (!payload || payload.error) {
@@ -1696,7 +1801,7 @@
1696
1801
  runtime: getActiveReplRuntime(),
1697
1802
  skippedChunks: payload.skippedChunks,
1698
1803
  });
1699
- setStatus("Added prose to REPL Studio.", "success");
1804
+ setStatus("Added prose to Studio REPL record.", "success");
1700
1805
  renderReplViewIfActive({ force: true });
1701
1806
  } else {
1702
1807
  setStatus("No code or prose found to send.", "warning");
@@ -1770,6 +1875,11 @@
1770
1875
  let fileBackedBaselineText = null;
1771
1876
  let activePane = "left";
1772
1877
  let paneFocusTarget = "off";
1878
+ let paneSplitPercent = 50;
1879
+ const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
1880
+ const PANE_SPLIT_MIN_PERCENT = 20;
1881
+ const PANE_SPLIT_MAX_PERCENT = 80;
1882
+ const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
1773
1883
  const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
1774
1884
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
1775
1885
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
@@ -2430,7 +2540,7 @@
2430
2540
 
2431
2541
  function getTerminalBusyStatus() {
2432
2542
  if (terminalActivityPhase === "tool") {
2433
- if (terminalActivityLabel) {
2543
+ if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
2434
2544
  return "Terminal: " + withEllipsis(terminalActivityLabel);
2435
2545
  }
2436
2546
  return terminalActivityToolName
@@ -2474,7 +2584,7 @@
2474
2584
  const action = getStudioActionLabel(kind);
2475
2585
  const queueSuffix = studioRunChainActive ? formatQueuedSteeringSuffix() : "";
2476
2586
  if (terminalActivityPhase === "tool") {
2477
- if (terminalActivityLabel) {
2587
+ if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
2478
2588
  return "Studio: " + withEllipsis(terminalActivityLabel) + queueSuffix;
2479
2589
  }
2480
2590
  return terminalActivityToolName
@@ -3001,6 +3111,142 @@
3001
3111
  setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
3002
3112
  }
3003
3113
 
3114
+ function clampPaneSplitPercent(value) {
3115
+ const numeric = Number(value);
3116
+ if (!Number.isFinite(numeric)) return 50;
3117
+ const clamped = Math.max(PANE_SPLIT_MIN_PERCENT, Math.min(PANE_SPLIT_MAX_PERCENT, Math.round(numeric * 10) / 10));
3118
+ return Math.abs(clamped - 50) <= PANE_SPLIT_SNAP_TO_CENTER_PERCENT ? 50 : clamped;
3119
+ }
3120
+
3121
+ function applyPaneSplitPercent(percent, options) {
3122
+ paneSplitPercent = clampPaneSplitPercent(percent);
3123
+ const rightPercent = Math.round((100 - paneSplitPercent) * 10) / 10;
3124
+ document.documentElement.style.setProperty("--studio-left-pane-fr", paneSplitPercent + "fr");
3125
+ document.documentElement.style.setProperty("--studio-right-pane-fr", rightPercent + "fr");
3126
+ if (paneResizeHandleEl) {
3127
+ paneResizeHandleEl.setAttribute("aria-valuemin", String(PANE_SPLIT_MIN_PERCENT));
3128
+ paneResizeHandleEl.setAttribute("aria-valuemax", String(PANE_SPLIT_MAX_PERCENT));
3129
+ paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(paneSplitPercent)));
3130
+ paneResizeHandleEl.setAttribute("aria-valuetext", "Editor " + Math.round(paneSplitPercent) + " percent, response " + Math.round(rightPercent) + " percent");
3131
+ }
3132
+ if (!options || options.persist !== false) {
3133
+ try {
3134
+ if (window.localStorage) window.localStorage.setItem(PANE_SPLIT_STORAGE_KEY, String(paneSplitPercent));
3135
+ } catch {
3136
+ // Ignore localStorage failures.
3137
+ }
3138
+ }
3139
+ }
3140
+
3141
+ function resetPaneSplitPercent() {
3142
+ applyPaneSplitPercent(50);
3143
+ setStatus("Pane split reset to 50/50.");
3144
+ }
3145
+
3146
+ function loadPaneSplitPercent() {
3147
+ if (isEditorOnlyMode) return;
3148
+ let stored = "";
3149
+ try {
3150
+ stored = window.localStorage ? String(window.localStorage.getItem(PANE_SPLIT_STORAGE_KEY) || "") : "";
3151
+ } catch {
3152
+ stored = "";
3153
+ }
3154
+ applyPaneSplitPercent(stored ? Number(stored) : 50, { persist: false });
3155
+ }
3156
+
3157
+ function getPaneSplitPercentFromPointerEvent(event) {
3158
+ const mainEl = paneResizeHandleEl && typeof paneResizeHandleEl.closest === "function"
3159
+ ? paneResizeHandleEl.closest("main")
3160
+ : null;
3161
+ if (!mainEl || typeof mainEl.getBoundingClientRect !== "function") return paneSplitPercent;
3162
+ const rect = mainEl.getBoundingClientRect();
3163
+ if (!rect.width) return paneSplitPercent;
3164
+ const x = typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2);
3165
+ return ((x - rect.left) / rect.width) * 100;
3166
+ }
3167
+
3168
+ function setupPaneResizeHandle() {
3169
+ if (!paneResizeHandleEl || isEditorOnlyMode) return;
3170
+ loadPaneSplitPercent();
3171
+ let dragging = false;
3172
+ let movedDuringDrag = false;
3173
+ let pointerStartX = 0;
3174
+ let activePaneResizePointerId = null;
3175
+ const finishDrag = () => {
3176
+ if (!dragging) return;
3177
+ dragging = false;
3178
+ if (document.body && document.body.classList) document.body.classList.remove("pane-resizing");
3179
+ try {
3180
+ if (typeof paneResizeHandleEl.releasePointerCapture === "function" && activePaneResizePointerId != null) {
3181
+ paneResizeHandleEl.releasePointerCapture(activePaneResizePointerId);
3182
+ }
3183
+ } catch {
3184
+ // Ignore pointer-capture cleanup failures.
3185
+ }
3186
+ activePaneResizePointerId = null;
3187
+ if (movedDuringDrag) {
3188
+ setStatus("Pane split: editor " + Math.round(paneSplitPercent) + "%, response " + Math.round(100 - paneSplitPercent) + "%.");
3189
+ }
3190
+ movedDuringDrag = false;
3191
+ };
3192
+ paneResizeHandleEl.addEventListener("pointerdown", (event) => {
3193
+ if (event.button != null && event.button !== 0) return;
3194
+ event.preventDefault();
3195
+ event.stopPropagation();
3196
+ dragging = true;
3197
+ movedDuringDrag = false;
3198
+ pointerStartX = typeof event.clientX === "number" ? event.clientX : 0;
3199
+ activePaneResizePointerId = event.pointerId;
3200
+ if (document.body && document.body.classList) document.body.classList.add("pane-resizing");
3201
+ try {
3202
+ if (typeof paneResizeHandleEl.focus === "function") paneResizeHandleEl.focus({ preventScroll: true });
3203
+ } catch {
3204
+ try { paneResizeHandleEl.focus(); } catch {}
3205
+ }
3206
+ try {
3207
+ if (typeof paneResizeHandleEl.setPointerCapture === "function") paneResizeHandleEl.setPointerCapture(event.pointerId);
3208
+ } catch {
3209
+ // Ignore pointer-capture failures.
3210
+ }
3211
+ });
3212
+ paneResizeHandleEl.addEventListener("pointermove", (event) => {
3213
+ if (!dragging) return;
3214
+ const movement = typeof event.clientX === "number" ? Math.abs(event.clientX - pointerStartX) : 0;
3215
+ if (!movedDuringDrag && movement < 3) return;
3216
+ movedDuringDrag = true;
3217
+ event.preventDefault();
3218
+ applyPaneSplitPercent(getPaneSplitPercentFromPointerEvent(event));
3219
+ });
3220
+ paneResizeHandleEl.addEventListener("pointerup", finishDrag);
3221
+ paneResizeHandleEl.addEventListener("pointercancel", finishDrag);
3222
+ paneResizeHandleEl.addEventListener("dblclick", (event) => {
3223
+ event.preventDefault();
3224
+ event.stopPropagation();
3225
+ resetPaneSplitPercent();
3226
+ });
3227
+ paneResizeHandleEl.addEventListener("keydown", (event) => {
3228
+ if (event.key === "Home") {
3229
+ event.preventDefault();
3230
+ applyPaneSplitPercent(PANE_SPLIT_MIN_PERCENT);
3231
+ return;
3232
+ }
3233
+ if (event.key === "End") {
3234
+ event.preventDefault();
3235
+ applyPaneSplitPercent(PANE_SPLIT_MAX_PERCENT);
3236
+ return;
3237
+ }
3238
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
3239
+ event.preventDefault();
3240
+ resetPaneSplitPercent();
3241
+ return;
3242
+ }
3243
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
3244
+ event.preventDefault();
3245
+ const step = event.shiftKey ? 10 : 5;
3246
+ applyPaneSplitPercent(paneSplitPercent + (event.key === "ArrowLeft" ? -step : step));
3247
+ });
3248
+ }
3249
+
3004
3250
  function updatePaneFocusButtons() {
3005
3251
  [
3006
3252
  [leftFocusBtn, "left"],
@@ -3239,6 +3485,40 @@
3239
3485
  return false;
3240
3486
  }
3241
3487
 
3488
+ function triggerResponseHistoryShortcut(action) {
3489
+ if (isEditorOnlyMode) {
3490
+ setStatus("Response history is unavailable in editor-only Studio.", "warning");
3491
+ return false;
3492
+ }
3493
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
3494
+ if (total <= 0) {
3495
+ setStatus("No response history available yet.", "warning");
3496
+ return false;
3497
+ }
3498
+ if (action === "previous") {
3499
+ if (responseHistoryIndex <= 0) {
3500
+ setStatus("Already at the first response.", "warning");
3501
+ return false;
3502
+ }
3503
+ return selectHistoryIndex(responseHistoryIndex - 1);
3504
+ }
3505
+ if (action === "next") {
3506
+ if (responseHistoryIndex >= total - 1) {
3507
+ setStatus("Already at the latest response.", "warning");
3508
+ return false;
3509
+ }
3510
+ return selectHistoryIndex(responseHistoryIndex + 1);
3511
+ }
3512
+ if (action === "latest") {
3513
+ if (responseHistoryIndex >= total - 1) {
3514
+ setStatus("Already viewing the latest response.");
3515
+ return false;
3516
+ }
3517
+ return selectHistoryIndex(total - 1);
3518
+ }
3519
+ return false;
3520
+ }
3521
+
3242
3522
  function isTextEntryShortcutTarget(target) {
3243
3523
  if (!(target instanceof Element)) return false;
3244
3524
  const editable = target.closest("input, textarea, select, [contenteditable]");
@@ -3254,6 +3534,7 @@
3254
3534
  if (!event || event.defaultPrevented) return;
3255
3535
 
3256
3536
  const key = typeof event.key === "string" ? event.key : "";
3537
+ const code = typeof event.code === "string" ? event.code : "";
3257
3538
  const plainEscape = key === "Escape"
3258
3539
  && !event.metaKey
3259
3540
  && !event.ctrlKey
@@ -3354,6 +3635,24 @@
3354
3635
  return;
3355
3636
  }
3356
3637
 
3638
+ if (!isTextEntryShortcutTarget(event.target) && !event.metaKey && !event.ctrlKey && event.altKey && !event.shiftKey) {
3639
+ if (key === "ArrowLeft") {
3640
+ event.preventDefault();
3641
+ triggerResponseHistoryShortcut("previous");
3642
+ return;
3643
+ }
3644
+ if (key === "ArrowRight") {
3645
+ event.preventDefault();
3646
+ triggerResponseHistoryShortcut("next");
3647
+ return;
3648
+ }
3649
+ if (key.toLowerCase() === "l" || code === "KeyL") {
3650
+ event.preventDefault();
3651
+ triggerResponseHistoryShortcut("latest");
3652
+ return;
3653
+ }
3654
+ }
3655
+
3357
3656
  const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
3358
3657
  if (isPaneSwitchShortcut) {
3359
3658
  event.preventDefault();
@@ -3968,6 +4267,11 @@
3968
4267
  + " root ? root.offsetHeight : 0\n"
3969
4268
  + " ));\n"
3970
4269
  + " }\n"
4270
+ + " function getScrollTop() {\n"
4271
+ + " const body = document.body;\n"
4272
+ + " const root = document.documentElement;\n"
4273
+ + " return window.scrollY || (root ? root.scrollTop : 0) || (body ? body.scrollTop : 0) || 0;\n"
4274
+ + " }\n"
3971
4275
  + " function sendHeight() {\n"
3972
4276
  + " scheduled = false;\n"
3973
4277
  + " const height = measureHeight();\n"
@@ -3980,13 +4284,301 @@
3980
4284
  + " scheduled = true;\n"
3981
4285
  + " requestAnimationFrame(sendHeight);\n"
3982
4286
  + " }\n"
4287
+ + " function decodeFragment(value) {\n"
4288
+ + " const text = String(value || '').replace(/^#/, '');\n"
4289
+ + " try { return decodeURIComponent(text); } catch { return text; }\n"
4290
+ + " }\n"
4291
+ + " function findNamedFragmentTarget(fragment) {\n"
4292
+ + " const decoded = decodeFragment(fragment);\n"
4293
+ + " if (!decoded) return document.documentElement || document.body;\n"
4294
+ + " return document.getElementById(decoded) || document.getElementsByName(decoded)[0] || null;\n"
4295
+ + " }\n"
4296
+ + " function postFragmentScroll(target) {\n"
4297
+ + " if (!target || typeof target.getBoundingClientRect !== 'function') return;\n"
4298
+ + " const rect = target.getBoundingClientRect();\n"
4299
+ + " const scrollTop = getScrollTop();\n"
4300
+ + " try {\n"
4301
+ + " parent.postMessage({ type: 'pi-studio-html-artifact-fragment', id: PREVIEW_ID, targetTop: Math.max(0, rect.top + scrollTop), scrollTop, viewportHeight: window.innerHeight || 0, documentHeight: measureHeight() }, '*');\n"
4302
+ + " } catch {}\n"
4303
+ + " }\n"
4304
+ + " function scrollFragmentIntoView(fragment, options) {\n"
4305
+ + " const target = findNamedFragmentTarget(fragment);\n"
4306
+ + " if (!target) return false;\n"
4307
+ + " const behavior = options && options.smooth === false ? 'auto' : 'smooth';\n"
4308
+ + " try { target.scrollIntoView({ block: 'start', inline: 'nearest', behavior }); } catch { try { target.scrollIntoView(true); } catch {} }\n"
4309
+ + " postFragmentScroll(target);\n"
4310
+ + " setTimeout(() => postFragmentScroll(target), 80);\n"
4311
+ + " setTimeout(() => postFragmentScroll(target), 300);\n"
4312
+ + " return true;\n"
4313
+ + " }\n"
4314
+ + " function getAnchorFromClickTarget(target) {\n"
4315
+ + " let node = target;\n"
4316
+ + " if (node && node.nodeType === 3) node = node.parentElement;\n"
4317
+ + " return node && typeof node.closest === 'function' ? node.closest('a[href]') : null;\n"
4318
+ + " }\n"
4319
+ + " function getSameDocumentFragment(anchor) {\n"
4320
+ + " if (!anchor || typeof anchor.getAttribute !== 'function') return null;\n"
4321
+ + " if (anchor.hasAttribute('download')) return null;\n"
4322
+ + " const target = String(anchor.getAttribute('target') || '').trim().toLowerCase();\n"
4323
+ + " if (target && target !== '_self') return null;\n"
4324
+ + " const rawHref = String(anchor.getAttribute('href') || '').trim();\n"
4325
+ + " if (!rawHref) return null;\n"
4326
+ + " if (rawHref.charAt(0) === '#') return rawHref.slice(1);\n"
4327
+ + " const hashIndex = rawHref.indexOf('#');\n"
4328
+ + " if (hashIndex < 0) return null;\n"
4329
+ + " const beforeHash = rawHref.slice(0, hashIndex);\n"
4330
+ + " const currentWithoutHash = String(window.location && window.location.href || '').split('#')[0];\n"
4331
+ + " if (!beforeHash || beforeHash === currentWithoutHash || beforeHash === 'about:srcdoc') return rawHref.slice(hashIndex + 1);\n"
4332
+ + " return null;\n"
4333
+ + " }\n"
4334
+ + " function writeFragmentHistory(fragment) {\n"
4335
+ + " try {\n"
4336
+ + " if (history && typeof history.pushState === 'function') {\n"
4337
+ + " history.pushState(null, '', fragment ? '#' + encodeURIComponent(decodeFragment(fragment)) : '#');\n"
4338
+ + " }\n"
4339
+ + " } catch {}\n"
4340
+ + " }\n"
4341
+ + " function handleFragmentAnchorClick(event) {\n"
4342
+ + " if (!event || event.defaultPrevented) return;\n"
4343
+ + " if (typeof event.button === 'number' && event.button !== 0) return;\n"
4344
+ + " if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n"
4345
+ + " const anchor = getAnchorFromClickTarget(event.target);\n"
4346
+ + " const fragment = getSameDocumentFragment(anchor);\n"
4347
+ + " if (fragment == null) return;\n"
4348
+ + " if (!scrollFragmentIntoView(fragment)) return;\n"
4349
+ + " event.preventDefault();\n"
4350
+ + " writeFragmentHistory(fragment);\n"
4351
+ + " }\n"
4352
+ + " const htmlMathPlaceholders = new Map();\n"
4353
+ + " let htmlMathSerial = 0;\n"
4354
+ + " let htmlMathScanScheduled = false;\n"
4355
+ + " function delimiterListIncludes(list, start, end) {\n"
4356
+ + " if (!Array.isArray(list)) return false;\n"
4357
+ + " return list.some((entry) => Array.isArray(entry) && entry[0] === start && entry[1] === end);\n"
4358
+ + " }\n"
4359
+ + " function getHtmlMathDelimiterConfig() {\n"
4360
+ + " const mathJax = window.MathJax && typeof window.MathJax === 'object' ? window.MathJax : null;\n"
4361
+ + " const tex = mathJax && mathJax.tex && typeof mathJax.tex === 'object' ? mathJax.tex : null;\n"
4362
+ + " return {\n"
4363
+ + " inlineDollar: Boolean(tex && delimiterListIncludes(tex.inlineMath, '$', '$')),\n"
4364
+ + " displayDollar: Boolean(tex && delimiterListIncludes(tex.displayMath, '$$', '$$')),\n"
4365
+ + " };\n"
4366
+ + " }\n"
4367
+ + " function isEscapedAt(text, index) {\n"
4368
+ + " let count = 0;\n"
4369
+ + " let pos = index - 1;\n"
4370
+ + " while (pos >= 0 && text.charAt(pos) === '\\\\') { count += 1; pos -= 1; }\n"
4371
+ + " return count % 2 === 1;\n"
4372
+ + " }\n"
4373
+ + " function findUnescapedDelimiter(text, delimiter, fromIndex) {\n"
4374
+ + " let index = Math.max(0, Number(fromIndex) || 0);\n"
4375
+ + " while (index < text.length) {\n"
4376
+ + " index = text.indexOf(delimiter, index);\n"
4377
+ + " if (index < 0) return -1;\n"
4378
+ + " if (!isEscapedAt(text, index)) return index;\n"
4379
+ + " index += Math.max(1, delimiter.length);\n"
4380
+ + " }\n"
4381
+ + " return -1;\n"
4382
+ + " }\n"
4383
+ + " function textMightContainMath(text, config) {\n"
4384
+ + " if (!text) return false;\n"
4385
+ + " if (text.indexOf('\\\\(') !== -1 || text.indexOf('\\\\[') !== -1) return true;\n"
4386
+ + " return Boolean((config.inlineDollar || config.displayDollar) && text.indexOf('$') !== -1);\n"
4387
+ + " }\n"
4388
+ + " function findNextMathSegment(text, startIndex, config) {\n"
4389
+ + " for (let index = startIndex; index < text.length; index += 1) {\n"
4390
+ + " if (text.startsWith('\\\\(', index)) {\n"
4391
+ + " const end = findUnescapedDelimiter(text, '\\\\)', index + 2);\n"
4392
+ + " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: false };\n"
4393
+ + " }\n"
4394
+ + " if (text.startsWith('\\\\[', index)) {\n"
4395
+ + " const end = findUnescapedDelimiter(text, '\\\\]', index + 2);\n"
4396
+ + " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: true };\n"
4397
+ + " }\n"
4398
+ + " if (config.displayDollar && text.startsWith('$$', index) && !isEscapedAt(text, index)) {\n"
4399
+ + " const end = findUnescapedDelimiter(text, '$$', index + 2);\n"
4400
+ + " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: true };\n"
4401
+ + " }\n"
4402
+ + " if (config.inlineDollar && text.charAt(index) === '$' && text.charAt(index + 1) !== '$' && !isEscapedAt(text, index)) {\n"
4403
+ + " const end = findUnescapedDelimiter(text, '$', index + 1);\n"
4404
+ + " if (end > index + 1) return { start: index, end: end + 1, tex: text.slice(index + 1, end).trim(), display: false };\n"
4405
+ + " }\n"
4406
+ + " }\n"
4407
+ + " return null;\n"
4408
+ + " }\n"
4409
+ + " function parseHtmlMathSegments(text, config, maxCount) {\n"
4410
+ + " const segments = [];\n"
4411
+ + " let index = 0;\n"
4412
+ + " const maxSegments = Math.max(1, Number(maxCount) || 1);\n"
4413
+ + " while (index < text.length && segments.length < maxSegments) {\n"
4414
+ + " const segment = findNextMathSegment(text, index, config);\n"
4415
+ + " if (!segment) break;\n"
4416
+ + " if (segment.tex) segments.push(segment);\n"
4417
+ + " index = Math.max(segment.end, segment.start + 1);\n"
4418
+ + " }\n"
4419
+ + " return segments;\n"
4420
+ + " }\n"
4421
+ + " function shouldSkipHtmlMathTextNode(node) {\n"
4422
+ + " let el = node && node.parentElement;\n"
4423
+ + " while (el) {\n"
4424
+ + " const tag = el.tagName ? el.tagName.toLowerCase() : '';\n"
4425
+ + " if (['script', 'style', 'textarea', 'pre', 'code', 'math', 'svg', 'mjx-container'].indexOf(tag) !== -1) return true;\n"
4426
+ + " if (el.classList && (el.classList.contains('pi-studio-html-math') || el.classList.contains('MathJax'))) return true;\n"
4427
+ + " el = el.parentElement;\n"
4428
+ + " }\n"
4429
+ + " return false;\n"
4430
+ + " }\n"
4431
+ + " function replaceTextNodeWithHtmlMathPlaceholders(node, segments) {\n"
4432
+ + " if (!node || !node.parentNode || !segments || segments.length === 0) return [];\n"
4433
+ + " const text = String(node.nodeValue || '');\n"
4434
+ + " const fragment = document.createDocumentFragment();\n"
4435
+ + " const items = [];\n"
4436
+ + " let index = 0;\n"
4437
+ + " segments.forEach((segment) => {\n"
4438
+ + " if (segment.start > index) fragment.appendChild(document.createTextNode(text.slice(index, segment.start)));\n"
4439
+ + " const mathId = PREVIEW_ID + '_math_' + (++htmlMathSerial).toString(36);\n"
4440
+ + " const span = document.createElement('span');\n"
4441
+ + " span.className = 'pi-studio-html-math pi-studio-html-math-' + (segment.display ? 'display' : 'inline');\n"
4442
+ + " span.setAttribute('data-pi-studio-html-math-id', mathId);\n"
4443
+ + " span.setAttribute('aria-busy', 'true');\n"
4444
+ + " span.textContent = text.slice(segment.start, segment.end);\n"
4445
+ + " htmlMathPlaceholders.set(mathId, span);\n"
4446
+ + " items.push({ mathId, tex: segment.tex, display: Boolean(segment.display) });\n"
4447
+ + " fragment.appendChild(span);\n"
4448
+ + " index = segment.end;\n"
4449
+ + " });\n"
4450
+ + " if (index < text.length) fragment.appendChild(document.createTextNode(text.slice(index)));\n"
4451
+ + " node.parentNode.replaceChild(fragment, node);\n"
4452
+ + " return items;\n"
4453
+ + " }\n"
4454
+ + " function applyRenderedHtmlMath(results) {\n"
4455
+ + " if (!Array.isArray(results)) return;\n"
4456
+ + " results.forEach((result) => {\n"
4457
+ + " if (!result || typeof result !== 'object') return;\n"
4458
+ + " const mathId = typeof result.mathId === 'string' ? result.mathId : '';\n"
4459
+ + " const placeholder = mathId ? htmlMathPlaceholders.get(mathId) : null;\n"
4460
+ + " if (!placeholder || !placeholder.isConnected) return;\n"
4461
+ + " placeholder.removeAttribute('aria-busy');\n"
4462
+ + " if (result.ok === true && typeof result.html === 'string' && result.html.trim()) {\n"
4463
+ + " placeholder.innerHTML = result.html;\n"
4464
+ + " placeholder.classList.add('pi-studio-html-math-rendered');\n"
4465
+ + " } else {\n"
4466
+ + " placeholder.classList.add('pi-studio-html-math-failed');\n"
4467
+ + " if (typeof result.error === 'string' && result.error) placeholder.title = result.error;\n"
4468
+ + " }\n"
4469
+ + " htmlMathPlaceholders.delete(mathId);\n"
4470
+ + " });\n"
4471
+ + " scheduleHeight();\n"
4472
+ + " }\n"
4473
+ + " function runHtmlMathRenderScan() {\n"
4474
+ + " htmlMathScanScheduled = false;\n"
4475
+ + " if (!document.body || typeof document.createTreeWalker !== 'function') return;\n"
4476
+ + " const nodeFilterApi = typeof NodeFilter !== 'undefined' ? NodeFilter : { SHOW_TEXT: 4, FILTER_ACCEPT: 1, FILTER_REJECT: 2 };\n"
4477
+ + " const config = getHtmlMathDelimiterConfig();\n"
4478
+ + " const nodes = [];\n"
4479
+ + " const walker = document.createTreeWalker(document.body, nodeFilterApi.SHOW_TEXT, {\n"
4480
+ + " acceptNode(node) {\n"
4481
+ + " const text = String(node && node.nodeValue || '');\n"
4482
+ + " if (!textMightContainMath(text, config)) return nodeFilterApi.FILTER_REJECT;\n"
4483
+ + " if (shouldSkipHtmlMathTextNode(node)) return nodeFilterApi.FILTER_REJECT;\n"
4484
+ + " return nodeFilterApi.FILTER_ACCEPT;\n"
4485
+ + " }\n"
4486
+ + " });\n"
4487
+ + " while (walker.nextNode()) nodes.push(walker.currentNode);\n"
4488
+ + " const items = [];\n"
4489
+ + " for (const node of nodes) {\n"
4490
+ + " const remaining = 250 - items.length;\n"
4491
+ + " if (remaining <= 0) break;\n"
4492
+ + " const text = String(node && node.nodeValue || '');\n"
4493
+ + " const segments = parseHtmlMathSegments(text, config, remaining);\n"
4494
+ + " if (segments.length === 0) continue;\n"
4495
+ + " items.push(...replaceTextNodeWithHtmlMathPlaceholders(node, segments));\n"
4496
+ + " }\n"
4497
+ + " if (items.length > 0) {\n"
4498
+ + " try { parent.postMessage({ type: 'pi-studio-html-artifact-render-math', id: PREVIEW_ID, items }, '*'); } catch {}\n"
4499
+ + " }\n"
4500
+ + " }\n"
4501
+ + " function scheduleHtmlMathRenderScan() {\n"
4502
+ + " if (htmlMathScanScheduled) return;\n"
4503
+ + " htmlMathScanScheduled = true;\n"
4504
+ + " requestAnimationFrame(runHtmlMathRenderScan);\n"
4505
+ + " }\n"
4506
+ + " const htmlResourcePlaceholders = new Map();\n"
4507
+ + " let htmlResourceSerial = 0;\n"
4508
+ + " let htmlResourceScanScheduled = false;\n"
4509
+ + " function shouldResolveHtmlPreviewResourceUrl(value) {\n"
4510
+ + " const raw = String(value || '').trim();\n"
4511
+ + " if (!raw || raw.charAt(0) === '#') return false;\n"
4512
+ + " if (/^(?:data|blob|http|https|about|javascript|mailto):/i.test(raw)) return false;\n"
4513
+ + " if (/^\\/\\//.test(raw)) return false;\n"
4514
+ + " return /\\.(?:png|jpe?g|gif|webp)(?:[?#].*)?$/i.test(raw);\n"
4515
+ + " }\n"
4516
+ + " function scanHtmlPreviewResources() {\n"
4517
+ + " htmlResourceScanScheduled = false;\n"
4518
+ + " if (!document.body) return;\n"
4519
+ + " const items = [];\n"
4520
+ + " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
4521
+ + " images.forEach((image) => {\n"
4522
+ + " if (!image || !image.getAttribute) return;\n"
4523
+ + " if (image.getAttribute('data-pi-studio-html-resource-resolved') === 'true') return;\n"
4524
+ + " const raw = String(image.getAttribute('src') || '').trim();\n"
4525
+ + " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
4526
+ + " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
4527
+ + " if (!resourceId) {\n"
4528
+ + " resourceId = PREVIEW_ID + '_resource_' + (++htmlResourceSerial).toString(36);\n"
4529
+ + " image.setAttribute('data-pi-studio-html-resource-id', resourceId);\n"
4530
+ + " }\n"
4531
+ + " htmlResourcePlaceholders.set(resourceId, image);\n"
4532
+ + " items.push({ resourceId, url: raw });\n"
4533
+ + " });\n"
4534
+ + " if (items.length > 0) {\n"
4535
+ + " try { parent.postMessage({ type: 'pi-studio-html-artifact-resolve-resources', id: PREVIEW_ID, resources: items.slice(0, 100) }, '*'); } catch {}\n"
4536
+ + " }\n"
4537
+ + " }\n"
4538
+ + " function scheduleHtmlPreviewResourceScan() {\n"
4539
+ + " if (htmlResourceScanScheduled) return;\n"
4540
+ + " htmlResourceScanScheduled = true;\n"
4541
+ + " requestAnimationFrame(scanHtmlPreviewResources);\n"
4542
+ + " }\n"
4543
+ + " function applyResolvedHtmlPreviewResources(results) {\n"
4544
+ + " if (!Array.isArray(results)) return;\n"
4545
+ + " results.forEach((result) => {\n"
4546
+ + " if (!result || typeof result !== 'object') return;\n"
4547
+ + " const resourceId = typeof result.resourceId === 'string' ? result.resourceId : '';\n"
4548
+ + " const image = resourceId ? htmlResourcePlaceholders.get(resourceId) : null;\n"
4549
+ + " if (!image || !image.isConnected) return;\n"
4550
+ + " if (result.ok === true && typeof result.dataUrl === 'string' && result.dataUrl) {\n"
4551
+ + " image.setAttribute('src', result.dataUrl);\n"
4552
+ + " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
4553
+ + " } else if (typeof result.error === 'string' && result.error) {\n"
4554
+ + " image.setAttribute('title', result.error);\n"
4555
+ + " }\n"
4556
+ + " htmlResourcePlaceholders.delete(resourceId);\n"
4557
+ + " });\n"
4558
+ + " scheduleHeight();\n"
4559
+ + " }\n"
3983
4560
  + " window.addEventListener('message', (event) => {\n"
3984
4561
  + " const data = event && event.data;\n"
3985
- + " if (!data || typeof data !== 'object') return;\n"
3986
- + " if (data.type !== 'pi-studio-html-artifact-zoom' || data.id !== PREVIEW_ID) return;\n"
3987
- + " applyZoom(data.zoom);\n"
4562
+ + " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
4563
+ + " if (data.type === 'pi-studio-html-artifact-zoom') {\n"
4564
+ + " applyZoom(data.zoom);\n"
4565
+ + " return;\n"
4566
+ + " }\n"
4567
+ + " if (data.type === 'pi-studio-html-artifact-math-rendered') {\n"
4568
+ + " applyRenderedHtmlMath(data.results);\n"
4569
+ + " return;\n"
4570
+ + " }\n"
4571
+ + " if (data.type === 'pi-studio-html-artifact-resources-resolved') {\n"
4572
+ + " applyResolvedHtmlPreviewResources(data.results);\n"
4573
+ + " }\n"
3988
4574
  + " });\n"
3989
- + " window.addEventListener('load', scheduleHeight);\n"
4575
+ + " document.addEventListener('click', handleFragmentAnchorClick);\n"
4576
+ + " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4577
+ + " window.addEventListener('hashchange', () => {\n"
4578
+ + " const hash = String(window.location && window.location.hash || '');\n"
4579
+ + " if (hash) scrollFragmentIntoView(hash.slice(1), { smooth: false });\n"
4580
+ + " });\n"
4581
+ + " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
3990
4582
  + " window.addEventListener('resize', scheduleHeight);\n"
3991
4583
  + " if (typeof ResizeObserver === 'function') {\n"
3992
4584
  + " const observer = new ResizeObserver(scheduleHeight);\n"
@@ -3994,20 +4586,32 @@
3994
4586
  + " if (document.body) observer.observe(document.body);\n"
3995
4587
  + " }\n"
3996
4588
  + " if (typeof MutationObserver === 'function') {\n"
3997
- + " const observer = new MutationObserver(scheduleHeight);\n"
4589
+ + " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
3998
4590
  + " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
3999
4591
  + " }\n"
4000
4592
  + " scheduleHeight();\n"
4001
4593
  + " setTimeout(scheduleHeight, 80);\n"
4002
4594
  + " setTimeout(scheduleHeight, 350);\n"
4595
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 0);\n"
4596
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 120);\n"
4597
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 500);\n"
4003
4598
  + "})();\n"
4004
4599
  + "<\/script>";
4005
4600
  }
4006
4601
 
4602
+ function buildHtmlArtifactPreviewMathStyle() {
4603
+ return "<style data-pi-studio-html-preview-math>\n"
4604
+ + ".pi-studio-html-math-display{display:block;margin:0.75em 0;overflow-x:auto;text-align:center;}\n"
4605
+ + ".pi-studio-html-math-display>math{display:block;margin:0 auto;}\n"
4606
+ + ".pi-studio-html-math-inline>math{vertical-align:-0.15em;}\n"
4607
+ + "</style>\n";
4608
+ }
4609
+
4007
4610
  function buildHtmlArtifactPreviewHeadMarkup(previewId) {
4008
4611
  return "<meta charset=\"utf-8\">\n"
4009
4612
  + "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
4010
4613
  + "<meta http-equiv=\"Content-Security-Policy\" content=\"" + escapeHtml(HTML_ARTIFACT_PREVIEW_CSP) + "\">\n"
4614
+ + buildHtmlArtifactPreviewMathStyle()
4011
4615
  + buildHtmlArtifactPreviewResizeScript(previewId);
4012
4616
  }
4013
4617
 
@@ -4067,7 +4671,244 @@
4067
4671
  }
4068
4672
  }
4069
4673
 
4674
+ function handleHtmlArtifactFrameFragmentMessage(event) {
4675
+ const data = event && event.data;
4676
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-fragment") return;
4677
+ const id = typeof data.id === "string" ? data.id : "";
4678
+ const record = id ? htmlArtifactFramesById.get(id) : null;
4679
+ if (!record || !record.iframe || !record.iframe.isConnected) {
4680
+ if (id) htmlArtifactFramesById.delete(id);
4681
+ return;
4682
+ }
4683
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
4684
+ if (record.shell && record.shell.classList && record.shell.classList.contains("is-focused")) return;
4685
+
4686
+ const scrollContainer = record.shell && typeof record.shell.closest === "function"
4687
+ ? record.shell.closest(".panel-scroll")
4688
+ : null;
4689
+ const isCapped = Boolean(record.iframe.classList && record.iframe.classList.contains("is-height-capped"));
4690
+ const documentHeight = Number(data.documentHeight);
4691
+ const viewportHeight = Number(data.viewportHeight);
4692
+ const isInternallyScrollable = isCapped
4693
+ || (Number.isFinite(documentHeight) && Number.isFinite(viewportHeight) && documentHeight > viewportHeight + 2);
4694
+ if (!scrollContainer || isInternallyScrollable) {
4695
+ if (typeof record.iframe.scrollIntoView === "function") {
4696
+ try {
4697
+ record.iframe.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
4698
+ } catch {
4699
+ record.iframe.scrollIntoView(false);
4700
+ }
4701
+ }
4702
+ return;
4703
+ }
4704
+
4705
+ const rawTargetTop = Number(data.targetTop);
4706
+ const offsetInFrame = Number.isFinite(rawTargetTop) && rawTargetTop > 0 ? rawTargetTop : 0;
4707
+ const iframeRect = record.iframe.getBoundingClientRect();
4708
+ const containerRect = scrollContainer.getBoundingClientRect();
4709
+ const topPadding = 12;
4710
+ const nextTop = Math.max(
4711
+ 0,
4712
+ scrollContainer.scrollTop + iframeRect.top - containerRect.top + offsetInFrame - topPadding,
4713
+ );
4714
+ try {
4715
+ scrollContainer.scrollTo({ top: nextTop, behavior: "smooth" });
4716
+ } catch {
4717
+ scrollContainer.scrollTop = nextTop;
4718
+ }
4719
+ }
4720
+
4721
+ function normalizeHtmlArtifactMathRenderItems(rawItems) {
4722
+ if (!Array.isArray(rawItems)) return [];
4723
+ return rawItems.slice(0, 250).map((item) => {
4724
+ const raw = item && typeof item === "object" ? item : null;
4725
+ const mathId = raw && typeof raw.mathId === "string" ? raw.mathId : "";
4726
+ const tex = raw && typeof raw.tex === "string" ? raw.tex : "";
4727
+ if (!mathId || !tex.trim()) return null;
4728
+ return {
4729
+ mathId,
4730
+ tex,
4731
+ display: Boolean(raw.display),
4732
+ };
4733
+ }).filter(Boolean);
4734
+ }
4735
+
4736
+ async function fetchRenderedHtmlArtifactMath(items) {
4737
+ const token = getToken();
4738
+ if (!token) {
4739
+ throw new Error("Missing Studio token in URL.");
4740
+ }
4741
+ const response = await fetchWithTimeout("/render-math?token=" + encodeURIComponent(token), {
4742
+ method: "POST",
4743
+ headers: {
4744
+ "Content-Type": "application/json",
4745
+ },
4746
+ body: JSON.stringify({ items }),
4747
+ }, HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS, "HTML preview math render");
4748
+
4749
+ const rawBody = await response.text();
4750
+ let payload = null;
4751
+ try {
4752
+ payload = rawBody ? JSON.parse(rawBody) : null;
4753
+ } catch {
4754
+ payload = null;
4755
+ }
4756
+ if (!response.ok) {
4757
+ const message = payload && typeof payload.error === "string"
4758
+ ? payload.error
4759
+ : "HTML preview math render failed with HTTP " + response.status + ".";
4760
+ throw new Error(message);
4761
+ }
4762
+ if (!payload || payload.ok !== true || !Array.isArray(payload.results)) {
4763
+ throw new Error("HTML preview math renderer returned an invalid payload.");
4764
+ }
4765
+ return payload.results;
4766
+ }
4767
+
4768
+ function postHtmlArtifactMathResults(record, results) {
4769
+ if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
4770
+ try {
4771
+ record.iframe.contentWindow.postMessage({
4772
+ type: "pi-studio-html-artifact-math-rendered",
4773
+ id: record.id || "",
4774
+ results: Array.isArray(results) ? results : [],
4775
+ }, "*");
4776
+ } catch {
4777
+ // Ignore iframe postMessage failures.
4778
+ }
4779
+ }
4780
+
4781
+ async function renderHtmlArtifactMathItems(record, items) {
4782
+ if (!record || !Array.isArray(items) || items.length === 0) return;
4783
+ if (record.detail) record.detail.textContent = "HTML preview · rendering math";
4784
+ try {
4785
+ const results = await fetchRenderedHtmlArtifactMath(items);
4786
+ postHtmlArtifactMathResults(record, results);
4787
+ } catch (error) {
4788
+ console.error("HTML preview math render failed:", error);
4789
+ postHtmlArtifactMathResults(record, items.map((item) => ({
4790
+ mathId: item.mathId,
4791
+ ok: false,
4792
+ error: error && error.message ? error.message : String(error || "HTML preview math render failed."),
4793
+ })));
4794
+ } finally {
4795
+ if (record.detail) record.detail.textContent = "HTML preview";
4796
+ }
4797
+ }
4798
+
4799
+ function handleHtmlArtifactFrameMathRenderMessage(event) {
4800
+ const data = event && event.data;
4801
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-render-math") return;
4802
+ const id = typeof data.id === "string" ? data.id : "";
4803
+ const record = id ? htmlArtifactFramesById.get(id) : null;
4804
+ if (!record || !record.iframe || !record.iframe.isConnected) {
4805
+ if (id) htmlArtifactFramesById.delete(id);
4806
+ return;
4807
+ }
4808
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
4809
+ const items = normalizeHtmlArtifactMathRenderItems(data.items);
4810
+ if (items.length === 0) return;
4811
+
4812
+ record.mathRenderBatchCount = Math.max(0, Number(record.mathRenderBatchCount) || 0) + 1;
4813
+ record.mathRenderItemCount = Math.max(0, Number(record.mathRenderItemCount) || 0) + items.length;
4814
+ if (record.mathRenderBatchCount > 24 || record.mathRenderItemCount > 1000) {
4815
+ postHtmlArtifactMathResults(record, items.map((item) => ({
4816
+ mathId: item.mathId,
4817
+ ok: false,
4818
+ error: "HTML preview math render limit reached.",
4819
+ })));
4820
+ return;
4821
+ }
4822
+
4823
+ void renderHtmlArtifactMathItems(record, items);
4824
+ }
4825
+
4826
+ function normalizeHtmlArtifactResourceItems(rawItems) {
4827
+ if (!Array.isArray(rawItems)) return [];
4828
+ return rawItems.slice(0, 100).map((item) => {
4829
+ const raw = item && typeof item === "object" ? item : null;
4830
+ const resourceId = raw && typeof raw.resourceId === "string" ? raw.resourceId : "";
4831
+ const url = raw && typeof raw.url === "string" ? raw.url : "";
4832
+ if (!resourceId || !url.trim()) return null;
4833
+ return { resourceId, url };
4834
+ }).filter(Boolean);
4835
+ }
4836
+
4837
+ function buildHtmlArtifactResourceFetchUrl(record, resourceUrl) {
4838
+ const token = getToken();
4839
+ if (!token) return "";
4840
+ const params = new URLSearchParams({ token, path: String(resourceUrl || "") });
4841
+ if (record && record.sourcePath) {
4842
+ params.set("sourcePath", record.sourcePath);
4843
+ } else if (record && record.resourceDir) {
4844
+ params.set("resourceDir", record.resourceDir);
4845
+ }
4846
+ return "/html-preview-resource?" + params.toString();
4847
+ }
4848
+
4849
+ function postHtmlArtifactResourceResults(record, results) {
4850
+ if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
4851
+ try {
4852
+ record.iframe.contentWindow.postMessage({
4853
+ type: "pi-studio-html-artifact-resources-resolved",
4854
+ id: record.id || "",
4855
+ results: Array.isArray(results) ? results : [],
4856
+ }, "*");
4857
+ } catch {
4858
+ // Ignore iframe postMessage failures.
4859
+ }
4860
+ }
4861
+
4862
+ async function fetchHtmlArtifactResource(record, item) {
4863
+ const resourceId = item && item.resourceId ? item.resourceId : "";
4864
+ try {
4865
+ const fetchUrl = buildHtmlArtifactResourceFetchUrl(record, item.url);
4866
+ if (!fetchUrl) throw new Error("Missing Studio token in URL.");
4867
+ const response = await fetchWithTimeout(fetchUrl, { method: "GET" }, HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS, "HTML preview resource load");
4868
+ const payload = await response.json().catch(() => null);
4869
+ if (!response.ok || !payload || payload.ok !== true || typeof payload.dataUrl !== "string") {
4870
+ const message = payload && typeof payload.error === "string" ? payload.error : "HTML preview resource load failed with HTTP " + response.status + ".";
4871
+ throw new Error(message);
4872
+ }
4873
+ return { resourceId, ok: true, dataUrl: payload.dataUrl };
4874
+ } catch (error) {
4875
+ return { resourceId, ok: false, error: error && error.message ? error.message : String(error || "HTML preview resource load failed.") };
4876
+ }
4877
+ }
4878
+
4879
+ async function resolveHtmlArtifactResources(record, items) {
4880
+ if (!record || !Array.isArray(items) || items.length === 0) return;
4881
+ if (record.detail) record.detail.textContent = "HTML preview · loading local images";
4882
+ const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
4883
+ postHtmlArtifactResourceResults(record, results);
4884
+ if (record.detail) record.detail.textContent = "HTML preview";
4885
+ }
4886
+
4887
+ function handleHtmlArtifactFrameResourceMessage(event) {
4888
+ const data = event && event.data;
4889
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-resolve-resources") return;
4890
+ const id = typeof data.id === "string" ? data.id : "";
4891
+ const record = id ? htmlArtifactFramesById.get(id) : null;
4892
+ if (!record || !record.iframe || !record.iframe.isConnected) {
4893
+ if (id) htmlArtifactFramesById.delete(id);
4894
+ return;
4895
+ }
4896
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
4897
+ const items = normalizeHtmlArtifactResourceItems(data.resources);
4898
+ if (items.length === 0) return;
4899
+ record.resourceResolveBatchCount = Math.max(0, Number(record.resourceResolveBatchCount) || 0) + 1;
4900
+ record.resourceResolveItemCount = Math.max(0, Number(record.resourceResolveItemCount) || 0) + items.length;
4901
+ if (record.resourceResolveBatchCount > 12 || record.resourceResolveItemCount > 300) {
4902
+ postHtmlArtifactResourceResults(record, items.map((item) => ({ resourceId: item.resourceId, ok: false, error: "HTML preview local image load limit reached." })));
4903
+ return;
4904
+ }
4905
+ void resolveHtmlArtifactResources(record, items);
4906
+ }
4907
+
4070
4908
  window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
4909
+ window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
4910
+ window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
4911
+ window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
4071
4912
 
4072
4913
  function isStudioHtmlFocusOpen() {
4073
4914
  return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
@@ -4375,7 +5216,19 @@
4375
5216
  iframe.addEventListener("load", () => { postArtifactZoom(); });
4376
5217
  iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
4377
5218
  shell.appendChild(iframe);
4378
- htmlArtifactFramesById.set(previewId, { iframe, shell, detail, zoomControls });
5219
+ htmlArtifactFramesById.set(previewId, {
5220
+ id: previewId,
5221
+ iframe,
5222
+ shell,
5223
+ detail,
5224
+ zoomControls,
5225
+ sourcePath: options && options.sourcePath ? String(options.sourcePath) : "",
5226
+ resourceDir: options && options.resourceDir ? String(options.resourceDir) : "",
5227
+ mathRenderBatchCount: 0,
5228
+ mathRenderItemCount: 0,
5229
+ resourceResolveBatchCount: 0,
5230
+ resourceResolveItemCount: 0,
5231
+ });
4379
5232
 
4380
5233
  targetEl.appendChild(shell);
4381
5234
 
@@ -5490,12 +6343,16 @@
5490
6343
  const action = actionBtn.getAttribute("data-repl-action") || "";
5491
6344
  if (action === "start" || action === "new-session") {
5492
6345
  const requestId = makeRequestId();
6346
+ const command = getCurrentReplStartCommandFromDom();
6347
+ setReplCommandOverride(replRuntime, command);
5493
6348
  replBusy = true;
5494
6349
  replError = "";
5495
- replMessage = (action === "new-session" ? "Starting new " : "Starting ") + replRuntime + " REPL…";
6350
+ replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
5496
6351
  syncActionButtons();
5497
6352
  renderReplViewIfActive({ force: true });
5498
- if (!sendMessage({ type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" })) {
6353
+ const message = { type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" };
6354
+ if (command) message.command = command;
6355
+ if (!sendMessage(message)) {
5499
6356
  replBusy = false;
5500
6357
  syncActionButtons();
5501
6358
  }
@@ -5599,13 +6456,29 @@
5599
6456
  if (!(target instanceof Element)) return;
5600
6457
  const runtimeSelect = target.closest("[data-repl-runtime]");
5601
6458
  if (runtimeSelect && "value" in runtimeSelect) {
6459
+ const previousActive = replActiveSessionName;
5602
6460
  setReplRuntime(runtimeSelect.value);
6461
+ selectReplSessionForRuntime(replRuntime, previousActive);
6462
+ replError = "";
6463
+ replMessage = "";
6464
+ if (replActiveSessionName) {
6465
+ requestReplCapture();
6466
+ } else {
6467
+ replTranscript = "";
6468
+ replCapturedAt = 0;
6469
+ syncActionButtons();
6470
+ }
5603
6471
  renderReplViewIfActive({ force: true });
5604
6472
  return;
5605
6473
  }
6474
+ const commandInput = target.closest("[data-repl-command]");
6475
+ if (commandInput && "value" in commandInput) {
6476
+ setReplCommandOverride(replRuntime, commandInput.value);
6477
+ return;
6478
+ }
5606
6479
  const sessionSelect = target.closest("[data-repl-session]");
5607
6480
  if (sessionSelect && "value" in sessionSelect) {
5608
- setActiveReplSession(sessionSelect.value);
6481
+ setActiveReplSessionForCurrentRuntime(sessionSelect.value);
5609
6482
  replError = "";
5610
6483
  replMessage = "";
5611
6484
  replFollow = true;
@@ -5719,12 +6592,34 @@
5719
6592
  const source = codeEl ? codeEl.textContent : preEl.textContent;
5720
6593
 
5721
6594
  const wrapper = document.createElement("div");
5722
- wrapper.className = "mermaid-container";
6595
+ wrapper.className = "mermaid-container studio-copyable-block";
6596
+ if (wrapper.dataset) {
6597
+ wrapper.dataset.mermaidSource = source || "";
6598
+ wrapper.dataset.studioCopyDecorated = "1";
6599
+ }
5723
6600
 
5724
6601
  const diagramEl = document.createElement("div");
5725
6602
  diagramEl.className = "mermaid";
5726
6603
  diagramEl.textContent = source || "";
5727
6604
 
6605
+ const copyBtn = document.createElement("button");
6606
+ copyBtn.type = "button";
6607
+ copyBtn.className = "studio-copy-block-btn studio-copy-mermaid-source-btn";
6608
+ copyBtn.textContent = "Copy source";
6609
+ copyBtn.title = "Copy this Mermaid source to the clipboard.";
6610
+ copyBtn.setAttribute("aria-label", "Copy Mermaid source to the clipboard");
6611
+ copyBtn.addEventListener("pointerdown", (event) => {
6612
+ event.stopPropagation();
6613
+ });
6614
+ copyBtn.addEventListener("mousedown", (event) => {
6615
+ event.stopPropagation();
6616
+ });
6617
+
6618
+ const toolbarEl = document.createElement("div");
6619
+ toolbarEl.className = "mermaid-source-toolbar";
6620
+ toolbarEl.appendChild(copyBtn);
6621
+
6622
+ wrapper.appendChild(toolbarEl);
5728
6623
  wrapper.appendChild(diagramEl);
5729
6624
  preEl.replaceWith(wrapper);
5730
6625
  });
@@ -5867,12 +6762,12 @@
5867
6762
  const exportingReplJournal = rightView === "repl";
5868
6763
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
5869
6764
  if (!rightPaneShowsPreview && !exportingReplJournal) {
5870
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
6765
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
5871
6766
  return;
5872
6767
  }
5873
6768
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
5874
6769
  if (exportingReplJournal && !replJournalExportEntries.length) {
5875
- setStatus("No REPL Studio entries to export for this session yet.", "warning");
6770
+ setStatus("No Studio REPL record entries to export for this session yet.", "warning");
5876
6771
  return;
5877
6772
  }
5878
6773
 
@@ -6040,12 +6935,12 @@
6040
6935
  const exportingReplJournal = rightView === "repl";
6041
6936
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6042
6937
  if (!rightPaneShowsPreview && !exportingReplJournal) {
6043
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
6938
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
6044
6939
  return;
6045
6940
  }
6046
6941
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6047
6942
  if (exportingReplJournal && !replJournalExportEntries.length) {
6048
- setStatus("No REPL Studio entries to export for this session yet.", "warning");
6943
+ setStatus("No Studio REPL record entries to export for this session yet.", "warning");
6049
6944
  return;
6050
6945
  }
6051
6946
 
@@ -6067,7 +6962,7 @@
6067
6962
  ? editorHtmlLanguage === "latex"
6068
6963
  : /\\documentclass\b|\\begin\{document\}/.test(markdown));
6069
6964
  let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
6070
- let titleHint = exportingReplJournal ? "REPL Studio" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
6965
+ let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
6071
6966
  if (sourcePath) {
6072
6967
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
6073
6968
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -6300,6 +7195,9 @@
6300
7195
 
6301
7196
  function getCopyablePreviewBlockText(blockEl) {
6302
7197
  if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
7198
+ if (blockEl.classList && blockEl.classList.contains("mermaid-container") && blockEl.dataset && typeof blockEl.dataset.mermaidSource === "string") {
7199
+ return normalizeCopyableBlockText(blockEl.dataset.mermaidSource);
7200
+ }
6303
7201
  if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
6304
7202
  return normalizeCopyableBlockText(
6305
7203
  Array.from(blockEl.querySelectorAll(".preview-code-line-content"))
@@ -6480,7 +7378,7 @@
6480
7378
  if (editorView !== "preview") return;
6481
7379
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
6482
7380
  if (isHtmlArtifactPreviewText(text, editorLanguage)) {
6483
- renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview" });
7381
+ renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
6484
7382
  return;
6485
7383
  }
6486
7384
  if (supportsCodePreviewCommentsForCurrentEditor()) {
@@ -6763,10 +7661,9 @@
6763
7661
  const visibleEntries = getVisibleReplJournalEntries();
6764
7662
  const hasEntries = visibleEntries.length > 0;
6765
7663
  const entryCount = visibleEntries.length;
6766
- const hiddenEntryCount = getHiddenReplJournalEntryCount();
6767
7664
  const sessionName = getActiveReplJournalSessionName();
6768
7665
  const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
6769
- const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show REPL Studio" : "Hide REPL Studio") + "</button>";
7666
+ const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
6770
7667
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
6771
7668
  const summaryText = hasEntries
6772
7669
  ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
@@ -6774,7 +7671,7 @@
6774
7671
  if (replJournalCollapsed) {
6775
7672
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
6776
7673
  + "<div class='repl-journal-compact-row'>"
6777
- + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>REPL Studio</span><span>" + escapeHtml(summaryText) + "</span></div>"
7674
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Studio REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
6778
7675
  + "<div class='repl-journal-actions'>" + toggleButton + "</div>"
6779
7676
  + "</div>"
6780
7677
  + "</section>";
@@ -6812,14 +7709,14 @@
6812
7709
  }).join("");
6813
7710
  const emptyText = sessionName
6814
7711
  ? (String(transcript || "").trim()
6815
- ? "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."
6816
- : "No REPL Studio entries for this tmux session yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
6817
- : "No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
7712
+ ? "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."
7713
+ : "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
7714
+ : "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
6818
7715
  const terminalContent = banner
6819
7716
  + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
6820
7717
  return "<section class='repl-journal'>"
6821
- + "<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>"
6822
- + (hiddenEntryCount ? "<div class='repl-journal-omitted'>" + escapeHtml(String(hiddenEntryCount)) + " entr" + (hiddenEntryCount === 1 ? "y" : "ies") + " from other REPL sessions hidden.</div>" : "")
7718
+ + "<div class='repl-journal-header'><h3>Studio REPL Record</h3>" + toggleActions + "</div>"
7719
+ + "<p class='repl-journal-description'>Clean record for the selected tmux session. Raw tmux mirror below.</p>"
6823
7720
  + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
6824
7721
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
6825
7722
  + "</section>";
@@ -6843,7 +7740,7 @@
6843
7740
  + "</section>";
6844
7741
  }
6845
7742
  return "<section class='repl-mirror'>"
6846
- + "<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>"
7743
+ + "<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>"
6847
7744
  + body
6848
7745
  + "</section>";
6849
7746
  }
@@ -6863,10 +7760,13 @@
6863
7760
  ["ghci", "GHCi"],
6864
7761
  ["clojure", "Clojure"],
6865
7762
  ].map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (replRuntime === value ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
6866
- const sessionOptions = replSessions.length
6867
- ? replSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
6868
- : "<option value=''>No REPL sessions</option>";
6869
- const activeSession = getActiveReplSession();
7763
+ const runtimeLabel = getReplRuntimeLabel(replRuntime);
7764
+ const visibleSessions = getReplSessionsForRuntime(replRuntime);
7765
+ const sessionOptions = visibleSessions.length
7766
+ ? visibleSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
7767
+ : "<option value=''>None</option>";
7768
+ const replCommand = getReplCommandOverride(replRuntime);
7769
+ const activeSession = getActiveReplSessionForCurrentRuntime();
6870
7770
  const transcript = trimReplTranscript(replTranscript);
6871
7771
  const emptyMessage = replTmuxAvailable === false
6872
7772
  ? "tmux is not available. Install tmux to use Studio REPL sessions."
@@ -6880,17 +7780,17 @@
6880
7780
  + "<div class='repl-toolbar'>"
6881
7781
  + "<div class='repl-controls'>"
6882
7782
  + "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
6883
- + "<button type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or attach to the default session for this runtime.'>Start</button>"
6884
- + "<label class='repl-control-label'>Session <select data-repl-session aria-label='REPL session'" + (replSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
7783
+ + "<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>"
7784
+ + "<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>"
7785
+ + "<label class='repl-control-label repl-session-label'>Session <select data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
6885
7786
  + "<details class='repl-more-controls'>"
6886
7787
  + "<summary title='More REPL actions'>More</summary>"
6887
7788
  + "<div class='repl-more-menu'>"
6888
- + "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
6889
7789
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
6890
7790
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
6891
7791
  + "<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>"
6892
7792
  + "<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>"
6893
- + "<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>"
7793
+ + "<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>"
6894
7794
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
6895
7795
  + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
6896
7796
  + "</div>"
@@ -7074,7 +7974,7 @@
7074
7974
  return;
7075
7975
  }
7076
7976
  if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
7077
- renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview" });
7977
+ renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
7078
7978
  return;
7079
7979
  }
7080
7980
  if (supportsCodePreviewCommentsForCurrentEditor()) {
@@ -7098,7 +7998,7 @@
7098
7998
 
7099
7999
  if (rightView === "preview") {
7100
8000
  if (isHtmlArtifactPreviewText(markdown, "")) {
7101
- renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview" });
8001
+ renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview", ...getHtmlPreviewResourceContextOptions() });
7102
8002
  return;
7103
8003
  }
7104
8004
  const nonce = ++responsePreviewRenderNonce;
@@ -7177,19 +8077,19 @@
7177
8077
  exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
7178
8078
  exportPdfBtn.textContent = previewExportInProgress
7179
8079
  ? "Exporting…"
7180
- : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
8080
+ : (exportingReplJournal ? "Export record" : "Export right preview");
7181
8081
  if (rightView === "trace") {
7182
8082
  exportPdfBtn.title = "Working view does not support preview export.";
7183
8083
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
7184
- exportPdfBtn.title = "No REPL Studio entries to export for this session yet.";
8084
+ exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
7185
8085
  } else if (rightView === "markdown") {
7186
- exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
8086
+ exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL to export.";
7187
8087
  } else if (!canExportPreview) {
7188
8088
  exportPdfBtn.title = "Nothing to export yet.";
7189
8089
  } else if (isHtmlArtifactPreview) {
7190
8090
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
7191
8091
  } else if (exportingReplJournal) {
7192
- exportPdfBtn.title = "Choose PDF or HTML and export REPL Studio.";
8092
+ exportPdfBtn.title = "Choose PDF or HTML and export the Studio REPL record.";
7193
8093
  } else {
7194
8094
  exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
7195
8095
  }
@@ -7198,20 +8098,20 @@
7198
8098
  exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
7199
8099
  exportPreviewPdfBtn.title = isHtmlArtifactPreview
7200
8100
  ? "Interactive HTML preview PDF export is not available yet."
7201
- : (exportingReplJournal ? "Export REPL Studio as PDF." : "Export the current right-pane preview as PDF.");
8101
+ : (exportingReplJournal ? "Export the Studio REPL record as PDF." : "Export the current right-pane preview as PDF.");
7202
8102
  }
7203
8103
  if (exportPreviewHtmlBtn) {
7204
8104
  exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
7205
8105
  exportPreviewHtmlBtn.title = isHtmlArtifactPreview
7206
8106
  ? "Export the authored HTML preview."
7207
- : (exportingReplJournal ? "Export REPL Studio as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
8107
+ : (exportingReplJournal ? "Export the Studio REPL record as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
7208
8108
  }
7209
8109
  if (exportPreviewControlsEl) {
7210
8110
  exportPreviewControlsEl.title = canExportPreview
7211
8111
  ? (exportingReplJournal
7212
- ? "Choose a format and export REPL Studio."
8112
+ ? "Choose a format and export the Studio REPL record."
7213
8113
  : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
7214
- : (exportingReplJournal ? "No REPL Studio entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
8114
+ : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
7215
8115
  }
7216
8116
  if (!canExportPreview || previewExportInProgress) {
7217
8117
  closeExportPreviewMenu();
@@ -7242,6 +8142,15 @@
7242
8142
  return null;
7243
8143
  }
7244
8144
 
8145
+ function getHtmlPreviewResourceContextOptions() {
8146
+ const sourcePath = getEffectiveSavePath() || sourceState.path || "";
8147
+ const resourceDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
8148
+ return {
8149
+ sourcePath,
8150
+ resourceDir: sourcePath ? "" : resourceDir,
8151
+ };
8152
+ }
8153
+
7245
8154
  function buildAnnotatedSaveSuggestion() {
7246
8155
  const effectivePath = getEffectiveSavePath() || sourceState.path || "";
7247
8156
  if (effectivePath) {
@@ -13835,7 +14744,7 @@
13835
14744
  sendRunBtn.classList.toggle("request-stop-active", directIsStop);
13836
14745
  sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
13837
14746
  sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
13838
- const replHint = rightView === "repl" && getActiveReplSession()
14747
+ const replHint = rightView === "repl" && getActiveReplSessionForCurrentRuntime()
13839
14748
  ? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
13840
14749
  : "";
13841
14750
  sendRunBtn.title = directIsStop
@@ -13856,7 +14765,7 @@
13856
14765
  : "Queue steering is available while Run editor text is active.";
13857
14766
  }
13858
14767
 
13859
- const hasReplSession = Boolean(getActiveReplSession());
14768
+ const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
13860
14769
  if (sendReplBtn) {
13861
14770
  const showReplSend = rightView === "repl";
13862
14771
  sendReplBtn.hidden = !showReplSend;
@@ -14224,7 +15133,7 @@
14224
15133
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
14225
15134
  const sessionsChanged = setReplSessions(message.sessions);
14226
15135
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
14227
- setActiveReplSession(message.activeSessionName);
15136
+ setActiveReplSessionForCurrentRuntime(message.activeSessionName);
14228
15137
  }
14229
15138
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
14230
15139
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
@@ -14277,7 +15186,7 @@
14277
15186
  }
14278
15187
  }
14279
15188
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
14280
- setActiveReplSession(message.activeSessionName);
15189
+ setActiveReplSessionForCurrentRuntime(message.activeSessionName);
14281
15190
  }
14282
15191
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
14283
15192
  if (typeof message.transcript === "string") {
@@ -15092,6 +16001,8 @@
15092
16001
  rightPaneEl.addEventListener("focusin", (event) => activatePaneFromInteraction("right", event));
15093
16002
  }
15094
16003
 
16004
+ setupPaneResizeHandle();
16005
+
15095
16006
  if (leftFocusBtn) {
15096
16007
  leftFocusBtn.addEventListener("click", () => {
15097
16008
  if (paneFocusTarget === "left") {