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