pi-studio 0.9.0 → 0.9.2

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.
@@ -104,6 +104,7 @@
104
104
  const sendEditorBtn = document.getElementById("sendEditorBtn");
105
105
  const openCompanionBtn = document.getElementById("openCompanionBtn");
106
106
  const getEditorBtn = document.getElementById("getEditorBtn");
107
+ const zenModeBtn = document.getElementById("zenModeBtn");
107
108
  const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
108
109
  const sendRunBtn = document.getElementById("sendRunBtn");
109
110
  const queueSteerBtn = document.getElementById("queueSteerBtn");
@@ -180,6 +181,16 @@
180
181
  let statusLevel = "";
181
182
  let reconnectTimer = null;
182
183
  let reconnectAttempt = 0;
184
+ let studioPdfFocusOverlayEl = null;
185
+ let studioPdfFocusDialogEl = null;
186
+ let studioPdfFocusFrameSlotEl = null;
187
+ let studioPdfFocusFrameEl = null;
188
+ let studioPdfFocusTitleEl = null;
189
+ let studioPdfFocusOpenLinkEl = null;
190
+ let studioPdfFocusFullscreenBtn = null;
191
+ let studioPdfFocusCloseBtn = null;
192
+ let studioPdfFocusLastFocusedEl = null;
193
+ let studioPdfFocusMovedFrameState = null;
183
194
  let pendingRequestId = null;
184
195
  let pendingKind = null;
185
196
  let stickyStudioKind = null;
@@ -221,6 +232,8 @@
221
232
  const REPL_TRANSCRIPT_MAX_CHARS = 200_000;
222
233
  const REPL_JOURNAL_OUTPUT_MAX_CHARS = 80_000;
223
234
  const REPL_JOURNAL_MAX_ENTRIES = 80;
235
+ const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
236
+ const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
224
237
  const EDITOR_TAB_TEXT = " ";
225
238
  let replTmuxAvailable = null;
226
239
  let replSessions = [];
@@ -241,18 +254,77 @@
241
254
  let replBusy = false;
242
255
  let replSendMode = (() => {
243
256
  try {
244
- return (window.localStorage && window.localStorage.getItem("piStudio.replSendMode")) || "scratch";
257
+ const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
258
+ return String(stored || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
245
259
  } catch {
246
- return "scratch";
260
+ return "raw";
247
261
  }
248
262
  })();
249
- let replJournalEntries = [];
263
+ function loadPersistedReplJournalEntries() {
264
+ try {
265
+ const raw = window.localStorage ? window.localStorage.getItem("piStudio.replStudioEntries.v1") : null;
266
+ const parsed = raw ? JSON.parse(raw) : [];
267
+ if (!Array.isArray(parsed)) return [];
268
+ return parsed.map((entry) => ({
269
+ id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
270
+ requestId: typeof entry.requestId === "string" ? entry.requestId : "",
271
+ createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
272
+ updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
273
+ sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
274
+ runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
275
+ label: typeof entry.label === "string" ? entry.label : "REPL send",
276
+ mode: typeof entry.mode === "string" ? entry.mode : "raw",
277
+ prose: typeof entry.prose === "string" ? entry.prose : "",
278
+ code: typeof entry.code === "string" ? entry.code : "",
279
+ output: typeof entry.output === "string" ? entry.output : "",
280
+ beforeTranscript: "",
281
+ status: typeof entry.status === "string" ? entry.status : "sent",
282
+ skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
283
+ })).filter((entry) => entry.code.trim() || entry.prose.trim() || entry.output.trim()).slice(-REPL_JOURNAL_MAX_ENTRIES);
284
+ } catch {
285
+ return [];
286
+ }
287
+ }
288
+
289
+ function persistReplJournalEntries() {
290
+ try {
291
+ if (!window.localStorage) return;
292
+ const compact = replJournalEntries.slice(-REPL_JOURNAL_MAX_ENTRIES).map((entry) => ({
293
+ id: entry.id,
294
+ requestId: entry.requestId,
295
+ createdAt: entry.createdAt,
296
+ updatedAt: entry.updatedAt,
297
+ sessionName: entry.sessionName,
298
+ runtime: entry.runtime,
299
+ label: entry.label,
300
+ mode: entry.mode,
301
+ prose: entry.prose,
302
+ code: entry.code,
303
+ output: entry.output,
304
+ status: entry.status,
305
+ skippedChunks: entry.skippedChunks,
306
+ }));
307
+ window.localStorage.setItem("piStudio.replStudioEntries.v1", JSON.stringify(compact));
308
+ } catch {
309
+ // Ignore local persistence failures.
310
+ }
311
+ }
312
+
313
+ let replJournalEntries = loadPersistedReplJournalEntries();
250
314
  let activeReplJournalEntryId = "";
251
315
  let replJournalCollapsed = (() => {
252
316
  try {
253
- const stored = window.localStorage ? window.localStorage.getItem("piStudio.replJournalCollapsed") : null;
254
- if (stored === "false") return false;
317
+ const stored = window.localStorage ? window.localStorage.getItem("piStudio.replStudioCollapsed") : null;
255
318
  if (stored === "true") return true;
319
+ return false;
320
+ } catch {
321
+ return false;
322
+ }
323
+ })();
324
+ let replMirrorCollapsed = (() => {
325
+ try {
326
+ const stored = window.localStorage ? window.localStorage.getItem("piStudio.rawReplMirrorCollapsed") : null;
327
+ if (stored === "false") return false;
256
328
  return true;
257
329
  } catch {
258
330
  return true;
@@ -818,7 +890,7 @@
818
890
  }
819
891
 
820
892
  function normalizeReplSendMode(value) {
821
- return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "scratch";
893
+ return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
822
894
  }
823
895
 
824
896
  function setReplSendMode(mode) {
@@ -834,20 +906,46 @@
834
906
  function setReplJournalCollapsed(collapsed) {
835
907
  replJournalCollapsed = Boolean(collapsed);
836
908
  try {
837
- if (window.localStorage) window.localStorage.setItem("piStudio.replJournalCollapsed", replJournalCollapsed ? "true" : "false");
909
+ if (window.localStorage) window.localStorage.setItem("piStudio.replStudioCollapsed", replJournalCollapsed ? "true" : "false");
910
+ } catch {
911
+ // Ignore storage failures.
912
+ }
913
+ renderReplViewIfActive({ force: true });
914
+ }
915
+
916
+ function setReplMirrorCollapsed(collapsed) {
917
+ replMirrorCollapsed = Boolean(collapsed);
918
+ try {
919
+ if (window.localStorage) window.localStorage.setItem("piStudio.rawReplMirrorCollapsed", replMirrorCollapsed ? "true" : "false");
838
920
  } catch {
839
921
  // Ignore storage failures.
840
922
  }
841
923
  renderReplViewIfActive({ force: true });
842
924
  }
843
925
 
926
+ function serializeReplSessionsForCompare(sessions) {
927
+ return JSON.stringify((Array.isArray(sessions) ? sessions : [])
928
+ .map(normalizeReplSession)
929
+ .filter(Boolean)
930
+ .map((session) => ({
931
+ sessionName: session.sessionName,
932
+ label: session.label,
933
+ runtime: session.runtime,
934
+ source: session.source,
935
+ target: session.target,
936
+ })));
937
+ }
938
+
844
939
  function setReplSessions(sessions) {
940
+ const previous = serializeReplSessionsForCompare(replSessions);
941
+ const previousActive = replActiveSessionName;
845
942
  replSessions = Array.isArray(sessions)
846
943
  ? sessions.map(normalizeReplSession).filter(Boolean)
847
944
  : [];
848
945
  if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
849
946
  replActiveSessionName = replSessions[0] ? replSessions[0].sessionName : "";
850
947
  }
948
+ return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
851
949
  }
852
950
 
853
951
  function getActiveReplSession() {
@@ -866,8 +964,8 @@
866
964
  "Session name: " + session.sessionName,
867
965
  "tmux target: " + (session.target || (session.sessionName + ":0.0")),
868
966
  "runtime: " + runtime,
869
- "Suggested shell command for direct interaction: tmux paste-buffer/send-keys targeting " + (session.target || (session.sessionName + ":0.0")),
870
- "Prefer existing REPL tools when they target this same session; otherwise use tmux directly.",
967
+ "Use the studio_repl_send tool for code execution in this REPL. Pass sessionName when targeting this exact session.",
968
+ "Do not improvise raw tmux paste commands for multiline code; Studio handles runtime-specific safe submission.",
871
969
  "[/Studio active REPL]",
872
970
  ].join("\n");
873
971
  }
@@ -1069,7 +1167,7 @@
1069
1167
  };
1070
1168
  }
1071
1169
 
