pi-studio 0.9.5 → 0.9.6

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.6] — 2026-05-19
8
+
9
+ ### Changed
10
+ - Cleaned up Studio REPL output handling: the raw tmux mirror now remains raw while REPL Studio and `studio_repl_send` results hide Studio's temp-file submission wrappers.
11
+ - Made REPL Studio session-scoped, so switching between Python, R, Julia, and other tmux sessions no longer mixes their clean records, and reloads can recover Studio-sent entries while the Studio server is still running.
12
+ - Refined Literate send so **Send selection/chunks** runs the selection, current fenced chunk, or all compatible chunks when the cursor is outside a chunk.
13
+ - Reworked REPL-mode editor controls so REPL send actions sit on their own row, show their shortcut, and **Run editor text** is visually secondary because it sends to Pi rather than the REPL.
14
+ - Normalised dropdown/menu styling in the refreshed UI so selects and custom menu triggers use neutral hover/open states instead of inconsistent accent borders or fills.
15
+
16
+ ### Added
17
+ - Added **More → Copy attach command** in the REPL pane to copy a `tmux attach -t <session>` command for the active REPL session.
18
+
19
+ ### Fixed
20
+ - R REPL parse errors no longer expose Studio's internal `.__pi_studio_code` parse wrapper.
21
+
7
22
  ## [0.9.5] — 2026-05-17
8
23
 
9
24
  ### Added
@@ -292,27 +292,33 @@
292
292
  return "raw";
293
293
  }
294
294
  })();
295
+ function normalizeReplJournalEntry(entry) {
296
+ if (!entry || typeof entry !== "object") return null;
297
+ const normalized = {
298
+ id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
299
+ requestId: typeof entry.requestId === "string" ? entry.requestId : "",
300
+ createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
301
+ updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
302
+ sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
303
+ runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
304
+ label: typeof entry.label === "string" ? entry.label : "REPL send",
305
+ mode: typeof entry.mode === "string" ? entry.mode : "raw",
306
+ prose: typeof entry.prose === "string" ? entry.prose : "",
307
+ code: typeof entry.code === "string" ? entry.code : "",
308
+ output: typeof entry.output === "string" ? entry.output : "",
309
+ beforeTranscript: "",
310
+ status: typeof entry.status === "string" ? entry.status : "sent",
311
+ skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
312
+ };
313
+ return (normalized.code.trim() || normalized.prose.trim() || normalized.output.trim()) ? normalized : null;
314
+ }
315
+
295
316
  function loadPersistedReplJournalEntries() {
296
317
  try {
297
318
  const raw = window.localStorage ? window.localStorage.getItem("piStudio.replStudioEntries.v1") : null;
298
319
  const parsed = raw ? JSON.parse(raw) : [];
299
320
  if (!Array.isArray(parsed)) return [];
300
- return parsed.map((entry) => ({
301
- id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
302
- requestId: typeof entry.requestId === "string" ? entry.requestId : "",
303
- createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
304
- updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
305
- sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
306
- runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
307
- label: typeof entry.label === "string" ? entry.label : "REPL send",
308
- mode: typeof entry.mode === "string" ? entry.mode : "raw",
309
- prose: typeof entry.prose === "string" ? entry.prose : "",
310
- code: typeof entry.code === "string" ? entry.code : "",
311
- output: typeof entry.output === "string" ? entry.output : "",
312
- beforeTranscript: "",
313
- status: typeof entry.status === "string" ? entry.status : "sent",
314
- skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
315
- })).filter((entry) => entry.code.trim() || entry.prose.trim() || entry.output.trim()).slice(-REPL_JOURNAL_MAX_ENTRIES);
321
+ return parsed.map(normalizeReplJournalEntry).filter(Boolean).slice(-REPL_JOURNAL_MAX_ENTRIES);
316
322
  } catch {
317
323
  return [];
318
324
  }
@@ -342,6 +348,47 @@
342
348
  }
343
349
  }
344
350
 
351
+ function mergeReplJournalEntries(entries) {
352
+ if (!Array.isArray(entries) || !entries.length) return false;
353
+ let changed = false;
354
+ const next = [...replJournalEntries];
355
+ for (const rawEntry of entries) {
356
+ const entry = normalizeReplJournalEntry(rawEntry);
357
+ if (!entry) continue;
358
+ const existingIndex = next.findIndex((candidate) => (
359
+ (entry.requestId && candidate.requestId === entry.requestId)
360
+ || candidate.id === entry.id
361
+ ));
362
+ if (existingIndex >= 0) {
363
+ const existing = next[existingIndex];
364
+ const merged = {
365
+ ...existing,
366
+ ...entry,
367
+ label: existing.label || entry.label,
368
+ mode: existing.mode || entry.mode,
369
+ prose: existing.prose || entry.prose,
370
+ beforeTranscript: existing.beforeTranscript || "",
371
+ createdAt: existing.createdAt || entry.createdAt,
372
+ updatedAt: Math.max(existing.updatedAt || 0, entry.updatedAt || 0),
373
+ skippedChunks: existing.skippedChunks || entry.skippedChunks,
374
+ };
375
+ if (JSON.stringify(existing) !== JSON.stringify(merged)) {
376
+ next[existingIndex] = merged;
377
+ changed = true;
378
+ }
379
+ } else {
380
+ next.push(entry);
381
+ changed = true;
382
+ }
383
+ }
384
+ if (!changed) return false;
385
+ replJournalEntries = next
386
+ .sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0))
387
+ .slice(-REPL_JOURNAL_MAX_ENTRIES);
388
+ persistReplJournalEntries();
389
+ return true;
390
+ }
391
+
345
392
  let replJournalEntries = loadPersistedReplJournalEntries();
346
393
  let activeReplJournalEntryId = "";
