pi-studio 0.9.0 → 0.9.1

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.
@@ -241,18 +241,77 @@
241
241
  let replBusy = false;
242
242
  let replSendMode = (() => {
243
243
  try {
244
- return (window.localStorage && window.localStorage.getItem("piStudio.replSendMode")) || "scratch";
244
+ const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
245
+ return String(stored || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
245
246
  } catch {
246
- return "scratch";
247
+ return "raw";
247
248
  }
248
249
  })();
249
- let replJournalEntries = [];
250
+ function loadPersistedReplJournalEntries() {
251
+ try {
252
+ const raw = window.localStorage ? window.localStorage.getItem("piStudio.replStudioEntries.v1") : null;
253
+ const parsed = raw ? JSON.parse(raw) : [];
254
+ if (!Array.isArray(parsed)) return [];
255
+ return parsed.map((entry) => ({
256
+ id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
257
+ requestId: typeof entry.requestId === "string" ? entry.requestId : "",
258
+ createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
259
+ updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
260
+ sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
261
+ runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
262
+ label: typeof entry.label === "string" ? entry.label : "REPL send",
263
+ mode: typeof entry.mode === "string" ? entry.mode : "raw",
264
+ prose: typeof entry.prose === "string" ? entry.prose : "",
265
+ code: typeof entry.code === "string" ? entry.code : "",
266
+ output: typeof entry.output === "string" ? entry.output : "",
267
+ beforeTranscript: "",
268
+ status: typeof entry.status === "string" ? entry.status : "sent",
269
+ skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
270
+ })).filter((entry) => entry.code.trim() || entry.prose.trim() || entry.output.trim()).slice(-REPL_JOURNAL_MAX_ENTRIES);
271
+ } catch {
272
+ return [];
273
+ }
274
+ }
275
+
276
+ function persistReplJournalEntries() {
277
+ try {
278
+ if (!window.localStorage) return;
279
+ const compact = replJournalEntries.slice(-REPL_JOURNAL_MAX_ENTRIES).map((entry) => ({
280
+ id: entry.id,
281
+ requestId: entry.requestId,
282
+ createdAt: entry.createdAt,
283
+ updatedAt: entry.updatedAt,
284
+ sessionName: entry.sessionName,
285
+ runtime: entry.runtime,
286
+ label: entry.label,
287
+ mode: entry.mode,
288
+ prose: entry.prose,
289
+ code: entry.code,
290
+ output: entry.output,
291
+ status: entry.status,
292
+ skippedChunks: entry.skippedChunks,
293
+ }));
294
+ window.localStorage.setItem("piStudio.replStudioEntries.v1", JSON.stringify(compact));
295
+ } catch {
296
+ // Ignore local persistence failures.
297
+ }
298
+ }
299
+
300
+ let replJournalEntries = loadPersistedReplJournalEntries();
250
301
  let activeReplJournalEntryId = "";
251
302
  let replJournalCollapsed = (() => {
252
303
  try {
253
- const stored = window.localStorage ? window.localStorage.getItem("piStudio.replJournalCollapsed") : null;
254
- if (stored === "false") return false;
304
+ const stored = window.localStorage ? window.localStorage.getItem("piStudio.replStudioCollapsed") : null;
255
305
  if (stored === "true") return true;
306
+ return false;
307
+ } catch {
308
+ return false;
309
+ }
310
+ })();
311
+ let replMirrorCollapsed = (() => {
312
+ try {
313
+ const stored = window.localStorage ? window.localStorage.getItem("piStudio.rawReplMirrorCollapsed") : null;
314
+ if (stored === "false") return false;
256
315
  return true;
257
316
  } catch {
258
317
  return true;
@@ -818,7 +877,7 @@
818
877
  }
819
878
 
820
879
  function normalizeReplSendMode(value) {
821
- return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "scratch";
880
+ return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
822
881
  }
823
882
 
824
883
  function setReplSendMode(mode) {
@@ -834,20 +893,46 @@
834
893
  function setReplJournalCollapsed(collapsed) {
835
894
  replJournalCollapsed = Boolean(collapsed);
836
895
  try {
837
- if (window.localStorage) window.localStorage.setItem("piStudio.replJournalCollapsed", replJournalCollapsed ? "true" : "false");
896
+ if (window.localStorage) window.localStorage.setItem("piStudio.replStudioCollapsed", replJournalCollapsed ? "true" : "false");
838
897
  } catch {
839
898
  // Ignore storage failures.
840
899
  }
841
900
  renderReplViewIfActive({ force: true });
842
901
  }
843
902
 
903
+ function setReplMirrorCollapsed(collapsed) {
904
+ replMirrorCollapsed = Boolean(collapsed);
905
+ try {
906
+ if (window.localStorage) window.localStorage.setItem("piStudio.rawReplMirrorCollapsed", replMirrorCollapsed ? "true" : "false");
907
+ } catch {
908
+ // Ignore storage failures.
909
+ }
910
+ renderReplViewIfActive({ force: true });
911
+ }
912
+
913
+ function serializeReplSessionsForCompare(sessions) {
914
+ return JSON.stringify((Array.isArray(sessions) ? sessions : [])
915
+ .map(normalizeReplSession)
916
+ .filter(Boolean)
917
+ .map((session) => ({
918
+ sessionName: session.sessionName,
919
+ label: session.label,
920
+ runtime: session.runtime,
921
+ source: session.source,
922
+ target: session.target,
923
+ })));
924
+ }
925
+
844
926
  function setReplSessions(sessions) {
927
+ const previous = serializeReplSessionsForCompare(replSessions);
928
+ const previousActive = replActiveSessionName;
845
929
  replSessions = Array.isArray(sessions)
846
930
  ? sessions.map(normalizeReplSession).filter(Boolean)
847
931
  : [];
848
932
  if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
849
933
  replActiveSessionName = replSessions[0] ? replSessions[0].sessionName : "";
850
934
  }
935
+ return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
851
936
  }
852
937
 
853
938
  function getActiveReplSession() {
@@ -866,8 +951,8 @@
866
951
  "Session name: " + session.sessionName,
867
952
  "tmux target: " + (session.target || (session.sessionName + ":0.0")),
868
953
  "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.",
954
+ "Use the studio_repl_send tool for code execution in this REPL. Pass sessionName when targeting this exact session.",
955
+ "Do not improvise raw tmux paste commands for multiline code; Studio handles runtime-specific safe submission.",
871
956
  "[/Studio active REPL]",
872
957
  ].join("\n");
873
958
  }
@@ -1069,7 +1154,7 @@
1069
1154
  };
1070
1155
  }
