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