1072
- function buildScratchReplSendPayload() {
1170
+ function buildRawReplSendPayload() {
1073
1171
  const range = getEditorSelectionRange();
1074
1172
  const selected = range.selected;
1075
1173
  const source = selected || range.raw;
@@ -1078,7 +1176,7 @@
1078
1176
  text: prepareEditorTextForSend(unwrapped ? unwrapped.code : source),
1079
1177
  prose: "",
1080
1178
  label: unwrapped ? unwrapped.label : (selected ? "selection" : "full editor"),
1081
- mode: "scratch",
1179
+ mode: "raw",
1082
1180
  noteOnly: false,
1083
1181
  skippedChunks: 0,
1084
1182
  };
@@ -1142,7 +1240,7 @@
1142
1240
 
1143
1241
  const allBlocks = parseMarkdownCodeFences(range.raw);
1144
1242
  if (allBlocks.length) {
1145
- return { error: "Place the cursor inside a code chunk, select text, or use Run all chunks. Switch send mode to Scratch to send the full editor." };
1243
+ 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." };
1146
1244
  }
1147
1245
 
1148
1246
  return {
@@ -1211,9 +1309,42 @@
1211
1309
  function addReplJournalEntry(details) {
1212
1310
  const entry = createReplJournalEntry(details || {});
1213
1311
  replJournalEntries = [...replJournalEntries, entry].slice(-REPL_JOURNAL_MAX_ENTRIES);
1312
+ persistReplJournalEntries();
1214
1313
  return entry;
1215
1314
  }
1216
1315
 
1316
+ function recordReplToolSend(message) {
1317
+ const requestId = typeof message.toolCallId === "string" && message.toolCallId.trim()
1318
+ ? "tool:" + message.toolCallId.trim()
1319
+ : (typeof message.requestId === "string" && message.requestId.trim() ? message.requestId.trim() : "");
1320
+ const code = String(message.code || "");
1321
+ if (!code.trim()) return false;
1322
+ const runtime = normalizeReplRuntime(message.runtime || getActiveReplRuntime());
1323
+ const sessionName = typeof message.sessionName === "string" ? message.sessionName : replActiveSessionName;
1324
+ const output = cleanReplCapturedOutput(String(message.output || ""), { code, runtime });
1325
+ const details = {
1326
+ requestId,
1327
+ sessionName,
1328
+ runtime,
1329
+ label: typeof message.label === "string" && message.label.trim() ? message.label.trim() : "Pi",
1330
+ mode: "agent",
1331
+ code,
1332
+ output,
1333
+ status: output.trim() ? "captured" : (message.timedOut ? "timeout" : "sent"),
1334
+ };
1335
+ activeReplJournalEntryId = "";
1336
+ if (requestId) {
1337
+ const existingIndex = replJournalEntries.findIndex((entry) => entry.requestId === requestId);
1338
+ if (existingIndex >= 0) {
1339
+ replJournalEntries = replJournalEntries.map((entry) => entry.requestId === requestId ? { ...entry, ...details, updatedAt: Date.now() } : entry);
1340
+ persistReplJournalEntries();
1341
+ return true;
1342
+ }
1343
+ }
1344
+ addReplJournalEntry(details);
1345
+ return true;
1346
+ }
1347
+
1217
1348
  function extractReplTranscriptDelta(before, after) {
1218
1349
  const previous = String(before || "");
1219
1350
  const current = String(after || "");
@@ -1232,17 +1363,54 @@
1232
1363
  return current;
1233
1364
  }
1234
1365
 
1366
+ function stripSubmittedCodeEchoFromReplDelta(delta, entry) {
1367
+ const value = String(delta || "").replace(/^\s+/, "");
1368
+ const code = String(entry && entry.code ? entry.code : "").trim();
1369
+ if (!value || !code) return value;
1370
+ const firstCodeLine = code.split("\n").map((line) => line.trim()).find(Boolean) || "";
1371
+ const lines = value.split("\n");
1372
+ if (!lines.length) return value;
1373
+ const promptlessFirst = lines[0].replace(/^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*/, "").trim();
1374
+ const isEcho = promptlessFirst === firstCodeLine
1375
+ || /^# Studio sent \d+-line snippet$/.test(promptlessFirst)
1376
+ || /^-- Studio sent \d+-line snippet$/.test(promptlessFirst)
1377
+ || /^;; Studio sent \d+-line snippet$/.test(promptlessFirst);
1378
+ return isEcho ? lines.slice(1).join("\n").replace(/^\s+/, "") : value;
1379
+ }
1380
+
1381
+ function stripTrailingReplPromptsFromOutput(output) {
1382
+ const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
1383
+ while (lines.length > 0 && /^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*$/.test(lines[lines.length - 1] || "")) {
1384
+ lines.pop();
1385
+ }
1386
+ return lines.join("\n").trimEnd();
1387
+ }
1388
+
1389
+ function stripSubsequentReplInputsFromOutput(output) {
1390
+ const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
1391
+ const nextInputIndex = lines.findIndex((line) => /^\s*(?:>>>|In \[\d+\]:|julia>|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s+\S/.test(line || ""));
1392
+ if (nextInputIndex <= 0) return lines.join("\n").trimEnd();
1393
+ return lines.slice(0, nextInputIndex).join("\n").trimEnd();
1394
+ }
1395
+
1396
+ function cleanReplCapturedOutput(delta, entry) {
1397
+ return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(stripSubsequentReplInputsFromOutput(stripSubmittedCodeEchoFromReplDelta(delta, entry))));
1398
+ }
1399
+
1235
1400
  function updateActiveReplJournalEntryFromTranscript(sessionName, transcript) {
1236
- if (!activeReplJournalEntryId) return;
1401
+ if (!activeReplJournalEntryId) return false;
1237
1402
  const entryIndex = replJournalEntries.findIndex((entry) => entry.id === activeReplJournalEntryId);
1238
- if (entryIndex < 0) return;
1403
+ if (entryIndex < 0) return false;
1239
1404
  const entry = replJournalEntries[entryIndex];
1240
- if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return;
1241
- const delta = trimReplJournalOutput(extractReplTranscriptDelta(entry.beforeTranscript, transcript));
1242
- if (!delta.trim()) return;
1405
+ if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return false;
1406
+ const delta = cleanReplCapturedOutput(extractReplTranscriptDelta(entry.beforeTranscript, transcript), entry);
1407
+ if (!delta.trim()) return false;
1408
+ if (entry.output === delta && entry.status === "captured") return false;
1243
1409
  replJournalEntries = replJournalEntries.map((candidate) => candidate.id === entry.id
1244
1410
  ? { ...candidate, output: delta, status: "captured", updatedAt: Date.now() }
1245
1411
  : candidate);
1412
+ persistReplJournalEntries();
1413
+ return true;
1246
1414
  }
1247
1415
 
1248
1416
  function getMarkdownFenceForText(text, language) {
@@ -1253,9 +1421,9 @@
1253
1421
  }
1254
1422
 
1255
1423
  function buildReplJournalMarkdown() {
1256
- const lines = ["# Studio REPL journal", "", "Generated: " + new Date().toLocaleString(), ""];
1424
+ const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString(), ""];
1257
1425
  if (!replJournalEntries.length) {
1258
- lines.push("_No journal entries yet._");
1426
+ lines.push("_No REPL Studio entries yet._");
1259
1427
  return lines.join("\n");
1260
1428
  }
1261
1429
  replJournalEntries.forEach((entry, index) => {
@@ -1286,11 +1454,11 @@
1286
1454
 
1287
1455
  async function copyReplJournalToClipboard() {
1288
1456
  if (!replJournalEntries.length) {
1289
- setStatus("No REPL journal entries to copy yet.", "warning");
1457
+ setStatus("No REPL Studio entries to copy yet.", "warning");
1290
1458
  return;
1291
1459
  }
1292
1460
  if (await writeTextToClipboard(buildReplJournalMarkdown())) {
1293
- setStatus("Copied REPL journal as Markdown.", "success");
1461
+ setStatus("Copied REPL Studio as Markdown.", "success");
1294
1462
  } else {
1295
1463
  setStatus("Clipboard write failed.", "warning");
1296
1464
  }
@@ -1298,7 +1466,7 @@
1298
1466
 
1299
1467
  function exportReplJournalMarkdown() {
1300
1468
  if (!replJournalEntries.length) {
1301
- setStatus("No REPL journal entries to export yet.", "warning");
1469
+ setStatus("No REPL Studio entries to export yet.", "warning");
1302
1470
  return;
1303
1471
  }
1304
1472
  const blob = new Blob([buildReplJournalMarkdown()], { type: "text/markdown;charset=utf-8" });
@@ -1306,37 +1474,38 @@
1306
1474
  const link = document.createElement("a");
1307
1475
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1308
1476
  link.href = blobUrl;
1309
- link.download = "studio-repl-journal-" + stamp + ".md";
1477
+ link.download = "repl-studio-" + stamp + ".md";
1310
1478
  document.body.appendChild(link);
1311
1479
  link.click();
1312
1480
  link.remove();
1313
1481
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1314
- setStatus("Exported REPL journal Markdown.", "success");
1482
+ setStatus("Exported REPL Studio Markdown.", "success");
1315
1483
  }
1316
1484
 
1317
1485
  function clearReplJournal() {
1318
1486
  replJournalEntries = [];
1319
1487
  activeReplJournalEntryId = "";
1320
- setStatus("Cleared REPL journal.", "success");
1488
+ persistReplJournalEntries();
1489
+ setStatus("Cleared REPL Studio.", "success");
1321
1490
  renderReplViewIfActive({ force: true });
1322
1491
  }
1323
1492
 
1324
1493
  function loadReplJournalIntoEditor() {
1325
1494
  if (!replJournalEntries.length) {
1326
- setStatus("No REPL journal entries to load yet.", "warning");
1495
+ setStatus("No REPL Studio entries to load yet.", "warning");
1327
1496
  return;
1328
1497
  }
1329
1498
  const markdown = buildReplJournalMarkdown();
1330
1499
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1331
- setSourceState({ source: "blank", label: "REPL journal", path: null });
1500
+ setSourceState({ source: "blank", label: "REPL Studio", path: null });
1332
1501
  setEditorLanguage("markdown");
1333
- setStatus("Loaded REPL journal into editor.", "success");
1502
+ setStatus("Loaded REPL Studio into editor.", "success");
1334
1503
  }
1335
1504
 
1336
1505
  function addSelectedReplJournalNote() {
1337
1506
  const note = getSelectedOrCurrentParagraphForReplNote();
1338
1507
  if (!note.trim()) {
1339
- setStatus("Select prose or place the cursor in a paragraph to journal a note.", "warning");
1508
+ setStatus("Select prose or place the cursor in a paragraph to add a REPL Studio note.", "warning");
1340
1509
  return;
1341
1510
  }
1342
1511
  addReplJournalEntry({
@@ -1347,7 +1516,7 @@
1347
1516
  sessionName: replActiveSessionName,
1348
1517
  runtime: getActiveReplRuntime(),
1349
1518
  });
1350
- setStatus("Added note to REPL journal.", "success");
1519
+ setStatus("Added note to REPL Studio.", "success");
1351
1520
  renderReplViewIfActive({ force: true });
1352
1521
  }
1353
1522
 
@@ -1372,7 +1541,7 @@
1372
1541
  runtime: getActiveReplRuntime(),
1373
1542
  skippedChunks: payload.skippedChunks,
1374
1543
  });
1375
- setStatus("Added prose to REPL journal.", "success");
1544
+ setStatus("Added prose to REPL Studio.", "success");
1376
1545
  renderReplViewIfActive({ force: true });
1377
1546
  } else {
1378
1547
  setStatus("No code or prose found to send.", "warning");
@@ -1406,6 +1575,7 @@
1406
1575
  if (!sendMessage({ type: "repl_send_request", requestId, sessionName: session.sessionName, text })) {
1407
1576
  replBusy = false;
1408
1577
  replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
1578
+ persistReplJournalEntries();
1409
1579
  syncActionButtons();
1410
1580
  }
1411
1581
  }
@@ -1420,7 +1590,7 @@
1420
1590
  addSelectedReplJournalNote();
1421
1591
  return;
1422
1592
  }
1423
- sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildScratchReplSendPayload());
1593
+ sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildRawReplSendPayload());
1424
1594
  }