1071
1156
 
1072
- function buildScratchReplSendPayload() {
1157
+ function buildRawReplSendPayload() {
1073
1158
  const range = getEditorSelectionRange();
1074
1159
  const selected = range.selected;
1075
1160
  const source = selected || range.raw;
@@ -1078,7 +1163,7 @@
1078
1163
  text: prepareEditorTextForSend(unwrapped ? unwrapped.code : source),
1079
1164
  prose: "",
1080
1165
  label: unwrapped ? unwrapped.label : (selected ? "selection" : "full editor"),
1081
- mode: "scratch",
1166
+ mode: "raw",
1082
1167
  noteOnly: false,
1083
1168
  skippedChunks: 0,
1084
1169
  };
@@ -1142,7 +1227,7 @@
1142
1227
 
1143
1228
  const allBlocks = parseMarkdownCodeFences(range.raw);
1144
1229
  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." };
1230
+ 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
1231
  }
1147
1232
 
1148
1233
  return {
@@ -1211,9 +1296,42 @@
1211
1296
  function addReplJournalEntry(details) {
1212
1297
  const entry = createReplJournalEntry(details || {});
1213
1298
  replJournalEntries = [...replJournalEntries, entry].slice(-REPL_JOURNAL_MAX_ENTRIES);
1299
+ persistReplJournalEntries();
1214
1300
  return entry;
1215
1301
  }
1216
1302
 
1303
+ function recordReplToolSend(message) {
1304
+ const requestId = typeof message.toolCallId === "string" && message.toolCallId.trim()
1305
+ ? "tool:" + message.toolCallId.trim()
1306
+ : (typeof message.requestId === "string" && message.requestId.trim() ? message.requestId.trim() : "");
1307
+ const code = String(message.code || "");
1308
+ if (!code.trim()) return false;
1309
+ const runtime = normalizeReplRuntime(message.runtime || getActiveReplRuntime());
1310
+ const sessionName = typeof message.sessionName === "string" ? message.sessionName : replActiveSessionName;
1311
+ const output = cleanReplCapturedOutput(String(message.output || ""), { code, runtime });
1312
+ const details = {
1313
+ requestId,
1314
+ sessionName,
1315
+ runtime,
1316
+ label: typeof message.label === "string" && message.label.trim() ? message.label.trim() : "Pi",
1317
+ mode: "agent",
1318
+ code,
1319
+ output,
1320
+ status: output.trim() ? "captured" : (message.timedOut ? "timeout" : "sent"),
1321
+ };
1322
+ activeReplJournalEntryId = "";
1323
+ if (requestId) {
1324
+ const existingIndex = replJournalEntries.findIndex((entry) => entry.requestId === requestId);
1325
+ if (existingIndex >= 0) {
1326
+ replJournalEntries = replJournalEntries.map((entry) => entry.requestId === requestId ? { ...entry, ...details, updatedAt: Date.now() } : entry);
1327
+ persistReplJournalEntries();
1328
+ return true;
1329
+ }
1330
+ }
1331
+ addReplJournalEntry(details);
1332
+ return true;
1333
+ }
1334
+
1217
1335
  function extractReplTranscriptDelta(before, after) {
1218
1336
  const previous = String(before || "");
1219
1337
  const current = String(after || "");
@@ -1232,17 +1350,54 @@
1232
1350
  return current;
1233
1351
  }
1234
1352
 
1353
+ function stripSubmittedCodeEchoFromReplDelta(delta, entry) {
1354
+ const value = String(delta || "").replace(/^\s+/, "");
1355
+ const code = String(entry && entry.code ? entry.code : "").trim();
1356
+ if (!value || !code) return value;
1357
+ const firstCodeLine = code.split("\n").map((line) => line.trim()).find(Boolean) || "";
1358
+ const lines = value.split("\n");
1359
+ if (!lines.length) return value;
1360
+ const promptlessFirst = lines[0].replace(/^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*/, "").trim();
1361
+ const isEcho = promptlessFirst === firstCodeLine
1362
+ || /^# Studio sent \d+-line snippet$/.test(promptlessFirst)
1363
+ || /^-- Studio sent \d+-line snippet$/.test(promptlessFirst)
1364
+ || /^;; Studio sent \d+-line snippet$/.test(promptlessFirst);
1365
+ return isEcho ? lines.slice(1).join("\n").replace(/^\s+/, "") : value;
1366
+ }
1367
+
1368
+ function stripTrailingReplPromptsFromOutput(output) {
1369
+ const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
1370
+ while (lines.length > 0 && /^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*$/.test(lines[lines.length - 1] || "")) {
1371
+ lines.pop();
1372
+ }
1373
+ return lines.join("\n").trimEnd();
1374
+ }
1375
+
1376
+ function stripSubsequentReplInputsFromOutput(output) {
1377
+ const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
1378
+ const nextInputIndex = lines.findIndex((line) => /^\s*(?:>>>|In \[\d+\]:|julia>|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s+\S/.test(line || ""));
1379
+ if (nextInputIndex <= 0) return lines.join("\n").trimEnd();
1380
+ return lines.slice(0, nextInputIndex).join("\n").trimEnd();
1381
+ }
1382
+
1383
+ function cleanReplCapturedOutput(delta, entry) {
1384
+ return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(stripSubsequentReplInputsFromOutput(stripSubmittedCodeEchoFromReplDelta(delta, entry))));
1385
+ }
1386
+
1235
1387
  function updateActiveReplJournalEntryFromTranscript(sessionName, transcript) {
1236
- if (!activeReplJournalEntryId) return;
1388
+ if (!activeReplJournalEntryId) return false;
1237
1389
  const entryIndex = replJournalEntries.findIndex((entry) => entry.id === activeReplJournalEntryId);
1238
- if (entryIndex < 0) return;
1390
+ if (entryIndex < 0) return false;
1239
1391
  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;
1392
+ if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return false;
1393
+ const delta = cleanReplCapturedOutput(extractReplTranscriptDelta(entry.beforeTranscript, transcript), entry);
1394
+ if (!delta.trim()) return false;
1395
+ if (entry.output === delta && entry.status === "captured") return false;
1243
1396
  replJournalEntries = replJournalEntries.map((candidate) => candidate.id === entry.id
1244
1397
  ? { ...candidate, output: delta, status: "captured", updatedAt: Date.now() }
1245
1398
  : candidate);
1399
+ persistReplJournalEntries();
1400
+ return true;
1246
1401
  }
1247
1402
 
1248
1403
  function getMarkdownFenceForText(text, language) {
@@ -1253,9 +1408,9 @@
1253
1408
  }
1254
1409
 
1255
1410
  function buildReplJournalMarkdown() {
1256
- const lines = ["# Studio REPL journal", "", "Generated: " + new Date().toLocaleString(), ""];
1411
+ const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString(), ""];
1257
1412
  if (!replJournalEntries.length) {
1258
- lines.push("_No journal entries yet._");
1413
+ lines.push("_No REPL Studio entries yet._");
1259
1414
  return lines.join("\n");
1260
1415
  }
1261
1416
  replJournalEntries.forEach((entry, index) => {
@@ -1286,11 +1441,11 @@
1286
1441
 
1287
1442
  async function copyReplJournalToClipboard() {
1288
1443
  if (!replJournalEntries.length) {
1289
- setStatus("No REPL journal entries to copy yet.", "warning");
1444
+ setStatus("No REPL Studio entries to copy yet.", "warning");
1290
1445
  return;
1291
1446
  }
1292
1447
  if (await writeTextToClipboard(buildReplJournalMarkdown())) {
1293
- setStatus("Copied REPL journal as Markdown.", "success");
1448
+ setStatus("Copied REPL Studio as Markdown.", "success");
1294
1449
  } else {
1295
1450
  setStatus("Clipboard write failed.", "warning");
1296
1451
  }
@@ -1298,7 +1453,7 @@
1298
1453
 
1299
1454
  function exportReplJournalMarkdown() {
1300
1455
  if (!replJournalEntries.length) {
1301
- setStatus("No REPL journal entries to export yet.", "warning");
1456
+ setStatus("No REPL Studio entries to export yet.", "warning");
1302
1457
  return;
1303
1458
  }
1304
1459
  const blob = new Blob([buildReplJournalMarkdown()], { type: "text/markdown;charset=utf-8" });
@@ -1306,37 +1461,38 @@
1306
1461
  const link = document.createElement("a");
1307
1462
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1308
1463
  link.href = blobUrl;
1309
- link.download = "studio-repl-journal-" + stamp + ".md";
1464
+ link.download = "repl-studio-" + stamp + ".md";
1310
1465
  document.body.appendChild(link);
1311
1466
  link.click();
1312
1467
  link.remove();
1313
1468
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1314
- setStatus("Exported REPL journal Markdown.", "success");
1469
+ setStatus("Exported REPL Studio Markdown.", "success");
1315
1470
  }
1316
1471
 
1317
1472
  function clearReplJournal() {
1318
1473
  replJournalEntries = [];
1319
1474
  activeReplJournalEntryId = "";
1320
- setStatus("Cleared REPL journal.", "success");
1475
+ persistReplJournalEntries();
1476
+ setStatus("Cleared REPL Studio.", "success");
1321
1477
  renderReplViewIfActive({ force: true });
1322
1478
  }
1323
1479
 
1324
1480
  function loadReplJournalIntoEditor() {
1325
1481
  if (!replJournalEntries.length) {
1326
- setStatus("No REPL journal entries to load yet.", "warning");
1482
+ setStatus("No REPL Studio entries to load yet.", "warning");
1327
1483
  return;
1328
1484
  }
1329
1485
  const markdown = buildReplJournalMarkdown();
1330
1486
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1331
- setSourceState({ source: "blank", label: "REPL journal", path: null });
1487
+ setSourceState({ source: "blank", label: "REPL Studio", path: null });
1332
1488
  setEditorLanguage("markdown");
1333
- setStatus("Loaded REPL journal into editor.", "success");
1489
+ setStatus("Loaded REPL Studio into editor.", "success");
1334
1490
  }
1335
1491
 
1336
1492
  function addSelectedReplJournalNote() {
1337
1493
  const note = getSelectedOrCurrentParagraphForReplNote();
1338
1494
  if (!note.trim()) {
1339
- setStatus("Select prose or place the cursor in a paragraph to journal a note.", "warning");
1495
+ setStatus("Select prose or place the cursor in a paragraph to add a REPL Studio note.", "warning");
1340
1496
  return;
1341
1497
  }
1342
1498
  addReplJournalEntry({
@@ -1347,7 +1503,7 @@
1347
1503
  sessionName: replActiveSessionName,
1348
1504
  runtime: getActiveReplRuntime(),
1349
1505
  });
1350
- setStatus("Added note to REPL journal.", "success");
1506
+ setStatus("Added note to REPL Studio.", "success");
1351
1507
  renderReplViewIfActive({ force: true });
1352
1508
  }
1353
1509
 
@@ -1372,7 +1528,7 @@
1372
1528
  runtime: getActiveReplRuntime(),
1373
1529
  skippedChunks: payload.skippedChunks,
1374
1530
  });
1375
- setStatus("Added prose to REPL journal.", "success");
1531
+ setStatus("Added prose to REPL Studio.", "success");
1376
1532
  renderReplViewIfActive({ force: true });
1377
1533
  } else {
1378
1534
  setStatus("No code or prose found to send.", "warning");
@@ -1406,6 +1562,7 @@
1406
1562
  if (!sendMessage({ type: "repl_send_request", requestId, sessionName: session.sessionName, text })) {
1407
1563
  replBusy = false;
1408
1564
  replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
1565
+ persistReplJournalEntries();
1409
1566
  syncActionButtons();
1410
1567
  }
1411
1568
  }
@@ -1420,7 +1577,7 @@
1420
1577
  addSelectedReplJournalNote();
1421
1578
  return;
1422
1579
  }