347
394
  let replJournalCollapsed = (() => {
@@ -925,6 +972,27 @@
925
972
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
926
973
  }
927
974
 
975
+ function isMacShortcutPlatform() {
976
+ try {
977
+ const platform = String((navigator && navigator.platform) || "");
978
+ return /Mac|iPhone|iPad|iPod/i.test(platform);
979
+ } catch {
980
+ return true;
981
+ }
982
+ }
983
+
984
+ function getStudioShortcutLabel(kind) {
985
+ const mac = isMacShortcutPlatform();
986
+ if (kind === "repl-send") return mac ? "⇧⌘↵" : "Ctrl+Shift+Enter";
987
+ if (kind === "run") return mac ? "⌘↵" : "Ctrl+Enter";
988
+ return "";
989
+ }
990
+
991
+ function withStudioShortcutLabel(label, kind) {
992
+ const shortcut = getStudioShortcutLabel(kind);
993
+ return shortcut ? (label + " " + shortcut) : label;
994
+ }
995
+
928
996
  function setReplSendMode(mode) {
929
997
  replSendMode = normalizeReplSendMode(mode);
930
998
  if (replSendModeSelect) replSendModeSelect.value = replSendMode;
@@ -1011,9 +1079,18 @@
1011
1079
  function setActiveReplSession(sessionName) {
1012
1080
  const name = String(sessionName || "").trim();
1013
1081
  if (!name) {
1082
+ if (replActiveSessionName) {
1083
+ replTranscript = "";
1084
+ replCapturedAt = 0;
1085
+ }
1014
1086
  replActiveSessionName = "";
1015
1087
  return;
1016
1088
  }
1089
+ if (replActiveSessionName && replActiveSessionName !== name) {
1090
+ replTranscript = "";
1091
+ replCapturedAt = 0;
1092
+ activeReplJournalEntryId = "";
1093
+ }
1017
1094
  replActiveSessionName = name;
1018
1095
  }
1019
1096
 
@@ -1272,7 +1349,7 @@
1272
1349
 
1273
1350
  const allBlocks = parseMarkdownCodeFences(range.raw);
1274
1351
  if (allBlocks.length) {
1275
- return { error: "Place the cursor inside a code chunk, select text, or use Run all chunks. Switch send mode to Raw send to send the full editor." };
1352
+ return buildAllChunksReplSendPayload();
1276
1353
  }
1277
1354
 
1278
1355
  return {
@@ -1410,6 +1487,24 @@
1410
1487
  return isEcho ? lines.slice(1).join("\n").replace(/^\s+/, "") : value;
1411
1488
  }
1412
1489
 
1490
+ function stripStudioReplSubmissionEcho(delta) {
1491
+ let value = String(delta || "").replace(/^\s+/, "");
1492
+ // The raw mirror below remains raw; REPL Studio cards hide only the
1493
+ // temp-file wrapper used to submit multiline snippets safely. The
1494
+ // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
1495
+ const submissionEchoPatterns = [
1496
+ /^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
1497
+ /^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
1498
+ /^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
1499
+ /^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
1500
+ /^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
1501
+ ];
1502
+ for (const pattern of submissionEchoPatterns) {
1503
+ value = value.replace(pattern, "");
1504
+ }
1505
+ return value.replace(/^(?:\s*\n)+/, "");
1506
+ }
1507
+
1413
1508
  function stripTrailingReplPromptsFromOutput(output) {
1414
1509
  const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
1415
1510
  while (lines.length > 0 && /^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*$/.test(lines[lines.length - 1] || "")) {
@@ -1426,7 +1521,10 @@
1426
1521
  }
1427
1522
 
1428
1523
  function cleanReplCapturedOutput(delta, entry) {
1429
- return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(stripSubsequentReplInputsFromOutput(stripSubmittedCodeEchoFromReplDelta(delta, entry))));
1524
+ const withoutSubmissionEcho = stripStudioReplSubmissionEcho(delta);
1525
+ const withoutCodeEcho = stripSubmittedCodeEchoFromReplDelta(withoutSubmissionEcho, entry);
1526
+ const withoutLaterInputs = stripSubsequentReplInputsFromOutput(withoutCodeEcho);
1527
+ return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(withoutLaterInputs));
1430
1528
  }
1431
1529
 
1432
1530
  function updateActiveReplJournalEntryFromTranscript(sessionName, transcript) {
@@ -1452,13 +1550,17 @@
1452
1550
  return fence + (language ? language : "") + "\n" + value.replace(/\s+$/, "") + "\n" + fence;
1453
1551
  }
1454
1552
 
1455
- function buildReplJournalMarkdown() {
1456
- const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString(), ""];
1457
- if (!replJournalEntries.length) {
1458
- lines.push("_No REPL Studio entries yet._");
1553
+ function buildReplJournalMarkdown(entries) {
1554
+ const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1555
+ const sessionName = getActiveReplJournalSessionName();
1556
+ const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString()];
1557
+ if (sessionName) lines.push("Session: `" + sessionName + "`");
1558
+ lines.push("");
1559
+ if (!visibleEntries.length) {
1560
+ lines.push(sessionName ? ("_No REPL Studio entries for `" + sessionName + "` yet._") : "_No REPL Studio entries yet._");
1459
1561
  return lines.join("\n");
1460
1562
  }
1461
- replJournalEntries.forEach((entry, index) => {
1563
+ visibleEntries.forEach((entry, index) => {
1462
1564
  lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
1463
1565
  lines.push("");
1464
1566
  lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toLocaleString());
@@ -1485,53 +1587,62 @@
1485
1587
  }
1486
1588
 
1487
1589
  async function copyReplJournalToClipboard() {
1488
- if (!replJournalEntries.length) {
1489
- setStatus("No REPL Studio entries to copy yet.", "warning");
1590
+ const entries = getVisibleReplJournalEntries();
1591
+ if (!entries.length) {
1592
+ setStatus("No REPL Studio entries to copy for this session yet.", "warning");
1490
1593
  return;
1491
1594
  }
1492
- if (await writeTextToClipboard(buildReplJournalMarkdown())) {
1493
- setStatus("Copied REPL Studio as Markdown.", "success");
1595
+ if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
1596
+ setStatus("Copied REPL Studio session as Markdown.", "success");
1494
1597
  } else {
1495
1598
  setStatus("Clipboard write failed.", "warning");
1496
1599
  }
1497
1600
  }
1498
1601
 
1499
1602
  function exportReplJournalMarkdown() {
1500
- if (!replJournalEntries.length) {
1501
- setStatus("No REPL Studio entries to export yet.", "warning");
1603
+ const entries = getVisibleReplJournalEntries();
1604
+ if (!entries.length) {
1605
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
1502
1606
  return;
1503
1607
  }
1504
- const blob = new Blob([buildReplJournalMarkdown()], { type: "text/markdown;charset=utf-8" });
1608
+ const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
1505
1609
  const blobUrl = URL.createObjectURL(blob);
1506
1610
  const link = document.createElement("a");
1507
1611
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1612
+ const sessionSlug = getActiveReplJournalSessionName().replace(/[^-_.A-Za-z0-9]+/g, "-");
1508
1613
  link.href = blobUrl;
1509
- link.download = "repl-studio-" + stamp + ".md";
1614
+ link.download = "repl-studio" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
1510
1615
  document.body.appendChild(link);
1511
1616
  link.click();
1512
1617
  link.remove();
1513
1618
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1514
- setStatus("Exported REPL Studio Markdown.", "success");
1619
+ setStatus("Exported REPL Studio session Markdown.", "success");
1515
1620
  }
1516
1621
 
1517
1622
  function clearReplJournal() {
1518
- replJournalEntries = [];
1623
+ const sessionName = getActiveReplJournalSessionName();
1624
+ if (sessionName) {
1625
+ replJournalEntries = replJournalEntries.filter((entry) => entry.sessionName !== sessionName);
1626
+ } else {
1627
+ replJournalEntries = [];
1628
+ }
1519
1629
  activeReplJournalEntryId = "";
1520
1630
  persistReplJournalEntries();
1521
- setStatus("Cleared REPL Studio.", "success");
1631
+ setStatus(sessionName ? "Cleared REPL Studio for this session." : "Cleared REPL Studio.", "success");
1522
1632
  renderReplViewIfActive({ force: true });
1523
1633
  }
1524
1634
 
1525
1635
  function loadReplJournalIntoEditor() {
1526
- if (!replJournalEntries.length) {
1527
- setStatus("No REPL Studio entries to load yet.", "warning");
1636
+ const entries = getVisibleReplJournalEntries();
1637
+ if (!entries.length) {
1638
+ setStatus("No REPL Studio entries to load for this session yet.", "warning");
1528
1639
  return;
1529
1640
  }
1530
- const markdown = buildReplJournalMarkdown();
1641
+ const markdown = buildReplJournalMarkdown(entries);
1531
1642
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1532
1643
  setSourceState({ source: "blank", label: "REPL Studio", path: null });
1533
1644
  setEditorLanguage("markdown");
1534
- setStatus("Loaded REPL Studio into editor.", "success");
1645
+ setStatus("Loaded REPL Studio session into editor.", "success");
1535
1646
  }
1536
1647
 
1537
1648
  function addSelectedReplJournalNote() {
@@ -2101,13 +2212,16 @@
2101
2212
  const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2102
2213
  if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
2103
2214
  if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
2104
- if (!isEditorOnlyMode && sendReplBtn) actionLineOneEl.appendChild(sendReplBtn);
2105
- if (!isEditorOnlyMode && replSendModeSelect) actionLineOneEl.appendChild(replSendModeSelect);
2215
+ const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2216
+ replActionLineEl.hidden = true;
2217
+ if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2218
+ if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2106
2219
  const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2107
2220
  actionLineTwoEl.appendChild(copyDraftBtn);
2108
2221
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
2109
2222
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2110
2223
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
2224
+ if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
2111
2225
  actionsEl.appendChild(actionLineTwoEl);
2112
2226
 
2113
2227
  const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
@@ -3125,6 +3239,7 @@
3125
3239
  && event.shiftKey
3126
3240
  && activePane === "left"
3127
3241
  && !isEditorOnlyMode
3242
+ && rightView === "repl"
3128
3243
  ) {
3129
3244
  event.preventDefault();
3130
3245
  if (sendReplBtn && !sendReplBtn.hidden && !sendReplBtn.disabled) {
@@ -5012,6 +5127,18 @@
5012
5127
  }
5013
5128
  return;
5014
5129
  }
5130
+ if (action === "copy-attach-command") {
5131
+ const session = getActiveReplSession();
5132
+ const text = getReplAttachCommand(session);
5133
+ if (!text.trim()) {
5134
+ setStatus("Start or select a REPL session first.", "warning");
5135
+ return;
5136
+ }
5137
+ void writeTextToClipboard(text).then((ok) => {
5138
+ setStatus(ok ? "Copied tmux attach command." : "Clipboard write failed.", ok ? "success" : "warning");
5139
+ });
5140
+ return;
5141
+ }
5015
5142
  if (action === "run-all-chunks") {
5016
5143
  sendEditorTextToRepl({ action: "all-chunks" });
5017
5144
  return;
@@ -5333,8 +5460,9 @@
5333
5460
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
5334
5461
  return;
5335
5462
  }
5336
- if (exportingReplJournal && !replJournalEntries.length) {
5337
- setStatus("No REPL Studio entries to export yet.", "warning");
5463
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
5464
+ if (exportingReplJournal && !replJournalExportEntries.length) {
5465
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
5338
5466
  return;
5339
5467
  }
5340
5468
 
@@ -5345,7 +5473,7 @@
5345
5473
  }
5346
5474
 
5347
5475
  const markdown = exportingReplJournal
5348
- ? buildReplJournalMarkdown()
5476
+ ? buildReplJournalMarkdown(replJournalExportEntries)
5349
5477
  : (rightView === "editor-preview"
5350
5478
  ? prepareEditorTextForPdfExport(sourceTextEl.value)
5351
5479
  : prepareEditorTextForPreview(latestResponseMarkdown));
@@ -5505,13 +5633,14 @@
5505
5633
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
5506
5634
  return;
5507
5635
  }
5508
- if (exportingReplJournal && !replJournalEntries.length) {
5509
- setStatus("No REPL Studio entries to export yet.", "warning");
5636
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
5637
+ if (exportingReplJournal && !replJournalExportEntries.length) {
5638
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
5510
5639
  return;
5511
5640
  }
5512
5641
 
5513
5642
  const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
5514
- const markdown = exportingReplJournal ? buildReplJournalMarkdown() : (htmlArtifactSource || (rightView === "editor-preview"
5643
+ const markdown = exportingReplJournal ? buildReplJournalMarkdown(replJournalExportEntries) : (htmlArtifactSource || (rightView === "editor-preview"
5515
5644
  ? prepareEditorTextForHtmlExport(sourceTextEl.value)
5516
5645
  : prepareEditorTextForPreview(latestResponseMarkdown)));
5517
5646
  if (!markdown || !markdown.trim()) {
@@ -6009,8 +6138,24 @@
6009
6138
  return remaining < 56;
6010
6139
  }
6011
6140
 
6141
+ function getActiveReplJournalSessionName() {
6142
+ return String(replActiveSessionName || "").trim();
6143
+ }
6144
+
6145
+ function getVisibleReplJournalEntries() {
6146
+ const sessionName = getActiveReplJournalSessionName();
6147
+ if (!sessionName) return replJournalEntries;
6148
+ return replJournalEntries.filter((entry) => entry.sessionName === sessionName);
6149
+ }
6150
+
6151
+ function getHiddenReplJournalEntryCount() {
6152
+ const sessionName = getActiveReplJournalSessionName();
6153
+ if (!sessionName) return 0;
6154
+ return replJournalEntries.filter((entry) => entry.sessionName && entry.sessionName !== sessionName).length;
6155
+ }
6156
+
6012
6157
  function isReplJournalExpanded() {
6013
- return rightView === "repl" && !replJournalCollapsed && replJournalEntries.length > 0;
6158
+ return rightView === "repl" && !replJournalCollapsed && getVisibleReplJournalEntries().length > 0;
6014
6159
  }
6015
6160
 
6016
6161
  function shouldAutoStickReplView() {
@@ -6196,23 +6341,26 @@
6196
6341
 
6197
6342
  function buildReplStudioActionsHtml() {
6198
6343
  if (replJournalCollapsed) return "";
6199
- const hasEntries = replJournalEntries.length > 0;
6344
+ const hasEntries = getVisibleReplJournalEntries().length > 0;
6200
6345
  const buttons = "<button type='button' data-repl-action='load-journal'" + (hasEntries ? "" : " disabled") + ">Load in editor</button>"
6201
6346
  + "<button type='button' data-repl-action='copy-journal'" + (hasEntries ? "" : " disabled") + ">Copy Markdown</button>"
6202
6347
  + "<button type='button' data-repl-action='export-journal'" + (hasEntries ? "" : " disabled") + ">Export .md</button>"
6203
- + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear</button>";
6348
+ + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear session</button>";
6204
6349
  return "<div class='repl-studio-below-actions'><div class='repl-journal-actions'>" + buttons + "</div></div>";
6205
6350
  }
6206
6351
 
6207
6352
  function buildReplJournalHtml(transcript) {
6208
- const hasEntries = replJournalEntries.length > 0;
6209
- const entryCount = replJournalEntries.length;
6353
+ const visibleEntries = getVisibleReplJournalEntries();
6354
+ const hasEntries = visibleEntries.length > 0;
6355
+ const entryCount = visibleEntries.length;
6356
+ const hiddenEntryCount = getHiddenReplJournalEntryCount();
6357
+ const sessionName = getActiveReplJournalSessionName();
6210
6358
  const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
6211
6359
  const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show REPL Studio" : "Hide REPL Studio") + "</button>";
6212
6360
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
6213
6361
  const summaryText = hasEntries
6214
- ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
6215
- : "Studio-sent code and notes will appear here.";
6362
+ ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
6363
+ : (sessionName ? "No Studio entries for " + sessionName + "." : "Studio-sent code and notes will appear here.");
6216
6364
  if (replJournalCollapsed) {
6217
6365
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
6218
6366
  + "<div class='repl-journal-compact-row'>"
@@ -6221,12 +6369,12 @@
6221
6369
  + "</div>"
6222
6370
  + "</section>";
6223
6371
  }
6224
- const omitted = Math.max(0, replJournalEntries.length - 12);
6372
+ const omitted = Math.max(0, visibleEntries.length - 12);
6225
6373
  const bannerText = extractReplStudioBanner(transcript, getActiveReplRuntime());
6226
6374
  const banner = bannerText
6227
6375
  ? "<pre class='repl-studio-banner'>" + escapeHtml(bannerText) + "</pre>"
6228
6376
  : "";
6229
- const cards = replJournalEntries.slice(-12).map((entry) => {
6377
+ const cards = visibleEntries.slice(-12).map((entry) => {
6230
6378
  const meta = buildReplStudioMeta(entry);
6231
6379
  const prompt = getReplStudioPrompt(entry.runtime);
6232
6380
  const codeText = String(entry.code || "").trimEnd();
@@ -6252,11 +6400,17 @@
6252
6400
  + pending
6253
6401
  + "</article>";
6254
6402
  }).join("");
6403
+ const emptyText = sessionName
6404
+ ? (String(transcript || "").trim()
6405
+ ? "No REPL Studio entries for this tmux session yet. The raw tmux mirror below still has this session's history; send code from Studio to build a clean record."
6406
+ : "No REPL Studio entries for this tmux session yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
6407
+ : "No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
6255
6408
  const terminalContent = banner
6256
- + (hasEntries ? cards : "<div class='repl-studio-empty'>No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.</div>");
6409
+ + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
6257
6410
  return "<section class='repl-journal'>"
6258
- + "<div class='repl-journal-header'><div><h3>REPL Studio</h3><p>Clean collaborative Studio REPL record. The raw tmux mirror is available below.</p></div>" + toggleActions + "</div>"
6259
- + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
6411
+ + "<div class='repl-journal-header'><div><h3>REPL Studio</h3><p>Clean collaborative Studio REPL record for the selected tmux session. The raw tmux mirror is available below.</p></div>" + toggleActions + "</div>"
6412
+ + (hiddenEntryCount ? "<div class='repl-journal-omitted'>" + escapeHtml(String(hiddenEntryCount)) + " entr" + (hiddenEntryCount === 1 ? "y" : "ies") + " from other REPL sessions hidden.</div>" : "")
6413
+ + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
6260
6414
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
6261
6415
  + "</section>";
6262
6416
  }
@@ -6284,6 +6438,11 @@
6284
6438
  + "</section>";
6285
6439
  }
6286
6440
 
6441
+ function getReplAttachCommand(session) {
6442
+ if (!session || !session.sessionName) return "";
6443
+ return "tmux attach -t " + String(session.sessionName || "");
6444
+ }
6445
+
6287
6446
  function buildReplPanelHtml() {
6288
6447
  const runtimeOptions = [
6289
6448
  ["shell", "Shell"],
@@ -6319,6 +6478,7 @@
6319
6478
  + "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
6320
6479
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
6321
6480
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
6481
+ + "<button type='button' data-repl-action='copy-attach-command'" + (activeSession ? "" : " disabled") + " title='Copy command for attaching to this tmux session in a terminal.'>Copy attach command</button>"
6322
6482
  + "<button type='button' data-repl-action='run-all-chunks'" + (canSendToActiveSession ? "" : " disabled") + " title='Literate send: send all fenced code chunks matching the active REPL runtime.'>Run all chunks</button>"
6323
6483
  + "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to REPL Studio (Literate send) without sending it to the runtime.'>Add note</button>"
6324
6484
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
@@ -6596,8 +6756,9 @@
6596
6756
 
6597
6757
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6598
6758
  const exportingReplJournal = rightView === "repl";
6759
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6599
6760
  const exportText = exportingReplJournal
6600
- ? (replJournalEntries.length ? buildReplJournalMarkdown() : "")
6761
+ ? (replJournalExportEntries.length ? buildReplJournalMarkdown(replJournalExportEntries) : "")
6601
6762
  : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown);
6602
6763
  const canExportPreview = (rightPaneShowsPreview || exportingReplJournal) && Boolean(String(exportText || "").trim());
6603
6764
  const htmlArtifactExportSource = canExportPreview && !exportingReplJournal ? getRightPaneHtmlArtifactSource() : "";
@@ -6609,8 +6770,8 @@
6609
6770
  : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
6610
6771
  if (rightView === "trace") {
6611
6772
  exportPdfBtn.title = "Working view does not support preview export.";
6612
- } else if (exportingReplJournal && !replJournalEntries.length) {
6613
- exportPdfBtn.title = "No REPL Studio entries to export yet.";
6773
+ } else if (exportingReplJournal && !replJournalExportEntries.length) {
6774
+ exportPdfBtn.title = "No REPL Studio entries to export for this session yet.";
6614
6775
  } else if (rightView === "markdown") {
6615
6776
  exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
6616
6777
  } else if (!canExportPreview) {
@@ -6640,7 +6801,7 @@
6640
6801
  ? (exportingReplJournal
6641
6802
  ? "Choose a format and export REPL Studio."
6642
6803
  : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
6643
- : (exportingReplJournal ? "No REPL Studio entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
6804
+ : (exportingReplJournal ? "No REPL Studio entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
6644
6805
  }
6645
6806
  if (!canExportPreview || previewExportInProgress) {
6646
6807
  closeExportPreviewMenu();
@@ -13185,7 +13346,7 @@
13185
13346
  if (isEditorOnlyMode) {
13186
13347
  if (sendRunBtn) {
13187
13348
  sendRunBtn.textContent = "Run editor text";
13188
- sendRunBtn.classList.remove("request-stop-active");
13349
+ sendRunBtn.classList.remove("request-stop-active", "repl-secondary-action");
13189
13350
  sendRunBtn.disabled = true;
13190
13351
  sendRunBtn.title = "Run is unavailable in editor-only mode.";
13191
13352
  }
@@ -13198,6 +13359,9 @@
13198
13359
  if (sendReplBtn) {
13199
13360
  sendReplBtn.hidden = true;
13200
13361
  sendReplBtn.disabled = true;
13362
+ sendReplBtn.classList.remove("repl-primary-action");
13363
+ const replActionLine = sendReplBtn.closest(".repl-action-line");
13364
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = true;
13201
13365
  }
13202
13366
  if (replSendModeSelect) {
13203
13367
  replSendModeSelect.hidden = true;
@@ -13218,11 +13382,12 @@
13218
13382
  }
13219
13383
 
13220
13384
  if (sendRunBtn) {
13221
- sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
13385
+ sendRunBtn.textContent = directIsStop ? "Stop" : (rightView === "repl" ? withStudioShortcutLabel("Run editor text", "run") : "Run editor text");
13222
13386
  sendRunBtn.classList.toggle("request-stop-active", directIsStop);
13387
+ sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
13223
13388
  sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
13224
13389
  const replHint = rightView === "repl" && getActiveReplSession()
13225
- ? " Includes active REPL identity for prompts that refer to it."
13390
+ ? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
13226
13391
  : "";
13227
13392
  sendRunBtn.title = directIsStop
13228
13393
  ? "Stop the active run. Shortcut: Esc."
@@ -13242,22 +13407,27 @@
13242
13407
  : "Queue steering is available while Run editor text is active.";
13243
13408
  }
13244
13409
 
13410
+ const hasReplSession = Boolean(getActiveReplSession());
13245
13411
  if (sendReplBtn) {
13246
- const hasSession = Boolean(getActiveReplSession());
13247
- sendReplBtn.hidden = rightView !== "repl";
13248
- sendReplBtn.disabled = wsState === "Disconnected" || uiBusy || replBusy || !hasSession;
13249
- sendReplBtn.title = hasSession
13412
+ const showReplSend = rightView === "repl";
13413
+ sendReplBtn.hidden = !showReplSend;
13414
+ sendReplBtn.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy || !hasReplSession;
13415
+ sendReplBtn.classList.toggle("repl-primary-action", showReplSend);
13416
+ sendReplBtn.textContent = showReplSend ? withStudioShortcutLabel(replSendMode === "literate" ? "Send selection/chunks" : "Send to REPL", "repl-send") : "Send to REPL";
13417
+ sendReplBtn.title = hasReplSession
13250
13418
  ? (replSendMode === "literate"
13251
- ? "Literate send: selected code/prose, or the current fenced code chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
13419
+ ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
13252
13420
  : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
13253
13421
  : "Start or select a REPL session in the right pane first.";
13422
+ const replActionLine = sendReplBtn.closest(".repl-action-line");
13423
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
13254
13424
  }
13255
13425
  if (replSendModeSelect) {
13256
13426
  replSendModeSelect.hidden = rightView !== "repl";
13257
13427
  replSendModeSelect.disabled = wsState === "Disconnected" || uiBusy || replBusy;
13258
13428
  replSendModeSelect.value = replSendMode;
13259
13429
  replSendModeSelect.title = replSendMode === "literate"
13260
- ? "Literate send: Send to REPL uses the selection/current fenced code chunk."
13430
+ ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
13261
13431
  : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
13262
13432
  }
13263
13433
 
@@ -13607,6 +13777,7 @@
13607
13777
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
13608
13778
  setActiveReplSession(message.activeSessionName);
13609
13779
  }
13780
+ const journalChanged = mergeReplJournalEntries(message.journalEntries);
13610
13781
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
13611
13782
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13612
13783
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
@@ -13621,6 +13792,7 @@
13621
13792
  || previousTranscript !== replTranscript
13622
13793
  || previousError !== replError
13623
13794
  || previousMessage !== replMessage
13795
+ || journalChanged
13624
13796
  || (!previousCapturedAt && replCapturedAt);
13625
13797
  if (viewChanged) renderReplViewIfActive();
13626
13798
  updateReferenceBadge();
@@ -13632,9 +13804,10 @@
13632
13804
  setActiveReplSession(message.sessionName);
13633
13805
  }
13634
13806
  const changed = recordReplToolSend(message);
13807
+ const journalChanged = mergeReplJournalEntries(message.journalEntries);
13635
13808
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
13636
13809
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13637
- if (changed) renderReplViewIfActive({ force: true });
13810
+ if (changed || journalChanged) renderReplViewIfActive({ force: true });
13638
13811
  updateReferenceBadge();
13639
13812
  return;
13640
13813
  }
@@ -13657,13 +13830,13 @@
13657
13830
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
13658
13831
  setActiveReplSession(message.activeSessionName);
13659
13832
  }
13660
- let journalChanged = false;
13833
+ let journalChanged = mergeReplJournalEntries(message.journalEntries);
13661
13834
  if (typeof message.transcript === "string") {
13662
13835
  replTranscript = trimReplTranscript(message.transcript);
13663
13836
  journalChanged = updateActiveReplJournalEntryFromTranscript(
13664
13837
  typeof message.activeSessionName === "string" && message.activeSessionName.trim() ? message.activeSessionName : replActiveSessionName,
13665
13838
  replTranscript
13666
- );
13839
+ ) || journalChanged;
13667
13840
  }
13668
13841
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13669
13842
  replError = typeof message.replError === "string" ? message.replError : "";
@@ -13686,6 +13859,7 @@
13686
13859
  replBusy = false;
13687
13860
  replMessage = "";
13688
13861
  replError = "";
13862
+ mergeReplJournalEntries(message.journalEntries);
13689
13863
  if (typeof message.requestId === "string") {
13690
13864
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
13691
13865
  persistReplJournalEntries();
package/client/studio.css CHANGED
@@ -177,7 +177,8 @@
177
177
  }
178
178
 
179
179
  #sendRunBtn,
180
- #queueSteerBtn {
180
+ #queueSteerBtn,
181
+ #sendReplBtn {
181
182
  min-width: 10rem;
182
183
  display: inline-flex;
183
184
  justify-content: center;
@@ -190,8 +191,9 @@
190
191
  align-items: center;
191
192
  }
192
193
 
193
- #sendRunBtn:not(:disabled):not(.request-stop-active),
194
+ #sendRunBtn:not(:disabled):not(.request-stop-active):not(.repl-secondary-action),
194
195
  #queueSteerBtn:not(:disabled),
196
+ #sendReplBtn.repl-primary-action:not(:disabled),
195
197
  #loadResponseBtn:not(:disabled):not([hidden]) {
196
198
  background: var(--accent);
197
199
  border-color: var(--accent);
@@ -199,8 +201,9 @@
199
201
  font-weight: 600;
200
202
  }
201
203
 
202
- #sendRunBtn:not(:disabled):not(.request-stop-active):hover,
204
+ #sendRunBtn:not(:disabled):not(.request-stop-active):not(.repl-secondary-action):hover,
203
205
  #queueSteerBtn:not(:disabled):hover,
206
+ #sendReplBtn.repl-primary-action:not(:disabled):hover,
204
207
  #loadResponseBtn:not(:disabled):not([hidden]):hover {
205
208
  filter: brightness(0.95);
206
209
  }
@@ -546,12 +549,33 @@
546
549
  min-width: 0;
547
550
  }
548
551
 
552
+ .source-actions-row[hidden],
553
+ .studio-refresh-action-line[hidden] {
554
+ display: none !important;
555
+ }
556
+
549
557
  .source-actions button,
550
558
  .source-actions select {
551
559
  padding: 6px 9px;
552
560
  font-size: 12px;
553
561
  }
554
562
 
563
+ .source-actions select {
564
+ border-color: transparent;
565
+ background: transparent;
566
+ }
567
+
568
+ .source-actions select:not(:disabled):hover,
569
+ .source-actions select:focus {
570
+ border-color: transparent;
571
+ background: var(--studio-header-action-hover-bg, var(--panel-2));
572
+ }
573
+
574
+ #sendReplBtn[hidden],
575
+ #replSendModeSelect[hidden] {
576
+ display: none !important;
577
+ }
578
+
555
579
  .resource-dir-btn {
556
580
  padding: 4px 10px;
557
581
  font-size: 12px;
@@ -2780,11 +2804,11 @@
2780
2804
  cursor: pointer;
2781
2805
  user-select: none;
2782
2806
  list-style: none;
2783
- border: 1px solid var(--control-border);
2807
+ border: 1px solid transparent;
2784
2808
  border-radius: 6px;
2785
2809
  padding: 4px 10px;
2786
2810
  color: var(--text);
2787
- background: var(--button-bg, var(--panel-2));
2811
+ background: transparent;
2788
2812
  }
2789
2813
 
2790
2814
  .repl-more-controls summary::-webkit-details-marker {
@@ -2798,9 +2822,19 @@
2798
2822
  font-size: 11px;
2799
2823
  }
2800
2824
 
2825
+ .repl-controls select {
2826
+ border-color: transparent;
2827
+ background: transparent;
2828
+ }
2829
+
2830
+ .repl-controls select:not(:disabled):hover,
2831
+ .repl-controls select:focus,
2832
+ .repl-more-controls summary:hover,
2833
+ .repl-more-controls summary:focus,
2801
2834
  .repl-more-controls[open] summary {
2802
- border-color: var(--accent);
2803
- background: var(--accent-soft);
2835
+ border-color: transparent;
2836
+ background: var(--studio-header-action-hover-bg, var(--panel-2));
2837
+ color: var(--text);
2804
2838
  }
2805
2839
 
2806
2840
  .repl-more-menu {
@@ -3815,11 +3849,15 @@
3815
3849
  }
3816
3850
 
3817
3851
  body.studio-ui-refresh #leftSectionHeader select:hover,
3852
+ body.studio-ui-refresh #leftSectionHeader select:focus,
3818
3853
  body.studio-ui-refresh #leftSectionHeader button:not(:disabled):hover,
3819
3854
  body.studio-ui-refresh #rightSectionHeader select:hover,
3855
+ body.studio-ui-refresh #rightSectionHeader select:focus,
3820
3856
  body.studio-ui-refresh #rightSectionHeader button:not(:disabled):hover,
3821
3857
  body.studio-ui-refresh .studio-refresh-toolbar button:not(:disabled):hover,
3822
- body.studio-ui-refresh .studio-refresh-toolbar select:not(:disabled):hover {
3858
+ body.studio-ui-refresh .studio-refresh-toolbar select:not(:disabled):hover,
3859
+ body.studio-ui-refresh .studio-refresh-toolbar select:focus {
3860
+ border-color: transparent;
3823
3861
  background: var(--studio-header-action-hover-bg, var(--panel-2));
3824
3862
  }
3825
3863
 
@@ -3939,8 +3977,8 @@
3939
3977
  body.studio-zen-mode #exportPdfBtn,
3940
3978
  body.studio-zen-mode .studio-refresh-tool-tab,
3941
3979
  body.studio-zen-mode .studio-refresh-toolbar-state,
3942
- body.studio-zen-mode .studio-refresh-toolbar-actions .studio-refresh-action-line:nth-child(n+2),
3943
- body.studio-zen-mode .source-actions-row:nth-child(n+2),
3980
+ body.studio-zen-mode .studio-refresh-toolbar-actions .studio-refresh-action-line:nth-child(n+3),
3981
+ body.studio-zen-mode .source-actions-row:nth-child(n+3),
3944
3982
  body.studio-zen-mode #copyDraftBtn,
3945
3983
  body.studio-zen-mode #openCompanionBtn,
3946
3984
  body.studio-zen-mode #sendEditorBtn {
@@ -4097,13 +4135,13 @@
4097
4135
  font-size: 12px;
4098
4136
  }
4099
4137
 
4100
- body.studio-ui-refresh .studio-refresh-toolbar button:not(#sendRunBtn):not(#queueSteerBtn):not(.request-stop-active),
4138
+ body.studio-ui-refresh .studio-refresh-toolbar button:not(#sendRunBtn):not(#queueSteerBtn):not(#sendReplBtn):not(.request-stop-active),
4101
4139
  body.studio-ui-refresh .studio-refresh-toolbar select {
4102
4140
  color: color-mix(in srgb, var(--text) 72%, var(--muted));
4103
4141
  font-weight: 500;
4104
4142
  }
4105
4143
 
4106
- body.studio-ui-refresh .studio-refresh-toolbar button:not(#sendRunBtn):not(#queueSteerBtn):not(.request-stop-active):hover,
4144
+ body.studio-ui-refresh .studio-refresh-toolbar button:not(#sendRunBtn):not(#queueSteerBtn):not(#sendReplBtn):not(.request-stop-active):hover,
4107
4145
  body.studio-ui-refresh .studio-refresh-toolbar select:not(:disabled):hover {
4108
4146
  color: var(--text);
4109
4147
  }
@@ -4229,6 +4267,7 @@
4229
4267
 
4230
4268
  body.studio-ui-refresh #sendRunBtn,
4231
4269
  body.studio-ui-refresh #queueSteerBtn,
4270
+ body.studio-ui-refresh #sendReplBtn,
4232
4271
  body.studio-ui-refresh #loadResponseBtn:not([hidden]) {
4233
4272
  height: 28px;
4234
4273
  min-height: 28px;
@@ -4239,7 +4278,8 @@
4239
4278
  }
4240
4279
 
4241
4280
  body.studio-ui-refresh #sendRunBtn,
4242
- body.studio-ui-refresh #queueSteerBtn {
4281
+ body.studio-ui-refresh #queueSteerBtn,
4282
+ body.studio-ui-refresh #sendReplBtn {
4243
4283
  min-width: 8.6rem;
4244
4284
  }
4245
4285
 
package/index.ts CHANGED
@@ -125,6 +125,22 @@ interface StudioReplSessionInfo {
125
125
  source: "studio" | "pi-repl" | "tmux";
126
126
  }
127
127
 
128
+ interface StudioReplJournalEntry {
129
+ id: string;
130
+ requestId: string;
131
+ createdAt: number;
132
+ updatedAt: number;
133
+ sessionName: string;
134
+ runtime: StudioReplRuntime | "unknown";
135
+ label: string;
136
+ mode: "raw" | "literate" | "agent";
137
+ prose: string;
138
+ code: string;
139
+ output: string;
140
+ status: "sent" | "captured" | "timeout" | "error" | "note";
141
+ skippedChunks: number;
142
+ }
143
+
128
144
  interface PreparedStudioPdfExport {
129
145
  pdf: Buffer;
130
146
  filename: string;
@@ -466,6 +482,7 @@ const STUDIO_REPL_CAPTURE_LINES = 800;
466
482
  const STUDIO_REPL_SEND_MAX_CHARS = 200_000;
467
483
  const STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS = 20_000;
468
484
  const STUDIO_REPL_SEND_MAX_TIMEOUT_MS = 120_000;
485
+ const STUDIO_REPL_JOURNAL_MAX_ENTRIES = 300;
469
486
  const STUDIO_REPL_CONTROL_ROOT = join(tmpdir(), "pi-studio-repl");
470
487
  const STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES = 2_000_000;
471
488
  const STUDIO_PANDOC_TIMEOUT_MS = readStudioPositiveEnvMs("PI_STUDIO_PANDOC_TIMEOUT_MS", 120_000, 5_000, 15 * 60_000);
@@ -651,7 +668,7 @@ $body$
651
668
  let studioPersistentStateCache: StudioPersistentState | null = null;
652
669
  let studioPersistentStateQueue: Promise<void> = Promise.resolve();
653
670
  let transientStudioDocuments: Map<string, { document: InitialStudioDocument; createdAt: number }> = new Map();
654
- const studioReplControlSubmissionLabels = new Map<string, string>();
671
+ let studioReplJournalEntries: StudioReplJournalEntry[] = [];
655
672
 
656
673
  function createEmptyStudioPersistentState(): StudioPersistentState {
657
674
  return {
@@ -7750,23 +7767,6 @@ function listStudioReplSessions(): { tmuxAvailable: boolean; sessions: StudioRep
7750
7767
  return { tmuxAvailable: true, sessions };
7751
7768
  }
7752
7769
 
7753
- function getStudioReplPromptPrefix(line: string): string {
7754
- const source = String(line || "");
7755
- const match = source.match(/^(\s*(?:(?:In \[\d+\]:)|(?:\.\.\.)|(?:>>>)|(?:julia>)|(?:ghci>)|(?:Prelude>)|(?:\*?[A-Za-z0-9_.:]+>)|(?:[^\s>]+=>)|(?:>)|(?:\+))\s*)/);
7756
- return match ? (match[1] || "") : "";
7757
- }
7758
-
7759
- function sanitizeStudioReplTranscript(transcript: string): string {
7760
- let value = String(transcript || "");
7761
- for (const [sourceFile, label] of studioReplControlSubmissionLabels) {
7762
- if (!value.includes(sourceFile)) continue;
7763
- const escaped = sourceFile.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7764
- const linePattern = new RegExp(`^.*${escaped}.*$`, "gm");
7765
- value = value.replace(linePattern, (line) => `${getStudioReplPromptPrefix(line)}${label}`.trimEnd());
7766
- }
7767
- return value.replace(/[\t ]+$/gm, "").trimEnd();
7768
- }
7769
-
7770
7770
  function captureStudioReplSession(sessionName: string): { ok: true; transcript: string; session: StudioReplSessionInfo } | { ok: false; message: string } {
7771
7771
  if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
7772
7772
  const inferred = inferStudioReplSessionRuntime(sessionName);
@@ -7779,7 +7779,7 @@ function captureStudioReplSession(sessionName: string): { ok: true; transcript:
7779
7779
  };
7780
7780
  const result = runStudioTmux(["capture-pane", "-J", "-p", "-t", session.target, "-S", `-${STUDIO_REPL_CAPTURE_LINES}`], { timeout: 3_000 });
7781
7781
  if (!result.ok) return { ok: false, message: result.message };
7782
- return { ok: true, transcript: sanitizeStudioReplTranscript(result.stdout), session };
7782
+ return { ok: true, transcript: String(result.stdout || "").replace(/[\t ]+$/gm, "").trimEnd(), session };
7783
7783
  }
7784
7784
 
7785
7785
  function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options?: { newSession?: boolean }): { ok: true; session: StudioReplSessionInfo; message: string } | { ok: false; message: string } {
@@ -7955,10 +7955,11 @@ function buildStudioRControlSource(code: string, doneFile: string): string {
7955
7955
  " if (.__pi_studio_visible) print(.__pi_studio_value)",
7956
7956
  " }, error = function(e) {",
7957
7957
  " .__pi_studio_call <- conditionCall(e)",
7958
- " if (is.null(.__pi_studio_call)) {",
7958
+ " .__pi_studio_call_text <- if (is.null(.__pi_studio_call)) \"\" else paste(deparse(.__pi_studio_call), collapse = \" \")",
7959
+ " if (is.null(.__pi_studio_call) || grepl(\"__pi_studio_code\", .__pi_studio_call_text, fixed = TRUE)) {",
7959
7960
  " message(\"Error: \", conditionMessage(e))",
7960
7961
  " } else {",
7961
- " message(\"Error in \", paste(deparse(.__pi_studio_call), collapse = \" \"), \": \", conditionMessage(e))",
7962
+ " message(\"Error in \", .__pi_studio_call_text, \": \", conditionMessage(e))",
7962
7963
  " }",
7963
7964
  " }, finally = {",
7964
7965
  " writeLines(\"done\", .__pi_studio_done_file)",
@@ -8002,30 +8003,6 @@ function buildStudioReplSubmissionLine(runtime: StudioReplRuntime, sourceFile: s
8002
8003
  return `exec(open(${quotedPath}, encoding="utf-8").read(), globals())`;
8003
8004
  }
8004
8005
 
8005
- function buildStudioReplPreviewComment(runtime: StudioReplRuntime, code: string): string | undefined {
8006
- const normalized = code.replace(/\r/g, "").trimEnd();
8007
- const lineCount = normalized ? normalized.split("\n").length : 0;
8008
- if (lineCount <= 1) return undefined;
8009
- const prefix = runtime === "ghci" ? "--" : runtime === "clojure" ? ";;" : "#";
8010
- return `${prefix} Studio sent ${lineCount}-line snippet`;
8011
- }
8012
-
8013
- function buildStudioReplDisplayLabel(runtime: StudioReplRuntime, code: string): string {
8014
- const normalized = code.replace(/\r/g, "").trim();
8015
- const singleLine = normalized && !normalized.includes("\n") ? normalized.replace(/\s+/g, " ") : "";
8016
- if (singleLine && singleLine.length <= 140) return singleLine;
8017
- return buildStudioReplPreviewComment(runtime, code) || "# Studio sent code";
8018
- }
8019
-
8020
- function rememberStudioReplControlSubmission(sourceFile: string, label: string): void {
8021
- studioReplControlSubmissionLabels.set(sourceFile, label);
8022
- while (studioReplControlSubmissionLabels.size > 300) {
8023
- const oldest = studioReplControlSubmissionLabels.keys().next().value;
8024
- if (!oldest) break;
8025
- studioReplControlSubmissionLabels.delete(oldest);
8026
- }
8027
- }
8028
-
8029
8006
  function prepareStudioReplSubmission(sessionName: string, source: string): StudioReplPreparedSubmission {
8030
8007
  const normalizedSource = String(source || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8031
8008
  const runtime = inferStudioReplSessionRuntime(sessionName).runtime;
@@ -8041,7 +8018,6 @@ function prepareStudioReplSubmission(sessionName: string, source: string): Studi
8041
8018
  }
8042
8019
  writeFileSync(controlFiles.sourceFile, controlSource, "utf-8");
8043
8020
  const submissionLine = buildStudioReplSubmissionLine(runtime, controlFiles.sourceFile);
8044
- rememberStudioReplControlSubmission(controlFiles.sourceFile, buildStudioReplDisplayLabel(runtime, normalizedSource));
8045
8021
  return {
8046
8022
  runtime,
8047
8023
  usedControlFile: true,
@@ -8111,6 +8087,112 @@ function extractStudioReplTranscriptDelta(before: string, after: string): string
8111
8087
  return current.trim();
8112
8088
  }
8113
8089
 
8090
+ function stripStudioReplSubmissionEcho(output: string): string {
8091
+ let value = String(output || "").replace(/^\s+/, "");
8092
+ // The raw tmux mirror should stay raw, but Studio/tool result output should not
8093
+ // expose the temp-file wrapper used to submit multiline snippets safely. The
8094
+ // `pi-studio-re` fragment intentionally catches IPython's wrapped display of
8095
+ // `pi-studio-repl/...` paths across continuation prompt lines.
8096
+ const submissionEchoPatterns = [
8097
+ /^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
8098
+ /^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
8099
+ /^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
8100
+ /^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
8101
+ /^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
8102
+ ];
8103
+ for (const pattern of submissionEchoPatterns) value = value.replace(pattern, "");
8104
+ return value.replace(/^(?:\s*\n)+/, "").replace(/[\t ]+$/gm, "").trimEnd();
8105
+ }
8106
+
8107
+ function stripTrailingStudioReplPrompts(output: string): string {
8108
+ const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
8109
+ while (lines.length > 0 && /^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*$/.test(lines[lines.length - 1] || "")) {
8110
+ lines.pop();
8111
+ }
8112
+ return lines.join("\n").trimEnd();
8113
+ }
8114
+
8115
+ function cleanStudioReplCapturedOutput(output: string): string {
8116
+ return stripTrailingStudioReplPrompts(stripStudioReplSubmissionEcho(output));
8117
+ }
8118
+
8119
+ function normalizeStudioReplJournalMode(mode: unknown): StudioReplJournalEntry["mode"] {
8120
+ return mode === "literate" || mode === "agent" ? mode : "raw";
8121
+ }
8122
+
8123
+ function normalizeStudioReplJournalStatus(status: unknown): StudioReplJournalEntry["status"] {
8124
+ return status === "captured" || status === "timeout" || status === "error" || status === "note" ? status : "sent";
8125
+ }
8126
+
8127
+ function makeStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code: string }): StudioReplJournalEntry {
8128
+ const now = Date.now();
8129
+ return {
8130
+ id: typeof details.id === "string" && details.id.trim() ? details.id.trim() : `repl-journal-${now.toString(36)}-${randomUUID().slice(0, 8)}`,
8131
+ requestId: typeof details.requestId === "string" ? details.requestId : "",
8132
+ createdAt: typeof details.createdAt === "number" && Number.isFinite(details.createdAt) ? details.createdAt : now,
8133
+ updatedAt: typeof details.updatedAt === "number" && Number.isFinite(details.updatedAt) ? details.updatedAt : now,
8134
+ sessionName: String(details.sessionName || ""),
8135
+ runtime: details.runtime || "unknown",
8136
+ label: typeof details.label === "string" && details.label.trim() ? details.label.trim() : "REPL send",
8137
+ mode: normalizeStudioReplJournalMode(details.mode),
8138
+ prose: typeof details.prose === "string" ? details.prose : "",
8139
+ code: String(details.code || ""),
8140
+ output: typeof details.output === "string" ? details.output : "",
8141
+ status: normalizeStudioReplJournalStatus(details.status),
8142
+ skippedChunks: Math.max(0, Math.floor(Number(details.skippedChunks) || 0)),
8143
+ };
8144
+ }
8145
+
8146
+ function upsertStudioReplJournalEntry(entry: StudioReplJournalEntry): StudioReplJournalEntry {
8147
+ const existingIndex = studioReplJournalEntries.findIndex((candidate) => (
8148
+ (entry.requestId && candidate.requestId === entry.requestId)
8149
+ || candidate.id === entry.id
8150
+ ));
8151
+ if (existingIndex >= 0) {
8152
+ const existing = studioReplJournalEntries[existingIndex];
8153
+ studioReplJournalEntries[existingIndex] = {
8154
+ ...existing,
8155
+ ...entry,
8156
+ createdAt: existing.createdAt || entry.createdAt,
8157
+ updatedAt: Math.max(existing.updatedAt || 0, entry.updatedAt || 0, Date.now()),
8158
+ };
8159
+ } else {
8160
+ studioReplJournalEntries.push(entry);
8161
+ }
8162
+ studioReplJournalEntries = studioReplJournalEntries
8163
+ .sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0))
8164
+ .slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES);
8165
+ return studioReplJournalEntries.find((candidate) => candidate.id === entry.id || (entry.requestId && candidate.requestId === entry.requestId)) || entry;
8166
+ }
8167
+
8168
+ function recordStudioReplJournalEntry(details: Partial<StudioReplJournalEntry> & { sessionName: string; code: string }): StudioReplJournalEntry {
8169
+ return upsertStudioReplJournalEntry(makeStudioReplJournalEntry(details));
8170
+ }
8171
+
8172
+ function updateStudioReplJournalEntryOutput(requestId: string, sessionName: string, output: string, status: StudioReplJournalEntry["status"]): void {
8173
+ const normalizedRequestId = String(requestId || "");
8174
+ const normalizedSessionName = String(sessionName || "");
8175
+ const existing = studioReplJournalEntries.find((entry) => (
8176
+ (normalizedRequestId && entry.requestId === normalizedRequestId)
8177
+ || (!normalizedRequestId && normalizedSessionName && entry.sessionName === normalizedSessionName && entry.status === "sent")
8178
+ ));
8179
+ if (!existing) return;
8180
+ upsertStudioReplJournalEntry({
8181
+ ...existing,
8182
+ output: String(output || ""),
8183
+ status,
8184
+ updatedAt: Date.now(),
8185
+ });
8186
+ }
8187
+
8188
+ function getStudioReplJournalEntries(sessionName: string | null | undefined): StudioReplJournalEntry[] {
8189
+ const normalizedSessionName = String(sessionName || "").trim();
8190
+ const entries = normalizedSessionName
8191
+ ? studioReplJournalEntries.filter((entry) => entry.sessionName === normalizedSessionName)
8192
+ : studioReplJournalEntries;
8193
+ return entries.slice(-STUDIO_REPL_JOURNAL_MAX_ENTRIES).map((entry) => ({ ...entry }));
8194
+ }
8195
+
8114
8196
  async function waitForStudioReplDoneFile(doneFile: string | undefined, timeoutMs: number): Promise<boolean> {
8115
8197
  if (!doneFile) return false;
8116
8198
  const deadline = Date.now() + clampStudioReplSendTimeout(timeoutMs);
@@ -8619,11 +8701,15 @@ ${cssVarsBlock}
8619
8701
  <div class="source-actions-row">
8620
8702
  <button id="sendRunBtn" type="button" title="Run editor text. While a direct run is active, this button becomes Stop. Cmd/Ctrl+Enter queues steering from the current editor text. Stop the active request with Esc.">Run editor text</button>
8621
8703
  <button id="queueSteerBtn" type="button" title="Queue steering is available while Run editor text is active." disabled>Queue steering</button>
8704
+ </div>
8705
+ <div class="source-actions-row repl-action-line" hidden>
8622
8706
  <button id="sendReplBtn" type="button" hidden title="Send the current selection, or the full editor text, to the active REPL session shown in the right pane.">Send to REPL</button>
8623
8707
  <select id="replSendModeSelect" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
8624
8708
  <option value="raw" selected>Send mode: Raw</option>
8625
8709
  <option value="literate">Send mode: Literate</option>
8626
8710
  </select>
8711
+ </div>
8712
+ <div class="source-actions-row">
8627
8713
  <button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy text</button>
8628
8714
  <button id="openCompanionBtn" type="button" title="Open a detached copy of the current editor text in a new editor-only Studio tab.">Open new editor</button>
8629
8715
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
@@ -9028,7 +9114,8 @@ export default function (pi: ExtensionAPI) {
9028
9114
  }
9029
9115
  const after = captureStudioReplSession(selected.session.sessionName);
9030
9116
  const afterTranscript = after.ok ? after.transcript : "";
9031
- const output = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
9117
+ const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
9118
+ const output = cleanStudioReplCapturedOutput(rawOutput);
9032
9119
  const statusLine = sent.controlFiles?.doneFile
9033
9120
  ? (completed ? "Completed." : `Timed out after ${timeoutMs} ms waiting for completion marker.`)
9034
9121
  : "Submitted.";
@@ -9037,6 +9124,16 @@ export default function (pi: ExtensionAPI) {
9037
9124
  output ? "" : undefined,
9038
9125
  output || undefined,
9039
9126
  ].filter(Boolean).join("\n");
9127
+ recordStudioReplJournalEntry({
9128
+ requestId: `tool:${toolCallId}`,
9129
+ sessionName: selected.session.sessionName,
9130
+ runtime: sent.runtime === "unknown" ? selected.session.runtime : sent.runtime,
9131
+ label: "Pi",
9132
+ mode: "agent",
9133
+ code: params.code,
9134
+ output,
9135
+ status: sent.controlFiles?.doneFile && !completed ? "timeout" : (output.trim() ? "captured" : "sent"),
9136
+ });
9040
9137
  broadcastStudioReplToolSend({
9041
9138
  toolCallId,
9042
9139
  sessionName: selected.session.sessionName,
@@ -9048,6 +9145,7 @@ export default function (pi: ExtensionAPI) {
9048
9145
  timedOut: Boolean(sent.controlFiles?.doneFile && !completed),
9049
9146
  transcript: afterTranscript,
9050
9147
  capturedAt: Date.now(),
9148
+ journalEntries: getStudioReplJournalEntries(selected.session.sessionName),
9051
9149
  });
9052
9150
  return {
9053
9151
  content: [{ type: "text", text }],
@@ -9512,6 +9610,7 @@ export default function (pi: ExtensionAPI) {
9512
9610
  tmuxAvailable: state.tmuxAvailable,
9513
9611
  sessions: state.sessions,
9514
9612
  activeSessionName: studioReplActiveSessionName,
9613
+ journalEntries: getStudioReplJournalEntries(studioReplActiveSessionName),
9515
9614
  error: state.error ?? null,
9516
9615
  ...extra,
9517
9616
  });
@@ -9525,6 +9624,7 @@ export default function (pi: ExtensionAPI) {
9525
9624
  sendReplStateToClient(client, {
9526
9625
  transcript: "",
9527
9626
  capturedAt: Date.now(),
9627
+ journalEntries: [],
9528
9628
  ...extra,
9529
9629
  });
9530
9630
  return;
@@ -9536,6 +9636,7 @@ export default function (pi: ExtensionAPI) {
9536
9636
  transcript: "",
9537
9637
  captureError: captured.message,
9538
9638
  capturedAt: Date.now(),
9639
+ journalEntries: getStudioReplJournalEntries(targetSession),
9539
9640
  ...extra,
9540
9641
  });
9541
9642
  return;
@@ -9547,6 +9648,7 @@ export default function (pi: ExtensionAPI) {
9547
9648
  activeSessionName: captured.session.sessionName,
9548
9649
  transcript: captured.transcript,
9549
9650
  capturedAt: Date.now(),
9651
+ journalEntries: getStudioReplJournalEntries(captured.session.sessionName),
9550
9652
  ...extra,
9551
9653
  });
9552
9654
  };
@@ -10557,6 +10659,8 @@ export default function (pi: ExtensionAPI) {
10557
10659
  sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
10558
10660
  return;
10559
10661
  }
10662
+ const before = captureStudioReplSession(msg.sessionName);
10663
+ const beforeTranscript = before.ok ? before.transcript : "";
10560
10664
  const sent = sendTextToStudioReplSession(msg.sessionName, msg.text);
10561
10665
  if (!sent.ok) {
10562
10666
  sendToClient(client, { type: "error", requestId: msg.requestId, message: sent.message });
@@ -10564,8 +10668,47 @@ export default function (pi: ExtensionAPI) {
10564
10668
  return;
10565
10669
  }
10566
10670
  studioReplActiveSessionName = msg.sessionName;
10567
- sendToClient(client, { type: "repl_send_ack", requestId: msg.requestId, sessionName: msg.sessionName, message: sent.message });
10568
- setTimeout(() => sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId }), 150);
10671
+ recordStudioReplJournalEntry({
10672
+ requestId: msg.requestId,
10673
+ sessionName: msg.sessionName,
10674
+ runtime: sent.runtime,
10675
+ label: "Studio",
10676
+ mode: "raw",
10677
+ code: msg.text,
10678
+ status: "sent",
10679
+ });
10680
+ sendToClient(client, {
10681
+ type: "repl_send_ack",
10682
+ requestId: msg.requestId,
10683
+ sessionName: msg.sessionName,
10684
+ message: sent.message,
10685
+ journalEntries: getStudioReplJournalEntries(msg.sessionName),
10686
+ });
10687
+ void (async () => {
10688
+ try {
10689
+ const timeoutMs = STUDIO_REPL_SEND_DEFAULT_TIMEOUT_MS;
10690
+ let completed = false;
10691
+ if (sent.controlFiles?.doneFile) {
10692
+ completed = await waitForStudioReplDoneFile(sent.controlFiles.doneFile, timeoutMs);
10693
+ } else {
10694
+ await sleep(Math.min(750, timeoutMs));
10695
+ }
10696
+ const after = captureStudioReplSession(msg.sessionName);
10697
+ const afterTranscript = after.ok ? after.transcript : "";
10698
+ const rawOutput = extractStudioReplTranscriptDelta(beforeTranscript, afterTranscript);
10699
+ const output = cleanStudioReplCapturedOutput(rawOutput);
10700
+ updateStudioReplJournalEntryOutput(
10701
+ msg.requestId,
10702
+ msg.sessionName,
10703
+ output,
10704
+ sent.controlFiles?.doneFile && !completed ? "timeout" : (output.trim() ? "captured" : "sent"),
10705
+ );
10706
+ sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId });
10707
+ } catch (error) {
10708
+ updateStudioReplJournalEntryOutput(msg.requestId, msg.sessionName, error instanceof Error ? error.message : String(error), "error");
10709
+ sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: error instanceof Error ? error.message : String(error) });
10710
+ }
10711
+ })();
10569
10712
  return;
10570
10713
  }
10571
10714
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",