1425
1595
 
1426
1596
  function renderTraceViewIfActive() {
@@ -1510,6 +1680,7 @@
1510
1680
  let lineNumbersRenderRaf = null;
1511
1681
  let annotationsEnabled = true;
1512
1682
  const STUDIO_UI_REFRESH_STORAGE_KEY = "piStudio.uiRefresh";
1683
+ const STUDIO_ZEN_MODE_STORAGE_KEY = "piStudio.zenMode";
1513
1684
  const studioUiRefreshEnabled = readStudioUiRefreshEnabled();
1514
1685
  const EDITOR_FONT_SIZE_OPTIONS = [10, 11, 12, 13, 14, 15, 16, 18];
1515
1686
  const RESPONSE_FONT_SIZE_OPTIONS = [11, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 18, 20];
@@ -1518,9 +1689,13 @@
1518
1689
  let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
1519
1690
  let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
1520
1691
  let studioUiRefreshUi = null;
1692
+ let studioZenModeEnabled = readStudioZenModeEnabled();
1521
1693
  if (studioUiRefreshEnabled && document.body) {
1522
1694
  document.body.classList.add("studio-ui-refresh");
1523
1695
  }
1696
+ if (studioZenModeEnabled && document.body) {
1697
+ document.body.classList.add("studio-zen-mode");
1698
+ }
1524
1699
  let scratchpadText = "";
1525
1700
  let scratchpadReturnFocusEl = null;
1526
1701
  let scratchpadPersistTimer = null;
@@ -1562,6 +1737,46 @@
1562
1737
  return true;
1563
1738
  }
1564
1739
 
1740
+ function readStudioZenModeEnabled() {
1741
+ const normalize = (value) => String(value == null ? "" : value).trim().toLowerCase();
1742
+ const isTruthy = (value) => ["1", "true", "yes", "on", "zen"].indexOf(normalize(value)) !== -1;
1743
+ const isFalsey = (value) => ["0", "false", "no", "off"].indexOf(normalize(value)) !== -1;
1744
+ const queryValue = initialQueryParams.has("zen") ? initialQueryParams.get("zen") : null;
1745
+ if (queryValue !== null) {
1746
+ const normalizedQuery = normalize(queryValue);
1747
+ const enabled = isTruthy(queryValue) || (!isFalsey(queryValue) && normalizedQuery !== "");
1748
+ try {
1749
+ window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, enabled ? "1" : "0");
1750
+ } catch {}
1751
+ return enabled;
1752
+ }
1753
+ try {
1754
+ const stored = window.localStorage ? window.localStorage.getItem(STUDIO_ZEN_MODE_STORAGE_KEY) : null;
1755
+ if (stored === null) return false;
1756
+ return isTruthy(stored) || (!isFalsey(stored) && normalize(stored) !== "");
1757
+ } catch {
1758
+ return false;
1759
+ }
1760
+ }
1761
+
1762
+ function syncStudioZenModeUi() {
1763
+ if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
1764
+ if (!zenModeBtn) return;
1765
+ zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "⊙ Zen";
1766
+ zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls." : "Hide secondary Studio controls.";
1767
+ zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
1768
+ }
1769
+
1770
+ function setStudioZenMode(enabled) {
1771
+ studioZenModeEnabled = Boolean(enabled);
1772
+ try {
1773
+ window.localStorage && window.localStorage.setItem(STUDIO_ZEN_MODE_STORAGE_KEY, studioZenModeEnabled ? "1" : "0");
1774
+ } catch {}
1775
+ closeStudioUiRefreshMenus();
1776
+ closeExportPreviewMenu();
1777
+ syncStudioZenModeUi();
1778
+ }
1779
+
1565
1780
  function makeStudioUiRefreshElement(tagName, className, text) {
1566
1781
  const element = document.createElement(tagName);
1567
1782
  if (className) element.className = className;
@@ -1578,9 +1793,16 @@
1578
1793
  svg.setAttribute("viewBox", "0 0 24 24");
1579
1794
  svg.setAttribute("aria-hidden", "true");
1580
1795
  svg.classList.add("studio-refresh-icon");
1581
- const paths = kind === "focus-exit"
1582
- ? ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"]
1583
- : ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
1796
+ let paths;
1797
+ if (kind === "focus-exit") {
1798
+ paths = ["M4 4l6 6", "M10 4v6H4", "M20 20l-6-6", "M14 20v-6h6"];
1799
+ } else if (kind === "fullscreen") {
1800
+ paths = ["M8 4H4v4", "M16 4h4v4", "M20 16v4h-4", "M4 16v4h4"];
1801
+ } else if (kind === "fullscreen-exit") {
1802
+ paths = ["M9 5v4H5", "M15 5v4h4", "M19 15h-4v4", "M5 15h4v4"];
1803
+ } else {
1804
+ paths = ["M14 4h6v6", "M20 4l-6 6", "M10 20H4v-6", "M4 20l6-6"];
1805
+ }
1584
1806
  for (const d of paths) {
1585
1807
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1586
1808
  path.setAttribute("d", d);
@@ -1941,6 +2163,7 @@
1941
2163
 
1942
2164
  setupStudioUiRefreshToggleButton();
1943
2165
  setupStudioUiRefreshPrototype();
2166
+ syncStudioZenModeUi();
1944
2167
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
1945
2168
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
1946
2169
  throw new Error("Studio annotation helpers failed to load.");
@@ -2822,6 +3045,18 @@
2822
3045
  && typeof outlineDialogEl.contains === "function"
2823
3046
  && outlineDialogEl.contains(event.target)
2824
3047
  );
3048
+ const pdfFocusOwnsEvent = Boolean(
3049
+ studioPdfFocusDialogEl
3050
+ && event.target
3051
+ && typeof studioPdfFocusDialogEl.contains === "function"
3052
+ && studioPdfFocusDialogEl.contains(event.target)
3053
+ );
3054
+
3055
+ if (isStudioPdfFocusOpen() && plainEscape) {
3056
+ event.preventDefault();
3057
+ closeStudioPdfFocusViewer();
3058
+ return;
3059
+ }
2825
3060
 
2826
3061
  if (isScratchpadOpen() && plainEscape) {
2827
3062
  event.preventDefault();
@@ -2841,7 +3076,7 @@
2841
3076
  return;
2842
3077
  }
2843
3078
 
2844
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
3079
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent) {
2845
3080
  return;
2846
3081
  }
2847
3082
 
@@ -3180,6 +3415,12 @@
3180
3415
 
