pi-studio 0.9.5 → 0.9.7

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.
@@ -192,6 +192,11 @@
192
192
  let studioPdfFocusCloseBtn = null;
193
193
  let studioPdfFocusLastFocusedEl = null;
194
194
  let studioPdfFocusMovedFrameState = null;
195
+ let studioHtmlFocusOverlayEl = null;
196
+ let studioHtmlFocusShellEl = null;
197
+ let studioHtmlFocusFullscreenBtn = null;
198
+ let studioHtmlFocusLastFocusedEl = null;
199
+ let studioHtmlFocusRestoreState = null;
195
200
  let pendingRequestId = null;
196
201
  let pendingKind = null;
197
202
  let stickyStudioKind = null;
@@ -292,27 +297,33 @@
292
297
  return "raw";
293
298
  }
294
299
  })();
300
+ function normalizeReplJournalEntry(entry) {
301
+ if (!entry || typeof entry !== "object") return null;
302
+ const normalized = {
303
+ id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
304
+ requestId: typeof entry.requestId === "string" ? entry.requestId : "",
305
+ createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
306
+ updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
307
+ sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
308
+ runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
309
+ label: typeof entry.label === "string" ? entry.label : "REPL send",
310
+ mode: typeof entry.mode === "string" ? entry.mode : "raw",
311
+ prose: typeof entry.prose === "string" ? entry.prose : "",
312
+ code: typeof entry.code === "string" ? entry.code : "",
313
+ output: typeof entry.output === "string" ? entry.output : "",
314
+ beforeTranscript: "",
315
+ status: typeof entry.status === "string" ? entry.status : "sent",
316
+ skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
317
+ };
318
+ return (normalized.code.trim() || normalized.prose.trim() || normalized.output.trim()) ? normalized : null;
319
+ }
320
+
295
321
  function loadPersistedReplJournalEntries() {
296
322
  try {
297
323
  const raw = window.localStorage ? window.localStorage.getItem("piStudio.replStudioEntries.v1") : null;
298
324
  const parsed = raw ? JSON.parse(raw) : [];
299
325
  if (!Array.isArray(parsed)) return [];
300
- return parsed.map((entry) => ({
301
- id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
302
- requestId: typeof entry.requestId === "string" ? entry.requestId : "",
303
- createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
304
- updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
305
- sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
306
- runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
307
- label: typeof entry.label === "string" ? entry.label : "REPL send",
308
- mode: typeof entry.mode === "string" ? entry.mode : "raw",
309
- prose: typeof entry.prose === "string" ? entry.prose : "",
310
- code: typeof entry.code === "string" ? entry.code : "",
311
- output: typeof entry.output === "string" ? entry.output : "",
312
- beforeTranscript: "",
313
- status: typeof entry.status === "string" ? entry.status : "sent",
314
- skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
315
- })).filter((entry) => entry.code.trim() || entry.prose.trim() || entry.output.trim()).slice(-REPL_JOURNAL_MAX_ENTRIES);
326
+ return parsed.map(normalizeReplJournalEntry).filter(Boolean).slice(-REPL_JOURNAL_MAX_ENTRIES);
316
327
  } catch {
317
328
  return [];
318
329
  }
@@ -342,6 +353,47 @@
342
353
  }
343
354
  }
344
355
 
356
+ function mergeReplJournalEntries(entries) {
357
+ if (!Array.isArray(entries) || !entries.length) return false;
358
+ let changed = false;
359
+ const next = [...replJournalEntries];
360
+ for (const rawEntry of entries) {
361
+ const entry = normalizeReplJournalEntry(rawEntry);
362
+ if (!entry) continue;
363
+ const existingIndex = next.findIndex((candidate) => (
364
+ (entry.requestId && candidate.requestId === entry.requestId)
365
+ || candidate.id === entry.id
366
+ ));
367
+ if (existingIndex >= 0) {
368
+ const existing = next[existingIndex];
369
+ const merged = {
370
+ ...existing,
371
+ ...entry,
372
+ label: existing.label || entry.label,
373
+ mode: existing.mode || entry.mode,
374
+ prose: existing.prose || entry.prose,
375
+ beforeTranscript: existing.beforeTranscript || "",
376
+ createdAt: existing.createdAt || entry.createdAt,
377
+ updatedAt: Math.max(existing.updatedAt || 0, entry.updatedAt || 0),
378
+ skippedChunks: existing.skippedChunks || entry.skippedChunks,
379
+ };
380
+ if (JSON.stringify(existing) !== JSON.stringify(merged)) {
381
+ next[existingIndex] = merged;
382
+ changed = true;
383
+ }
384
+ } else {
385
+ next.push(entry);
386
+ changed = true;
387
+ }
388
+ }
389
+ if (!changed) return false;
390
+ replJournalEntries = next
391
+ .sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0))
392
+ .slice(-REPL_JOURNAL_MAX_ENTRIES);
393
+ persistReplJournalEntries();
394
+ return true;
395
+ }
396
+
345
397
  let replJournalEntries = loadPersistedReplJournalEntries();
346
398
  let activeReplJournalEntryId = "";
