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 +15 -0
- package/client/studio-client.js +248 -74
- package/client/studio.css +53 -13
- package/index.ts +192 -49
- package/package.json +1 -1
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
|
package/client/studio-client.js
CHANGED
|
@@ -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((
|
|
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
|
|
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
|
-
|
|
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
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1527
|
-
|
|
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
|
-
|
|
2105
|
-
|
|
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
|
-
|
|
5337
|
-
|
|
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
|
-
|
|
5509
|
-
|
|
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 &&
|
|
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 =
|
|
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
|
|
6209
|
-
const
|
|
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,
|
|
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 =
|
|
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'>
|
|
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
|
-
+ (
|
|
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
|
-
? (
|
|
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 && !
|
|
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
|
-
? "
|
|
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
|
|
13247
|
-
sendReplBtn.hidden =
|
|
13248
|
-
sendReplBtn.disabled = wsState === "Disconnected" || uiBusy || replBusy || !
|
|
13249
|
-
sendReplBtn.
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
2807
|
+
border: 1px solid transparent;
|
|
2784
2808
|
border-radius: 6px;
|
|
2785
2809
|
padding: 4px 10px;
|
|
2786
2810
|
color: var(--text);
|
|
2787
|
-
background:
|
|
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:
|
|
2803
|
-
background: var(--
|
|
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+
|
|
3943
|
-
body.studio-zen-mode .source-actions-row:nth-child(n+
|
|
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
|
-
|
|
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:
|
|
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 \",
|
|
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
|
|
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
|
-
|
|
10568
|
-
|
|
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.
|
|
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",
|