pi-studio 0.9.11 → 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");
@@ -246,6 +247,7 @@
246
247
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
247
248
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
248
249
  const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
250
+ const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
249
251
  const EDITOR_TAB_TEXT = " ";
250
252
  const QUIZ_DEFAULT_COUNT = 5;
251
253
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
@@ -288,6 +290,15 @@
288
290
  return "python";
289
291
  }
290
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
+ })();
291
302
  let replTranscript = "";
292
303
  let replError = "";
293
304
  let replMessage = "";
@@ -952,11 +963,26 @@
952
963
  setStatus("Loaded visible working into editor.", "success");
953
964
  }
954
965
 
955
- function normalizeReplRuntime(value) {
966
+ function getKnownReplRuntime(value) {
956
967
  const runtime = String(value || "").trim().toLowerCase();
957
968
  return runtime === "shell" || runtime === "python" || runtime === "ipython" || runtime === "julia" || runtime === "r" || runtime === "ghci" || runtime === "clojure"
958
969
  ? runtime
959
- : "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";
960
986
  }
961
987
 
962
988
  function normalizeReplSession(session) {
@@ -981,6 +1007,47 @@
981
1007
  }
982
1008
  }
983
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
+
984
1051
  function normalizeReplSendMode(value) {
985
1052
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
986
1053
  }
@@ -1056,7 +1123,10 @@
1056
1123
  ? sessions.map(normalizeReplSession).filter(Boolean)
1057
1124
  : [];
1058
1125
  if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
1059
- replActiveSessionName = replSessions[0] ? replSessions[0].sessionName : "";
1126
+ setActiveReplSession("");
1127
+ }
1128
+ if (!getActiveReplSessionForCurrentRuntime()) {
1129
+ selectReplSessionForRuntime(replRuntime, replActiveSessionName);
1060
1130
  }
1061
1131
  return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
1062
1132
  }
@@ -1065,9 +1135,43 @@
1065
1135
  return replSessions.find((session) => session.sessionName === replActiveSessionName) || null;
1066
1136
  }