347
399
  let replJournalCollapsed = (() => {
@@ -925,6 +977,27 @@
925
977
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
926
978
  }
927
979
 
980
+ function isMacShortcutPlatform() {
981
+ try {
982
+ const platform = String((navigator && navigator.platform) || "");
983
+ return /Mac|iPhone|iPad|iPod/i.test(platform);
984
+ } catch {
985
+ return true;
986
+ }
987
+ }
988
+
989
+ function getStudioShortcutLabel(kind) {
990
+ const mac = isMacShortcutPlatform();
991
+ if (kind === "repl-send") return mac ? "⇧⌘↵" : "Ctrl+Shift+Enter";
992
+ if (kind === "run") return mac ? "⌘↵" : "Ctrl+Enter";
993
+ return "";
994
+ }
995
+
996
+ function withStudioShortcutLabel(label, kind) {
997
+ const shortcut = getStudioShortcutLabel(kind);
998
+ return shortcut ? (label + " " + shortcut) : label;
999
+ }
1000
+
928
1001
  function setReplSendMode(mode) {
929
1002
  replSendMode = normalizeReplSendMode(mode);
930
1003
  if (replSendModeSelect) replSendModeSelect.value = replSendMode;
@@ -1011,9 +1084,18 @@
1011
1084
  function setActiveReplSession(sessionName) {
1012
1085
  const name = String(sessionName || "").trim();
1013
1086
  if (!name) {
1087
+ if (replActiveSessionName) {
1088
+ replTranscript = "";
1089
+ replCapturedAt = 0;
1090
+ }
1014
1091
  replActiveSessionName = "";
1015
1092
  return;
1016
1093
  }
1094
+ if (replActiveSessionName && replActiveSessionName !== name) {
1095
+ replTranscript = "";
1096
+ replCapturedAt = 0;
1097
+ activeReplJournalEntryId = "";
1098
+ }
1017
1099
  replActiveSessionName = name;
1018
1100
  }
1019
1101
 
@@ -1272,7 +1354,7 @@
1272
1354
 
1273
1355
  const allBlocks = parseMarkdownCodeFences(range.raw);
1274
1356
  if (allBlocks.length) {
1275
- return { error: "Place the cursor inside a code chunk, select text, or use Run all chunks. Switch send mode to Raw send to send the full editor." };
1357
+ return buildAllChunksReplSendPayload();
1276
1358
  }
1277
1359
 
1278
1360
  return {
@@ -1410,6 +1492,24 @@
1410
1492
  return isEcho ? lines.slice(1).join("\n").replace(/^\s+/, "") : value;
1411
1493
  }
1412
1494
 
1495
+ function stripStudioReplSubmissionEcho(delta) {
1496
+ let value = String(delta || "").replace(/^\s+/, "");
1497
+ // The raw mirror below remains raw; REPL Studio cards hide only the
1498
+ // temp-file wrapper used to submit multiline snippets safely. The
1499
+ // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
1500
+ const submissionEchoPatterns = [
1501
+ /^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
1502
+ /^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
1503
+ /^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
1504
+ /^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
1505
+ /^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
1506
+ ];
1507
+ for (const pattern of submissionEchoPatterns) {
1508
+ value = value.replace(pattern, "");
1509
+ }
1510
+ return value.replace(/^(?:\s*\n)+/, "");
1511
+ }
1512
+
1413
1513
  function stripTrailingReplPromptsFromOutput(output) {
1414
1514
  const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
1415
1515
  while (lines.length > 0 && /^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*$/.test(lines[lines.length - 1] || "")) {
@@ -1426,7 +1526,10 @@
1426
1526
  }
1427
1527
 
1428
1528
  function cleanReplCapturedOutput(delta, entry) {
1429
- return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(stripSubsequentReplInputsFromOutput(stripSubmittedCodeEchoFromReplDelta(delta, entry))));
1529
+ const withoutSubmissionEcho = stripStudioReplSubmissionEcho(delta);
1530
+ const withoutCodeEcho = stripSubmittedCodeEchoFromReplDelta(withoutSubmissionEcho, entry);
1531
+ const withoutLaterInputs = stripSubsequentReplInputsFromOutput(withoutCodeEcho);
1532
+ return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(withoutLaterInputs));
1430
1533
  }
1431
1534
 
1432
1535
  function updateActiveReplJournalEntryFromTranscript(sessionName, transcript) {
@@ -1452,13 +1555,17 @@
1452
1555
  return fence + (language ? language : "") + "\n" + value.replace(/\s+$/, "") + "\n" + fence;
1453
1556
  }
1454
1557
 
1455
- function buildReplJournalMarkdown() {
1456
- const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString(), ""];
1457
- if (!replJournalEntries.length) {
1458
- lines.push("_No REPL Studio entries yet._");
1558
+ function buildReplJournalMarkdown(entries) {
1559
+ const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1560
+ const sessionName = getActiveReplJournalSessionName();
1561
+ const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString()];
1562
+ if (sessionName) lines.push("Session: `" + sessionName + "`");
1563
+ lines.push("");
1564
+ if (!visibleEntries.length) {
1565
+ lines.push(sessionName ? ("_No REPL Studio entries for `" + sessionName + "` yet._") : "_No REPL Studio entries yet._");
1459
1566
  return lines.join("\n");
1460
1567
  }
1461
- replJournalEntries.forEach((entry, index) => {
1568
+ visibleEntries.forEach((entry, index) => {
1462
1569
  lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
1463
1570
  lines.push("");
1464
1571
  lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toLocaleString());
@@ -1485,53 +1592,62 @@
1485
1592
  }
1486
1593
 
1487
1594
  async function copyReplJournalToClipboard() {
1488
- if (!replJournalEntries.length) {
1489
- setStatus("No REPL Studio entries to copy yet.", "warning");
1595
+ const entries = getVisibleReplJournalEntries();
1596
+ if (!entries.length) {
1597
+ setStatus("No REPL Studio entries to copy for this session yet.", "warning");
1490
1598
  return;
1491
1599
  }
1492
- if (await writeTextToClipboard(buildReplJournalMarkdown())) {
1493
- setStatus("Copied REPL Studio as Markdown.", "success");
1600
+ if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
1601
+ setStatus("Copied REPL Studio session as Markdown.", "success");
1494
1602
  } else {
1495
1603
  setStatus("Clipboard write failed.", "warning");
1496
1604
  }
1497
1605
  }
1498
1606
 
1499
1607
  function exportReplJournalMarkdown() {
1500
- if (!replJournalEntries.length) {
1501
- setStatus("No REPL Studio entries to export yet.", "warning");
1608
+ const entries = getVisibleReplJournalEntries();
1609
+ if (!entries.length) {
1610
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
1502
1611
  return;
1503
1612
  }
1504
- const blob = new Blob([buildReplJournalMarkdown()], { type: "text/markdown;charset=utf-8" });
1613
+ const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
1505
1614
  const blobUrl = URL.createObjectURL(blob);
1506
1615
  const link = document.createElement("a");
1507
1616
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1617
+ const sessionSlug = getActiveReplJournalSessionName().replace(/[^-_.A-Za-z0-9]+/g, "-");
1508
1618
  link.href = blobUrl;
1509
- link.download = "repl-studio-" + stamp + ".md";
1619
+ link.download = "repl-studio" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
1510
1620
  document.body.appendChild(link);
1511
1621
  link.click();
1512
1622
  link.remove();
1513
1623
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1514
- setStatus("Exported REPL Studio Markdown.", "success");
1624
+ setStatus("Exported REPL Studio session Markdown.", "success");
1515
1625
  }
1516
1626
 
1517
1627
  function clearReplJournal() {
1518
- replJournalEntries = [];
1628
+ const sessionName = getActiveReplJournalSessionName();
1629
+ if (sessionName) {
1630
+ replJournalEntries = replJournalEntries.filter((entry) => entry.sessionName !== sessionName);
1631
+ } else {
1632
+ replJournalEntries = [];
1633
+ }
1519
1634
  activeReplJournalEntryId = "";
1520
1635
  persistReplJournalEntries();
1521
- setStatus("Cleared REPL Studio.", "success");
1636
+ setStatus(sessionName ? "Cleared REPL Studio for this session." : "Cleared REPL Studio.", "success");
1522
1637
  renderReplViewIfActive({ force: true });
1523
1638
  }
1524
1639
 
1525
1640
  function loadReplJournalIntoEditor() {
1526
- if (!replJournalEntries.length) {
1527
- setStatus("No REPL Studio entries to load yet.", "warning");
1641
+ const entries = getVisibleReplJournalEntries();
1642
+ if (!entries.length) {
1643
+ setStatus("No REPL Studio entries to load for this session yet.", "warning");
1528
1644
  return;
1529
1645
  }
1530
- const markdown = buildReplJournalMarkdown();
1646
+ const markdown = buildReplJournalMarkdown(entries);
1531
1647
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1532
1648
  setSourceState({ source: "blank", label: "REPL Studio", path: null });
1533
1649
  setEditorLanguage("markdown");
1534
- setStatus("Loaded REPL Studio into editor.", "success");
1650
+ setStatus("Loaded REPL Studio session into editor.", "success");
1535
1651
  }
1536
1652
 
1537
1653
  function addSelectedReplJournalNote() {
@@ -2101,13 +2217,16 @@
2101
2217
  const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2102
2218
  if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
2103
2219
  if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
2104
- if (!isEditorOnlyMode && sendReplBtn) actionLineOneEl.appendChild(sendReplBtn);
2105
- if (!isEditorOnlyMode && replSendModeSelect) actionLineOneEl.appendChild(replSendModeSelect);
2220
+ const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2221
+ replActionLineEl.hidden = true;
2222
+ if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2223
+ if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2106
2224
  const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2107
2225
  actionLineTwoEl.appendChild(copyDraftBtn);
2108
2226
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
2109
2227
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2110
2228
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
2229
+ if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
2111
2230
  actionsEl.appendChild(actionLineTwoEl);
2112
2231
 
2113
2232
  const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
@@ -3042,6 +3161,12 @@
3042
3161
  && typeof studioPdfFocusDialogEl.contains === "function"
3043
3162
  && studioPdfFocusDialogEl.contains(event.target)
3044
3163
  );
3164
+ const htmlFocusOwnsEvent = Boolean(
3165
+ studioHtmlFocusShellEl
3166
+ && event.target
3167
+ && typeof studioHtmlFocusShellEl.contains === "function"
3168
+ && studioHtmlFocusShellEl.contains(event.target)
3169
+ );
3045
3170
  const quizOwnsEvent = Boolean(
3046
3171
  quizDialogEl
3047
3172
  && event.target
@@ -3061,6 +3186,12 @@
3061
3186
  return;
3062
3187
  }
3063
3188
 
3189
+ if (isStudioHtmlFocusOpen() && plainEscape) {
3190
+ event.preventDefault();
3191
+ closeStudioHtmlFocusViewer();
3192
+ return;
3193
+ }
3194
+
3064
3195
  if (isScratchpadOpen() && plainEscape) {
3065
3196
  event.preventDefault();
3066
3197
  closeScratchpad();
@@ -3079,7 +3210,7 @@
3079
3210
  return;
3080
3211
  }
3081
3212
 
3082
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || quizOwnsEvent) {
3213
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
3083
3214
  return;
3084
3215
  }
3085
3216
 
@@ -3125,6 +3256,7 @@
3125
3256
  && event.shiftKey
3126
3257
  && activePane === "left"
3127
3258
  && !isEditorOnlyMode
3259
+ && rightView === "repl"
3128
3260
  ) {
3129
3261
  event.preventDefault();
3130
3262
  if (sendReplBtn && !sendReplBtn.hidden && !sendReplBtn.disabled) {
@@ -3739,6 +3871,10 @@
3739
3871
  return;
3740
3872
  }
3741
3873
  if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
3874
+ if (record.shell && record.shell.classList && record.shell.classList.contains("is-focused")) {
3875
+ if (record.detail) record.detail.textContent = "HTML preview";
3876
+ return;
3877
+ }
3742
3878
  const rawHeight = Number(data.height);
3743
3879
  if (!Number.isFinite(rawHeight) || rawHeight <= 0) return;
3744
3880
  const measuredHeight = Math.ceil(rawHeight + 2);
@@ -3760,30 +3896,232 @@
3760
3896
 
3761
3897
  window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
3762
3898
 
3899
+ function isStudioHtmlFocusOpen() {
3900
+ return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
3901
+ }
3902
+
3903
+ function ensureStudioHtmlFocusViewer() {
3904
+ if (studioHtmlFocusOverlayEl) return studioHtmlFocusOverlayEl;
3905
+
3906
+ const overlay = document.createElement("div");
3907
+ overlay.className = "studio-pdf-focus-overlay studio-html-focus-overlay";
3908
+ overlay.hidden = true;
3909
+ overlay.setAttribute("aria-hidden", "true");
3910
+ overlay.addEventListener("click", (event) => {
3911
+ if (event.target === overlay) closeStudioHtmlFocusViewer();
3912
+ });
3913
+ document.addEventListener("fullscreenchange", syncStudioHtmlFocusFullscreenButton);
3914
+ document.body.appendChild(overlay);
3915
+ studioHtmlFocusOverlayEl = overlay;
3916
+ syncStudioHtmlFocusFullscreenButton();
3917
+ return overlay;
3918
+ }
3919
+
3920
+ function getStudioHtmlFocusButton(shell) {
3921
+ return shell && typeof shell.querySelector === "function"
3922
+ ? shell.querySelector(".studio-html-artifact-focus-btn")
3923
+ : null;
3924
+ }
3925
+
3926
+ function getStudioHtmlFullscreenButton(shell) {
3927
+ return shell && typeof shell.querySelector === "function"
3928
+ ? shell.querySelector(".studio-html-artifact-fullscreen-btn")
3929
+ : null;
3930
+ }
3931
+
3932
+ function setStudioHtmlFocusButtonMode(button, focused) {
3933
+ if (!button) return;
3934
+ button.replaceChildren(makeStudioUiRefreshIcon(focused ? "focus-exit" : "focus"));
3935
+ button.title = focused
3936
+ ? "Exit HTML preview focus view."
3937
+ : "Open this HTML preview in a larger Studio overlay.";
3938
+ button.setAttribute("aria-label", focused ? "Exit HTML preview focus view" : "Focus HTML preview");
3939
+ button.setAttribute("aria-pressed", focused ? "true" : "false");
3940
+ }
3941
+
3942
+ function syncStudioHtmlFocusFullscreenButton() {
3943
+ const button = studioHtmlFocusFullscreenBtn;
3944
+ if (!button) return;
3945
+ const shell = studioHtmlFocusShellEl;
3946
+ const isFullscreen = Boolean(shell && document.fullscreenElement && document.fullscreenElement === shell);
3947
+ button.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
3948
+ const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
3949
+ button.title = isFullscreen
3950
+ ? "Exit browser fullscreen and keep the HTML preview focus view open."
3951
+ : "Ask the browser to make this HTML preview fullscreen.";
3952
+ button.setAttribute("aria-label", label);
3953
+ button.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
3954
+ }
3955
+
3956
+ async function toggleStudioHtmlFocusFullscreen() {
3957
+ const shell = studioHtmlFocusShellEl;
3958
+ if (!shell) return;
3959
+ const isFullscreen = Boolean(document.fullscreenElement && document.fullscreenElement === shell);
3960
+ if (isFullscreen) {
3961
+ try {
3962
+ if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
3963
+ } catch (error) {
3964
+ setStatus("Could not exit HTML preview fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3965
+ } finally {
3966
+ syncStudioHtmlFocusFullscreenButton();
3967
+ }
3968
+ return;
3969
+ }
3970
+ if (typeof shell.requestFullscreen !== "function") {
3971
+ setStatus("Browser fullscreen is not available for this HTML preview.", "warning");
3972
+ return;
3973
+ }
3974
+ try {
3975
+ await shell.requestFullscreen();
3976
+ } catch (error) {
3977
+ setStatus("Could not enter HTML preview fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3978
+ } finally {
3979
+ syncStudioHtmlFocusFullscreenButton();
3980
+ }
3981
+ }
3982
+
3983
+ function openStudioHtmlFocusViewer(title, shell) {
3984
+ // Keep the existing sandboxed iframe in place. Reparenting srcdoc iframes can
3985
+ // recreate their browsing context and lose form/script state.
3986
+ const focusShell = shell instanceof HTMLElement ? shell : null;
3987
+ if (!focusShell || !focusShell.isConnected) return false;
3988
+ if (isStudioHtmlFocusOpen() && studioHtmlFocusShellEl === focusShell) return true;
3989
+ if (isStudioHtmlFocusOpen()) closeStudioHtmlFocusViewer();
3990
+ ensureStudioHtmlFocusViewer();
3991
+
3992
+ studioHtmlFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3993
+ studioHtmlFocusShellEl = focusShell;
3994
+ studioHtmlFocusRestoreState = {
3995
+ role: focusShell.getAttribute("role"),
3996
+ ariaModal: focusShell.getAttribute("aria-modal"),
3997
+ ariaLabel: focusShell.getAttribute("aria-label"),
3998
+ };
3999
+
4000
+ focusShell.classList.add("is-focused");
4001
+ focusShell.setAttribute("role", "dialog");
4002
+ focusShell.setAttribute("aria-modal", "true");
4003
+ focusShell.setAttribute("aria-label", String(title || "HTML preview").trim() || "HTML preview");
4004
+
4005
+ const focusButton = getStudioHtmlFocusButton(focusShell);
4006
+ setStudioHtmlFocusButtonMode(focusButton, true);
4007
+ studioHtmlFocusFullscreenBtn = getStudioHtmlFullscreenButton(focusShell);
4008
+ if (studioHtmlFocusFullscreenBtn) studioHtmlFocusFullscreenBtn.hidden = false;
4009
+
4010
+ if (document.body) document.body.classList.add("studio-html-focus-open", "studio-pdf-focus-open");
4011
+ if (studioHtmlFocusOverlayEl) {
4012
+ studioHtmlFocusOverlayEl.hidden = false;
4013
+ studioHtmlFocusOverlayEl.setAttribute("aria-hidden", "false");
4014
+ }
4015
+ syncStudioHtmlFocusFullscreenButton();
4016
+ closeStudioUiRefreshMenus();
4017
+ closeExportPreviewMenu();
4018
+ window.setTimeout(() => {
4019
+ if (focusButton && typeof focusButton.focus === "function") focusButton.focus();
4020
+ }, 0);
4021
+ return true;
4022
+ }
4023
+
4024
+ function closeStudioHtmlFocusViewer() {
4025
+ if (!isStudioHtmlFocusOpen()) return false;
4026
+ const shell = studioHtmlFocusShellEl;
4027
+ if (document.fullscreenElement && shell && document.fullscreenElement === shell) {
4028
+ try {
4029
+ const exitResult = document.exitFullscreen && document.exitFullscreen();
4030
+ if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
4031
+ } catch {}
4032
+ }
4033
+ if (studioHtmlFocusOverlayEl) {
4034
+ studioHtmlFocusOverlayEl.hidden = true;
4035
+ studioHtmlFocusOverlayEl.setAttribute("aria-hidden", "true");
4036
+ }
4037
+ if (shell) {
4038
+ shell.classList.remove("is-focused");
4039
+ const restore = studioHtmlFocusRestoreState || {};
4040
+ if (restore.role === null || typeof restore.role === "undefined") shell.removeAttribute("role");
4041
+ else shell.setAttribute("role", restore.role);
4042
+ if (restore.ariaModal === null || typeof restore.ariaModal === "undefined") shell.removeAttribute("aria-modal");
4043
+ else shell.setAttribute("aria-modal", restore.ariaModal);
4044
+ if (restore.ariaLabel === null || typeof restore.ariaLabel === "undefined") shell.removeAttribute("aria-label");
4045
+ else shell.setAttribute("aria-label", restore.ariaLabel);
4046
+ setStudioHtmlFocusButtonMode(getStudioHtmlFocusButton(shell), false);
4047
+ }
4048
+ if (studioHtmlFocusFullscreenBtn) studioHtmlFocusFullscreenBtn.hidden = true;
4049
+ studioHtmlFocusShellEl = null;
4050
+ studioHtmlFocusFullscreenBtn = null;
4051
+ studioHtmlFocusRestoreState = null;
4052
+ if (document.body) document.body.classList.remove("studio-html-focus-open", "studio-pdf-focus-open");
4053
+ const focusTarget = studioHtmlFocusLastFocusedEl;
4054
+ studioHtmlFocusLastFocusedEl = null;
4055
+ if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
4056
+ window.setTimeout(() => focusTarget.focus(), 0);
4057
+ }
4058
+ return true;
4059
+ }
4060
+
4061
+ function openStudioHtmlFocusFromButton(buttonEl) {
4062
+ if (!buttonEl) return false;
4063
+ const shell = buttonEl.closest && buttonEl.closest(".studio-html-artifact-shell");
4064
+ if (!shell) return false;
4065
+ if (isStudioHtmlFocusOpen() && studioHtmlFocusShellEl === shell) {
4066
+ return closeStudioHtmlFocusViewer();
4067
+ }
4068
+ const title = String(shell.dataset && shell.dataset.studioHtmlTitle ? shell.dataset.studioHtmlTitle : "").trim()
4069
+ || String(buttonEl.dataset && buttonEl.dataset.studioHtmlTitle ? buttonEl.dataset.studioHtmlTitle : "").trim()
4070
+ || "HTML preview";
4071
+ return openStudioHtmlFocusViewer(title, shell);
4072
+ }
4073
+
4074
+ function handleStudioHtmlFocusButtonClick(event) {
4075
+ const target = event && event.target;
4076
+ const buttonEl = target instanceof Element ? target.closest(".studio-html-artifact-focus-btn") : null;
4077
+ if (!buttonEl) return;
4078
+ event.preventDefault();
4079
+ event.stopPropagation();
4080
+ if (typeof event.stopImmediatePropagation === "function") {
4081
+ event.stopImmediatePropagation();
4082
+ }
4083
+ if (!openStudioHtmlFocusFromButton(buttonEl)) {
4084
+ setStatus("Could not open HTML preview focus view.", "warning");
4085
+ }
4086
+ }
4087
+
3763
4088
  function renderHtmlArtifactPreview(targetEl, html, pane, options) {
3764
4089
  if (!targetEl) return;
3765
4090
  const title = options && options.title ? String(options.title) : "HTML preview";
3766
4091
  const previewId = "html_artifact_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
3767
4092
  pruneDisconnectedHtmlArtifactFrames();
4093
+ if (isStudioHtmlFocusOpen()) closeStudioHtmlFocusViewer();
3768
4094
  clearPreviewJumpHighlight(targetEl);
3769
4095
  finishPreviewRender(targetEl);
3770
4096
  targetEl.innerHTML = "";
3771
4097
 
3772
4098
  const shell = document.createElement("div");
3773
4099
  shell.className = "studio-html-artifact-shell";
4100
+ if (shell.dataset) shell.dataset.studioHtmlTitle = title;
3774
4101
 
3775
4102
  const toolbar = document.createElement("div");
3776
4103
  toolbar.className = "studio-html-artifact-toolbar";
4104
+ const titleGroup = document.createElement("div");
4105
+ titleGroup.className = "studio-html-artifact-title-group";
4106
+ const focusBtn = document.createElement("button");
4107
+ focusBtn.type = "button";
4108
+ focusBtn.className = "studio-html-artifact-focus-btn";
4109
+ focusBtn.title = "Open this HTML preview in a larger Studio overlay.";
4110
+ focusBtn.setAttribute("aria-label", "Focus HTML preview");
4111
+ if (focusBtn.dataset) focusBtn.dataset.studioHtmlTitle = title;
4112
+ focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
4113
+ focusBtn.addEventListener("click", handleStudioHtmlFocusButtonClick);
4114
+ titleGroup.appendChild(focusBtn);
3777
4115
  const label = document.createElement("span");
3778
4116
  label.className = "studio-html-artifact-label";
3779
4117
  label.textContent = title;
4118
+ titleGroup.appendChild(label);
3780
4119
  const detail = document.createElement("span");
3781
4120
  detail.className = "studio-html-artifact-detail";
3782
4121
  detail.textContent = "HTML preview";
3783
4122
 
3784
4123
  const tools = document.createElement("span");
3785
4124
  tools.className = "studio-html-artifact-tools";
3786
- tools.appendChild(detail);
3787
4125
 
3788
4126
  const zoomControls = document.createElement("span");
3789
4127
  zoomControls.className = "studio-html-artifact-zoom-controls";
@@ -3833,10 +4171,24 @@
3833
4171
  zoomControls.appendChild(zoomOutBtn);
3834
4172
  zoomControls.appendChild(zoomResetBtn);
3835
4173
  zoomControls.appendChild(zoomInBtn);
4174
+ const fullscreenBtn = document.createElement("button");
4175
+ fullscreenBtn.type = "button";
4176
+ fullscreenBtn.className = "studio-html-artifact-fullscreen-btn";
4177
+ fullscreenBtn.hidden = true;
4178
+ fullscreenBtn.addEventListener("pointerdown", (event) => { event.stopPropagation(); });
4179
+ fullscreenBtn.addEventListener("mousedown", (event) => { event.stopPropagation(); });
4180
+ fullscreenBtn.addEventListener("click", (event) => {
4181
+ event.preventDefault();
4182
+ event.stopPropagation();
4183
+ toggleStudioHtmlFocusFullscreen();
4184
+ });
4185
+ fullscreenBtn.appendChild(makeStudioUiRefreshIcon("fullscreen"));
3836
4186
  updateZoomUi();
4187
+ tools.appendChild(detail);
3837
4188
  tools.appendChild(zoomControls);
4189
+ tools.appendChild(fullscreenBtn);
3838
4190
 
3839
- toolbar.appendChild(label);
4191
+ toolbar.appendChild(titleGroup);
3840
4192
  toolbar.appendChild(tools);
3841
4193
  shell.appendChild(toolbar);
3842
4194
 
@@ -5012,6 +5364,18 @@
5012
5364
  }
5013
5365
  return;
5014
5366
  }
5367
+ if (action === "copy-attach-command") {
5368
+ const session = getActiveReplSession();
5369
+ const text = getReplAttachCommand(session);
5370
+ if (!text.trim()) {
5371
+ setStatus("Start or select a REPL session first.", "warning");
5372
+ return;
5373
+ }
5374
+ void writeTextToClipboard(text).then((ok) => {
5375
+ setStatus(ok ? "Copied tmux attach command." : "Clipboard write failed.", ok ? "success" : "warning");
5376
+ });
5377
+ return;
5378
+ }
5015
5379
  if (action === "run-all-chunks") {
5016
5380
  sendEditorTextToRepl({ action: "all-chunks" });
5017
5381
  return;
@@ -5333,8 +5697,9 @@
5333
5697
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
5334
5698
  return;
5335
5699
  }
5336
- if (exportingReplJournal && !replJournalEntries.length) {
5337
- setStatus("No REPL Studio entries to export yet.", "warning");
5700
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
5701
+ if (exportingReplJournal && !replJournalExportEntries.length) {
5702
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
5338
5703
  return;
5339
5704
  }
5340
5705
 
@@ -5345,7 +5710,7 @@
5345
5710
  }
5346
5711
 
5347
5712
  const markdown = exportingReplJournal
5348
- ? buildReplJournalMarkdown()
5713
+ ? buildReplJournalMarkdown(replJournalExportEntries)
5349
5714
  : (rightView === "editor-preview"
5350
5715
  ? prepareEditorTextForPdfExport(sourceTextEl.value)
5351
5716
  : prepareEditorTextForPreview(latestResponseMarkdown));
@@ -5505,13 +5870,14 @@
5505
5870
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
5506
5871
  return;
5507
5872
  }
5508
- if (exportingReplJournal && !replJournalEntries.length) {
5509
- setStatus("No REPL Studio entries to export yet.", "warning");
5873
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
5874
+ if (exportingReplJournal && !replJournalExportEntries.length) {
5875
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
5510
5876
  return;
5511
5877
  }
5512
5878
 
5513
5879
  const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
5514
- const markdown = exportingReplJournal ? buildReplJournalMarkdown() : (htmlArtifactSource || (rightView === "editor-preview"
5880
+ const markdown = exportingReplJournal ? buildReplJournalMarkdown(replJournalExportEntries) : (htmlArtifactSource || (rightView === "editor-preview"
5515
5881
  ? prepareEditorTextForHtmlExport(sourceTextEl.value)
5516
5882
  : prepareEditorTextForPreview(latestResponseMarkdown)));
5517
5883
  if (!markdown || !markdown.trim()) {
@@ -6009,8 +6375,24 @@
6009
6375
  return remaining < 56;
6010
6376
  }
6011
6377
 
6378
+ function getActiveReplJournalSessionName() {
6379
+ return String(replActiveSessionName || "").trim();
6380
+ }
6381
+
6382
+ function getVisibleReplJournalEntries() {
6383
+ const sessionName = getActiveReplJournalSessionName();
6384
+ if (!sessionName) return replJournalEntries;
6385
+ return replJournalEntries.filter((entry) => entry.sessionName === sessionName);
6386
+ }
6387
+
6388
+ function getHiddenReplJournalEntryCount() {
6389
+ const sessionName = getActiveReplJournalSessionName();
6390
+ if (!sessionName) return 0;
6391
+ return replJournalEntries.filter((entry) => entry.sessionName && entry.sessionName !== sessionName).length;
6392
+ }
6393
+
6012
6394
  function isReplJournalExpanded() {
6013
- return rightView === "repl" && !replJournalCollapsed && replJournalEntries.length > 0;
6395
+ return rightView === "repl" && !replJournalCollapsed && getVisibleReplJournalEntries().length > 0;
6014
6396
  }
6015
6397
 
6016
6398
  function shouldAutoStickReplView() {
@@ -6196,23 +6578,26 @@
6196
6578
 
6197
6579
  function buildReplStudioActionsHtml() {
6198
6580
  if (replJournalCollapsed) return "";
6199
- const hasEntries = replJournalEntries.length > 0;
6581
+ const hasEntries = getVisibleReplJournalEntries().length > 0;
6200
6582
  const buttons = "<button type='button' data-repl-action='load-journal'" + (hasEntries ? "" : " disabled") + ">Load in editor</button>"
6201
6583
  + "<button type='button' data-repl-action='copy-journal'" + (hasEntries ? "" : " disabled") + ">Copy Markdown</button>"
6202
6584
  + "<button type='button' data-repl-action='export-journal'" + (hasEntries ? "" : " disabled") + ">Export .md</button>"
6203
- + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear</button>";
6585
+ + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear session</button>";
6204
6586
  return "<div class='repl-studio-below-actions'><div class='repl-journal-actions'>" + buttons + "</div></div>";
6205
6587
  }
6206
6588
 
6207
6589
  function buildReplJournalHtml(transcript) {
6208
- const hasEntries = replJournalEntries.length > 0;
6209
- const entryCount = replJournalEntries.length;
6590
+ const visibleEntries = getVisibleReplJournalEntries();
6591
+ const hasEntries = visibleEntries.length > 0;
6592
+ const entryCount = visibleEntries.length;
6593
+ const hiddenEntryCount = getHiddenReplJournalEntryCount();
6594
+ const sessionName = getActiveReplJournalSessionName();
6210
6595
  const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
6211
6596
  const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show REPL Studio" : "Hide REPL Studio") + "</button>";
6212
6597
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
6213
6598
  const summaryText = hasEntries
6214
- ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
6215
- : "Studio-sent code and notes will appear here.";
6599
+ ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
6600
+ : (sessionName ? "No Studio entries for " + sessionName + "." : "Studio-sent code and notes will appear here.");
6216
6601
  if (replJournalCollapsed) {
6217
6602
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
6218
6603
  + "<div class='repl-journal-compact-row'>"
@@ -6221,12 +6606,12 @@
6221
6606
  + "</div>"
6222
6607
  + "</section>";
6223
6608
  }
6224
- const omitted = Math.max(0, replJournalEntries.length - 12);
6609
+ const omitted = Math.max(0, visibleEntries.length - 12);
6225
6610
  const bannerText = extractReplStudioBanner(transcript, getActiveReplRuntime());
6226
6611
  const banner = bannerText
6227
6612
  ? "<pre class='repl-studio-banner'>" + escapeHtml(bannerText) + "</pre>"
6228
6613
  : "";
6229
- const cards = replJournalEntries.slice(-12).map((entry) => {
6614
+ const cards = visibleEntries.slice(-12).map((entry) => {
6230
6615
  const meta = buildReplStudioMeta(entry);
6231
6616
  const prompt = getReplStudioPrompt(entry.runtime);
6232
6617
  const codeText = String(entry.code || "").trimEnd();
@@ -6252,11 +6637,17 @@
6252
6637
  + pending
6253
6638
  + "</article>";
6254
6639
  }).join("");
6640
+ const emptyText = sessionName
6641
+ ? (String(transcript || "").trim()
6642
+ ? "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."
6643
+ : "No REPL Studio entries for this tmux session yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
6644
+ : "No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
6255
6645
  const terminalContent = banner
6256
- + (hasEntries ? cards : "<div class='repl-studio-empty'>No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.</div>");
6646
+ + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
6257
6647
  return "<section class='repl-journal'>"
6258
- + "<div class='repl-journal-header'><div><h3>REPL Studio</h3><p>Clean collaborative Studio REPL record. The raw tmux mirror is available below.</p></div>" + toggleActions + "</div>"
6259
- + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
6648
+ + "<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>"
6649
+ + (hiddenEntryCount ? "<div class='repl-journal-omitted'>" + escapeHtml(String(hiddenEntryCount)) + " entr" + (hiddenEntryCount === 1 ? "y" : "ies") + " from other REPL sessions hidden.</div>" : "")
6650
+ + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
6260
6651
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
6261
6652
  + "</section>";
6262
6653
  }
@@ -6284,6 +6675,11 @@
6284
6675
  + "</section>";
6285
6676
  }
6286
6677
 
6678
+ function getReplAttachCommand(session) {
6679
+ if (!session || !session.sessionName) return "";
6680
+ return "tmux attach -t " + String(session.sessionName || "");
6681
+ }
6682
+
6287
6683
  function buildReplPanelHtml() {
6288
6684
  const runtimeOptions = [
6289
6685
  ["shell", "Shell"],
@@ -6319,6 +6715,7 @@
6319
6715
  + "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
6320
6716
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
6321
6717
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
6718
+ + "<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>"
6322
6719
  + "<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>"
6323
6720
  + "<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>"
6324
6721
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
@@ -6596,8 +6993,9 @@
6596
6993
 
6597
6994
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6598
6995
  const exportingReplJournal = rightView === "repl";
6996
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6599
6997
  const exportText = exportingReplJournal
6600
- ? (replJournalEntries.length ? buildReplJournalMarkdown() : "")
6998
+ ? (replJournalExportEntries.length ? buildReplJournalMarkdown(replJournalExportEntries) : "")
6601
6999
  : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown);
6602
7000
  const canExportPreview = (rightPaneShowsPreview || exportingReplJournal) && Boolean(String(exportText || "").trim());
6603
7001
  const htmlArtifactExportSource = canExportPreview && !exportingReplJournal ? getRightPaneHtmlArtifactSource() : "";
@@ -6609,8 +7007,8 @@
6609
7007
  : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
6610
7008
  if (rightView === "trace") {
6611
7009
  exportPdfBtn.title = "Working view does not support preview export.";
6612
- } else if (exportingReplJournal && !replJournalEntries.length) {
6613
- exportPdfBtn.title = "No REPL Studio entries to export yet.";
7010
+ } else if (exportingReplJournal && !replJournalExportEntries.length) {
7011
+ exportPdfBtn.title = "No REPL Studio entries to export for this session yet.";
6614
7012
  } else if (rightView === "markdown") {
6615
7013
  exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
6616
7014
  } else if (!canExportPreview) {
@@ -6640,7 +7038,7 @@
6640
7038
  ? (exportingReplJournal
6641
7039
  ? "Choose a format and export REPL Studio."
6642
7040
  : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
6643
- : (exportingReplJournal ? "No REPL Studio entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
7041
+ : (exportingReplJournal ? "No REPL Studio entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
6644
7042
  }
6645
7043
  if (!canExportPreview || previewExportInProgress) {
6646
7044
  closeExportPreviewMenu();
@@ -13185,7 +13583,7 @@
13185
13583
  if (isEditorOnlyMode) {
13186
13584
  if (sendRunBtn) {
13187
13585
  sendRunBtn.textContent = "Run editor text";
13188
- sendRunBtn.classList.remove("request-stop-active");
13586
+ sendRunBtn.classList.remove("request-stop-active", "repl-secondary-action");
13189
13587
  sendRunBtn.disabled = true;
13190
13588
  sendRunBtn.title = "Run is unavailable in editor-only mode.";
13191
13589
  }
@@ -13198,6 +13596,9 @@
13198
13596
  if (sendReplBtn) {
13199
13597
  sendReplBtn.hidden = true;
13200
13598
  sendReplBtn.disabled = true;
13599
+ sendReplBtn.classList.remove("repl-primary-action");
13600
+ const replActionLine = sendReplBtn.closest(".repl-action-line");
13601
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = true;
13201
13602
  }
13202
13603
  if (replSendModeSelect) {
13203
13604
  replSendModeSelect.hidden = true;
@@ -13218,11 +13619,12 @@
13218
13619
  }
13219
13620
 
13220
13621
  if (sendRunBtn) {
13221
- sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
13622
+ sendRunBtn.textContent = directIsStop ? "Stop" : (rightView === "repl" ? withStudioShortcutLabel("Run editor text", "run") : "Run editor text");
13222
13623
  sendRunBtn.classList.toggle("request-stop-active", directIsStop);
13624
+ sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
13223
13625
  sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
13224
13626
  const replHint = rightView === "repl" && getActiveReplSession()
13225
- ? " Includes active REPL identity for prompts that refer to it."
13627
+ ? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
13226
13628
  : "";
13227
13629
  sendRunBtn.title = directIsStop
13228
13630
  ? "Stop the active run. Shortcut: Esc."
@@ -13242,22 +13644,27 @@
13242
13644
  : "Queue steering is available while Run editor text is active.";
13243
13645
  }
13244
13646
 
13647
+ const hasReplSession = Boolean(getActiveReplSession());
13245
13648
  if (sendReplBtn) {
13246
- const hasSession = Boolean(getActiveReplSession());
13247
- sendReplBtn.hidden = rightView !== "repl";
13248
- sendReplBtn.disabled = wsState === "Disconnected" || uiBusy || replBusy || !hasSession;
13249
- sendReplBtn.title = hasSession
13649
+ const showReplSend = rightView === "repl";
13650
+ sendReplBtn.hidden = !showReplSend;
13651
+ sendReplBtn.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy || !hasReplSession;
13652
+ sendReplBtn.classList.toggle("repl-primary-action", showReplSend);
13653
+ sendReplBtn.textContent = showReplSend ? withStudioShortcutLabel(replSendMode === "literate" ? "Send selection/chunks" : "Send to REPL", "repl-send") : "Send to REPL";
13654
+ sendReplBtn.title = hasReplSession
13250
13655
  ? (replSendMode === "literate"
13251
- ? "Literate send: selected code/prose, or the current fenced code chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
13656
+ ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
13252
13657
  : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
13253
13658
  : "Start or select a REPL session in the right pane first.";
13659
+ const replActionLine = sendReplBtn.closest(".repl-action-line");
13660
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
13254
13661
  }
13255
13662
  if (replSendModeSelect) {
13256
13663
  replSendModeSelect.hidden = rightView !== "repl";
13257
13664
  replSendModeSelect.disabled = wsState === "Disconnected" || uiBusy || replBusy;
13258
13665
  replSendModeSelect.value = replSendMode;
13259
13666
  replSendModeSelect.title = replSendMode === "literate"
13260
- ? "Literate send: Send to REPL uses the selection/current fenced code chunk."
13667
+ ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
13261
13668
  : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
13262
13669
  }
13263
13670
 
@@ -13607,6 +14014,7 @@
13607
14014
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
13608
14015
  setActiveReplSession(message.activeSessionName);
13609
14016
  }
14017
+ const journalChanged = mergeReplJournalEntries(message.journalEntries);
13610
14018
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
13611
14019
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13612
14020
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
@@ -13621,6 +14029,7 @@
13621
14029
  || previousTranscript !== replTranscript
13622
14030
  || previousError !== replError
13623
14031
  || previousMessage !== replMessage
14032
+ || journalChanged
13624
14033
  || (!previousCapturedAt && replCapturedAt);
13625
14034
  if (viewChanged) renderReplViewIfActive();
13626
14035
  updateReferenceBadge();
@@ -13632,9 +14041,10 @@
13632
14041
  setActiveReplSession(message.sessionName);
13633
14042
  }
13634
14043
  const changed = recordReplToolSend(message);
14044
+ const journalChanged = mergeReplJournalEntries(message.journalEntries);
13635
14045
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
13636
14046
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13637
- if (changed) renderReplViewIfActive({ force: true });
14047
+ if (changed || journalChanged) renderReplViewIfActive({ force: true });
13638
14048
  updateReferenceBadge();
13639
14049
  return;
13640
14050
  }
@@ -13657,13 +14067,13 @@
13657
14067
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
13658
14068
  setActiveReplSession(message.activeSessionName);
13659
14069
  }
13660
- let journalChanged = false;
14070
+ let journalChanged = mergeReplJournalEntries(message.journalEntries);
13661
14071
  if (typeof message.transcript === "string") {
13662
14072
  replTranscript = trimReplTranscript(message.transcript);
13663
14073
  journalChanged = updateActiveReplJournalEntryFromTranscript(
13664
14074
  typeof message.activeSessionName === "string" && message.activeSessionName.trim() ? message.activeSessionName : replActiveSessionName,
13665
14075
  replTranscript
13666
- );
14076
+ ) || journalChanged;
13667
14077
  }
13668
14078
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13669
14079
  replError = typeof message.replError === "string" ? message.replError : "";
@@ -13686,6 +14096,7 @@
13686
14096
  replBusy = false;
13687
14097
  replMessage = "";
13688
14098
  replError = "";
14099
+ mergeReplJournalEntries(message.journalEntries);
13689
14100
  if (typeof message.requestId === "string") {
13690
14101
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
13691
14102
  persistReplJournalEntries();
@@ -15224,6 +15635,13 @@
15224
15635
  handleStudioPdfFocusButtonClick(event);
15225
15636
  }, true);
15226
15637
 
15638
+ document.addEventListener("click", (event) => {
15639
+ const target = event.target;
15640
+ const focusBtn = target instanceof Element ? target.closest(".studio-html-artifact-focus-btn") : null;
15641
+ if (!focusBtn) return;
15642
+ handleStudioHtmlFocusButtonClick(event);
15643
+ }, true);
15644
+
15227
15645
  document.addEventListener("click", (event) => {
15228
15646
  const target = event.target;
15229
15647
  const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;