3181
3416
  function updateReferenceBadge() {
3182
3417
  if (!referenceBadgeEl) return;
3418
+ const referenceMetaEl = referenceBadgeEl.closest(".reference-meta");
3419
+ if (rightView === "repl") {
3420
+ if (referenceMetaEl instanceof HTMLElement) referenceMetaEl.hidden = true;
3421
+ return;
3422
+ }
3423
+ if (referenceMetaEl instanceof HTMLElement) referenceMetaEl.hidden = false;
3183
3424
 
3184
3425
  if (rightView === "trace") {
3185
3426
  const state = traceState || createEmptyTraceState();
@@ -3209,18 +3450,6 @@
3209
3450
  return;
3210
3451
  }
3211
3452
 
3212
- if (rightView === "repl") {
3213
- const session = getActiveReplSession();
3214
- if (replTmuxAvailable === false) {
3215
- referenceBadgeEl.textContent = "REPL: tmux unavailable";
3216
- return;
3217
- }
3218
- referenceBadgeEl.textContent = session
3219
- ? ("REPL: " + session.label + (replCapturedAt ? (" · updated " + formatReferenceTime(replCapturedAt)) : ""))
3220
- : "REPL: no session selected";
3221
- return;
3222
- }
3223
-
3224
3453
  if (rightView === "editor-preview") {
3225
3454
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
3226
3455
  if (hasResponse) {
@@ -3696,50 +3925,316 @@
3696
3925
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
3697
3926
  }
3698
3927
 
3699
- function buildStudioPdfResourceUrl(options) {
3928
+ function isStudioPdfFocusOpen() {
3929
+ return Boolean(studioPdfFocusOverlayEl && studioPdfFocusOverlayEl.hidden === false);
3930
+ }
3931
+
3932
+ function ensureStudioPdfFocusViewer() {
3933
+ if (studioPdfFocusOverlayEl) return studioPdfFocusOverlayEl;
3934
+
3935
+ const overlay = document.createElement("div");
3936
+ overlay.className = "studio-pdf-focus-overlay";
3937
+ overlay.hidden = true;
3938
+ overlay.setAttribute("role", "dialog");
3939
+ overlay.setAttribute("aria-modal", "true");
3940
+ overlay.setAttribute("aria-labelledby", "studioPdfFocusTitle");
3941
+
3942
+ const dialog = document.createElement("div");
3943
+ dialog.className = "studio-pdf-focus-dialog";
3944
+
3945
+ const header = document.createElement("div");
3946
+ header.className = "studio-pdf-focus-header";
3947
+
3948
+ const titleGroup = document.createElement("div");
3949
+ titleGroup.className = "studio-pdf-focus-title-group";
3950
+
3951
+ const closeBtn = document.createElement("button");
3952
+ closeBtn.type = "button";
3953
+ closeBtn.className = "studio-pdf-focus-btn studio-pdf-focus-close";
3954
+ closeBtn.title = "Exit PDF focus view.";
3955
+ closeBtn.setAttribute("aria-label", "Exit PDF focus view");
3956
+ closeBtn.appendChild(makeStudioUiRefreshIcon("focus-exit"));
3957
+ closeBtn.addEventListener("click", () => closeStudioPdfFocusViewer());
3958
+ titleGroup.appendChild(closeBtn);
3959
+
3960
+ const titleEl = document.createElement("div");
3961
+ titleEl.id = "studioPdfFocusTitle";
3962
+ titleEl.className = "studio-pdf-focus-title";
3963
+ titleEl.textContent = "PDF preview";
3964
+ titleGroup.appendChild(titleEl);
3965
+ header.appendChild(titleGroup);
3966
+
3967
+ const actions = document.createElement("div");
3968
+ actions.className = "studio-pdf-focus-actions";
3969
+
3970
+ const openLink = document.createElement("a");
3971
+ openLink.className = "studio-pdf-focus-link";
3972
+ openLink.target = "_blank";
3973
+ openLink.rel = "noopener noreferrer";
3974
+ openLink.textContent = "Open PDF";
3975
+ actions.appendChild(openLink);
3976
+
3977
+ const fullscreenBtn = document.createElement("button");
3978
+ fullscreenBtn.type = "button";
3979
+ fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
3980
+ fullscreenBtn.addEventListener("click", async () => {
3981
+ const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
3982
+ if (isFullscreen) {
3983
+ try {
3984
+ if (typeof document.exitFullscreen === "function") await document.exitFullscreen();
3985
+ } catch (error) {
3986
+ setStatus("Could not exit PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3987
+ } finally {
3988
+ syncStudioPdfFocusFullscreenButton();
3989
+ }
3990
+ return;
3991
+ }
3992
+ if (!studioPdfFocusDialogEl || typeof studioPdfFocusDialogEl.requestFullscreen !== "function") {
3993
+ setStatus("Browser fullscreen is not available for this PDF viewer.", "warning");
3994
+ return;
3995
+ }
3996
+ try {
3997
+ await studioPdfFocusDialogEl.requestFullscreen();
3998
+ } catch (error) {
3999
+ setStatus("Could not enter PDF fullscreen: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
4000
+ } finally {
4001
+ syncStudioPdfFocusFullscreenButton();
4002
+ }
4003
+ });
4004
+ actions.appendChild(fullscreenBtn);
4005
+
4006
+ header.appendChild(actions);
4007
+ dialog.appendChild(header);
4008
+
4009
+ const frameSlot = document.createElement("div");
4010
+ frameSlot.className = "studio-pdf-focus-frame-slot";
4011
+ const frame = document.createElement("iframe");
4012
+ frame.className = "studio-pdf-focus-frame";
4013
+ frame.title = "PDF focus viewer";
4014
+ frame.loading = "eager";
4015
+ frameSlot.appendChild(frame);
4016
+ dialog.appendChild(frameSlot);
4017
+
4018
+ overlay.appendChild(dialog);
4019
+ overlay.addEventListener("click", (event) => {
4020
+ if (event.target === overlay) closeStudioPdfFocusViewer();
4021
+ });
4022
+ document.addEventListener("fullscreenchange", syncStudioPdfFocusFullscreenButton);
4023
+
4024
+ document.body.appendChild(overlay);
4025
+ studioPdfFocusOverlayEl = overlay;
4026
+ studioPdfFocusDialogEl = dialog;
4027
+ studioPdfFocusFrameSlotEl = frameSlot;
4028
+ studioPdfFocusFrameEl = frame;
4029
+ studioPdfFocusTitleEl = titleEl;
4030
+ studioPdfFocusOpenLinkEl = openLink;
4031
+ studioPdfFocusFullscreenBtn = fullscreenBtn;
4032
+ studioPdfFocusCloseBtn = closeBtn;
4033
+ syncStudioPdfFocusFullscreenButton();
4034
+ return overlay;
4035
+ }
4036
+
4037
+ function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame) {
4038
+ const src = String(viewerUrl || "").trim();
4039
+ if (!src) return;
4040
+ ensureStudioPdfFocusViewer();
4041
+ studioPdfFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
4042
+ if (studioPdfFocusTitleEl) studioPdfFocusTitleEl.textContent = String(title || "PDF preview").trim() || "PDF preview";
4043
+ if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = src;
4044
+ setStudioPdfFocusFrameSource(src, title, sourceFrame);
4045
+ if (document.body) document.body.classList.add("studio-pdf-focus-open");
4046
+ if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = false;
4047
+ syncStudioPdfFocusFullscreenButton();
4048
+ closeStudioUiRefreshMenus();
4049
+ closeExportPreviewMenu();
4050
+ window.setTimeout(() => {
4051
+ if (studioPdfFocusCloseBtn && typeof studioPdfFocusCloseBtn.focus === "function") {
4052
+ studioPdfFocusCloseBtn.focus();
4053
+ }
4054
+ }, 0);
4055
+ }
4056
+
4057
+ function closeStudioPdfFocusViewer() {
4058
+ if (!isStudioPdfFocusOpen()) return false;
4059
+ if (document.fullscreenElement && studioPdfFocusDialogEl && studioPdfFocusDialogEl.contains(document.fullscreenElement)) {
4060
+ try {
4061
+ const exitResult = document.exitFullscreen && document.exitFullscreen();
4062
+ if (exitResult && typeof exitResult.catch === "function") exitResult.catch(() => {});
4063
+ } catch {}
4064
+ }
4065
+ if (studioPdfFocusOverlayEl) studioPdfFocusOverlayEl.hidden = true;
4066
+ restoreStudioPdfFocusMovedFrame();
4067
+ if (studioPdfFocusFrameEl) studioPdfFocusFrameEl.src = "about:blank";
4068
+ if (document.body) document.body.classList.remove("studio-pdf-focus-open");
4069
+ syncStudioPdfFocusFullscreenButton();
4070
+ const focusTarget = studioPdfFocusLastFocusedEl;
4071
+ studioPdfFocusLastFocusedEl = null;
4072
+ if (focusTarget && typeof focusTarget.focus === "function" && document.contains(focusTarget)) {
4073
+ window.setTimeout(() => focusTarget.focus(), 0);
4074
+ }
4075
+ return true;
4076
+ }
4077
+
4078
+ function buildStudioPdfResourceUrl(options, useEditorResourceContext) {
3700
4079
  const token = getToken();
3701
4080
  if (!token) return "";
3702
4081
  const pdfPath = String(options && options.path ? options.path : "").trim();
3703
4082
  if (!pdfPath) return "";
3704
4083
  const effectivePath = getEffectiveSavePath();
3705
- const sourcePath = effectivePath || sourceState.path || "";
4084
+ const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
4085
+ const resourceDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
3706
4086
  const params = new URLSearchParams({ token, path: pdfPath });
3707
4087
  if (sourcePath) {
3708
4088
  params.set("sourcePath", sourcePath);
3709
- } else if (resourceDirInput && resourceDirInput.value.trim()) {
3710
- params.set("resourceDir", resourceDirInput.value.trim());
4089
+ } else if (resourceDir) {
4090
+ params.set("resourceDir", resourceDir);
3711
4091
  }
3712
4092
  return "/pdf-resource?" + params.toString();
3713
4093
  }
3714
4094
 
3715
- function createStudioPdfCard(block) {
4095
+ function syncStudioPdfFocusFullscreenButton() {
4096
+ if (!studioPdfFocusFullscreenBtn) return;
4097
+ const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
4098
+ studioPdfFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
4099
+ const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
4100
+ studioPdfFocusFullscreenBtn.title = isFullscreen
4101
+ ? "Exit browser fullscreen and keep the PDF focus viewer open."
4102
+ : "Ask the browser to make this PDF viewer fullscreen.";
4103
+ studioPdfFocusFullscreenBtn.setAttribute("aria-label", label);
4104
+ studioPdfFocusFullscreenBtn.setAttribute("aria-pressed", isFullscreen ? "true" : "false");
4105
+ }
4106
+
4107
+ function restoreStudioPdfFocusMovedFrame() {
4108
+ const state = studioPdfFocusMovedFrameState;
4109
+ studioPdfFocusMovedFrameState = null;
4110
+ if (!state || !state.frame) return;
4111
+ const frame = state.frame;
4112
+ frame.className = state.className;
4113
+ frame.style.cssText = state.styleCssText;
4114
+ if (state.title !== null) frame.setAttribute("title", state.title);
4115
+ else frame.removeAttribute("title");
4116
+ if (state.placeholder && state.placeholder.parentNode) {
4117
+ state.placeholder.parentNode.insertBefore(frame, state.placeholder);
4118
+ state.placeholder.remove();
4119
+ } else if (state.parent && state.parent.isConnected) {
4120
+ state.parent.insertBefore(frame, state.nextSibling && state.nextSibling.parentNode === state.parent ? state.nextSibling : null);
4121
+ }
4122
+ }
4123
+
4124
+ function setStudioPdfFocusFrameSource(src, title, sourceFrame) {
4125
+ if (!studioPdfFocusFrameSlotEl || !studioPdfFocusFrameEl) return;
4126
+ restoreStudioPdfFocusMovedFrame();
4127
+ const sourceIframe = sourceFrame instanceof HTMLIFrameElement ? sourceFrame : null;
4128
+ if (sourceIframe && sourceIframe.isConnected) {
4129
+ const placeholder = document.createElement("span");
4130
+ placeholder.hidden = true;
4131
+ const parent = sourceIframe.parentNode;
4132
+ parent && parent.insertBefore(placeholder, sourceIframe);
4133
+ studioPdfFocusMovedFrameState = {
4134
+ frame: sourceIframe,
4135
+ parent,
4136
+ nextSibling: placeholder.nextSibling,
4137
+ placeholder,
4138
+ className: sourceIframe.className,
4139
+ styleCssText: sourceIframe.style.cssText,
4140
+ title: sourceIframe.getAttribute("title"),
4141
+ };
4142
+ if (studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameEl.parentNode.removeChild(studioPdfFocusFrameEl);
4143
+ sourceIframe.classList.add("studio-pdf-focus-frame");
4144
+ sourceIframe.style.height = "auto";
4145
+ sourceIframe.style.flex = "1 1 auto";
4146
+ sourceIframe.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
4147
+ studioPdfFocusFrameSlotEl.appendChild(sourceIframe);
4148
+ return;
4149
+ }
4150
+ if (!studioPdfFocusFrameEl.parentNode) studioPdfFocusFrameSlotEl.appendChild(studioPdfFocusFrameEl);
4151
+ studioPdfFocusFrameEl.src = src;
4152
+ studioPdfFocusFrameEl.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
4153
+ }
4154
+
4155
+ function openStudioPdfFocusFromButton(buttonEl) {
4156
+ if (!buttonEl) return false;
4157
+ const card = buttonEl.closest && buttonEl.closest(".studio-pdf-card");
4158
+ const viewerUrl = String(buttonEl.dataset && buttonEl.dataset.studioPdfViewerUrl ? buttonEl.dataset.studioPdfViewerUrl : "").trim()
4159
+ || String(card && card.dataset ? (card.dataset.studioPdfViewerUrl || "") : "").trim();
4160
+ const title = String(buttonEl.dataset && buttonEl.dataset.studioPdfTitle ? buttonEl.dataset.studioPdfTitle : "").trim()
4161
+ || String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
4162
+ || "PDF preview";
4163
+ const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
4164
+ if (!viewerUrl) return false;
4165
+ openStudioPdfFocusViewer(viewerUrl, title, sourceFrame);
4166
+ return true;
4167
+ }
4168
+
4169
+ function handleStudioPdfFocusButtonClick(event) {
4170
+ const target = event && event.target;
4171
+ const buttonEl = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
4172
+ if (!buttonEl) return;
4173
+ event.preventDefault();
4174
+ event.stopPropagation();
4175
+ if (typeof event.stopImmediatePropagation === "function") {
4176
+ event.stopImmediatePropagation();
4177
+ }
4178
+ if (!openStudioPdfFocusFromButton(buttonEl)) {
4179
+ setStatus("Could not open PDF focus view for this card.", "warning");
4180
+ }
4181
+ }
4182
+
4183
+ function createStudioPdfCard(block, useEditorResourceContext) {
3716
4184
  const options = block && block.options ? block.options : {};
3717
4185
  const path = String(options.path || "").trim();
3718
4186
  const title = String(options.title || path || "Embedded PDF").trim();
3719
4187
  const caption = String(options.caption || "").trim();
3720
4188
  const height = normalizeStudioPdfHeight(options.height);
3721
4189
  const page = normalizeStudioPdfPage(options.page);
3722
- const resourceUrl = buildStudioPdfResourceUrl(options);
4190
+ const resourceUrl = buildStudioPdfResourceUrl(options, useEditorResourceContext);
3723
4191
  const viewerUrl = resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
3724
4192
 
3725
4193
  const card = document.createElement("figure");
3726
4194
  card.className = "studio-pdf-card";
4195
+ if (card.dataset) {
4196
+ card.dataset.studioPdfViewerUrl = viewerUrl || "";
4197
+ card.dataset.studioPdfTitle = title;
4198
+ }
3727
4199
 
3728
4200
  const header = document.createElement("figcaption");
3729
4201
  header.className = "studio-pdf-card-header";
4202
+
4203
+ const titleGroup = document.createElement("div");
4204
+ titleGroup.className = "studio-pdf-card-title-group";
4205
+ if (resourceUrl) {
4206
+ const focusBtn = document.createElement("button");
4207
+ focusBtn.type = "button";
4208
+ focusBtn.className = "studio-pdf-card-action studio-pdf-card-focus";
4209
+ focusBtn.title = "Open this PDF in a larger Studio overlay.";
4210
+ focusBtn.setAttribute("aria-label", "Focus PDF");
4211
+ if (focusBtn.dataset) {
4212
+ focusBtn.dataset.studioPdfViewerUrl = viewerUrl;
4213
+ focusBtn.dataset.studioPdfTitle = title;
4214
+ }
4215
+ focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
4216
+ focusBtn.addEventListener("click", handleStudioPdfFocusButtonClick);
4217
+ titleGroup.appendChild(focusBtn);
4218
+ }
3730
4219
  const label = document.createElement("div");
3731
4220
  label.className = "studio-pdf-card-title";
3732
4221
  label.textContent = title;
3733
- header.appendChild(label);
4222
+ titleGroup.appendChild(label);
4223
+ header.appendChild(titleGroup);
3734
4224
 
3735
4225
  if (resourceUrl) {
4226
+ const actions = document.createElement("div");
4227
+ actions.className = "studio-pdf-card-actions";
4228
+
3736
4229
  const openLink = document.createElement("a");
3737
- openLink.className = "studio-pdf-card-link";
4230
+ openLink.className = "studio-pdf-card-link studio-pdf-card-action";
3738
4231
  openLink.href = viewerUrl;
3739
4232
  openLink.target = "_blank";
3740
4233
  openLink.rel = "noopener noreferrer";
3741
4234
  openLink.textContent = "Open PDF";
3742
- header.appendChild(openLink);
4235
+ actions.appendChild(openLink);
4236
+
4237
+ header.appendChild(actions);
3743
4238
  }
3744
4239
  card.appendChild(header);
3745
4240
 
@@ -3768,7 +4263,7 @@
3768
4263
  return card;
3769
4264
  }
3770
4265
 
3771
- function renderStudioPdfBlocksInElement(targetEl, blocks) {
4266
+ function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
3772
4267
  if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
3773
4268
  const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
3774
4269
  blocks.forEach((block) => {
@@ -3776,7 +4271,7 @@
3776
4271
  if (!placeholder) return;
3777
4272
  const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
3778
4273
  if (match && match.parentNode) {
3779
- match.replaceWith(createStudioPdfCard(block));
4274
+ match.replaceWith(createStudioPdfCard(block, useEditorResourceContext));
3780
4275
  }
3781
4276
  });
3782
4277
  }
@@ -4526,6 +5021,10 @@
4526
5021
  setReplJournalCollapsed(!replJournalCollapsed);
4527
5022
  return;
4528
5023
  }
5024
+ if (action === "mirror-toggle") {
5025
+ setReplMirrorCollapsed(!replMirrorCollapsed);
5026
+ return;
5027
+ }
4529
5028
  if (action === "load-journal") {
4530
5029
  loadReplJournalIntoEditor();
4531
5030
  return;
@@ -4797,6 +5296,22 @@
4797
5296
  return "";
4798
5297
  }
4799
5298
 
5299
+ async function fetchWithTimeout(url, options, timeoutMs, timeoutLabel) {
5300
+ if (typeof AbortController === "undefined") return fetch(url, options);
5301
+ const controller = new AbortController();
5302
+ const timer = window.setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || PDF_EXPORT_FETCH_TIMEOUT_MS));
5303
+ try {
5304
+ return await fetch(url, { ...(options || {}), signal: controller.signal });
5305
+ } catch (error) {
5306
+ if (error && error.name === "AbortError") {
5307
+ throw new Error((timeoutLabel || "Request") + " timed out. Try a smaller export or check the PDF toolchain.");
5308
+ }
5309
+ throw error;
5310
+ } finally {
5311
+ window.clearTimeout(timer);
5312
+ }
5313
+ }
5314
+
4800
5315
  async function exportRightPanePdf() {
4801
5316
  if (uiBusy || previewExportInProgress) {
4802
5317
  setStatus("Studio is busy.", "warning");
@@ -4812,11 +5327,11 @@
4812
5327
  const exportingReplJournal = rightView === "repl";
4813
5328
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4814
5329
  if (!rightPaneShowsPreview && !exportingReplJournal) {
4815
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export PDF.", "warning");
5330
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
4816
5331
  return;
4817
5332
  }
4818
5333
  if (exportingReplJournal && !replJournalEntries.length) {
4819
- setStatus("No REPL journal entries to export yet.", "warning");
5334
+ setStatus("No REPL Studio entries to export yet.", "warning");
4820
5335
  return;
4821
5336
  }
4822
5337
 
@@ -4844,7 +5359,7 @@
4844
5359
  const isLatex = isEditorPreview
4845
5360
  ? editorPdfLanguage === "latex"
4846
5361
  : /\\documentclass\b|\\begin\{document\}/.test(markdown);
4847
- let filenameHint = exportingReplJournal ? "studio-repl-journal.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
5362
+ let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
4848
5363
  if (sourcePath) {
4849
5364
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
4850
5365
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -4856,7 +5371,7 @@
4856
5371
  setStatus("Exporting PDF…", "warning");
4857
5372
 
4858
5373
  try {
4859
- const response = await fetch("/export-pdf?token=" + encodeURIComponent(token), {
5374
+ const response = await fetchWithTimeout("/export-pdf?token=" + encodeURIComponent(token), {
4860
5375
  method: "POST",
4861
5376
  headers: {
4862
5377
  "Content-Type": "application/json",
@@ -4869,7 +5384,7 @@
4869
5384
  editorPdfLanguage: editorPdfLanguage,
4870
5385
  filenameHint: filenameHint,
4871
5386
  }),
4872
- });
5387
+ }, PDF_EXPORT_FETCH_TIMEOUT_MS, "PDF export");
4873
5388
 
4874
5389
  const contentType = String(response.headers.get("content-type") || "").toLowerCase();
4875
5390
  if (!response.ok) {
@@ -4984,11 +5499,11 @@
4984
5499
  const exportingReplJournal = rightView === "repl";
4985
5500
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4986
5501
  if (!rightPaneShowsPreview && !exportingReplJournal) {
4987
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export HTML.", "warning");
5502
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
4988
5503
  return;
4989
5504
  }
4990
5505
  if (exportingReplJournal && !replJournalEntries.length) {
4991
- setStatus("No REPL journal entries to export yet.", "warning");
5506
+ setStatus("No REPL Studio entries to export yet.", "warning");
4992
5507
  return;
4993
5508
  }
4994
5509
 
@@ -5009,8 +5524,8 @@
5009
5524
  const isLatex = htmlArtifactSource ? false : (isEditorPreview
5010
5525
  ? editorHtmlLanguage === "latex"
5011
5526
  : /\\documentclass\b|\\begin\{document\}/.test(markdown));
5012
- let filenameHint = exportingReplJournal ? "studio-repl-journal.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
5013
- let titleHint = exportingReplJournal ? "Studio REPL journal" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
5527
+ let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
5528
+ let titleHint = exportingReplJournal ? "REPL Studio" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
5014
5529
  if (sourcePath) {
5015
5530
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
5016
5531
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -5023,7 +5538,7 @@
5023
5538
  setStatus("Exporting HTML…", "warning");
5024
5539
 
5025
5540
  try {
5026
- const response = await fetch("/export-html?token=" + encodeURIComponent(token), {
5541
+ const response = await fetchWithTimeout("/export-html?token=" + encodeURIComponent(token), {
5027
5542
  method: "POST",
5028
5543
  headers: {
5029
5544
  "Content-Type": "application/json",
@@ -5037,7 +5552,7 @@
5037
5552
  filenameHint: filenameHint,
5038
5553
  title: titleHint,
5039
5554
  }),
5040
- });
5555
+ }, HTML_EXPORT_FETCH_TIMEOUT_MS, "HTML export");
5041
5556
 
5042
5557
  const contentType = String(response.headers.get("content-type") || "").toLowerCase();
5043
5558
  if (!response.ok) {
@@ -5366,7 +5881,7 @@
5366
5881
  clearPreviewJumpHighlight(targetEl);
5367
5882
  finishPreviewRender(targetEl);
5368
5883
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
5369
- renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks);
5884
+ renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
5370
5885
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
5371
5886
  await renderAnnotationMathInElement(targetEl);
5372
5887
  decoratePdfEmbeds(targetEl);
@@ -5491,6 +6006,16 @@
5491
6006
  return remaining < 56;
5492
6007
  }
5493
6008
 
6009
+ function isReplJournalExpanded() {
6010
+ return rightView === "repl" && !replJournalCollapsed && replJournalEntries.length > 0;
6011
+ }
6012
+
6013
+ function shouldAutoStickReplView() {
6014
+ if (!critiqueViewEl) return true;
6015
+ if (isReplJournalExpanded()) return shouldStickTraceToBottom();
6016
+ return replFollow || shouldStickTraceToBottom();
6017
+ }
6018
+
5494
6019
  function formatTraceOutputSize(text) {
5495
6020
  const value = String(text || "");
5496
6021
  const chars = value.length;
@@ -5611,58 +6136,148 @@
5611
6136
  return "<pre class='repl-transcript repl-transcript-highlight'>" + body + "</pre>";
5612
6137
  }
5613
6138
 
5614
- function buildReplJournalHtml() {
6139
+ function getReplStudioPrompt(runtime) {
6140
+ const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
6141
+ if (normalized === "julia") return "julia>";
6142
+ if (normalized === "r") return ">";
6143
+ if (normalized === "shell") return "$";
6144
+ if (normalized === "ghci") return "ghci>";
6145
+ if (normalized === "clojure") return "user=>";
6146
+ return ">>>";
6147
+ }
6148
+
6149
+ function getReplStudioEntryKind(entry) {
6150
+ if (entry.status === "note") return "Note";
6151
+ if (entry.mode === "agent") return "Pi";
6152
+ if (entry.mode === "literate") return "Literate";
6153
+ return "Raw";
6154
+ }
6155
+
6156
+ function buildReplStudioMeta(entry) {
6157
+ const parts = [];
6158
+ const kind = getReplStudioEntryKind(entry);
6159
+ if (kind !== "Raw") parts.push(kind);
6160
+ const time = formatReferenceTime(entry.createdAt);
6161
+ if (time) parts.push(time);
6162
+ if (entry.skippedChunks) parts.push("skipped " + String(entry.skippedChunks));
6163
+ return parts.join(" · ");
6164
+ }
6165
+
6166
+ function isReplStudioPromptLine(line, runtime) {
6167
+ const source = String(line || "");
6168
+ const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
6169
+ if (normalized === "python") return /^\s*(?:>>>|\.\.\.)\s?/.test(source);
6170
+ if (normalized === "ipython") return /^\s*(?:In \[\d+\]:|\.\.\.?:)\s?/.test(source);
6171
+ if (normalized === "julia") return /^\s*julia>\s?/.test(source);
6172
+ if (normalized === "r") return /^\s*(?:>|\+)\s?/.test(source);
6173
+ if (normalized === "ghci") return /^\s*(?:ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>)\s?/.test(source);
6174
+ if (normalized === "clojure") return /^\s*[A-Za-z0-9_.-]+=>\s?/.test(source);
6175
+ return false;
6176
+ }
6177
+
6178
+ function extractReplStudioBanner(transcript, runtime) {
6179
+ const normalizedRuntime = normalizeReplRuntime(runtime || getActiveReplRuntime());
6180
+ if (normalizedRuntime === "shell") return "";
6181
+ const lines = String(transcript || "").replace(/\r\n/g, "\n").split("\n");
6182
+ const bannerLines = [];
6183
+ for (const line of lines) {
6184
+ if (!bannerLines.length && !String(line || "").trim()) continue;
6185
+ if (isReplStudioPromptLine(line, normalizedRuntime)) break;
6186
+ bannerLines.push(line);
6187
+ if (bannerLines.length >= 16) break;
6188
+ }
6189
+ const banner = bannerLines.join("\n").trim();
6190
+ if (!/^(?:Python\s|IPython\s|R version\s|GHCi,\s|Clojure\s|Julia\s|julia\s)/i.test(banner)) return "";
6191
+ return banner;
6192
+ }
6193
+
6194
+ function buildReplStudioActionsHtml() {
6195
+ if (replJournalCollapsed) return "";
6196
+ const hasEntries = replJournalEntries.length > 0;
6197
+ const buttons = "<button type='button' data-repl-action='load-journal'" + (hasEntries ? "" : " disabled") + ">Load in editor</button>"
6198
+ + "<button type='button' data-repl-action='copy-journal'" + (hasEntries ? "" : " disabled") + ">Copy Markdown</button>"
6199
+ + "<button type='button' data-repl-action='export-journal'" + (hasEntries ? "" : " disabled") + ">Export .md</button>"
6200
+ + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear</button>";
6201
+ return "<div class='repl-studio-below-actions'><div class='repl-journal-actions'>" + buttons + "</div></div>";
6202
+ }
6203
+
6204
+ function buildReplJournalHtml(transcript) {
5615
6205
  const hasEntries = replJournalEntries.length > 0;
5616
6206
  const entryCount = replJournalEntries.length;
5617
6207
  const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
5618
- const actions = "<div class='repl-journal-actions'>"
5619
- + "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show journal" : "Hide journal") + "</button>"
5620
- + "<button type='button' data-repl-action='load-journal'" + (hasEntries ? "" : " disabled") + ">Load in editor</button>"
5621
- + "<button type='button' data-repl-action='copy-journal'" + (hasEntries ? "" : " disabled") + ">Copy journal</button>"
5622
- + "<button type='button' data-repl-action='export-journal'" + (hasEntries ? "" : " disabled") + ">Export .md</button>"
5623
- + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear</button>"
5624
- + "</div>";
6208
+ const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show REPL Studio" : "Hide REPL Studio") + "</button>";
6209
+ const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
5625
6210
  const summaryText = hasEntries
5626
- ? (entryCount + " journal entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
5627
- : "Runs and notes you send from Studio will appear here.";
5628
- if (replJournalCollapsed || !hasEntries) {
6211
+ ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
6212
+ : "Studio-sent code and notes will appear here.";
6213
+ if (replJournalCollapsed) {
5629
6214
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
5630
6215
  + "<div class='repl-journal-compact-row'>"
5631
- + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Journal</span><span>" + escapeHtml(summaryText) + "</span></div>"
5632
- + actions
6216
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>REPL Studio</span><span>" + escapeHtml(summaryText) + "</span></div>"
6217
+ + "<div class='repl-journal-actions'>" + toggleButton + "</div>"
5633
6218
  + "</div>"
5634
6219
  + "</section>";
5635
6220
  }
5636
6221
  const omitted = Math.max(0, replJournalEntries.length - 12);
6222
+ const bannerText = extractReplStudioBanner(transcript, getActiveReplRuntime());
6223
+ const banner = bannerText
6224
+ ? "<pre class='repl-studio-banner'>" + escapeHtml(bannerText) + "</pre>"
6225
+ : "";
5637
6226
  const cards = replJournalEntries.slice(-12).map((entry) => {
5638
- const time = formatReferenceTime(entry.createdAt) || "journal";
5639
- const code = String(entry.code || "").trim()
5640
- ? "<div class='repl-journal-section'><div class='repl-journal-label'>Code</div><pre class='repl-journal-code response-markdown-highlight'>" + renderHighlightedReplCode(entry.code, entry.runtime) + "</pre></div>"
6227
+ const meta = buildReplStudioMeta(entry);
6228
+ const prompt = getReplStudioPrompt(entry.runtime);
6229
+ const codeText = String(entry.code || "").trimEnd();
6230
+ const proseText = String(entry.prose || "").trim();
6231
+ const outputText = trimReplJournalOutput(entry.output || "").trimEnd();
6232
+ const code = codeText.trim()
6233
+ ? "<div class='repl-studio-code-row'><span class='repl-prompt repl-studio-prompt'>" + escapeHtml(prompt) + "</span><pre class='repl-studio-input'>" + renderHighlightedReplCode(codeText, entry.runtime) + "</pre></div>"
5641
6234
  : "";
5642
- const prose = String(entry.prose || "").trim()
5643
- ? "<div class='repl-journal-section'><div class='repl-journal-label'>Note</div><div class='repl-journal-prose'>" + escapeHtml(entry.prose) + "</div></div>"
6235
+ const prose = proseText
6236
+ ? "<div class='repl-studio-note'>" + escapeHtml(proseText) + "</div>"
5644
6237
  : "";
5645
- const output = String(entry.output || "").trim()
5646
- ? "<div class='repl-journal-section'><div class='repl-journal-label'>Output</div><pre class='repl-journal-output'>" + escapeHtml(trimReplJournalOutput(entry.output)) + "</pre></div>"
6238
+ const output = outputText
6239
+ ? "<div class='repl-studio-output-row'><span class='repl-studio-output-label'>Out:</span><pre class='repl-studio-output'>" + escapeHtml(outputText) + "</pre></div>"
5647
6240
  : "";
5648
- const skipped = entry.skippedChunks ? "<span class='trace-card-meta'>skipped " + escapeHtml(String(entry.skippedChunks)) + "</span>" : "";
5649
- return "<article class='repl-journal-card'>"
5650
- + "<div class='repl-journal-card-header'>"
5651
- + "<span class='trace-kind-badge'>" + escapeHtml(entry.mode === "literate" ? "Literate" : (entry.status === "note" ? "Note" : "Scratch")) + "</span>"
5652
- + "<span class='trace-card-title'>" + escapeHtml(entry.label || "REPL entry") + "</span>"
5653
- + "<span class='trace-card-meta'>" + escapeHtml(time) + "</span>"
5654
- + (entry.runtime ? "<span class='trace-card-meta'>" + escapeHtml(entry.runtime) + "</span>" : "")
5655
- + skipped
5656
- + "</div>"
6241
+ const pending = !output && entry.status === "sending"
6242
+ ? "<div class='repl-studio-pending'>Running…</div>"
6243
+ : "";
6244
+ return "<article class='repl-journal-card repl-studio-entry'>"
6245
+ + (meta ? "<div class='repl-studio-entry-meta'>" + escapeHtml(meta) + "</div>" : "")
5657
6246
  + prose
5658
6247
  + code
5659
- + (output || "<div class='trace-empty-inline'>" + escapeHtml(entry.status === "note" ? "Journal note only." : "Waiting for captured output…") + "</div>")
6248
+ + output
6249
+ + pending
5660
6250
  + "</article>";
5661
6251
  }).join("");
6252
+ const terminalContent = banner
6253
+ + (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>");
5662
6254
  return "<section class='repl-journal'>"
5663
- + "<div class='repl-journal-header'><div><h3>Journal</h3><p>Side log of notes, code sends, and captured output. Separate from the live REPL transcript.</p></div>" + actions + "</div>"
6255
+ + "<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>"
5664
6256
  + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
5665
- + "<div class='repl-journal-list'>" + cards + "</div>"
6257
+ + "<div class='repl-journal-list'>" + terminalContent + "</div>"
6258
+ + "</section>";
6259
+ }
6260
+
6261
+ function buildReplMirrorHtml(body, transcript) {
6262
+ const hasTranscript = Boolean(String(transcript || "").trim());
6263
+ const summary = hasTranscript
6264
+ ? "Raw tmux mirror · " + formatCompactNumber(String(transcript || "").length) + " chars"
6265
+ : "Raw tmux mirror";
6266
+ const shouldCollapse = replMirrorCollapsed;
6267
+ const actions = "<div class='repl-journal-actions'>"
6268
+ + "<button type='button' data-repl-action='mirror-toggle' aria-expanded='" + (shouldCollapse ? "false" : "true") + "'>" + (shouldCollapse ? "Show mirror" : "Hide mirror") + "</button>"
6269
+ + "</div>";
6270
+ if (shouldCollapse) {
6271
+ return "<section class='repl-mirror repl-mirror-compact'>"
6272
+ + "<div class='repl-journal-compact-row'>"
6273
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Mirror</span><span>" + escapeHtml(summary) + "</span></div>"
6274
+ + actions
6275
+ + "</div>"
6276
+ + "</section>";
6277
+ }
6278
+ return "<section class='repl-mirror'>"
6279
+ + "<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>"
6280
+ + body
5666
6281
  + "</section>";
5667
6282
  }
5668
6283
 
@@ -5680,10 +6295,6 @@
5680
6295
  ? replSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
5681
6296
  : "<option value=''>No REPL sessions</option>";
5682
6297
  const activeSession = getActiveReplSession();
5683
- const statusLabel = replTmuxAvailable === false
5684
- ? "tmux missing"
5685
- : (activeSession ? "Mirroring" : "Idle");
5686
- const captured = replCapturedAt ? formatReferenceTime(replCapturedAt) : "";
5687
6298
  const transcript = trimReplTranscript(replTranscript);
5688
6299
  const emptyMessage = replTmuxAvailable === false
5689
6300
  ? "tmux is not available. Install tmux to use Studio REPL sessions."
@@ -5695,12 +6306,6 @@
5695
6306
  const canStopActiveSession = Boolean(activeSession && activeSession.source === "studio" && !replBusy && replTmuxAvailable !== false);
5696
6307
  return "<div class='repl-panel'>"
5697
6308
  + "<div class='repl-toolbar'>"
5698
- + "<div class='repl-summary'>"
5699
- + "<span class='trace-summary-badge'>REPL</span>"
5700
- + "<span class='trace-summary-status trace-status-" + (activeSession ? "running" : "idle") + "'>" + escapeHtml(statusLabel) + "</span>"
5701
- + (activeSession ? "<span class='trace-summary-meta'>" + escapeHtml(activeSession.sessionName) + "</span>" : "")
5702
- + (captured ? "<span class='trace-summary-meta'>Updated " + escapeHtml(captured) + "</span>" : "")
5703
- + "</div>"
5704
6309
  + "<div class='repl-controls'>"
5705
6310
  + "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
5706
6311
  + "<button type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or attach to the default session for this runtime.'>Start</button>"
@@ -5711,8 +6316,8 @@
5711
6316
  + "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
5712
6317
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
5713
6318
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
5714
- + "<button type='button' data-repl-action='run-all-chunks'" + (canSendToActiveSession ? "" : " disabled") + " title='Literate mode: send all fenced code chunks matching the active REPL runtime.'>Run all chunks</button>"
5715
- + "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to the literate journal without sending it to the runtime.'>Journal note</button>"
6319
+ + "<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>"
6320
+ + "<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>"
5716
6321
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
5717
6322
  + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
5718
6323
  + "</div>"
@@ -5721,8 +6326,9 @@
5721
6326
  + "</div>"
5722
6327
  + (replMessage ? "<div class='repl-notice repl-notice-info'>" + escapeHtml(replMessage) + "</div>" : "")
5723
6328
  + (replError ? "<div class='repl-notice repl-notice-error'>" + escapeHtml(replError) + "</div>" : "")
5724
- + body
5725
- + buildReplJournalHtml()
6329
+ + buildReplJournalHtml(transcript)
6330
+ + buildReplStudioActionsHtml()
6331
+ + buildReplMirrorHtml(body, transcript)
5726
6332
  + "</div>";
5727
6333
  }
5728
6334
 
@@ -5862,7 +6468,7 @@
5862
6468
 
5863
6469
  function renderReplView() {
5864
6470
  if (!critiqueViewEl) return;
5865
- const shouldStick = replFollow || shouldStickTraceToBottom();
6471
+ const shouldStick = shouldAutoStickReplView();
5866
6472
  const previousScrollTop = critiqueViewEl.scrollTop;
5867
6473
  finishPreviewRender(critiqueViewEl);
5868
6474
  critiqueViewEl.innerHTML = buildReplPanelHtml();
@@ -5997,19 +6603,19 @@
5997
6603
  exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
5998
6604
  exportPdfBtn.textContent = previewExportInProgress
5999
6605
  ? "Exporting…"
6000
- : (exportingReplJournal ? "Export REPL journal" : "Export right preview");
6606
+ : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
6001
6607
  if (rightView === "trace") {
6002
6608
  exportPdfBtn.title = "Working view does not support preview export.";
6003
6609
  } else if (exportingReplJournal && !replJournalEntries.length) {
6004
- exportPdfBtn.title = "No REPL journal entries to export yet.";
6610
+ exportPdfBtn.title = "No REPL Studio entries to export yet.";
6005
6611
  } else if (rightView === "markdown") {
6006
- exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export.";
6612
+ exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
6007
6613
  } else if (!canExportPreview) {
6008
6614
  exportPdfBtn.title = "Nothing to export yet.";
6009
6615
  } else if (isHtmlArtifactPreview) {
6010
6616
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
6011
6617
  } else if (exportingReplJournal) {
6012
- exportPdfBtn.title = "Choose PDF or HTML and export the REPL journal.";
6618
+ exportPdfBtn.title = "Choose PDF or HTML and export REPL Studio.";
6013
6619
  } else {
6014
6620
  exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
6015
6621
  }
@@ -6018,20 +6624,20 @@
6018
6624
  exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
6019
6625
  exportPreviewPdfBtn.title = isHtmlArtifactPreview
6020
6626
  ? "Interactive HTML preview PDF export is not available yet."
6021
- : (exportingReplJournal ? "Export the REPL journal as PDF." : "Export the current right-pane preview as PDF.");
6627
+ : (exportingReplJournal ? "Export REPL Studio as PDF." : "Export the current right-pane preview as PDF.");
6022
6628
  }
6023
6629
  if (exportPreviewHtmlBtn) {
6024
6630
  exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
6025
6631
  exportPreviewHtmlBtn.title = isHtmlArtifactPreview
6026
6632
  ? "Export the authored HTML preview."
6027
- : (exportingReplJournal ? "Export the REPL journal as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
6633
+ : (exportingReplJournal ? "Export REPL Studio as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
6028
6634
  }
6029
6635
  if (exportPreviewControlsEl) {
6030
6636
  exportPreviewControlsEl.title = canExportPreview
6031
6637
  ? (exportingReplJournal
6032
- ? "Choose a format and export the REPL journal."
6638
+ ? "Choose a format and export REPL Studio."
6033
6639
  : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
6034
- : (exportingReplJournal ? "No REPL journal entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
6640
+ : (exportingReplJournal ? "No REPL Studio entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
6035
6641
  }
6036
6642
  if (!canExportPreview || previewExportInProgress) {
6037
6643
  closeExportPreviewMenu();
@@ -11874,7 +12480,7 @@
11874
12480
  sendReplBtn.title = hasSession
11875
12481
  ? (replSendMode === "literate"
11876
12482
  ? "Literate send: selected code/prose, or the current fenced code chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
11877
- : "Scratch send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
12483
+ : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
11878
12484
  : "Start or select a REPL session in the right pane first.";
11879
12485
  }
11880
12486
  if (replSendModeSelect) {
@@ -11883,7 +12489,7 @@
11883
12489
  replSendModeSelect.value = replSendMode;
11884
12490
  replSendModeSelect.title = replSendMode === "literate"
11885
12491
  ? "Literate send: Send to REPL uses the selection/current fenced code chunk."
11886
- : "Scratch send: Send to REPL uses the selection, or full editor if no selection.";
12492
+ : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
11887
12493
  }
11888
12494
 
11889
12495
  if (critiqueBtn) {
@@ -12201,8 +12807,15 @@
12201
12807
  }
12202
12808
 
12203
12809
  if (message.type === "repl_state") {
12810
+ const previousTmuxAvailable = replTmuxAvailable;
12811
+ const previousActiveSessionName = replActiveSessionName;
12812
+ const previousTranscript = replTranscript;
12813
+ const previousCapturedAt = replCapturedAt;
12814
+ const previousError = replError;
12815
+ const previousMessage = replMessage;
12816
+ const wasBusy = replBusy;
12204
12817
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
12205
- setReplSessions(message.sessions);
12818
+ const sessionsChanged = setReplSessions(message.sessions);
12206
12819
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
12207
12820
  setActiveReplSession(message.activeSessionName);
12208
12821
  }
@@ -12211,25 +12824,55 @@
12211
12824
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
12212
12825
  replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
12213
12826
  replBusy = false;
12214
- syncActionButtons();
12215
- renderReplViewIfActive();
12827
+ const controlsChanged = wasBusy
12828
+ || sessionsChanged
12829
+ || previousTmuxAvailable !== replTmuxAvailable
12830
+ || previousActiveSessionName !== replActiveSessionName;
12831
+ if (controlsChanged) syncActionButtons();
12832
+ const viewChanged = controlsChanged
12833
+ || previousTranscript !== replTranscript
12834
+ || previousError !== replError
12835
+ || previousMessage !== replMessage
12836
+ || (!previousCapturedAt && replCapturedAt);
12837
+ if (viewChanged) renderReplViewIfActive();
12838
+ updateReferenceBadge();
12839
+ return;
12840
+ }
12841
+
12842
+ if (message.type === "repl_tool_send") {
12843
+ if (typeof message.sessionName === "string" && message.sessionName.trim()) {
12844
+ setActiveReplSession(message.sessionName);
12845
+ }
12846
+ const changed = recordReplToolSend(message);
12847
+ if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
12848
+ if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
12849
+ if (changed) renderReplViewIfActive({ force: true });
12216
12850
  updateReferenceBadge();
12217
12851
  return;
12218
12852
  }
12219
12853
 
12220
12854
  if (message.type === "repl_capture") {
12855
+ const previousActiveSessionName = replActiveSessionName;
12856
+ const previousTranscript = replTranscript;
12857
+ const previousCapturedAt = replCapturedAt;
12858
+ const previousError = replError;
12859
+ const previousMessage = replMessage;
12860
+ const wasBusy = replBusy;
12861
+ let sessionsChanged = false;
12221
12862
  if (message.session) {
12222
12863
  const session = normalizeReplSession(message.session);
12223
12864
  if (session && !replSessions.some((candidate) => candidate.sessionName === session.sessionName)) {
12224
12865
  replSessions = [...replSessions, session];
12866
+ sessionsChanged = true;
12225
12867
  }
12226
12868
  }
12227
12869
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
12228
12870
  setActiveReplSession(message.activeSessionName);
12229
12871
  }
12872
+ let journalChanged = false;
12230
12873
  if (typeof message.transcript === "string") {
12231
12874
  replTranscript = trimReplTranscript(message.transcript);
12232
- updateActiveReplJournalEntryFromTranscript(
12875
+ journalChanged = updateActiveReplJournalEntryFromTranscript(
12233
12876
  typeof message.activeSessionName === "string" && message.activeSessionName.trim() ? message.activeSessionName : replActiveSessionName,
12234
12877
  replTranscript
12235
12878
  );
@@ -12238,20 +12881,28 @@
12238
12881
  replError = typeof message.replError === "string" ? message.replError : "";
12239
12882
  if (typeof message.replMessage === "string") replMessage = message.replMessage;
12240
12883
  replBusy = false;
12241
- syncActionButtons();
12242
- renderReplViewIfActive();
12884
+ const controlsChanged = wasBusy || sessionsChanged || previousActiveSessionName !== replActiveSessionName;
12885
+ if (controlsChanged) syncActionButtons();
12886
+ const viewChanged = controlsChanged
12887
+ || previousTranscript !== replTranscript
12888
+ || previousError !== replError
12889
+ || previousMessage !== replMessage
12890
+ || journalChanged
12891
+ || (!previousCapturedAt && replCapturedAt);
12892
+ if (viewChanged) renderReplViewIfActive();
12243
12893
  updateReferenceBadge();
12244
12894
  return;
12245
12895
  }
12246
12896
 
12247
12897
  if (message.type === "repl_send_ack") {
12248
12898
  replBusy = false;
12249
- replMessage = typeof message.message === "string" ? message.message : "Sent editor text to REPL.";
12899
+ replMessage = "";
12250
12900
  replError = "";
12251
12901
  if (typeof message.requestId === "string") {
12252
12902
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
12903
+ persistReplJournalEntries();
12253
12904
  }
12254
- setStatus(replMessage, "success");
12905
+ setStatus("Sent to REPL.", "success");
12255
12906
  syncActionButtons();
12256
12907
  renderReplViewIfActive({ force: true });
12257
12908
  return;
@@ -12662,6 +13313,7 @@
12662
13313
  replError = typeof message.message === "string" ? message.message : "REPL request failed.";
12663
13314
  if (typeof message.requestId === "string") {
12664
13315
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "error", output: replError, updatedAt: Date.now() } : entry);
13316
+ persistReplJournalEntries();
12665
13317
  }
12666
13318
  renderReplViewIfActive({ force: true });
12667
13319
  }
@@ -13584,6 +14236,12 @@
13584
14236
  });
13585
14237
  }
13586
14238
 
14239
+ if (zenModeBtn) {
14240
+ zenModeBtn.addEventListener("click", () => {
14241
+ setStudioZenMode(!studioZenModeEnabled);
14242
+ });
14243
+ }
14244
+
13587
14245
  sendRunBtn.addEventListener("click", () => {
13588
14246
  if (getAbortablePendingKind() === "direct") {
13589
14247
  requestCancelForPendingRequest("direct");
@@ -13765,6 +14423,13 @@
13765
14423
  });
13766
14424
  }
13767
14425
 
14426
+ document.addEventListener("click", (event) => {
14427
+ const target = event.target;
14428
+ const focusBtn = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
14429
+ if (!focusBtn) return;
14430
+ handleStudioPdfFocusButtonClick(event);
14431
+ }, true);
14432
+
13768
14433
  document.addEventListener("click", (event) => {
13769
14434
  const target = event.target;
13770
14435
  const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;