1067
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
+
1068
1172
  function buildActiveReplPromptContext() {
1069
1173
  if (rightView !== "repl") return "";
1070
- const session = getActiveReplSession();
1174
+ const session = getActiveReplSessionForCurrentRuntime();
1071
1175
  if (!session) return "";
1072
1176
  const runtime = session.runtime && session.runtime !== "unknown" ? session.runtime : "unknown";
1073
1177
  return [
@@ -1162,7 +1266,7 @@
1162
1266
  }
1163
1267
 
1164
1268
  function getActiveReplRuntime() {
1165
- const session = getActiveReplSession();
1269
+ const session = getActiveReplSessionForCurrentRuntime();
1166
1270
  if (session && session.runtime && session.runtime !== "unknown") return normalizeReplRuntime(session.runtime);
1167
1271
  return normalizeReplRuntime(replRuntime);
1168
1272
  }
@@ -1502,7 +1606,7 @@
1502
1606
 
1503
1607
  function stripStudioReplSubmissionEcho(delta) {
1504
1608
  let value = String(delta || "").replace(/^\s+/, "");
1505
- // 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
1506
1610
  // temp-file wrapper used to submit multiline snippets safely. The
1507
1611
  // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
1508
1612
  const submissionEchoPatterns = [
@@ -1566,11 +1670,11 @@
1566
1670
  function buildReplJournalMarkdown(entries) {
1567
1671
  const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1568
1672
  const sessionName = getActiveReplJournalSessionName();
1569
- const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString()];
1673
+ const lines = ["# Studio REPL Record", "", "Generated: " + new Date().toLocaleString()];
1570
1674
  if (sessionName) lines.push("Session: `" + sessionName + "`");
1571
1675
  lines.push("");
1572
1676
  if (!visibleEntries.length) {
1573
- 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._");
1574
1678
  return lines.join("\n");
1575
1679
  }
1576
1680
  visibleEntries.forEach((entry, index) => {
@@ -1602,11 +1706,11 @@
1602
1706
  async function copyReplJournalToClipboard() {
1603
1707
  const entries = getVisibleReplJournalEntries();
1604
1708
  if (!entries.length) {
1605
- 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");
1606
1710
  return;
1607
1711
  }
1608
1712
  if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
1609
- setStatus("Copied REPL Studio session as Markdown.", "success");
1713
+ setStatus("Copied Studio REPL record as Markdown.", "success");
1610
1714
  } else {
1611
1715
  setStatus("Clipboard write failed.", "warning");
1612
1716
  }
@@ -1615,7 +1719,7 @@
1615
1719
  function exportReplJournalMarkdown() {
1616
1720
  const entries = getVisibleReplJournalEntries();
1617
1721
  if (!entries.length) {
1618
- 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");
1619
1723
  return;
1620
1724
  }
1621
1725
  const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
@@ -1629,7 +1733,7 @@
1629
1733
  link.click();
1630
1734
  link.remove();
1631
1735
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1632
- setStatus("Exported REPL Studio session Markdown.", "success");
1736
+ setStatus("Exported Studio REPL record Markdown.", "success");
1633
1737
  }
1634
1738
 
1635
1739
  function clearReplJournal() {
@@ -1641,27 +1745,27 @@
1641
1745
  }
1642
1746
  activeReplJournalEntryId = "";
1643
1747
  persistReplJournalEntries();
1644
- 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");
1645
1749
  renderReplViewIfActive({ force: true });
1646
1750
  }
1647
1751
 
1648
1752
  function loadReplJournalIntoEditor() {
1649
1753
  const entries = getVisibleReplJournalEntries();
1650
1754
  if (!entries.length) {
1651
- 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");
1652
1756
  return;
1653
1757
  }
1654
1758
  const markdown = buildReplJournalMarkdown(entries);
1655
1759
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1656
- setSourceState({ source: "blank", label: "REPL Studio", path: null });
1760
+ setSourceState({ source: "blank", label: "Studio REPL Record", path: null });
1657
1761
  setEditorLanguage("markdown");
1658
- setStatus("Loaded REPL Studio session into editor.", "success");
1762
+ setStatus("Loaded Studio REPL record into editor.", "success");
1659
1763
  }
1660
1764
 
1661
1765
  function addSelectedReplJournalNote() {
1662
1766
  const note = getSelectedOrCurrentParagraphForReplNote();
1663
1767
  if (!note.trim()) {
1664
- 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");
1665
1769
  return;
1666
1770
  }
1667
1771
  addReplJournalEntry({
@@ -1672,14 +1776,14 @@
1672
1776
  sessionName: replActiveSessionName,
1673
1777
  runtime: getActiveReplRuntime(),
1674
1778
  });
1675
- setStatus("Added note to REPL Studio.", "success");
1779
+ setStatus("Added note to Studio REPL record.", "success");
1676
1780
  renderReplViewIfActive({ force: true });
1677
1781
  }
1678
1782
 
1679
1783
  function sendReplPayload(payload) {
1680
- const session = getActiveReplSession();
1784
+ const session = getActiveReplSessionForCurrentRuntime();
1681
1785
  if (!session) {
1682
- setStatus("Start or select a REPL session first.", "warning");
1786
+ setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
1683
1787
  return;
1684
1788
  }
1685
1789
  if (!payload || payload.error) {
@@ -1697,7 +1801,7 @@
1697
1801
  runtime: getActiveReplRuntime(),
1698
1802
  skippedChunks: payload.skippedChunks,
1699
1803
  });
1700
- setStatus("Added prose to REPL Studio.", "success");
1804
+ setStatus("Added prose to Studio REPL record.", "success");
1701
1805
  renderReplViewIfActive({ force: true });
1702
1806
  } else {
1703
1807
  setStatus("No code or prose found to send.", "warning");
@@ -1771,6 +1875,11 @@
1771
1875
  let fileBackedBaselineText = null;
1772
1876
  let activePane = "left";
1773
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;
1774
1883
  const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
1775
1884
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
1776
1885
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
@@ -2431,7 +2540,7 @@
2431
2540
 
2432
2541
  function getTerminalBusyStatus() {
2433
2542
  if (terminalActivityPhase === "tool") {
2434
- if (terminalActivityLabel) {
2543
+ if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
2435
2544
  return "Terminal: " + withEllipsis(terminalActivityLabel);
2436
2545
  }
2437
2546
  return terminalActivityToolName
@@ -2475,7 +2584,7 @@
2475
2584
  const action = getStudioActionLabel(kind);
2476
2585
  const queueSuffix = studioRunChainActive ? formatQueuedSteeringSuffix() : "";
2477
2586
  if (terminalActivityPhase === "tool") {
2478
- if (terminalActivityLabel) {
2587
+ if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
2479
2588
  return "Studio: " + withEllipsis(terminalActivityLabel) + queueSuffix;
2480
2589
  }
2481
2590
  return terminalActivityToolName
@@ -3002,6 +3111,142 @@
3002
3111
  setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
3003
3112
  }
3004
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
+
3005
3250
  function updatePaneFocusButtons() {
3006
3251
  [
3007
3252
  [leftFocusBtn, "left"],
@@ -3240,6 +3485,40 @@
3240
3485
  return false;
3241
3486
  }
3242
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
+
3243
3522
  function isTextEntryShortcutTarget(target) {
3244
3523
  if (!(target instanceof Element)) return false;
3245
3524
  const editable = target.closest("input, textarea, select, [contenteditable]");
@@ -3255,6 +3534,7 @@
3255
3534
  if (!event || event.defaultPrevented) return;
3256
3535
 
3257
3536
  const key = typeof event.key === "string" ? event.key : "";
3537
+ const code = typeof event.code === "string" ? event.code : "";
3258
3538
  const plainEscape = key === "Escape"
3259
3539
  && !event.metaKey
3260
3540
  && !event.ctrlKey
@@ -3355,6 +3635,24 @@
3355
3635
  return;
3356
3636
  }
3357
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
+
3358
3656
  const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
3359
3657
  if (isPaneSwitchShortcut) {
3360
3658
  event.preventDefault();
@@ -4205,6 +4503,60 @@
4205
4503
  + " htmlMathScanScheduled = true;\n"
4206
4504
  + " requestAnimationFrame(runHtmlMathRenderScan);\n"
4207
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"
4208
4560
  + " window.addEventListener('message', (event) => {\n"
4209
4561
  + " const data = event && event.data;\n"
4210
4562
  + " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
@@ -4214,15 +4566,19 @@
4214
4566
  + " }\n"
4215
4567
  + " if (data.type === 'pi-studio-html-artifact-math-rendered') {\n"
4216
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"
4217
4573
  + " }\n"
4218
4574
  + " });\n"
4219
4575
  + " document.addEventListener('click', handleFragmentAnchorClick);\n"
4220
- + " document.addEventListener('DOMContentLoaded', scheduleHtmlMathRenderScan);\n"
4576
+ + " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4221
4577
  + " window.addEventListener('hashchange', () => {\n"
4222
4578
  + " const hash = String(window.location && window.location.hash || '');\n"
4223
4579
  + " if (hash) scrollFragmentIntoView(hash.slice(1), { smooth: false });\n"
4224
4580
  + " });\n"
4225
- + " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
4581
+ + " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4226
4582
  + " window.addEventListener('resize', scheduleHeight);\n"
4227
4583
  + " if (typeof ResizeObserver === 'function') {\n"
4228
4584
  + " const observer = new ResizeObserver(scheduleHeight);\n"
@@ -4230,15 +4586,15 @@
4230
4586
  + " if (document.body) observer.observe(document.body);\n"
4231
4587
  + " }\n"
4232
4588
  + " if (typeof MutationObserver === 'function') {\n"
4233
- + " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
4589
+ + " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
4234
4590
  + " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
4235
4591
  + " }\n"
4236
4592
  + " scheduleHeight();\n"
4237
4593
  + " setTimeout(scheduleHeight, 80);\n"
4238
4594
  + " setTimeout(scheduleHeight, 350);\n"
4239
- + " setTimeout(scheduleHtmlMathRenderScan, 0);\n"
4240
- + " setTimeout(scheduleHtmlMathRenderScan, 120);\n"
4241
- + " setTimeout(scheduleHtmlMathRenderScan, 500);\n"
4595
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 0);\n"
4596
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 120);\n"
4597
+ + " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 500);\n"
4242
4598
  + "})();\n"
4243
4599
  + "<\/script>";
4244
4600
  }
@@ -4467,9 +4823,92 @@
4467
4823
  void renderHtmlArtifactMathItems(record, items);
4468
4824
  }
4469
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
+
4470
4908
  window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
4471
4909
  window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
4472
4910
  window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
4911
+ window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
4473
4912
 
4474
4913
  function isStudioHtmlFocusOpen() {
4475
4914
  return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
@@ -4777,7 +5216,19 @@
4777
5216
  iframe.addEventListener("load", () => { postArtifactZoom(); });
4778
5217
  iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
4779
5218
  shell.appendChild(iframe);
4780
- htmlArtifactFramesById.set(previewId, { id: previewId, iframe, shell, detail, zoomControls, mathRenderBatchCount: 0, mathRenderItemCount: 0 });
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
+ });
4781
5232
 
4782
5233
  targetEl.appendChild(shell);
4783
5234
 
@@ -5892,12 +6343,16 @@
5892
6343
  const action = actionBtn.getAttribute("data-repl-action") || "";
5893
6344
  if (action === "start" || action === "new-session") {
5894
6345
  const requestId = makeRequestId();
6346
+ const command = getCurrentReplStartCommandFromDom();
6347
+ setReplCommandOverride(replRuntime, command);
5895
6348
  replBusy = true;
5896
6349
  replError = "";
5897
- replMessage = (action === "new-session" ? "Starting new " : "Starting ") + replRuntime + " REPL…";
6350
+ replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
5898
6351
  syncActionButtons();
5899
6352
  renderReplViewIfActive({ force: true });
5900
- 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)) {
5901
6356
  replBusy = false;
5902
6357
  syncActionButtons();
5903
6358
  }
@@ -6001,13 +6456,29 @@
6001
6456
  if (!(target instanceof Element)) return;
6002
6457
  const runtimeSelect = target.closest("[data-repl-runtime]");
6003
6458
  if (runtimeSelect && "value" in runtimeSelect) {
6459
+ const previousActive = replActiveSessionName;
6004
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
+ }
6005
6471
  renderReplViewIfActive({ force: true });
6006
6472
  return;
6007
6473
  }
6474
+ const commandInput = target.closest("[data-repl-command]");
6475
+ if (commandInput && "value" in commandInput) {
6476
+ setReplCommandOverride(replRuntime, commandInput.value);
6477
+ return;
6478
+ }
6008
6479
  const sessionSelect = target.closest("[data-repl-session]");
6009
6480
  if (sessionSelect && "value" in sessionSelect) {
6010
- setActiveReplSession(sessionSelect.value);
6481
+ setActiveReplSessionForCurrentRuntime(sessionSelect.value);
6011
6482
  replError = "";
6012
6483
  replMessage = "";
6013
6484
  replFollow = true;
@@ -6291,12 +6762,12 @@
6291
6762
  const exportingReplJournal = rightView === "repl";
6292
6763
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6293
6764
  if (!rightPaneShowsPreview && !exportingReplJournal) {
6294
- 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");
6295
6766
  return;
6296
6767
  }
6297
6768
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6298
6769
  if (exportingReplJournal && !replJournalExportEntries.length) {
6299
- 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");
6300
6771
  return;
6301
6772
  }
6302
6773
 
@@ -6464,12 +6935,12 @@
6464
6935
  const exportingReplJournal = rightView === "repl";
6465
6936
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6466
6937
  if (!rightPaneShowsPreview && !exportingReplJournal) {
6467
- 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");
6468
6939
  return;
6469
6940
  }
6470
6941
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6471
6942
  if (exportingReplJournal && !replJournalExportEntries.length) {
6472
- 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");
6473
6944
  return;
6474
6945
  }
6475
6946
 
@@ -6491,7 +6962,7 @@
6491
6962
  ? editorHtmlLanguage === "latex"
6492
6963
  : /\\documentclass\b|\\begin\{document\}/.test(markdown));
6493
6964
  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");
6965
+ let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
6495
6966
  if (sourcePath) {
6496
6967
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
6497
6968
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -6907,7 +7378,7 @@
6907
7378
  if (editorView !== "preview") return;
6908
7379
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
6909
7380
  if (isHtmlArtifactPreviewText(text, editorLanguage)) {
6910
- renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview" });
7381
+ renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
6911
7382
  return;
6912
7383
  }
6913
7384
  if (supportsCodePreviewCommentsForCurrentEditor()) {
@@ -7190,10 +7661,9 @@
7190
7661
  const visibleEntries = getVisibleReplJournalEntries();
7191
7662
  const hasEntries = visibleEntries.length > 0;
7192
7663
  const entryCount = visibleEntries.length;
7193
- const hiddenEntryCount = getHiddenReplJournalEntryCount();
7194
7664
  const sessionName = getActiveReplJournalSessionName();
7195
7665
  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>";
7666
+ const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
7197
7667
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
7198
7668
  const summaryText = hasEntries
7199
7669
  ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
@@ -7201,7 +7671,7 @@
7201
7671
  if (replJournalCollapsed) {
7202
7672
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
7203
7673
  + "<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>"
7674
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Studio REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
7205
7675
  + "<div class='repl-journal-actions'>" + toggleButton + "</div>"
7206
7676
  + "</div>"
7207
7677
  + "</section>";
@@ -7239,14 +7709,14 @@
7239
7709
  }).join("");
7240
7710
  const emptyText = sessionName
7241
7711
  ? (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.";
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.";
7245
7715
  const terminalContent = banner
7246
7716
  + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
7247
7717
  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>" : "")
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>"
7250
7720
  + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
7251
7721
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
7252
7722
  + "</section>";
@@ -7270,7 +7740,7 @@
7270
7740
  + "</section>";
7271
7741
  }
7272
7742
  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>"
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>"
7274
7744
  + body
7275
7745
  + "</section>";
7276
7746
  }
@@ -7290,10 +7760,13 @@
7290
7760
  ["ghci", "GHCi"],
7291
7761
  ["clojure", "Clojure"],
7292
7762
  ].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();
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();
7297
7770
  const transcript = trimReplTranscript(replTranscript);
