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.
@@ -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 !== "trace") return;
3706
- traceAutoScroll = shouldStickTraceToBottom();
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) or Editor (Preview) to export PDF.", "warning");
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 = rightView === "editor-preview"
3992
- ? prepareEditorTextForPdfExport(sourceTextEl.value)
3993
- : prepareEditorTextForPreview(latestResponseMarkdown);
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) or Editor (Preview) to export HTML.", "warning");
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 showingTrace = rightView === "trace";
6209
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
4940
6210
 
4941
6211
  if (responseWrapEl) {
4942
- responseWrapEl.hidden = showingTrace;
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 exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
4966
- const canExportPreview = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
4967
- const htmlArtifactExportSource = canExportPreview ? getRightPaneHtmlArtifactSource() : "";
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 ? "Exporting…" : "Export right preview";
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) or Editor (Preview) to export.";
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
- ? (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview.")
4999
- : "Switch right pane to a non-empty preview before exporting.";
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
- : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"));
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 = prepareEditorTextForSend(sourceTextEl.value);
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 = prepareEditorTextForSend(sourceTextEl.value);
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);