1423
- sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildScratchReplSendPayload());
1580
+ sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildRawReplSendPayload());
1424
1581
  }
1425
1582
 
1426
1583
  function renderTraceViewIfActive() {
@@ -3180,6 +3337,12 @@
3180
3337
 
3181
3338
  function updateReferenceBadge() {
3182
3339
  if (!referenceBadgeEl) return;
3340
+ const referenceMetaEl = referenceBadgeEl.closest(".reference-meta");
3341
+ if (rightView === "repl") {
3342
+ if (referenceMetaEl instanceof HTMLElement) referenceMetaEl.hidden = true;
3343
+ return;
3344
+ }
3345
+ if (referenceMetaEl instanceof HTMLElement) referenceMetaEl.hidden = false;
3183
3346
 
3184
3347
  if (rightView === "trace") {
3185
3348
  const state = traceState || createEmptyTraceState();
@@ -3209,18 +3372,6 @@
3209
3372
  return;
3210
3373
  }
3211
3374
 
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
3375
  if (rightView === "editor-preview") {
3225
3376
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
3226
3377
  if (hasResponse) {
@@ -4526,6 +4677,10 @@
4526
4677
  setReplJournalCollapsed(!replJournalCollapsed);
4527
4678
  return;
4528
4679
  }
4680
+ if (action === "mirror-toggle") {
4681
+ setReplMirrorCollapsed(!replMirrorCollapsed);
4682
+ return;
4683
+ }
4529
4684
  if (action === "load-journal") {
4530
4685
  loadReplJournalIntoEditor();
4531
4686
  return;
@@ -4812,11 +4967,11 @@
4812
4967
  const exportingReplJournal = rightView === "repl";
4813
4968
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4814
4969
  if (!rightPaneShowsPreview && !exportingReplJournal) {
4815
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export PDF.", "warning");
4970
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
4816
4971
  return;
4817
4972
  }
4818
4973
  if (exportingReplJournal && !replJournalEntries.length) {
4819
- setStatus("No REPL journal entries to export yet.", "warning");
4974
+ setStatus("No REPL Studio entries to export yet.", "warning");
4820
4975
  return;
4821
4976
  }
4822
4977
 
@@ -4844,7 +4999,7 @@
4844
4999
  const isLatex = isEditorPreview
4845
5000
  ? editorPdfLanguage === "latex"
4846
5001
  : /\\documentclass\b|\\begin\{document\}/.test(markdown);
4847
- let filenameHint = exportingReplJournal ? "studio-repl-journal.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
5002
+ let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
4848
5003
  if (sourcePath) {
4849
5004
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
4850
5005
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -4984,11 +5139,11 @@
4984
5139
  const exportingReplJournal = rightView === "repl";
4985
5140
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4986
5141
  if (!rightPaneShowsPreview && !exportingReplJournal) {
4987
- setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export HTML.", "warning");
5142
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
4988
5143
  return;
4989
5144
  }
4990
5145
  if (exportingReplJournal && !replJournalEntries.length) {
4991
- setStatus("No REPL journal entries to export yet.", "warning");
5146
+ setStatus("No REPL Studio entries to export yet.", "warning");
4992
5147
  return;
4993
5148
  }
4994
5149
 
@@ -5009,8 +5164,8 @@
5009
5164
  const isLatex = htmlArtifactSource ? false : (isEditorPreview
5010
5165
  ? editorHtmlLanguage === "latex"
5011
5166
  : /\\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");
5167
+ let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
5168
+ let titleHint = exportingReplJournal ? "REPL Studio" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
5014
5169
  if (sourcePath) {
5015
5170
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
5016
5171
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -5491,6 +5646,16 @@
5491
5646
  return remaining < 56;
5492
5647
  }
5493
5648
 
5649
+ function isReplJournalExpanded() {
5650
+ return rightView === "repl" && !replJournalCollapsed && replJournalEntries.length > 0;
5651
+ }
5652
+
5653
+ function shouldAutoStickReplView() {
5654
+ if (!critiqueViewEl) return true;
5655
+ if (isReplJournalExpanded()) return shouldStickTraceToBottom();
5656
+ return replFollow || shouldStickTraceToBottom();
5657
+ }
5658
+
5494
5659
  function formatTraceOutputSize(text) {
5495
5660
  const value = String(text || "");
5496
5661
  const chars = value.length;
@@ -5611,58 +5776,148 @@
5611
5776
  return "<pre class='repl-transcript repl-transcript-highlight'>" + body + "</pre>";
5612
5777
  }
5613
5778
 
5614
- function buildReplJournalHtml() {
5779
+ function getReplStudioPrompt(runtime) {
5780
+ const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
5781
+ if (normalized === "julia") return "julia>";
5782
+ if (normalized === "r") return ">";
5783
+ if (normalized === "shell") return "$";
5784
+ if (normalized === "ghci") return "ghci>";
5785
+ if (normalized === "clojure") return "user=>";
5786
+ return ">>>";
5787
+ }
5788
+
5789
+ function getReplStudioEntryKind(entry) {
5790
+ if (entry.status === "note") return "Note";
5791
+ if (entry.mode === "agent") return "Pi";
5792
+ if (entry.mode === "literate") return "Literate";
5793
+ return "Raw";
5794
+ }
5795
+
5796
+ function buildReplStudioMeta(entry) {
5797
+ const parts = [];
5798
+ const kind = getReplStudioEntryKind(entry);
5799
+ if (kind !== "Raw") parts.push(kind);
5800
+ const time = formatReferenceTime(entry.createdAt);
5801
+ if (time) parts.push(time);
5802
+ if (entry.skippedChunks) parts.push("skipped " + String(entry.skippedChunks));
5803
+ return parts.join(" · ");
5804
+ }
5805
+
5806
+ function isReplStudioPromptLine(line, runtime) {
5807
+ const source = String(line || "");
5808
+ const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
5809
+ if (normalized === "python") return /^\s*(?:>>>|\.\.\.)\s?/.test(source);
5810
+ if (normalized === "ipython") return /^\s*(?:In \[\d+\]:|\.\.\.?:)\s?/.test(source);
5811
+ if (normalized === "julia") return /^\s*julia>\s?/.test(source);
5812
+ if (normalized === "r") return /^\s*(?:>|\+)\s?/.test(source);
5813
+ if (normalized === "ghci") return /^\s*(?:ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>)\s?/.test(source);
5814
+ if (normalized === "clojure") return /^\s*[A-Za-z0-9_.-]+=>\s?/.test(source);
5815
+ return false;
5816
+ }
5817
+
5818
+ function extractReplStudioBanner(transcript, runtime) {
5819
+ const normalizedRuntime = normalizeReplRuntime(runtime || getActiveReplRuntime());
5820
+ if (normalizedRuntime === "shell") return "";
5821
+ const lines = String(transcript || "").replace(/\r\n/g, "\n").split("\n");
5822
+ const bannerLines = [];
5823
+ for (const line of lines) {
5824
+ if (!bannerLines.length && !String(line || "").trim()) continue;
5825
+ if (isReplStudioPromptLine(line, normalizedRuntime)) break;
5826
+ bannerLines.push(line);
5827
+ if (bannerLines.length >= 16) break;
5828
+ }
5829
+ const banner = bannerLines.join("\n").trim();
5830
+ if (!/^(?:Python\s|IPython\s|R version\s|GHCi,\s|Clojure\s|Julia\s|julia\s)/i.test(banner)) return "";
5831
+ return banner;
5832
+ }
5833
+
5834
+ function buildReplStudioActionsHtml() {
5835
+ if (replJournalCollapsed) return "";
5836
+ const hasEntries = replJournalEntries.length > 0;
5837
+ const buttons = "<button type='button' data-repl-action='load-journal'" + (hasEntries ? "" : " disabled") + ">Load in editor</button>"
5838
+ + "<button type='button' data-repl-action='copy-journal'" + (hasEntries ? "" : " disabled") + ">Copy Markdown</button>"
5839
+ + "<button type='button' data-repl-action='export-journal'" + (hasEntries ? "" : " disabled") + ">Export .md</button>"
5840
+ + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear</button>";
5841
+ return "<div class='repl-studio-below-actions'><div class='repl-journal-actions'>" + buttons + "</div></div>";
5842
+ }
5843
+
5844
+ function buildReplJournalHtml(transcript) {
5615
5845
  const hasEntries = replJournalEntries.length > 0;
5616
5846
  const entryCount = replJournalEntries.length;
5617
5847
  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>";
5848
+ const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show REPL Studio" : "Hide REPL Studio") + "</button>";
5849
+ const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
5625
5850
  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) {
5851
+ ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
5852
+ : "Studio-sent code and notes will appear here.";
5853
+ if (replJournalCollapsed) {
5629
5854
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
5630
5855
  + "<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
5856
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>REPL Studio</span><span>" + escapeHtml(summaryText) + "</span></div>"
5857
+ + "<div class='repl-journal-actions'>" + toggleButton + "</div>"
5633
5858
  + "</div>"
5634
5859
  + "</section>";
5635
5860
  }
5636
5861
  const omitted = Math.max(0, replJournalEntries.length - 12);
5862
+ const bannerText = extractReplStudioBanner(transcript, getActiveReplRuntime());
5863
+ const banner = bannerText
5864
+ ? "<pre class='repl-studio-banner'>" + escapeHtml(bannerText) + "</pre>"
5865
+ : "";
5637
5866
  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>"
5867
+ const meta = buildReplStudioMeta(entry);
5868
+ const prompt = getReplStudioPrompt(entry.runtime);
5869
+ const codeText = String(entry.code || "").trimEnd();
5870
+ const proseText = String(entry.prose || "").trim();
5871
+ const outputText = trimReplJournalOutput(entry.output || "").trimEnd();
5872
+ const code = codeText.trim()
5873
+ ? "<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
5874
  : "";
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>"
5875
+ const prose = proseText
5876
+ ? "<div class='repl-studio-note'>" + escapeHtml(proseText) + "</div>"
5644
5877
  : "";
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>"
5878
+ const output = outputText
5879
+ ? "<div class='repl-studio-output-row'><span class='repl-studio-output-label'>Out:</span><pre class='repl-studio-output'>" + escapeHtml(outputText) + "</pre></div>"
5647
5880
  : "";
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>"
5881
+ const pending = !output && entry.status === "sending"
5882
+ ? "<div class='repl-studio-pending'>Running…</div>"
5883
+ : "";
5884
+ return "<article class='repl-journal-card repl-studio-entry'>"
5885
+ + (meta ? "<div class='repl-studio-entry-meta'>" + escapeHtml(meta) + "</div>" : "")
5657
5886
  + prose
5658
5887
  + code
5659
- + (output || "<div class='trace-empty-inline'>" + escapeHtml(entry.status === "note" ? "Journal note only." : "Waiting for captured output…") + "</div>")
5888
+ + output
5889
+ + pending
5660
5890
  + "</article>";
5661
5891
  }).join("");
5892
+ const terminalContent = banner
5893
+ + (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
5894
  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>"
5895
+ + "<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
5896
  + (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>"
5897
+ + "<div class='repl-journal-list'>" + terminalContent + "</div>"
5898
+ + "</section>";
5899
+ }
5900
+
5901
+ function buildReplMirrorHtml(body, transcript) {
5902
+ const hasTranscript = Boolean(String(transcript || "").trim());
5903
+ const summary = hasTranscript
5904
+ ? "Raw tmux mirror · " + formatCompactNumber(String(transcript || "").length) + " chars"
5905
+ : "Raw tmux mirror";
5906
+ const shouldCollapse = replMirrorCollapsed;
5907
+ const actions = "<div class='repl-journal-actions'>"
5908
+ + "<button type='button' data-repl-action='mirror-toggle' aria-expanded='" + (shouldCollapse ? "false" : "true") + "'>" + (shouldCollapse ? "Show mirror" : "Hide mirror") + "</button>"
5909
+ + "</div>";
5910
+ if (shouldCollapse) {
5911
+ return "<section class='repl-mirror repl-mirror-compact'>"
5912
+ + "<div class='repl-journal-compact-row'>"
5913
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Mirror</span><span>" + escapeHtml(summary) + "</span></div>"
5914
+ + actions
5915
+ + "</div>"
5916
+ + "</section>";
5917
+ }
5918
+ return "<section class='repl-mirror'>"
5919
+ + "<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>"
5920
+ + body
5666
5921
  + "</section>";
5667
5922
  }
5668
5923
 
@@ -5680,10 +5935,6 @@
5680
5935
  ? replSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
5681
5936
  : "<option value=''>No REPL sessions</option>";
5682
5937
  const activeSession = getActiveReplSession();
5683
- const statusLabel = replTmuxAvailable === false
5684
- ? "tmux missing"
5685
- : (activeSession ? "Mirroring" : "Idle");
5686
- const captured = replCapturedAt ? formatReferenceTime(replCapturedAt) : "";
5687
5938
  const transcript = trimReplTranscript(replTranscript);
5688
5939
  const emptyMessage = replTmuxAvailable === false
5689
5940
  ? "tmux is not available. Install tmux to use Studio REPL sessions."
@@ -5695,12 +5946,6 @@
5695
5946
  const canStopActiveSession = Boolean(activeSession && activeSession.source === "studio" && !replBusy && replTmuxAvailable !== false);
5696
5947
  return "<div class='repl-panel'>"
5697
5948
  + "<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
5949
  + "<div class='repl-controls'>"
5705
5950
  + "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
5706
5951
  + "<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 +5956,8 @@
5711
5956
  + "<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
5957
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
5713
5958
  + "<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>"
5959
+ + "<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>"
5960
+ + "<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
5961
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
5717
5962
  + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
5718
5963
  + "</div>"
@@ -5721,8 +5966,9 @@
5721
5966
  + "</div>"
5722
5967
  + (replMessage ? "<div class='repl-notice repl-notice-info'>" + escapeHtml(replMessage) + "</div>" : "")
5723
5968
  + (replError ? "<div class='repl-notice repl-notice-error'>" + escapeHtml(replError) + "</div>" : "")
5724
- + body
5725
- + buildReplJournalHtml()
5969
+ + buildReplJournalHtml(transcript)
5970
+ + buildReplStudioActionsHtml()
5971
+ + buildReplMirrorHtml(body, transcript)
5726
5972
  + "</div>";
5727
5973
  }
5728
5974
 
@@ -5862,7 +6108,7 @@
5862
6108
 
5863
6109
  function renderReplView() {
5864
6110
  if (!critiqueViewEl) return;
5865
- const shouldStick = replFollow || shouldStickTraceToBottom();
6111
+ const shouldStick = shouldAutoStickReplView();
5866
6112
  const previousScrollTop = critiqueViewEl.scrollTop;
5867
6113
  finishPreviewRender(critiqueViewEl);
5868
6114
  critiqueViewEl.innerHTML = buildReplPanelHtml();
@@ -5997,19 +6243,19 @@
5997
6243
  exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
5998
6244
  exportPdfBtn.textContent = previewExportInProgress
5999
6245
  ? "Exporting…"
6000
- : (exportingReplJournal ? "Export REPL journal" : "Export right preview");
6246
+ : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
6001
6247
  if (rightView === "trace") {
6002
6248
  exportPdfBtn.title = "Working view does not support preview export.";
6003
6249
  } else if (exportingReplJournal && !replJournalEntries.length) {
6004
- exportPdfBtn.title = "No REPL journal entries to export yet.";
6250
+ exportPdfBtn.title = "No REPL Studio entries to export yet.";
6005
6251
  } else if (rightView === "markdown") {
6006
- exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export.";
6252
+ exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
6007
6253
  } else if (!canExportPreview) {
6008
6254
  exportPdfBtn.title = "Nothing to export yet.";
6009
6255
  } else if (isHtmlArtifactPreview) {
6010
6256
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
6011
6257
  } else if (exportingReplJournal) {
6012
- exportPdfBtn.title = "Choose PDF or HTML and export the REPL journal.";
6258
+ exportPdfBtn.title = "Choose PDF or HTML and export REPL Studio.";
6013
6259
  } else {
6014
6260
  exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
6015
6261
  }
@@ -6018,20 +6264,20 @@
6018
6264
  exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
6019
6265
  exportPreviewPdfBtn.title = isHtmlArtifactPreview
6020
6266
  ? "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.");
6267
+ : (exportingReplJournal ? "Export REPL Studio as PDF." : "Export the current right-pane preview as PDF.");
6022
6268
  }
6023
6269
  if (exportPreviewHtmlBtn) {
6024
6270
  exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
6025
6271
  exportPreviewHtmlBtn.title = isHtmlArtifactPreview
6026
6272
  ? "Export the authored HTML preview."
6027
- : (exportingReplJournal ? "Export the REPL journal as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
6273
+ : (exportingReplJournal ? "Export REPL Studio as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
6028
6274
  }
6029
6275
  if (exportPreviewControlsEl) {
6030
6276
  exportPreviewControlsEl.title = canExportPreview
6031
6277
  ? (exportingReplJournal
6032
- ? "Choose a format and export the REPL journal."
6278
+ ? "Choose a format and export REPL Studio."
6033
6279
  : (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.");
6280
+ : (exportingReplJournal ? "No REPL Studio entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
6035
6281
  }
6036
6282
  if (!canExportPreview || previewExportInProgress) {
6037
6283
  closeExportPreviewMenu();
@@ -11874,7 +12120,7 @@
11874
12120
  sendReplBtn.title = hasSession
11875
12121
  ? (replSendMode === "literate"
11876
12122
  ? "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.")
12123
+ : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
11878
12124
  : "Start or select a REPL session in the right pane first.";
11879
12125
  }
11880
12126
  if (replSendModeSelect) {
@@ -11883,7 +12129,7 @@
11883
12129
  replSendModeSelect.value = replSendMode;
11884
12130
  replSendModeSelect.title = replSendMode === "literate"
11885
12131
  ? "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.";
12132
+ : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
11887
12133
  }
11888
12134
 
11889
12135
  if (critiqueBtn) {
@@ -12201,8 +12447,15 @@
12201
12447
  }
12202
12448
 
12203
12449
  if (message.type === "repl_state") {
12450
+ const previousTmuxAvailable = replTmuxAvailable;
12451
+ const previousActiveSessionName = replActiveSessionName;
12452
+ const previousTranscript = replTranscript;
12453
+ const previousCapturedAt = replCapturedAt;
12454
+ const previousError = replError;
12455
+ const previousMessage = replMessage;
12456
+ const wasBusy = replBusy;
12204
12457
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
12205
- setReplSessions(message.sessions);
12458
+ const sessionsChanged = setReplSessions(message.sessions);
12206
12459
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
12207
12460
  setActiveReplSession(message.activeSessionName);
12208
12461
  }
@@ -12211,25 +12464,55 @@
12211
12464
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
12212
12465
  replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
12213
12466
  replBusy = false;
12214
- syncActionButtons();
12215
- renderReplViewIfActive();
12467
+ const controlsChanged = wasBusy
12468
+ || sessionsChanged
12469
+ || previousTmuxAvailable !== replTmuxAvailable
12470
+ || previousActiveSessionName !== replActiveSessionName;
12471
+ if (controlsChanged) syncActionButtons();
12472
+ const viewChanged = controlsChanged
12473
+ || previousTranscript !== replTranscript
12474
+ || previousError !== replError
12475
+ || previousMessage !== replMessage
12476
+ || (!previousCapturedAt && replCapturedAt);
12477
+ if (viewChanged) renderReplViewIfActive();
12478
+ updateReferenceBadge();
12479
+ return;
12480
+ }
12481
+
12482
+ if (message.type === "repl_tool_send") {
12483
+ if (typeof message.sessionName === "string" && message.sessionName.trim()) {
12484
+ setActiveReplSession(message.sessionName);
12485
+ }
12486
+ const changed = recordReplToolSend(message);
12487
+ if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
12488
+ if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
12489
+ if (changed) renderReplViewIfActive({ force: true });
12216
12490
  updateReferenceBadge();
12217
12491
  return;
12218
12492
  }
12219
12493
 
12220
12494
  if (message.type === "repl_capture") {
12495
+ const previousActiveSessionName = replActiveSessionName;
12496
+ const previousTranscript = replTranscript;
12497
+ const previousCapturedAt = replCapturedAt;
12498
+ const previousError = replError;
12499
+ const previousMessage = replMessage;
12500
+ const wasBusy = replBusy;
12501
+ let sessionsChanged = false;
12221
12502
  if (message.session) {
12222
12503
  const session = normalizeReplSession(message.session);
12223
12504
  if (session && !replSessions.some((candidate) => candidate.sessionName === session.sessionName)) {
12224
12505
  replSessions = [...replSessions, session];
12506
+ sessionsChanged = true;
12225
12507
  }
12226
12508
  }
12227
12509
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
12228
12510
  setActiveReplSession(message.activeSessionName);
12229
12511
  }
12512
+ let journalChanged = false;
12230
12513
  if (typeof message.transcript === "string") {
12231
12514
  replTranscript = trimReplTranscript(message.transcript);
12232
- updateActiveReplJournalEntryFromTranscript(
12515
+ journalChanged = updateActiveReplJournalEntryFromTranscript(
12233
12516
  typeof message.activeSessionName === "string" && message.activeSessionName.trim() ? message.activeSessionName : replActiveSessionName,
12234
12517
  replTranscript
12235
12518
  );
@@ -12238,20 +12521,28 @@
12238
12521
  replError = typeof message.replError === "string" ? message.replError : "";
12239
12522
  if (typeof message.replMessage === "string") replMessage = message.replMessage;
12240
12523
  replBusy = false;
12241
- syncActionButtons();
12242
- renderReplViewIfActive();
12524
+ const controlsChanged = wasBusy || sessionsChanged || previousActiveSessionName !== replActiveSessionName;
12525
+ if (controlsChanged) syncActionButtons();
12526
+ const viewChanged = controlsChanged
12527
+ || previousTranscript !== replTranscript
12528
+ || previousError !== replError
12529
+ || previousMessage !== replMessage
12530
+ || journalChanged
12531
+ || (!previousCapturedAt && replCapturedAt);
12532
+ if (viewChanged) renderReplViewIfActive();
12243
12533
  updateReferenceBadge();
12244
12534
  return;
12245
12535
  }
12246
12536
 
12247
12537
  if (message.type === "repl_send_ack") {
12248
12538
  replBusy = false;
12249
- replMessage = typeof message.message === "string" ? message.message : "Sent editor text to REPL.";
12539
+ replMessage = "";
12250
12540
  replError = "";
12251
12541
  if (typeof message.requestId === "string") {
12252
12542
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
12543
+ persistReplJournalEntries();
12253
12544
  }
12254
- setStatus(replMessage, "success");
12545
+ setStatus("Sent to REPL.", "success");
12255
12546
  syncActionButtons();
12256
12547
  renderReplViewIfActive({ force: true });
12257
12548
  return;
@@ -12662,6 +12953,7 @@
12662
12953
  replError = typeof message.message === "string" ? message.message : "REPL request failed.";
12663
12954
  if (typeof message.requestId === "string") {
12664
12955
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "error", output: replError, updatedAt: Date.now() } : entry);
12956
+ persistReplJournalEntries();
12665
12957
  }
12666
12958
  renderReplViewIfActive({ force: true });
12667
12959
  }