7298
7771
  const emptyMessage = replTmuxAvailable === false
7299
7772
  ? "tmux is not available. Install tmux to use Studio REPL sessions."
@@ -7307,17 +7780,17 @@
7307
7780
  + "<div class='repl-toolbar'>"
7308
7781
  + "<div class='repl-controls'>"
7309
7782
  + "<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>"
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>"
7312
7786
  + "<details class='repl-more-controls'>"
7313
7787
  + "<summary title='More REPL actions'>More</summary>"
7314
7788
  + "<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
7789
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
7317
7790
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
7318
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>"
7319
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>"
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>"
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>"
7321
7794
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
7322
7795
  + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
7323
7796
  + "</div>"
@@ -7501,7 +7974,7 @@
7501
7974
  return;
7502
7975
  }
7503
7976
  if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
7504
- renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview" });
7977
+ renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
7505
7978
  return;
7506
7979
  }
7507
7980
  if (supportsCodePreviewCommentsForCurrentEditor()) {
@@ -7525,7 +7998,7 @@
7525
7998
 
7526
7999
  if (rightView === "preview") {
7527
8000
  if (isHtmlArtifactPreviewText(markdown, "")) {
7528
- renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview" });
8001
+ renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview", ...getHtmlPreviewResourceContextOptions() });
7529
8002
  return;
7530
8003
  }
7531
8004
  const nonce = ++responsePreviewRenderNonce;
@@ -7604,19 +8077,19 @@
7604
8077
  exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
7605
8078
  exportPdfBtn.textContent = previewExportInProgress
7606
8079
  ? "Exporting…"
7607
- : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
8080
+ : (exportingReplJournal ? "Export record" : "Export right preview");
7608
8081
  if (rightView === "trace") {
7609
8082
  exportPdfBtn.title = "Working view does not support preview export.";
7610
8083
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
7611
- 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.";
7612
8085
  } else if (rightView === "markdown") {
7613
- 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.";
7614
8087
  } else if (!canExportPreview) {
7615
8088
  exportPdfBtn.title = "Nothing to export yet.";
7616
8089
  } else if (isHtmlArtifactPreview) {
7617
8090
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
7618
8091
  } else if (exportingReplJournal) {
7619
- exportPdfBtn.title = "Choose PDF or HTML and export REPL Studio.";
8092
+ exportPdfBtn.title = "Choose PDF or HTML and export the Studio REPL record.";
7620
8093
  } else {
7621
8094
  exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
7622
8095
  }
@@ -7625,20 +8098,20 @@
7625
8098
  exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
7626
8099
  exportPreviewPdfBtn.title = isHtmlArtifactPreview
7627
8100
  ? "Interactive HTML preview PDF export is not available yet."
7628
- : (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.");
7629
8102
  }
7630
8103
  if (exportPreviewHtmlBtn) {
7631
8104
  exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
7632
8105
  exportPreviewHtmlBtn.title = isHtmlArtifactPreview
7633
8106
  ? "Export the authored HTML preview."
7634
- : (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.");
7635
8108
  }
7636
8109
  if (exportPreviewControlsEl) {
7637
8110
  exportPreviewControlsEl.title = canExportPreview
7638
8111
  ? (exportingReplJournal
7639
- ? "Choose a format and export REPL Studio."
8112
+ ? "Choose a format and export the Studio REPL record."
7640
8113
  : (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.");
8114
+ : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
7642
8115
  }
7643
8116
  if (!canExportPreview || previewExportInProgress) {
7644
8117
  closeExportPreviewMenu();
@@ -7669,6 +8142,15 @@
7669
8142
  return null;
7670
8143
  }
7671
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
+
7672
8154
  function buildAnnotatedSaveSuggestion() {
7673
8155
  const effectivePath = getEffectiveSavePath() || sourceState.path || "";
7674
8156
  if (effectivePath) {
@@ -14262,7 +14744,7 @@
14262
14744
  sendRunBtn.classList.toggle("request-stop-active", directIsStop);
14263
14745
  sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
14264
14746
  sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
14265
- const replHint = rightView === "repl" && getActiveReplSession()
14747
+ const replHint = rightView === "repl" && getActiveReplSessionForCurrentRuntime()
14266
14748
  ? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
14267
14749
  : "";
14268
14750
  sendRunBtn.title = directIsStop
@@ -14283,7 +14765,7 @@
14283
14765
  : "Queue steering is available while Run editor text is active.";
14284
14766
  }
14285
14767
 
14286
- const hasReplSession = Boolean(getActiveReplSession());
14768
+ const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
14287
14769
  if (sendReplBtn) {
14288
14770
  const showReplSend = rightView === "repl";
14289
14771
  sendReplBtn.hidden = !showReplSend;
@@ -14651,7 +15133,7 @@
14651
15133
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
14652
15134
  const sessionsChanged = setReplSessions(message.sessions);
14653
15135
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
14654
- setActiveReplSession(message.activeSessionName);
15136
+ setActiveReplSessionForCurrentRuntime(message.activeSessionName);
14655
15137
  }
14656
15138
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
14657
15139
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
@@ -14704,7 +15186,7 @@
14704
15186
  }
14705
15187
  }
14706
15188
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
14707
- setActiveReplSession(message.activeSessionName);
15189
+ setActiveReplSessionForCurrentRuntime(message.activeSessionName);
14708
15190
  }
14709
15191
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
14710
15192
  if (typeof message.transcript === "string") {
@@ -15519,6 +16001,8 @@
15519
16001
  rightPaneEl.addEventListener("focusin", (event) => activatePaneFromInteraction("right", event));
15520
16002
  }
15521
16003
 
16004
+ setupPaneResizeHandle();
16005
+
15522
16006
  if (leftFocusBtn) {
15523
16007
  leftFocusBtn.addEventListener("click", () => {
15524
16008
  if (paneFocusTarget === "left") {