pi-studio 0.8.4 → 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 +33 -0
- package/README.md +3 -1
- package/client/studio-client.js +1583 -34
- package/client/studio.css +364 -0
- package/index.ts +913 -2
- package/package.json +5 -4
package/client/studio-client.js
CHANGED
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
|
|
108
108
|
const sendRunBtn = document.getElementById("sendRunBtn");
|
|
109
109
|
const queueSteerBtn = document.getElementById("queueSteerBtn");
|
|
110
|
+
const sendReplBtn = document.getElementById("sendReplBtn");
|
|
111
|
+
const replSendModeSelect = document.getElementById("replSendModeSelect");
|
|
110
112
|
const copyDraftBtn = document.getElementById("copyDraftBtn");
|
|
111
113
|
const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
|
|
112
114
|
const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
|
|
@@ -215,6 +217,106 @@
|
|
|
215
217
|
const TRACE_OUTPUT_PREVIEW_MAX_LINES = 50;
|
|
216
218
|
const TRACE_OUTPUT_PREVIEW_MAX_CHARS = 8000;
|
|
217
219
|
const TRACE_IMAGE_SAFE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
220
|
+
const REPL_POLL_INTERVAL_MS = 1000;
|
|
221
|
+
const REPL_TRANSCRIPT_MAX_CHARS = 200_000;
|
|
222
|
+
const REPL_JOURNAL_OUTPUT_MAX_CHARS = 80_000;
|
|
223
|
+
const REPL_JOURNAL_MAX_ENTRIES = 80;
|
|
224
|
+
const EDITOR_TAB_TEXT = " ";
|
|
225
|
+
let replTmuxAvailable = null;
|
|
226
|
+
let replSessions = [];
|
|
227
|
+
let replActiveSessionName = "";
|
|
228
|
+
let replRuntime = (() => {
|
|
229
|
+
try {
|
|
230
|
+
return (window.localStorage && window.localStorage.getItem("piStudio.replRuntime")) || "python";
|
|
231
|
+
} catch {
|
|
232
|
+
return "python";
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
235
|
+
let replTranscript = "";
|
|
236
|
+
let replError = "";
|
|
237
|
+
let replMessage = "";
|
|
238
|
+
let replCapturedAt = 0;
|
|
239
|
+
let replFollow = true;
|
|
240
|
+
let replPollTimer = null;
|
|
241
|
+
let replBusy = false;
|
|
242
|
+
let replSendMode = (() => {
|
|
243
|
+
try {
|
|
244
|
+
const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
|
|
245
|
+
return String(stored || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
|
|
246
|
+
} catch {
|
|
247
|
+
return "raw";
|
|
248
|
+
}
|
|
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();
|
|
301
|
+
let activeReplJournalEntryId = "";
|
|
302
|
+
let replJournalCollapsed = (() => {
|
|
303
|
+
try {
|
|
304
|
+
const stored = window.localStorage ? window.localStorage.getItem("piStudio.replStudioCollapsed") : null;
|
|
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;
|
|
315
|
+
return true;
|
|
316
|
+
} catch {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
})();
|
|
218
320
|
let studioRunChainActive = false;
|
|
219
321
|
let queuedSteeringCount = 0;
|
|
220
322
|
let agentBusyFromServer = false;
|
|
@@ -745,6 +847,739 @@
|
|
|
745
847
|
setStatus("Loaded visible working into editor.", "success");
|
|
746
848
|
}
|
|
747
849
|
|
|
850
|
+
function normalizeReplRuntime(value) {
|
|
851
|
+
const runtime = String(value || "").trim().toLowerCase();
|
|
852
|
+
return runtime === "shell" || runtime === "python" || runtime === "ipython" || runtime === "julia" || runtime === "r" || runtime === "ghci" || runtime === "clojure"
|
|
853
|
+
? runtime
|
|
854
|
+
: "python";
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function normalizeReplSession(session) {
|
|
858
|
+
if (!session || typeof session !== "object") return null;
|
|
859
|
+
const sessionName = typeof session.sessionName === "string" && session.sessionName.trim() ? session.sessionName.trim() : "";
|
|
860
|
+
if (!sessionName) return null;
|
|
861
|
+
return {
|
|
862
|
+
sessionName,
|
|
863
|
+
target: typeof session.target === "string" ? session.target : (sessionName + ":0.0"),
|
|
864
|
+
runtime: typeof session.runtime === "string" ? session.runtime : "unknown",
|
|
865
|
+
label: typeof session.label === "string" && session.label.trim() ? session.label.trim() : sessionName,
|
|
866
|
+
source: typeof session.source === "string" ? session.source : "tmux",
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function setReplRuntime(runtime) {
|
|
871
|
+
replRuntime = normalizeReplRuntime(runtime);
|
|
872
|
+
try {
|
|
873
|
+
if (window.localStorage) window.localStorage.setItem("piStudio.replRuntime", replRuntime);
|
|
874
|
+
} catch {
|
|
875
|
+
// Ignore storage failures.
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function normalizeReplSendMode(value) {
|
|
880
|
+
return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function setReplSendMode(mode) {
|
|
884
|
+
replSendMode = normalizeReplSendMode(mode);
|
|
885
|
+
if (replSendModeSelect) replSendModeSelect.value = replSendMode;
|
|
886
|
+
try {
|
|
887
|
+
if (window.localStorage) window.localStorage.setItem("piStudio.replSendMode", replSendMode);
|
|
888
|
+
} catch {
|
|
889
|
+
// Ignore storage failures.
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function setReplJournalCollapsed(collapsed) {
|
|
894
|
+
replJournalCollapsed = Boolean(collapsed);
|
|
895
|
+
try {
|
|
896
|
+
if (window.localStorage) window.localStorage.setItem("piStudio.replStudioCollapsed", replJournalCollapsed ? "true" : "false");
|
|
897
|
+
} catch {
|
|
898
|
+
// Ignore storage failures.
|
|
899
|
+
}
|
|
900
|
+
renderReplViewIfActive({ force: true });
|
|
901
|
+
}
|
|
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
|
+
|
|
926
|
+
function setReplSessions(sessions) {
|
|
927
|
+
const previous = serializeReplSessionsForCompare(replSessions);
|
|
928
|
+
const previousActive = replActiveSessionName;
|
|
929
|
+
replSessions = Array.isArray(sessions)
|
|
930
|
+
? sessions.map(normalizeReplSession).filter(Boolean)
|
|
931
|
+
: [];
|
|
932
|
+
if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
|
|
933
|
+
replActiveSessionName = replSessions[0] ? replSessions[0].sessionName : "";
|
|
934
|
+
}
|
|
935
|
+
return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function getActiveReplSession() {
|
|
939
|
+
return replSessions.find((session) => session.sessionName === replActiveSessionName) || null;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function buildActiveReplPromptContext() {
|
|
943
|
+
if (rightView !== "repl") return "";
|
|
944
|
+
const session = getActiveReplSession();
|
|
945
|
+
if (!session) return "";
|
|
946
|
+
const runtime = session.runtime && session.runtime !== "unknown" ? session.runtime : "unknown";
|
|
947
|
+
return [
|
|
948
|
+
"[Studio active REPL]",
|
|
949
|
+
"The right pane is mirroring an active tmux-backed REPL session.",
|
|
950
|
+
"If the user refers to the active REPL, send code to this session rather than inventing a separate one.",
|
|
951
|
+
"Session name: " + session.sessionName,
|
|
952
|
+
"tmux target: " + (session.target || (session.sessionName + ":0.0")),
|
|
953
|
+
"runtime: " + runtime,
|
|
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.",
|
|
956
|
+
"[/Studio active REPL]",
|
|
957
|
+
].join("\n");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function prepareEditorTextForRunRequest(text) {
|
|
961
|
+
const prepared = prepareEditorTextForSend(text);
|
|
962
|
+
const replContext = buildActiveReplPromptContext();
|
|
963
|
+
return replContext ? (replContext + "\n\n" + prepared) : prepared;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function setActiveReplSession(sessionName) {
|
|
967
|
+
const name = String(sessionName || "").trim();
|
|
968
|
+
if (!name) {
|
|
969
|
+
replActiveSessionName = "";
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
replActiveSessionName = name;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function trimReplTranscript(text) {
|
|
976
|
+
const value = String(text || "");
|
|
977
|
+
if (value.length <= REPL_TRANSCRIPT_MAX_CHARS) return value;
|
|
978
|
+
return "… " + formatCompactNumber(value.length - REPL_TRANSCRIPT_MAX_CHARS) + " earlier chars omitted …\n" + value.slice(value.length - REPL_TRANSCRIPT_MAX_CHARS);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function requestReplList() {
|
|
982
|
+
if (wsState === "Disconnected") return false;
|
|
983
|
+
return sendMessage({ type: "repl_list_request" });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function requestReplCapture() {
|
|
987
|
+
if (wsState === "Disconnected") return false;
|
|
988
|
+
if (replActiveSessionName) {
|
|
989
|
+
return sendMessage({ type: "repl_capture_request", sessionName: replActiveSessionName });
|
|
990
|
+
}
|
|
991
|
+
return sendMessage({ type: "repl_list_request" });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function isReplControlFocused() {
|
|
995
|
+
const activeEl = document.activeElement;
|
|
996
|
+
return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls"));
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function renderReplViewIfActive(options) {
|
|
1000
|
+
if (rightView !== "repl") return;
|
|
1001
|
+
if ((!options || options.force !== true) && isReplControlFocused()) return;
|
|
1002
|
+
if (traceRenderRaf !== null) return;
|
|
1003
|
+
traceRenderRaf = window.requestAnimationFrame(() => {
|
|
1004
|
+
traceRenderRaf = null;
|
|
1005
|
+
refreshResponseUi();
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function startReplPolling() {
|
|
1010
|
+
if (rightView !== "repl") return;
|
|
1011
|
+
if (replPollTimer !== null) return;
|
|
1012
|
+
requestReplCapture();
|
|
1013
|
+
replPollTimer = window.setInterval(() => {
|
|
1014
|
+
if (rightView !== "repl") {
|
|
1015
|
+
stopReplPolling();
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
requestReplCapture();
|
|
1019
|
+
}, REPL_POLL_INTERVAL_MS);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function stopReplPolling() {
|
|
1023
|
+
if (replPollTimer !== null) {
|
|
1024
|
+
window.clearInterval(replPollTimer);
|
|
1025
|
+
replPollTimer = null;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function getActiveReplRuntime() {
|
|
1030
|
+
const session = getActiveReplSession();
|
|
1031
|
+
if (session && session.runtime && session.runtime !== "unknown") return normalizeReplRuntime(session.runtime);
|
|
1032
|
+
return normalizeReplRuntime(replRuntime);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function getEditorSelectionRange() {
|
|
1036
|
+
const raw = String(sourceTextEl.value || "");
|
|
1037
|
+
const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
|
|
1038
|
+
const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
|
|
1039
|
+
const safeStart = Math.max(0, Math.min(start, raw.length));
|
|
1040
|
+
const safeEnd = Math.max(safeStart, Math.min(end, raw.length));
|
|
1041
|
+
return { raw, start: safeStart, end: safeEnd, selected: safeEnd > safeStart ? raw.slice(safeStart, safeEnd) : "" };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function normalizeMarkdownFenceLanguage(info) {
|
|
1045
|
+
let value = String(info || "").trim();
|
|
1046
|
+
if (!value) return "";
|
|
1047
|
+
let first = "";
|
|
1048
|
+
if (value.startsWith("{")) {
|
|
1049
|
+
const closeIndex = value.indexOf("}");
|
|
1050
|
+
const inner = closeIndex >= 0 ? value.slice(1, closeIndex) : value.slice(1);
|
|
1051
|
+
first = inner.split(/[\s,]+/)[0] || "";
|
|
1052
|
+
} else {
|
|
1053
|
+
first = value.split(/\s+/)[0] || "";
|
|
1054
|
+
}
|
|
1055
|
+
first = first.replace(/^\./, "").trim().toLowerCase();
|
|
1056
|
+
if (first === "py") return "python";
|
|
1057
|
+
if (first === "jl") return "julia";
|
|
1058
|
+
if (first === "sh" || first === "zsh" || first === "fish") return "shell";
|
|
1059
|
+
if (first === "bash") return "shell";
|
|
1060
|
+
if (first === "hs" || first === "haskell") return "ghci";
|
|
1061
|
+
if (first === "clj" || first === "cljc") return "clojure";
|
|
1062
|
+
return first;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function parseMarkdownCodeFences(markdown) {
|
|
1066
|
+
const text = String(markdown || "");
|
|
1067
|
+
const blocks = [];
|
|
1068
|
+
let offset = 0;
|
|
1069
|
+
let open = null;
|
|
1070
|
+
while (offset <= text.length) {
|
|
1071
|
+
const newlineIndex = text.indexOf("\n", offset);
|
|
1072
|
+
const lineEnd = newlineIndex >= 0 ? newlineIndex : text.length;
|
|
1073
|
+
const lineWithNewlineEnd = newlineIndex >= 0 ? newlineIndex + 1 : lineEnd;
|
|
1074
|
+
const line = text.slice(offset, lineEnd);
|
|
1075
|
+
if (!open) {
|
|
1076
|
+
const openMatch = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
1077
|
+
if (openMatch) {
|
|
1078
|
+
const fence = openMatch[1] || "";
|
|
1079
|
+
open = {
|
|
1080
|
+
start: offset,
|
|
1081
|
+
fence,
|
|
1082
|
+
fenceChar: fence.charAt(0),
|
|
1083
|
+
fenceLength: fence.length,
|
|
1084
|
+
info: openMatch[2] || "",
|
|
1085
|
+
contentStart: lineWithNewlineEnd,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
} else {
|
|
1089
|
+
const closePattern = new RegExp("^ {0,3}" + open.fenceChar + "{" + open.fenceLength + ",}[ \\t]*$");
|
|
1090
|
+
if (closePattern.test(line)) {
|
|
1091
|
+
blocks.push({
|
|
1092
|
+
start: open.start,
|
|
1093
|
+
end: lineWithNewlineEnd,
|
|
1094
|
+
contentStart: open.contentStart,
|
|
1095
|
+
contentEnd: offset,
|
|
1096
|
+
info: open.info,
|
|
1097
|
+
language: normalizeMarkdownFenceLanguage(open.info),
|
|
1098
|
+
code: text.slice(open.contentStart, offset),
|
|
1099
|
+
});
|
|
1100
|
+
open = null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (newlineIndex < 0) break;
|
|
1104
|
+
offset = lineWithNewlineEnd;
|
|
1105
|
+
}
|
|
1106
|
+
return blocks;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function isFenceLanguageCompatibleWithRuntime(language, runtime) {
|
|
1110
|
+
const lang = normalizeMarkdownFenceLanguage(language);
|
|
1111
|
+
if (!lang) return true;
|
|
1112
|
+
const activeRuntime = normalizeReplRuntime(runtime || getActiveReplRuntime());
|
|
1113
|
+
if (activeRuntime === "python" || activeRuntime === "ipython") return lang === "python" || lang === "ipython";
|
|
1114
|
+
if (activeRuntime === "r") return lang === "r";
|
|
1115
|
+
if (activeRuntime === "julia") return lang === "julia";
|
|
1116
|
+
if (activeRuntime === "shell") return lang === "shell" || lang === "bash" || lang === "sh";
|
|
1117
|
+
if (activeRuntime === "ghci") return lang === "ghci" || lang === "haskell";
|
|
1118
|
+
if (activeRuntime === "clojure") return lang === "clojure";
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function stripFencedBlocksFromMarkdown(markdown, blocks) {
|
|
1123
|
+
const text = String(markdown || "");
|
|
1124
|
+
const ranges = Array.isArray(blocks) ? blocks : parseMarkdownCodeFences(text);
|
|
1125
|
+
if (!ranges.length) return text.trim();
|
|
1126
|
+
let cursor = 0;
|
|
1127
|
+
const pieces = [];
|
|
1128
|
+
ranges.forEach((block) => {
|
|
1129
|
+
pieces.push(text.slice(cursor, block.start));
|
|
1130
|
+
cursor = block.end;
|
|
1131
|
+
});
|
|
1132
|
+
pieces.push(text.slice(cursor));
|
|
1133
|
+
return pieces.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function getCurrentMarkdownCodeFence(markdown, caretOffset) {
|
|
1137
|
+
const text = String(markdown || "");
|
|
1138
|
+
const safeCaret = Math.max(0, Math.min(Math.floor(Number(caretOffset) || 0), text.length));
|
|
1139
|
+
return parseMarkdownCodeFences(text).find((block) => safeCaret >= block.contentStart && safeCaret <= block.contentEnd) || null;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function unwrapSingleMarkdownCodeFenceForReplSend(text) {
|
|
1143
|
+
const source = String(text || "");
|
|
1144
|
+
const blocks = parseMarkdownCodeFences(source);
|
|
1145
|
+
if (blocks.length !== 1) return null;
|
|
1146
|
+
const block = blocks[0];
|
|
1147
|
+
if (source.slice(0, block.start).trim() || source.slice(block.end).trim()) return null;
|
|
1148
|
+
if (!isFenceLanguageCompatibleWithRuntime(block.language, getActiveReplRuntime())) return null;
|
|
1149
|
+
const code = String(block.code || "").trimEnd();
|
|
1150
|
+
if (!code.trim()) return null;
|
|
1151
|
+
return {
|
|
1152
|
+
code,
|
|
1153
|
+
label: "single " + (block.language || getActiveReplRuntime()) + " chunk",
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function buildRawReplSendPayload() {
|
|
1158
|
+
const range = getEditorSelectionRange();
|
|
1159
|
+
const selected = range.selected;
|
|
1160
|
+
const source = selected || range.raw;
|
|
1161
|
+
const unwrapped = unwrapSingleMarkdownCodeFenceForReplSend(source);
|
|
1162
|
+
return {
|
|
1163
|
+
text: prepareEditorTextForSend(unwrapped ? unwrapped.code : source),
|
|
1164
|
+
prose: "",
|
|
1165
|
+
label: unwrapped ? unwrapped.label : (selected ? "selection" : "full editor"),
|
|
1166
|
+
mode: "raw",
|
|
1167
|
+
noteOnly: false,
|
|
1168
|
+
skippedChunks: 0,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function buildLiterateReplSendPayload() {
|
|
1173
|
+
const range = getEditorSelectionRange();
|
|
1174
|
+
const runtime = getActiveReplRuntime();
|
|
1175
|
+
if (range.selected) {
|
|
1176
|
+
const blocks = parseMarkdownCodeFences(range.selected);
|
|
1177
|
+
if (blocks.length) {
|
|
1178
|
+
const compatibleBlocks = blocks.filter((block) => isFenceLanguageCompatibleWithRuntime(block.language, runtime));
|
|
1179
|
+
const code = compatibleBlocks.map((block) => String(block.code || "").trimEnd()).filter((chunk) => chunk.trim()).join("\n\n");
|
|
1180
|
+
const prose = stripFencedBlocksFromMarkdown(range.selected, blocks);
|
|
1181
|
+
if (code.trim()) {
|
|
1182
|
+
return {
|
|
1183
|
+
text: prepareEditorTextForSend(code),
|
|
1184
|
+
prose,
|
|
1185
|
+
label: "selection · " + compatibleBlocks.length + " code chunk" + (compatibleBlocks.length === 1 ? "" : "s"),
|
|
1186
|
+
mode: "literate",
|
|
1187
|
+
noteOnly: false,
|
|
1188
|
+
skippedChunks: Math.max(0, blocks.length - compatibleBlocks.length),
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
if (prose.trim()) {
|
|
1192
|
+
return {
|
|
1193
|
+
text: "",
|
|
1194
|
+
prose,
|
|
1195
|
+
label: "selected prose",
|
|
1196
|
+
mode: "literate",
|
|
1197
|
+
noteOnly: true,
|
|
1198
|
+
skippedChunks: blocks.length,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
return { error: "Selected code chunks do not match the active REPL runtime." };
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
text: prepareEditorTextForSend(range.selected),
|
|
1205
|
+
prose: "",
|
|
1206
|
+
label: "selection",
|
|
1207
|
+
mode: "literate",
|
|
1208
|
+
noteOnly: false,
|
|
1209
|
+
skippedChunks: 0,
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const currentBlock = getCurrentMarkdownCodeFence(range.raw, range.start);
|
|
1214
|
+
if (currentBlock) {
|
|
1215
|
+
if (!isFenceLanguageCompatibleWithRuntime(currentBlock.language, runtime)) {
|
|
1216
|
+
return { error: "Current code chunk is marked " + (currentBlock.language || "unknown") + ", but the active REPL is " + runtime + "." };
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
text: prepareEditorTextForSend(String(currentBlock.code || "").trimEnd()),
|
|
1220
|
+
prose: "",
|
|
1221
|
+
label: "current " + (currentBlock.language || runtime) + " chunk",
|
|
1222
|
+
mode: "literate",
|
|
1223
|
+
noteOnly: false,
|
|
1224
|
+
skippedChunks: 0,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const allBlocks = parseMarkdownCodeFences(range.raw);
|
|
1229
|
+
if (allBlocks.length) {
|
|
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." };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return {
|
|
1234
|
+
text: prepareEditorTextForSend(range.raw),
|
|
1235
|
+
prose: "",
|
|
1236
|
+
label: "full editor",
|
|
1237
|
+
mode: "literate",
|
|
1238
|
+
noteOnly: false,
|
|
1239
|
+
skippedChunks: 0,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function buildAllChunksReplSendPayload() {
|
|
1244
|
+
const range = getEditorSelectionRange();
|
|
1245
|
+
const runtime = getActiveReplRuntime();
|
|
1246
|
+
const blocks = parseMarkdownCodeFences(range.raw);
|
|
1247
|
+
if (!blocks.length) return { error: "No fenced code chunks found in the editor." };
|
|
1248
|
+
const compatibleBlocks = blocks.filter((block) => isFenceLanguageCompatibleWithRuntime(block.language, runtime));
|
|
1249
|
+
const code = compatibleBlocks.map((block) => String(block.code || "").trimEnd()).filter((chunk) => chunk.trim()).join("\n\n");
|
|
1250
|
+
if (!code.trim()) return { error: "No code chunks match the active REPL runtime." };
|
|
1251
|
+
return {
|
|
1252
|
+
text: prepareEditorTextForSend(code),
|
|
1253
|
+
prose: "",
|
|
1254
|
+
label: "all " + runtime + " chunks · " + compatibleBlocks.length + " of " + blocks.length,
|
|
1255
|
+
mode: "literate",
|
|
1256
|
+
noteOnly: false,
|
|
1257
|
+
skippedChunks: Math.max(0, blocks.length - compatibleBlocks.length),
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function getSelectedOrCurrentParagraphForReplNote() {
|
|
1262
|
+
const range = getEditorSelectionRange();
|
|
1263
|
+
if (range.selected.trim()) return range.selected.trim();
|
|
1264
|
+
const before = range.raw.lastIndexOf("\n\n", Math.max(0, range.start - 1));
|
|
1265
|
+
const after = range.raw.indexOf("\n\n", range.start);
|
|
1266
|
+
const start = before >= 0 ? before + 2 : 0;
|
|
1267
|
+
const end = after >= 0 ? after : range.raw.length;
|
|
1268
|
+
return range.raw.slice(start, end).trim();
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function trimReplJournalOutput(text) {
|
|
1272
|
+
const value = String(text || "").trimEnd();
|
|
1273
|
+
if (value.length <= REPL_JOURNAL_OUTPUT_MAX_CHARS) return value;
|
|
1274
|
+
return "… " + formatCompactNumber(value.length - REPL_JOURNAL_OUTPUT_MAX_CHARS) + " earlier chars omitted …\n" + value.slice(value.length - REPL_JOURNAL_OUTPUT_MAX_CHARS);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function createReplJournalEntry(details) {
|
|
1278
|
+
return {
|
|
1279
|
+
id: "repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8),
|
|
1280
|
+
requestId: details.requestId || "",
|
|
1281
|
+
createdAt: Date.now(),
|
|
1282
|
+
updatedAt: Date.now(),
|
|
1283
|
+
sessionName: details.sessionName || "",
|
|
1284
|
+
runtime: details.runtime || getActiveReplRuntime(),
|
|
1285
|
+
label: details.label || "REPL send",
|
|
1286
|
+
mode: details.mode || replSendMode,
|
|
1287
|
+
prose: String(details.prose || ""),
|
|
1288
|
+
code: String(details.code || ""),
|
|
1289
|
+
output: String(details.output || ""),
|
|
1290
|
+
beforeTranscript: String(details.beforeTranscript || ""),
|
|
1291
|
+
status: details.status || "sent",
|
|
1292
|
+
skippedChunks: Math.max(0, Math.floor(Number(details.skippedChunks) || 0)),
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function addReplJournalEntry(details) {
|
|
1297
|
+
const entry = createReplJournalEntry(details || {});
|
|
1298
|
+
replJournalEntries = [...replJournalEntries, entry].slice(-REPL_JOURNAL_MAX_ENTRIES);
|
|
1299
|
+
persistReplJournalEntries();
|
|
1300
|
+
return entry;
|
|
1301
|
+
}
|
|
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
|
+
|
|
1335
|
+
function extractReplTranscriptDelta(before, after) {
|
|
1336
|
+
const previous = String(before || "");
|
|
1337
|
+
const current = String(after || "");
|
|
1338
|
+
if (!current) return "";
|
|
1339
|
+
if (!previous) return current;
|
|
1340
|
+
const directIndex = current.indexOf(previous);
|
|
1341
|
+
if (directIndex >= 0) return current.slice(directIndex + previous.length).replace(/^\s+/, "");
|
|
1342
|
+
const previousLines = previous.split("\n");
|
|
1343
|
+
const maxSuffixLines = Math.min(24, previousLines.length);
|
|
1344
|
+
for (let count = maxSuffixLines; count >= 1; count -= 1) {
|
|
1345
|
+
const suffix = previousLines.slice(previousLines.length - count).join("\n");
|
|
1346
|
+
if (!suffix.trim()) continue;
|
|
1347
|
+
const suffixIndex = current.indexOf(suffix);
|
|
1348
|
+
if (suffixIndex >= 0) return current.slice(suffixIndex + suffix.length).replace(/^\s+/, "");
|
|
1349
|
+
}
|
|
1350
|
+
return current;
|
|
1351
|
+
}
|
|
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
|
+
|
|
1387
|
+
function updateActiveReplJournalEntryFromTranscript(sessionName, transcript) {
|
|
1388
|
+
if (!activeReplJournalEntryId) return false;
|
|
1389
|
+
const entryIndex = replJournalEntries.findIndex((entry) => entry.id === activeReplJournalEntryId);
|
|
1390
|
+
if (entryIndex < 0) return false;
|
|
1391
|
+
const entry = replJournalEntries[entryIndex];
|
|
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;
|
|
1396
|
+
replJournalEntries = replJournalEntries.map((candidate) => candidate.id === entry.id
|
|
1397
|
+
? { ...candidate, output: delta, status: "captured", updatedAt: Date.now() }
|
|
1398
|
+
: candidate);
|
|
1399
|
+
persistReplJournalEntries();
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function getMarkdownFenceForText(text, language) {
|
|
1404
|
+
const value = String(text || "");
|
|
1405
|
+
let fence = "```";
|
|
1406
|
+
while (value.includes(fence)) fence += "`";
|
|
1407
|
+
return fence + (language ? language : "") + "\n" + value.replace(/\s+$/, "") + "\n" + fence;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function buildReplJournalMarkdown() {
|
|
1411
|
+
const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString(), ""];
|
|
1412
|
+
if (!replJournalEntries.length) {
|
|
1413
|
+
lines.push("_No REPL Studio entries yet._");
|
|
1414
|
+
return lines.join("\n");
|
|
1415
|
+
}
|
|
1416
|
+
replJournalEntries.forEach((entry, index) => {
|
|
1417
|
+
lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
|
|
1418
|
+
lines.push("");
|
|
1419
|
+
lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toLocaleString());
|
|
1420
|
+
if (entry.runtime) lines.push("- Runtime: " + entry.runtime);
|
|
1421
|
+
if (entry.sessionName) lines.push("- Session: `" + entry.sessionName + "`");
|
|
1422
|
+
if (entry.skippedChunks) lines.push("- Skipped chunks: " + entry.skippedChunks);
|
|
1423
|
+
lines.push("");
|
|
1424
|
+
if (String(entry.prose || "").trim()) {
|
|
1425
|
+
lines.push(String(entry.prose).trim());
|
|
1426
|
+
lines.push("");
|
|
1427
|
+
}
|
|
1428
|
+
if (String(entry.code || "").trim()) {
|
|
1429
|
+
lines.push(getMarkdownFenceForText(entry.code, entry.runtime === "ipython" ? "python" : entry.runtime));
|
|
1430
|
+
lines.push("");
|
|
1431
|
+
}
|
|
1432
|
+
if (String(entry.output || "").trim()) {
|
|
1433
|
+
lines.push("Output:");
|
|
1434
|
+
lines.push("");
|
|
1435
|
+
lines.push(getMarkdownFenceForText(entry.output, "text"));
|
|
1436
|
+
lines.push("");
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
return lines.join("\n").replace(/\n{4,}/g, "\n\n\n").trimEnd() + "\n";
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
async function copyReplJournalToClipboard() {
|
|
1443
|
+
if (!replJournalEntries.length) {
|
|
1444
|
+
setStatus("No REPL Studio entries to copy yet.", "warning");
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (await writeTextToClipboard(buildReplJournalMarkdown())) {
|
|
1448
|
+
setStatus("Copied REPL Studio as Markdown.", "success");
|
|
1449
|
+
} else {
|
|
1450
|
+
setStatus("Clipboard write failed.", "warning");
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function exportReplJournalMarkdown() {
|
|
1455
|
+
if (!replJournalEntries.length) {
|
|
1456
|
+
setStatus("No REPL Studio entries to export yet.", "warning");
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const blob = new Blob([buildReplJournalMarkdown()], { type: "text/markdown;charset=utf-8" });
|
|
1460
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
1461
|
+
const link = document.createElement("a");
|
|
1462
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1463
|
+
link.href = blobUrl;
|
|
1464
|
+
link.download = "repl-studio-" + stamp + ".md";
|
|
1465
|
+
document.body.appendChild(link);
|
|
1466
|
+
link.click();
|
|
1467
|
+
link.remove();
|
|
1468
|
+
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
|
1469
|
+
setStatus("Exported REPL Studio Markdown.", "success");
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function clearReplJournal() {
|
|
1473
|
+
replJournalEntries = [];
|
|
1474
|
+
activeReplJournalEntryId = "";
|
|
1475
|
+
persistReplJournalEntries();
|
|
1476
|
+
setStatus("Cleared REPL Studio.", "success");
|
|
1477
|
+
renderReplViewIfActive({ force: true });
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function loadReplJournalIntoEditor() {
|
|
1481
|
+
if (!replJournalEntries.length) {
|
|
1482
|
+
setStatus("No REPL Studio entries to load yet.", "warning");
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const markdown = buildReplJournalMarkdown();
|
|
1486
|
+
setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
|
|
1487
|
+
setSourceState({ source: "blank", label: "REPL Studio", path: null });
|
|
1488
|
+
setEditorLanguage("markdown");
|
|
1489
|
+
setStatus("Loaded REPL Studio into editor.", "success");
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function addSelectedReplJournalNote() {
|
|
1493
|
+
const note = getSelectedOrCurrentParagraphForReplNote();
|
|
1494
|
+
if (!note.trim()) {
|
|
1495
|
+
setStatus("Select prose or place the cursor in a paragraph to add a REPL Studio note.", "warning");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
addReplJournalEntry({
|
|
1499
|
+
label: "note",
|
|
1500
|
+
prose: note,
|
|
1501
|
+
status: "note",
|
|
1502
|
+
mode: "literate",
|
|
1503
|
+
sessionName: replActiveSessionName,
|
|
1504
|
+
runtime: getActiveReplRuntime(),
|
|
1505
|
+
});
|
|
1506
|
+
setStatus("Added note to REPL Studio.", "success");
|
|
1507
|
+
renderReplViewIfActive({ force: true });
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function sendReplPayload(payload) {
|
|
1511
|
+
const session = getActiveReplSession();
|
|
1512
|
+
if (!session) {
|
|
1513
|
+
setStatus("Start or select a REPL session first.", "warning");
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (!payload || payload.error) {
|
|
1517
|
+
setStatus(payload && payload.error ? payload.error : "Nothing to send to REPL.", "warning");
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (payload.noteOnly) {
|
|
1521
|
+
if (String(payload.prose || "").trim()) {
|
|
1522
|
+
addReplJournalEntry({
|
|
1523
|
+
label: payload.label || "note",
|
|
1524
|
+
prose: payload.prose,
|
|
1525
|
+
status: "note",
|
|
1526
|
+
mode: payload.mode || "literate",
|
|
1527
|
+
sessionName: session.sessionName,
|
|
1528
|
+
runtime: getActiveReplRuntime(),
|
|
1529
|
+
skippedChunks: payload.skippedChunks,
|
|
1530
|
+
});
|
|
1531
|
+
setStatus("Added prose to REPL Studio.", "success");
|
|
1532
|
+
renderReplViewIfActive({ force: true });
|
|
1533
|
+
} else {
|
|
1534
|
+
setStatus("No code or prose found to send.", "warning");
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const text = String(payload.text || "");
|
|
1539
|
+
if (!text.trim()) {
|
|
1540
|
+
setStatus("Editor text is empty.", "warning");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const requestId = makeRequestId();
|
|
1544
|
+
const journalEntry = addReplJournalEntry({
|
|
1545
|
+
requestId,
|
|
1546
|
+
sessionName: session.sessionName,
|
|
1547
|
+
runtime: getActiveReplRuntime(),
|
|
1548
|
+
label: payload.label,
|
|
1549
|
+
mode: payload.mode,
|
|
1550
|
+
prose: payload.prose,
|
|
1551
|
+
code: text,
|
|
1552
|
+
beforeTranscript: replTranscript,
|
|
1553
|
+
status: "sending",
|
|
1554
|
+
skippedChunks: payload.skippedChunks,
|
|
1555
|
+
});
|
|
1556
|
+
activeReplJournalEntryId = journalEntry.id;
|
|
1557
|
+
replBusy = true;
|
|
1558
|
+
syncActionButtons();
|
|
1559
|
+
renderReplViewIfActive({ force: true });
|
|
1560
|
+
const skippedSuffix = payload.skippedChunks ? " (skipped " + payload.skippedChunks + " incompatible chunk" + (payload.skippedChunks === 1 ? "" : "s") + ")" : "";
|
|
1561
|
+
setStatus("Sending " + (payload.label || "editor text") + " to REPL…" + skippedSuffix, "info");
|
|
1562
|
+
if (!sendMessage({ type: "repl_send_request", requestId, sessionName: session.sessionName, text })) {
|
|
1563
|
+
replBusy = false;
|
|
1564
|
+
replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
|
|
1565
|
+
persistReplJournalEntries();
|
|
1566
|
+
syncActionButtons();
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function sendEditorTextToRepl(options) {
|
|
1571
|
+
const action = options && options.action ? options.action : "default";
|
|
1572
|
+
if (action === "all-chunks") {
|
|
1573
|
+
sendReplPayload(buildAllChunksReplSendPayload());
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (action === "note") {
|
|
1577
|
+
addSelectedReplJournalNote();
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildRawReplSendPayload());
|
|
1581
|
+
}
|
|
1582
|
+
|
|
748
1583
|
function renderTraceViewIfActive() {
|
|
749
1584
|
if (rightView !== "trace") return;
|
|
750
1585
|
if (traceRenderRaf !== null) return;
|
|
@@ -1209,6 +2044,8 @@
|
|
|
1209
2044
|
const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
1210
2045
|
if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
|
|
1211
2046
|
if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
|
|
2047
|
+
if (!isEditorOnlyMode && sendReplBtn) actionLineOneEl.appendChild(sendReplBtn);
|
|
2048
|
+
if (!isEditorOnlyMode && replSendModeSelect) actionLineOneEl.appendChild(replSendModeSelect);
|
|
1212
2049
|
const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
1213
2050
|
actionLineTwoEl.appendChild(copyDraftBtn);
|
|
1214
2051
|
if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
|
|
@@ -2200,6 +3037,23 @@
|
|
|
2200
3037
|
return;
|
|
2201
3038
|
}
|
|
2202
3039
|
|
|
3040
|
+
if (
|
|
3041
|
+
key === "Enter"
|
|
3042
|
+
&& (event.metaKey || event.ctrlKey)
|
|
3043
|
+
&& !event.altKey
|
|
3044
|
+
&& event.shiftKey
|
|
3045
|
+
&& activePane === "left"
|
|
3046
|
+
&& !isEditorOnlyMode
|
|
3047
|
+
) {
|
|
3048
|
+
event.preventDefault();
|
|
3049
|
+
if (sendReplBtn && !sendReplBtn.hidden && !sendReplBtn.disabled) {
|
|
3050
|
+
sendReplBtn.click();
|
|
3051
|
+
} else {
|
|
3052
|
+
setStatus("Open REPL view and start/select a session before sending to REPL.", "warning");
|
|
3053
|
+
}
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
2203
3057
|
if (
|
|
2204
3058
|
key === "Enter"
|
|
2205
3059
|
&& (event.metaKey || event.ctrlKey)
|
|
@@ -2483,6 +3337,12 @@
|
|
|
2483
3337
|
|
|
2484
3338
|
function updateReferenceBadge() {
|
|
2485
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;
|
|
2486
3346
|
|
|
2487
3347
|
if (rightView === "trace") {
|
|
2488
3348
|
const state = traceState || createEmptyTraceState();
|
|
@@ -3702,8 +4562,13 @@
|
|
|
3702
4562
|
}
|
|
3703
4563
|
|
|
3704
4564
|
function handleTracePaneScroll() {
|
|
3705
|
-
if (rightView
|
|
3706
|
-
|
|
4565
|
+
if (rightView === "trace") {
|
|
4566
|
+
traceAutoScroll = shouldStickTraceToBottom();
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
if (rightView === "repl") {
|
|
4570
|
+
replFollow = shouldStickTraceToBottom();
|
|
4571
|
+
}
|
|
3707
4572
|
}
|
|
3708
4573
|
|
|
3709
4574
|
async function handleTracePaneClick(event) {
|
|
@@ -3744,10 +4609,133 @@
|
|
|
3744
4609
|
}
|
|
3745
4610
|
}
|
|
3746
4611
|
|
|
4612
|
+
function handleReplPaneClick(event) {
|
|
4613
|
+
if (rightView !== "repl") return;
|
|
4614
|
+
const target = event.target;
|
|
4615
|
+
const actionBtn = target instanceof Element ? target.closest("[data-repl-action]") : null;
|
|
4616
|
+
if (!actionBtn) return;
|
|
4617
|
+
event.preventDefault();
|
|
4618
|
+
const action = actionBtn.getAttribute("data-repl-action") || "";
|
|
4619
|
+
if (action === "start" || action === "new-session") {
|
|
4620
|
+
const requestId = makeRequestId();
|
|
4621
|
+
replBusy = true;
|
|
4622
|
+
replError = "";
|
|
4623
|
+
replMessage = (action === "new-session" ? "Starting new " : "Starting ") + replRuntime + " REPL…";
|
|
4624
|
+
syncActionButtons();
|
|
4625
|
+
renderReplViewIfActive({ force: true });
|
|
4626
|
+
if (!sendMessage({ type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" })) {
|
|
4627
|
+
replBusy = false;
|
|
4628
|
+
syncActionButtons();
|
|
4629
|
+
}
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
if (action === "stop-session") {
|
|
4633
|
+
const session = getActiveReplSession();
|
|
4634
|
+
if (!session) {
|
|
4635
|
+
setStatus("Start or select a REPL session first.", "warning");
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
const requestId = makeRequestId();
|
|
4639
|
+
replBusy = true;
|
|
4640
|
+
replError = "";
|
|
4641
|
+
replMessage = "Stopping " + session.sessionName + "…";
|
|
4642
|
+
syncActionButtons();
|
|
4643
|
+
renderReplViewIfActive({ force: true });
|
|
4644
|
+
if (!sendMessage({ type: "repl_stop_request", requestId, sessionName: session.sessionName })) {
|
|
4645
|
+
replBusy = false;
|
|
4646
|
+
syncActionButtons();
|
|
4647
|
+
}
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
if (action === "interrupt") {
|
|
4651
|
+
const session = getActiveReplSession();
|
|
4652
|
+
if (!session) {
|
|
4653
|
+
setStatus("Start or select a REPL session first.", "warning");
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
const requestId = makeRequestId();
|
|
4657
|
+
replBusy = true;
|
|
4658
|
+
replError = "";
|
|
4659
|
+
replMessage = "Interrupting REPL…";
|
|
4660
|
+
syncActionButtons();
|
|
4661
|
+
renderReplViewIfActive({ force: true });
|
|
4662
|
+
if (!sendMessage({ type: "repl_interrupt_request", requestId, sessionName: session.sessionName })) {
|
|
4663
|
+
replBusy = false;
|
|
4664
|
+
syncActionButtons();
|
|
4665
|
+
}
|
|
4666
|
+
return;
|
|
4667
|
+
}
|
|
4668
|
+
if (action === "run-all-chunks") {
|
|
4669
|
+
sendEditorTextToRepl({ action: "all-chunks" });
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
if (action === "journal-note") {
|
|
4673
|
+
sendEditorTextToRepl({ action: "note" });
|
|
4674
|
+
return;
|
|
4675
|
+
}
|
|
4676
|
+
if (action === "journal-toggle") {
|
|
4677
|
+
setReplJournalCollapsed(!replJournalCollapsed);
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4680
|
+
if (action === "mirror-toggle") {
|
|
4681
|
+
setReplMirrorCollapsed(!replMirrorCollapsed);
|
|
4682
|
+
return;
|
|
4683
|
+
}
|
|
4684
|
+
if (action === "load-journal") {
|
|
4685
|
+
loadReplJournalIntoEditor();
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
if (action === "copy-journal") {
|
|
4689
|
+
void copyReplJournalToClipboard();
|
|
4690
|
+
return;
|
|
4691
|
+
}
|
|
4692
|
+
if (action === "export-journal") {
|
|
4693
|
+
exportReplJournalMarkdown();
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (action === "clear-journal") {
|
|
4697
|
+
clearReplJournal();
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
if (action === "refresh") {
|
|
4701
|
+
replError = "";
|
|
4702
|
+
replMessage = "";
|
|
4703
|
+
requestReplCapture();
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
if (action === "follow") {
|
|
4707
|
+
replFollow = !replFollow;
|
|
4708
|
+
renderReplViewIfActive({ force: true });
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
function handleReplPaneChange(event) {
|
|
4713
|
+
if (rightView !== "repl") return;
|
|
4714
|
+
const target = event.target;
|
|
4715
|
+
if (!(target instanceof Element)) return;
|
|
4716
|
+
const runtimeSelect = target.closest("[data-repl-runtime]");
|
|
4717
|
+
if (runtimeSelect && "value" in runtimeSelect) {
|
|
4718
|
+
setReplRuntime(runtimeSelect.value);
|
|
4719
|
+
renderReplViewIfActive({ force: true });
|
|
4720
|
+
return;
|
|
4721
|
+
}
|
|
4722
|
+
const sessionSelect = target.closest("[data-repl-session]");
|
|
4723
|
+
if (sessionSelect && "value" in sessionSelect) {
|
|
4724
|
+
setActiveReplSession(sessionSelect.value);
|
|
4725
|
+
replError = "";
|
|
4726
|
+
replMessage = "";
|
|
4727
|
+
replFollow = true;
|
|
4728
|
+
requestReplCapture();
|
|
4729
|
+
renderReplViewIfActive({ force: true });
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
|
|
3747
4733
|
function attachResponsePaneInteractionHandlers() {
|
|
3748
4734
|
if (!critiqueViewEl) return;
|
|
3749
4735
|
critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
|
|
3750
4736
|
critiqueViewEl.addEventListener("click", handleTracePaneClick);
|
|
4737
|
+
critiqueViewEl.addEventListener("click", handleReplPaneClick);
|
|
4738
|
+
critiqueViewEl.addEventListener("change", handleReplPaneChange);
|
|
3751
4739
|
}
|
|
3752
4740
|
|
|
3753
4741
|
function replaceResponsePaneWithClone() {
|
|
@@ -3976,35 +4964,42 @@
|
|
|
3976
4964
|
return;
|
|
3977
4965
|
}
|
|
3978
4966
|
|
|
4967
|
+
const exportingReplJournal = rightView === "repl";
|
|
3979
4968
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
3980
|
-
if (!rightPaneShowsPreview) {
|
|
3981
|
-
setStatus("Switch right pane to Response (Preview)
|
|
4969
|
+
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
4970
|
+
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
if (exportingReplJournal && !replJournalEntries.length) {
|
|
4974
|
+
setStatus("No REPL Studio entries to export yet.", "warning");
|
|
3982
4975
|
return;
|
|
3983
4976
|
}
|
|
3984
4977
|
|
|
3985
|
-
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
4978
|
+
const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
|
|
3986
4979
|
if (htmlArtifactSource) {
|
|
3987
4980
|
setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
|
|
3988
4981
|
return;
|
|
3989
4982
|
}
|
|
3990
4983
|
|
|
3991
|
-
const markdown =
|
|
3992
|
-
?
|
|
3993
|
-
:
|
|
4984
|
+
const markdown = exportingReplJournal
|
|
4985
|
+
? buildReplJournalMarkdown()
|
|
4986
|
+
: (rightView === "editor-preview"
|
|
4987
|
+
? prepareEditorTextForPdfExport(sourceTextEl.value)
|
|
4988
|
+
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
3994
4989
|
if (!markdown || !markdown.trim()) {
|
|
3995
4990
|
setStatus("Nothing to export yet.", "warning");
|
|
3996
4991
|
return;
|
|
3997
4992
|
}
|
|
3998
4993
|
|
|
3999
4994
|
const effectivePath = getEffectiveSavePath();
|
|
4000
|
-
const sourcePath = effectivePath || sourceState.path || "";
|
|
4995
|
+
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
4001
4996
|
const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
|
|
4002
4997
|
const isEditorPreview = rightView === "editor-preview";
|
|
4003
4998
|
const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
|
|
4004
4999
|
const isLatex = isEditorPreview
|
|
4005
5000
|
? editorPdfLanguage === "latex"
|
|
4006
5001
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
4007
|
-
let filenameHint = isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf";
|
|
5002
|
+
let filenameHint = exportingReplJournal ? "repl-studio.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
|
|
4008
5003
|
if (sourcePath) {
|
|
4009
5004
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
4010
5005
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
@@ -4141,31 +5136,36 @@
|
|
|
4141
5136
|
return;
|
|
4142
5137
|
}
|
|
4143
5138
|
|
|
5139
|
+
const exportingReplJournal = rightView === "repl";
|
|
4144
5140
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4145
|
-
if (!rightPaneShowsPreview) {
|
|
4146
|
-
setStatus("Switch right pane to Response (Preview)
|
|
5141
|
+
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
5142
|
+
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
if (exportingReplJournal && !replJournalEntries.length) {
|
|
5146
|
+
setStatus("No REPL Studio entries to export yet.", "warning");
|
|
4147
5147
|
return;
|
|
4148
5148
|
}
|
|
4149
5149
|
|
|
4150
|
-
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
4151
|
-
const markdown = htmlArtifactSource || (rightView === "editor-preview"
|
|
5150
|
+
const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
|
|
5151
|
+
const markdown = exportingReplJournal ? buildReplJournalMarkdown() : (htmlArtifactSource || (rightView === "editor-preview"
|
|
4152
5152
|
? prepareEditorTextForHtmlExport(sourceTextEl.value)
|
|
4153
|
-
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
5153
|
+
: prepareEditorTextForPreview(latestResponseMarkdown)));
|
|
4154
5154
|
if (!markdown || !markdown.trim()) {
|
|
4155
5155
|
setStatus("Nothing to export yet.", "warning");
|
|
4156
5156
|
return;
|
|
4157
5157
|
}
|
|
4158
5158
|
|
|
4159
5159
|
const effectivePath = getEffectiveSavePath();
|
|
4160
|
-
const sourcePath = effectivePath || sourceState.path || "";
|
|
5160
|
+
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
4161
5161
|
const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
|
|
4162
5162
|
const isEditorPreview = rightView === "editor-preview";
|
|
4163
5163
|
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
|
|
4164
5164
|
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
4165
5165
|
? editorHtmlLanguage === "latex"
|
|
4166
5166
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
4167
|
-
let filenameHint = isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html";
|
|
4168
|
-
let titleHint = isEditorPreview ? "Studio editor preview" : "Studio response preview";
|
|
5167
|
+
let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
|
|
5168
|
+
let titleHint = exportingReplJournal ? "REPL Studio" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
|
|
4169
5169
|
if (sourcePath) {
|
|
4170
5170
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
4171
5171
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
@@ -4646,6 +5646,16 @@
|
|
|
4646
5646
|
return remaining < 56;
|
|
4647
5647
|
}
|
|
4648
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
|
+
|
|
4649
5659
|
function formatTraceOutputSize(text) {
|
|
4650
5660
|
const value = String(text || "");
|
|
4651
5661
|
const chars = value.length;
|
|
@@ -4722,6 +5732,246 @@
|
|
|
4722
5732
|
return "<div class='trace-image-gallery'>" + cards + "</div>";
|
|
4723
5733
|
}
|
|
4724
5734
|
|
|
5735
|
+
function getReplRuntimeHighlightLanguage(runtime) {
|
|
5736
|
+
const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
|
|
5737
|
+
if (normalized === "shell") return "bash";
|
|
5738
|
+
if (normalized === "ipython") return "python";
|
|
5739
|
+
if (normalized === "python" || normalized === "julia" || normalized === "r") return normalized;
|
|
5740
|
+
return "text";
|
|
5741
|
+
}
|
|
5742
|
+
|
|
5743
|
+
function renderHighlightedReplCode(text, runtime) {
|
|
5744
|
+
const source = String(text || "");
|
|
5745
|
+
const language = getReplRuntimeHighlightLanguage(runtime);
|
|
5746
|
+
if (!source.trim() || language === "text") return escapeHtml(source);
|
|
5747
|
+
return highlightCode(source, language, "preview");
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
function renderHighlightedReplTranscriptLine(line, runtime) {
|
|
5751
|
+
const source = String(line || "");
|
|
5752
|
+
const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
|
|
5753
|
+
const language = getReplRuntimeHighlightLanguage(normalized);
|
|
5754
|
+
let match = null;
|
|
5755
|
+
if (normalized === "python" || normalized === "ipython") {
|
|
5756
|
+
match = source.match(/^(\s*(?:>>>|\.\.\.|In \[\d+\]:|\.\.\.?:)\s?)(.*)$/);
|
|
5757
|
+
} else if (normalized === "r") {
|
|
5758
|
+
match = source.match(/^(\s*(?:>|\+)\s?)(.*)$/);
|
|
5759
|
+
} else if (normalized === "julia") {
|
|
5760
|
+
match = source.match(/^(\s*julia>\s?)(.*)$/);
|
|
5761
|
+
} else if (normalized === "shell") {
|
|
5762
|
+
match = source.match(/^(.+(?:[$%#])\s+)(.+)$/);
|
|
5763
|
+
} else if (normalized === "ghci") {
|
|
5764
|
+
match = source.match(/^(\s*(?:ghci>|[A-Za-z0-9_.]+>)\s?)(.*)$/);
|
|
5765
|
+
} else if (normalized === "clojure") {
|
|
5766
|
+
match = source.match(/^(\s*(?:[A-Za-z0-9_.-]+=>)\s?)(.*)$/);
|
|
5767
|
+
}
|
|
5768
|
+
if (!match || !String(match[2] || "").trim() || language === "text") return escapeHtml(source);
|
|
5769
|
+
return "<span class='repl-prompt'>" + escapeHtml(match[1] || "") + "</span>" + highlightCodeLine(match[2] || "", language, "preview");
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
function renderReplTranscriptHtml(transcript, runtime) {
|
|
5773
|
+
const source = String(transcript || "");
|
|
5774
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
5775
|
+
const body = lines.map((line) => renderHighlightedReplTranscriptLine(line, runtime)).join("\n");
|
|
5776
|
+
return "<pre class='repl-transcript repl-transcript-highlight'>" + body + "</pre>";
|
|
5777
|
+
}
|
|
5778
|
+
|
|
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) {
|
|
5845
|
+
const hasEntries = replJournalEntries.length > 0;
|
|
5846
|
+
const entryCount = replJournalEntries.length;
|
|
5847
|
+
const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
|
|
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>";
|
|
5850
|
+
const summaryText = hasEntries
|
|
5851
|
+
? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
|
|
5852
|
+
: "Studio-sent code and notes will appear here.";
|
|
5853
|
+
if (replJournalCollapsed) {
|
|
5854
|
+
return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
|
|
5855
|
+
+ "<div class='repl-journal-compact-row'>"
|
|
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>"
|
|
5858
|
+
+ "</div>"
|
|
5859
|
+
+ "</section>";
|
|
5860
|
+
}
|
|
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
|
+
: "";
|
|
5866
|
+
const cards = replJournalEntries.slice(-12).map((entry) => {
|
|
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>"
|
|
5874
|
+
: "";
|
|
5875
|
+
const prose = proseText
|
|
5876
|
+
? "<div class='repl-studio-note'>" + escapeHtml(proseText) + "</div>"
|
|
5877
|
+
: "";
|
|
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>"
|
|
5880
|
+
: "";
|
|
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>" : "")
|
|
5886
|
+
+ prose
|
|
5887
|
+
+ code
|
|
5888
|
+
+ output
|
|
5889
|
+
+ pending
|
|
5890
|
+
+ "</article>";
|
|
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>");
|
|
5894
|
+
return "<section class='repl-journal'>"
|
|
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>"
|
|
5896
|
+
+ (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
|
|
5897
|
+
+ "<div class='repl-journal-list'>" + terminalContent + "</div>"
|
|
5898
|
+
+ "</section>";
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5901
|
+
function buildReplMirrorHtml(body, transcript) {
|
|
5902
|
+
const hasTranscript = Boolean(String(transcript || "").trim());
|
|
5903
|
+
const summary = hasTranscript
|
|
5904
|
+
? "Raw tmux mirror · " + formatCompactNumber(String(transcript || "").length) + " chars"
|
|
5905
|
+
: "Raw tmux mirror";
|
|
5906
|
+
const shouldCollapse = replMirrorCollapsed;
|
|
5907
|
+
const actions = "<div class='repl-journal-actions'>"
|
|
5908
|
+
+ "<button type='button' data-repl-action='mirror-toggle' aria-expanded='" + (shouldCollapse ? "false" : "true") + "'>" + (shouldCollapse ? "Show mirror" : "Hide mirror") + "</button>"
|
|
5909
|
+
+ "</div>";
|
|
5910
|
+
if (shouldCollapse) {
|
|
5911
|
+
return "<section class='repl-mirror repl-mirror-compact'>"
|
|
5912
|
+
+ "<div class='repl-journal-compact-row'>"
|
|
5913
|
+
+ "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Mirror</span><span>" + escapeHtml(summary) + "</span></div>"
|
|
5914
|
+
+ actions
|
|
5915
|
+
+ "</div>"
|
|
5916
|
+
+ "</section>";
|
|
5917
|
+
}
|
|
5918
|
+
return "<section class='repl-mirror'>"
|
|
5919
|
+
+ "<div class='repl-journal-header'><div><h3>Raw REPL mirror</h3><p>Best-effort tmux pane mirror. Useful for directly typed commands and debugging; REPL Studio above is the cleaner record.</p></div>" + actions + "</div>"
|
|
5920
|
+
+ body
|
|
5921
|
+
+ "</section>";
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5924
|
+
function buildReplPanelHtml() {
|
|
5925
|
+
const runtimeOptions = [
|
|
5926
|
+
["shell", "Shell"],
|
|
5927
|
+
["python", "Python"],
|
|
5928
|
+
["ipython", "IPython"],
|
|
5929
|
+
["julia", "Julia"],
|
|
5930
|
+
["r", "R"],
|
|
5931
|
+
["ghci", "GHCi"],
|
|
5932
|
+
["clojure", "Clojure"],
|
|
5933
|
+
].map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (replRuntime === value ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
|
|
5934
|
+
const sessionOptions = replSessions.length
|
|
5935
|
+
? replSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
|
|
5936
|
+
: "<option value=''>No REPL sessions</option>";
|
|
5937
|
+
const activeSession = getActiveReplSession();
|
|
5938
|
+
const transcript = trimReplTranscript(replTranscript);
|
|
5939
|
+
const emptyMessage = replTmuxAvailable === false
|
|
5940
|
+
? "tmux is not available. Install tmux to use Studio REPL sessions."
|
|
5941
|
+
: (activeSession ? "No REPL output captured yet." : "Start a REPL session, or attach to a detected pi-repl session, to mirror it here.");
|
|
5942
|
+
const body = transcript
|
|
5943
|
+
? renderReplTranscriptHtml(transcript, activeSession ? activeSession.runtime : replRuntime)
|
|
5944
|
+
: "<div class='repl-empty'>" + escapeHtml(emptyMessage) + "</div>";
|
|
5945
|
+
const canSendToActiveSession = Boolean(activeSession) && !replBusy && replTmuxAvailable !== false;
|
|
5946
|
+
const canStopActiveSession = Boolean(activeSession && activeSession.source === "studio" && !replBusy && replTmuxAvailable !== false);
|
|
5947
|
+
return "<div class='repl-panel'>"
|
|
5948
|
+
+ "<div class='repl-toolbar'>"
|
|
5949
|
+
+ "<div class='repl-controls'>"
|
|
5950
|
+
+ "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
|
|
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>"
|
|
5952
|
+
+ "<label class='repl-control-label'>Session <select data-repl-session aria-label='REPL session'" + (replSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
|
|
5953
|
+
+ "<details class='repl-more-controls'>"
|
|
5954
|
+
+ "<summary title='More REPL actions'>More</summary>"
|
|
5955
|
+
+ "<div class='repl-more-menu'>"
|
|
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>"
|
|
5957
|
+
+ "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
|
|
5958
|
+
+ "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
|
|
5959
|
+
+ "<button type='button' data-repl-action='run-all-chunks'" + (canSendToActiveSession ? "" : " disabled") + " title='Literate send: send all fenced code chunks matching the active REPL runtime.'>Run all chunks</button>"
|
|
5960
|
+
+ "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to REPL Studio (Literate send) without sending it to the runtime.'>Add note</button>"
|
|
5961
|
+
+ "<button type='button' data-repl-action='refresh'>Refresh</button>"
|
|
5962
|
+
+ "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
|
|
5963
|
+
+ "</div>"
|
|
5964
|
+
+ "</details>"
|
|
5965
|
+
+ "</div>"
|
|
5966
|
+
+ "</div>"
|
|
5967
|
+
+ (replMessage ? "<div class='repl-notice repl-notice-info'>" + escapeHtml(replMessage) + "</div>" : "")
|
|
5968
|
+
+ (replError ? "<div class='repl-notice repl-notice-error'>" + escapeHtml(replError) + "</div>" : "")
|
|
5969
|
+
+ buildReplJournalHtml(transcript)
|
|
5970
|
+
+ buildReplStudioActionsHtml()
|
|
5971
|
+
+ buildReplMirrorHtml(body, transcript)
|
|
5972
|
+
+ "</div>";
|
|
5973
|
+
}
|
|
5974
|
+
|
|
4725
5975
|
function buildTracePanelHtml() {
|
|
4726
5976
|
const state = traceState || createEmptyTraceState();
|
|
4727
5977
|
const filter = normalizeTraceFilter(traceFilter);
|
|
@@ -4856,12 +6106,32 @@
|
|
|
4856
6106
|
scheduleResponsePaneRepaintNudge();
|
|
4857
6107
|
}
|
|
4858
6108
|
|
|
6109
|
+
function renderReplView() {
|
|
6110
|
+
if (!critiqueViewEl) return;
|
|
6111
|
+
const shouldStick = shouldAutoStickReplView();
|
|
6112
|
+
const previousScrollTop = critiqueViewEl.scrollTop;
|
|
6113
|
+
finishPreviewRender(critiqueViewEl);
|
|
6114
|
+
critiqueViewEl.innerHTML = buildReplPanelHtml();
|
|
6115
|
+
critiqueViewEl.classList.remove("response-scroll-resetting");
|
|
6116
|
+
if (shouldStick) {
|
|
6117
|
+
critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
|
|
6118
|
+
} else {
|
|
6119
|
+
critiqueViewEl.scrollTop = previousScrollTop;
|
|
6120
|
+
}
|
|
6121
|
+
scheduleResponsePaneRepaintNudge();
|
|
6122
|
+
}
|
|
6123
|
+
|
|
4859
6124
|
function renderActiveResult() {
|
|
4860
6125
|
if (rightView === "trace") {
|
|
4861
6126
|
renderTraceView();
|
|
4862
6127
|
return;
|
|
4863
6128
|
}
|
|
4864
6129
|
|
|
6130
|
+
if (rightView === "repl") {
|
|
6131
|
+
renderReplView();
|
|
6132
|
+
return;
|
|
6133
|
+
}
|
|
6134
|
+
|
|
4865
6135
|
if (rightView === "editor-preview") {
|
|
4866
6136
|
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
4867
6137
|
if (!editorText.trim()) {
|
|
@@ -4936,10 +6206,10 @@
|
|
|
4936
6206
|
: normalizeForCompare(sourceTextEl.value);
|
|
4937
6207
|
const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
|
|
4938
6208
|
const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
|
|
4939
|
-
const
|
|
6209
|
+
const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
|
|
4940
6210
|
|
|
4941
6211
|
if (responseWrapEl) {
|
|
4942
|
-
responseWrapEl.hidden =
|
|
6212
|
+
responseWrapEl.hidden = showingAuxiliaryRightPane;
|
|
4943
6213
|
}
|
|
4944
6214
|
|
|
4945
6215
|
const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
|
|
@@ -4962,21 +6232,30 @@
|
|
|
4962
6232
|
copyResponseBtn.textContent = "Copy response text";
|
|
4963
6233
|
|
|
4964
6234
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4965
|
-
const
|
|
4966
|
-
const
|
|
4967
|
-
|
|
6235
|
+
const exportingReplJournal = rightView === "repl";
|
|
6236
|
+
const exportText = exportingReplJournal
|
|
6237
|
+
? (replJournalEntries.length ? buildReplJournalMarkdown() : "")
|
|
6238
|
+
: (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown);
|
|
6239
|
+
const canExportPreview = (rightPaneShowsPreview || exportingReplJournal) && Boolean(String(exportText || "").trim());
|
|
6240
|
+
const htmlArtifactExportSource = canExportPreview && !exportingReplJournal ? getRightPaneHtmlArtifactSource() : "";
|
|
4968
6241
|
const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
|
|
4969
6242
|
if (exportPdfBtn) {
|
|
4970
6243
|
exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4971
|
-
exportPdfBtn.textContent = previewExportInProgress
|
|
6244
|
+
exportPdfBtn.textContent = previewExportInProgress
|
|
6245
|
+
? "Exporting…"
|
|
6246
|
+
: (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
|
|
4972
6247
|
if (rightView === "trace") {
|
|
4973
6248
|
exportPdfBtn.title = "Working view does not support preview export.";
|
|
6249
|
+
} else if (exportingReplJournal && !replJournalEntries.length) {
|
|
6250
|
+
exportPdfBtn.title = "No REPL Studio entries to export yet.";
|
|
4974
6251
|
} else if (rightView === "markdown") {
|
|
4975
|
-
exportPdfBtn.title = "Switch right pane to Response (Preview)
|
|
6252
|
+
exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
|
|
4976
6253
|
} else if (!canExportPreview) {
|
|
4977
6254
|
exportPdfBtn.title = "Nothing to export yet.";
|
|
4978
6255
|
} else if (isHtmlArtifactPreview) {
|
|
4979
6256
|
exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
|
|
6257
|
+
} else if (exportingReplJournal) {
|
|
6258
|
+
exportPdfBtn.title = "Choose PDF or HTML and export REPL Studio.";
|
|
4980
6259
|
} else {
|
|
4981
6260
|
exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
|
|
4982
6261
|
}
|
|
@@ -4985,18 +6264,20 @@
|
|
|
4985
6264
|
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
4986
6265
|
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
4987
6266
|
? "Interactive HTML preview PDF export is not available yet."
|
|
4988
|
-
: "Export the current right-pane preview as PDF.";
|
|
6267
|
+
: (exportingReplJournal ? "Export REPL Studio as PDF." : "Export the current right-pane preview as PDF.");
|
|
4989
6268
|
}
|
|
4990
6269
|
if (exportPreviewHtmlBtn) {
|
|
4991
6270
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4992
6271
|
exportPreviewHtmlBtn.title = isHtmlArtifactPreview
|
|
4993
6272
|
? "Export the authored HTML preview."
|
|
4994
|
-
: "Export the current right-pane preview as standalone HTML.";
|
|
6273
|
+
: (exportingReplJournal ? "Export REPL Studio as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
|
|
4995
6274
|
}
|
|
4996
6275
|
if (exportPreviewControlsEl) {
|
|
4997
6276
|
exportPreviewControlsEl.title = canExportPreview
|
|
4998
|
-
? (
|
|
4999
|
-
|
|
6277
|
+
? (exportingReplJournal
|
|
6278
|
+
? "Choose a format and export REPL Studio."
|
|
6279
|
+
: (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
|
|
6280
|
+
: (exportingReplJournal ? "No REPL Studio entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
|
|
5000
6281
|
}
|
|
5001
6282
|
if (!canExportPreview || previewExportInProgress) {
|
|
5002
6283
|
closeExportPreviewMenu();
|
|
@@ -5186,6 +6467,108 @@
|
|
|
5186
6467
|
updateOutlineUi();
|
|
5187
6468
|
}
|
|
5188
6469
|
|
|
6470
|
+
function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
|
|
6471
|
+
const value = String(nextText || "");
|
|
6472
|
+
sourceTextEl.value = value;
|
|
6473
|
+
const maxIndex = value.length;
|
|
6474
|
+
const safeStart = Math.max(0, Math.min(Math.floor(Number(selectionStart) || 0), maxIndex));
|
|
6475
|
+
const safeEnd = Math.max(safeStart, Math.min(Math.floor(Number(selectionEnd) || safeStart), maxIndex));
|
|
6476
|
+
sourceTextEl.setSelectionRange(safeStart, safeEnd);
|
|
6477
|
+
sourceTextEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
6478
|
+
syncEditorHighlightScroll();
|
|
6479
|
+
if (editorView === "markdown") {
|
|
6480
|
+
scheduleEditorLineNumberRender();
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
|
|
6484
|
+
function getSourceTextLineEditBounds(text, selectionStart, selectionEnd) {
|
|
6485
|
+
const source = String(text || "");
|
|
6486
|
+
const safeStart = Math.max(0, Math.min(Math.floor(Number(selectionStart) || 0), source.length));
|
|
6487
|
+
const safeEnd = Math.max(safeStart, Math.min(Math.floor(Number(selectionEnd) || safeStart), source.length));
|
|
6488
|
+
const rangeEnd = safeEnd > safeStart && source.charAt(safeEnd - 1) === "\n" ? safeEnd - 1 : safeEnd;
|
|
6489
|
+
const lineStart = source.lastIndexOf("\n", Math.max(0, safeStart - 1)) + 1;
|
|
6490
|
+
const nextNewline = source.indexOf("\n", Math.max(lineStart, rangeEnd));
|
|
6491
|
+
const lineEnd = nextNewline >= 0 ? nextNewline : source.length;
|
|
6492
|
+
return { lineStart, lineEnd, selectionStart: safeStart, selectionEnd: safeEnd };
|
|
6493
|
+
}
|
|
6494
|
+
|
|
6495
|
+
function countSourceTextLines(text) {
|
|
6496
|
+
if (!text) return 1;
|
|
6497
|
+
return String(text).split("\n").length;
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
function getSourceLineUnindentLength(line) {
|
|
6501
|
+
const source = String(line || "");
|
|
6502
|
+
if (source.startsWith(EDITOR_TAB_TEXT)) return EDITOR_TAB_TEXT.length;
|
|
6503
|
+
if (source.startsWith("\t")) return 1;
|
|
6504
|
+
const spaces = source.match(/^ +/);
|
|
6505
|
+
return spaces ? Math.min(EDITOR_TAB_TEXT.length, spaces[0].length) : 0;
|
|
6506
|
+
}
|
|
6507
|
+
|
|
6508
|
+
function getRemovedIndentBeforePosition(lineStart, removeLength, position) {
|
|
6509
|
+
if (removeLength <= 0 || position <= lineStart) return 0;
|
|
6510
|
+
if (position <= lineStart + removeLength) return position - lineStart;
|
|
6511
|
+
return removeLength;
|
|
6512
|
+
}
|
|
6513
|
+
|
|
6514
|
+
function indentSourceTextSelection() {
|
|
6515
|
+
const source = String(sourceTextEl.value || "");
|
|
6516
|
+
const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : source.length;
|
|
6517
|
+
const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
|
|
6518
|
+
const selected = source.slice(Math.min(start, end), Math.max(start, end));
|
|
6519
|
+
if (start === end || !selected.includes("\n")) {
|
|
6520
|
+
const before = source.slice(0, Math.min(start, end));
|
|
6521
|
+
const after = source.slice(Math.max(start, end));
|
|
6522
|
+
const nextCaret = before.length + EDITOR_TAB_TEXT.length;
|
|
6523
|
+
applySourceTextEdit(before + EDITOR_TAB_TEXT + after, nextCaret, nextCaret);
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6527
|
+
const bounds = getSourceTextLineEditBounds(source, start, end);
|
|
6528
|
+
const segment = source.slice(bounds.lineStart, bounds.lineEnd);
|
|
6529
|
+
const lineCount = countSourceTextLines(segment);
|
|
6530
|
+
const replacement = segment.replace(/^/gm, EDITOR_TAB_TEXT);
|
|
6531
|
+
const next = source.slice(0, bounds.lineStart) + replacement + source.slice(bounds.lineEnd);
|
|
6532
|
+
const nextStart = bounds.selectionStart + (bounds.lineStart < bounds.selectionStart ? EDITOR_TAB_TEXT.length : 0);
|
|
6533
|
+
const nextEnd = bounds.selectionEnd + (lineCount * EDITOR_TAB_TEXT.length);
|
|
6534
|
+
applySourceTextEdit(next, nextStart, nextEnd);
|
|
6535
|
+
}
|
|
6536
|
+
|
|
6537
|
+
function unindentSourceTextSelection() {
|
|
6538
|
+
const source = String(sourceTextEl.value || "");
|
|
6539
|
+
const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : source.length;
|
|
6540
|
+
const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
|
|
6541
|
+
const bounds = getSourceTextLineEditBounds(source, start, end);
|
|
6542
|
+
const segment = source.slice(bounds.lineStart, bounds.lineEnd);
|
|
6543
|
+
const lines = segment.split("\n");
|
|
6544
|
+
let absoluteLineStart = bounds.lineStart;
|
|
6545
|
+
let removedBeforeStart = 0;
|
|
6546
|
+
let removedBeforeEnd = 0;
|
|
6547
|
+
const nextLines = lines.map((line, index) => {
|
|
6548
|
+
const removeLength = getSourceLineUnindentLength(line);
|
|
6549
|
+
removedBeforeStart += getRemovedIndentBeforePosition(absoluteLineStart, removeLength, bounds.selectionStart);
|
|
6550
|
+
removedBeforeEnd += getRemovedIndentBeforePosition(absoluteLineStart, removeLength, bounds.selectionEnd);
|
|
6551
|
+
absoluteLineStart += line.length + (index < lines.length - 1 ? 1 : 0);
|
|
6552
|
+
return removeLength > 0 ? line.slice(removeLength) : line;
|
|
6553
|
+
});
|
|
6554
|
+
const replacement = nextLines.join("\n");
|
|
6555
|
+
if (replacement === segment) return;
|
|
6556
|
+
const next = source.slice(0, bounds.lineStart) + replacement + source.slice(bounds.lineEnd);
|
|
6557
|
+
const nextStart = Math.max(bounds.lineStart, bounds.selectionStart - removedBeforeStart);
|
|
6558
|
+
const nextEnd = Math.max(nextStart, bounds.selectionEnd - removedBeforeEnd);
|
|
6559
|
+
applySourceTextEdit(next, nextStart, nextEnd);
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
function handleSourceTextTabKey(event) {
|
|
6563
|
+
if (!event || event.key !== "Tab" || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
6564
|
+
event.preventDefault();
|
|
6565
|
+
if (event.shiftKey) {
|
|
6566
|
+
unindentSourceTextSelection();
|
|
6567
|
+
} else {
|
|
6568
|
+
indentSourceTextSelection();
|
|
6569
|
+
}
|
|
6570
|
+
}
|
|
6571
|
+
|
|
5189
6572
|
function setEditorView(nextView) {
|
|
5190
6573
|
editorView = nextView === "preview" ? "preview" : "markdown";
|
|
5191
6574
|
editorViewSelect.value = editorView;
|
|
@@ -5227,11 +6610,19 @@
|
|
|
5227
6610
|
? "preview"
|
|
5228
6611
|
: (nextView === "editor-preview"
|
|
5229
6612
|
? "editor-preview"
|
|
5230
|
-
: (
|
|
6613
|
+
: (nextView === "repl"
|
|
6614
|
+
? "repl"
|
|
6615
|
+
: ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown")));
|
|
5231
6616
|
rightViewSelect.value = rightView;
|
|
5232
6617
|
if (rightView === "trace" && previousView !== "trace") {
|
|
5233
6618
|
traceAutoScroll = true;
|
|
5234
6619
|
}
|
|
6620
|
+
if (rightView === "repl" && previousView !== "repl") {
|
|
6621
|
+
replFollow = true;
|
|
6622
|
+
startReplPolling();
|
|
6623
|
+
} else if (rightView !== "repl" && previousView === "repl") {
|
|
6624
|
+
stopReplPolling();
|
|
6625
|
+
}
|
|
5235
6626
|
|
|
5236
6627
|
if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
|
|
5237
6628
|
window.clearTimeout(responseEditorPreviewTimer);
|
|
@@ -10679,6 +12070,14 @@
|
|
|
10679
12070
|
queueSteerBtn.classList.remove("request-stop-active");
|
|
10680
12071
|
queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
|
|
10681
12072
|
}
|
|
12073
|
+
if (sendReplBtn) {
|
|
12074
|
+
sendReplBtn.hidden = true;
|
|
12075
|
+
sendReplBtn.disabled = true;
|
|
12076
|
+
}
|
|
12077
|
+
if (replSendModeSelect) {
|
|
12078
|
+
replSendModeSelect.hidden = true;
|
|
12079
|
+
replSendModeSelect.disabled = true;
|
|
12080
|
+
}
|
|
10682
12081
|
if (critiqueBtn) {
|
|
10683
12082
|
critiqueBtn.textContent = "Critique text";
|
|
10684
12083
|
critiqueBtn.classList.remove("request-stop-active");
|
|
@@ -10693,11 +12092,14 @@
|
|
|
10693
12092
|
sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
|
|
10694
12093
|
sendRunBtn.classList.toggle("request-stop-active", directIsStop);
|
|
10695
12094
|
sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
|
|
12095
|
+
const replHint = rightView === "repl" && getActiveReplSession()
|
|
12096
|
+
? " Includes active REPL identity for prompts that refer to it."
|
|
12097
|
+
: "";
|
|
10696
12098
|
sendRunBtn.title = directIsStop
|
|
10697
12099
|
? "Stop the active run. Shortcut: Esc."
|
|
10698
12100
|
: (annotationsEnabled
|
|
10699
|
-
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc."
|
|
10700
|
-
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc.");
|
|
12101
|
+
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint
|
|
12102
|
+
: "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint);
|
|
10701
12103
|
}
|
|
10702
12104
|
|
|
10703
12105
|
if (queueSteerBtn) {
|
|
@@ -10711,6 +12113,25 @@
|
|
|
10711
12113
|
: "Queue steering is available while Run editor text is active.";
|
|
10712
12114
|
}
|
|
10713
12115
|
|
|
12116
|
+
if (sendReplBtn) {
|
|
12117
|
+
const hasSession = Boolean(getActiveReplSession());
|
|
12118
|
+
sendReplBtn.hidden = rightView !== "repl";
|
|
12119
|
+
sendReplBtn.disabled = wsState === "Disconnected" || uiBusy || replBusy || !hasSession;
|
|
12120
|
+
sendReplBtn.title = hasSession
|
|
12121
|
+
? (replSendMode === "literate"
|
|
12122
|
+
? "Literate send: selected code/prose, or the current fenced code chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
|
|
12123
|
+
: "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
|
|
12124
|
+
: "Start or select a REPL session in the right pane first.";
|
|
12125
|
+
}
|
|
12126
|
+
if (replSendModeSelect) {
|
|
12127
|
+
replSendModeSelect.hidden = rightView !== "repl";
|
|
12128
|
+
replSendModeSelect.disabled = wsState === "Disconnected" || uiBusy || replBusy;
|
|
12129
|
+
replSendModeSelect.value = replSendMode;
|
|
12130
|
+
replSendModeSelect.title = replSendMode === "literate"
|
|
12131
|
+
? "Literate send: Send to REPL uses the selection/current fenced code chunk."
|
|
12132
|
+
: "Raw send: Send to REPL uses the selection, or full editor if no selection.";
|
|
12133
|
+
}
|
|
12134
|
+
|
|
10714
12135
|
if (critiqueBtn) {
|
|
10715
12136
|
critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique text";
|
|
10716
12137
|
critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
|
|
@@ -11025,6 +12446,108 @@
|
|
|
11025
12446
|
return;
|
|
11026
12447
|
}
|
|
11027
12448
|
|
|
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;
|
|
12457
|
+
replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
|
|
12458
|
+
const sessionsChanged = setReplSessions(message.sessions);
|
|
12459
|
+
if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
|
|
12460
|
+
setActiveReplSession(message.activeSessionName);
|
|
12461
|
+
}
|
|
12462
|
+
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
12463
|
+
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
12464
|
+
replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
|
|
12465
|
+
replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
|
|
12466
|
+
replBusy = false;
|
|
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 });
|
|
12490
|
+
updateReferenceBadge();
|
|
12491
|
+
return;
|
|
12492
|
+
}
|
|
12493
|
+
|
|
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;
|
|
12502
|
+
if (message.session) {
|
|
12503
|
+
const session = normalizeReplSession(message.session);
|
|
12504
|
+
if (session && !replSessions.some((candidate) => candidate.sessionName === session.sessionName)) {
|
|
12505
|
+
replSessions = [...replSessions, session];
|
|
12506
|
+
sessionsChanged = true;
|
|
12507
|
+
}
|
|
12508
|
+
}
|
|
12509
|
+
if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
|
|
12510
|
+
setActiveReplSession(message.activeSessionName);
|
|
12511
|
+
}
|
|
12512
|
+
let journalChanged = false;
|
|
12513
|
+
if (typeof message.transcript === "string") {
|
|
12514
|
+
replTranscript = trimReplTranscript(message.transcript);
|
|
12515
|
+
journalChanged = updateActiveReplJournalEntryFromTranscript(
|
|
12516
|
+
typeof message.activeSessionName === "string" && message.activeSessionName.trim() ? message.activeSessionName : replActiveSessionName,
|
|
12517
|
+
replTranscript
|
|
12518
|
+
);
|
|
12519
|
+
}
|
|
12520
|
+
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
12521
|
+
replError = typeof message.replError === "string" ? message.replError : "";
|
|
12522
|
+
if (typeof message.replMessage === "string") replMessage = message.replMessage;
|
|
12523
|
+
replBusy = false;
|
|
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();
|
|
12533
|
+
updateReferenceBadge();
|
|
12534
|
+
return;
|
|
12535
|
+
}
|
|
12536
|
+
|
|
12537
|
+
if (message.type === "repl_send_ack") {
|
|
12538
|
+
replBusy = false;
|
|
12539
|
+
replMessage = "";
|
|
12540
|
+
replError = "";
|
|
12541
|
+
if (typeof message.requestId === "string") {
|
|
12542
|
+
replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
|
|
12543
|
+
persistReplJournalEntries();
|
|
12544
|
+
}
|
|
12545
|
+
setStatus("Sent to REPL.", "success");
|
|
12546
|
+
syncActionButtons();
|
|
12547
|
+
renderReplViewIfActive({ force: true });
|
|
12548
|
+
return;
|
|
12549
|
+
}
|
|
12550
|
+
|
|
11028
12551
|
if (message.type === "request_started") {
|
|
11029
12552
|
pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
11030
12553
|
pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
|
|
@@ -11425,6 +12948,15 @@
|
|
|
11425
12948
|
if (typeof message.requestId === "string") {
|
|
11426
12949
|
clearArmedTitleAttention(message.requestId);
|
|
11427
12950
|
}
|
|
12951
|
+
if (replBusy) {
|
|
12952
|
+
replBusy = false;
|
|
12953
|
+
replError = typeof message.message === "string" ? message.message : "REPL request failed.";
|
|
12954
|
+
if (typeof message.requestId === "string") {
|
|
12955
|
+
replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "error", output: replError, updatedAt: Date.now() } : entry);
|
|
12956
|
+
persistReplJournalEntries();
|
|
12957
|
+
}
|
|
12958
|
+
renderReplViewIfActive({ force: true });
|
|
12959
|
+
}
|
|
11428
12960
|
stickyStudioKind = null;
|
|
11429
12961
|
setBusy(false);
|
|
11430
12962
|
setWsState("Ready");
|
|
@@ -11977,6 +13509,8 @@
|
|
|
11977
13509
|
requestLatestResponse();
|
|
11978
13510
|
});
|
|
11979
13511
|
|
|
13512
|
+
sourceTextEl.addEventListener("keydown", handleSourceTextTabKey);
|
|
13513
|
+
|
|
11980
13514
|
sourceTextEl.addEventListener("input", () => {
|
|
11981
13515
|
if (activePreviewCommentSelection) {
|
|
11982
13516
|
clearPreviewCommentSelection();
|
|
@@ -12348,7 +13882,7 @@
|
|
|
12348
13882
|
return;
|
|
12349
13883
|
}
|
|
12350
13884
|
|
|
12351
|
-
const prepared =
|
|
13885
|
+
const prepared = prepareEditorTextForRunRequest(sourceTextEl.value);
|
|
12352
13886
|
if (!prepared.trim()) {
|
|
12353
13887
|
setStatus("Editor is empty. Nothing to run.", "warning");
|
|
12354
13888
|
return;
|
|
@@ -12372,7 +13906,7 @@
|
|
|
12372
13906
|
|
|
12373
13907
|
if (queueSteerBtn) {
|
|
12374
13908
|
queueSteerBtn.addEventListener("click", () => {
|
|
12375
|
-
const prepared =
|
|
13909
|
+
const prepared = prepareEditorTextForRunRequest(sourceTextEl.value);
|
|
12376
13910
|
if (!prepared.trim()) {
|
|
12377
13911
|
setStatus("Editor is empty. Nothing to queue.", "warning");
|
|
12378
13912
|
return;
|
|
@@ -12394,6 +13928,20 @@
|
|
|
12394
13928
|
});
|
|
12395
13929
|
}
|
|
12396
13930
|
|
|
13931
|
+
if (sendReplBtn) {
|
|
13932
|
+
sendReplBtn.addEventListener("click", () => {
|
|
13933
|
+
sendEditorTextToRepl();
|
|
13934
|
+
});
|
|
13935
|
+
}
|
|
13936
|
+
|
|
13937
|
+
if (replSendModeSelect) {
|
|
13938
|
+
replSendModeSelect.addEventListener("change", () => {
|
|
13939
|
+
setReplSendMode(replSendModeSelect.value);
|
|
13940
|
+
syncActionButtons();
|
|
13941
|
+
renderReplViewIfActive({ force: true });
|
|
13942
|
+
});
|
|
13943
|
+
}
|
|
13944
|
+
|
|
12397
13945
|
copyDraftBtn.addEventListener("click", async () => {
|
|
12398
13946
|
const content = sourceTextEl.value;
|
|
12399
13947
|
if (!content.trim()) {
|
|
@@ -12812,6 +14360,7 @@
|
|
|
12812
14360
|
const storedAnnotationsEnabled = readStoredAnnotationsEnabled();
|
|
12813
14361
|
const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
|
|
12814
14362
|
setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
|
|
14363
|
+
setReplSendMode(replSendMode);
|
|
12815
14364
|
|
|
12816
14365
|
setEditorView(editorView);
|
|
12817
14366
|
setRightView(rightView);
|