pi-studio 0.8.4 → 0.9.0

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,47 @@
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
+ return (window.localStorage && window.localStorage.getItem("piStudio.replSendMode")) || "scratch";
245
+ } catch {
246
+ return "scratch";
247
+ }
248
+ })();
249
+ let replJournalEntries = [];
250
+ let activeReplJournalEntryId = "";
251
+ let replJournalCollapsed = (() => {
252
+ try {
253
+ const stored = window.localStorage ? window.localStorage.getItem("piStudio.replJournalCollapsed") : null;
254
+ if (stored === "false") return false;
255
+ if (stored === "true") return true;
256
+ return true;
257
+ } catch {
258
+ return true;
259
+ }
260
+ })();
218
261
  let studioRunChainActive = false;
219
262
  let queuedSteeringCount = 0;
220
263
  let agentBusyFromServer = false;
@@ -745,6 +788,641 @@
745
788
  setStatus("Loaded visible working into editor.", "success");
746
789
  }
747
790
 
791
+ function normalizeReplRuntime(value) {
792
+ const runtime = String(value || "").trim().toLowerCase();
793
+ return runtime === "shell" || runtime === "python" || runtime === "ipython" || runtime === "julia" || runtime === "r" || runtime === "ghci" || runtime === "clojure"
794
+ ? runtime
795
+ : "python";
796
+ }
797
+
798
+ function normalizeReplSession(session) {
799
+ if (!session || typeof session !== "object") return null;
800
+ const sessionName = typeof session.sessionName === "string" && session.sessionName.trim() ? session.sessionName.trim() : "";
801
+ if (!sessionName) return null;
802
+ return {
803
+ sessionName,
804
+ target: typeof session.target === "string" ? session.target : (sessionName + ":0.0"),
805
+ runtime: typeof session.runtime === "string" ? session.runtime : "unknown",
806
+ label: typeof session.label === "string" && session.label.trim() ? session.label.trim() : sessionName,
807
+ source: typeof session.source === "string" ? session.source : "tmux",
808
+ };
809
+ }
810
+
811
+ function setReplRuntime(runtime) {
812
+ replRuntime = normalizeReplRuntime(runtime);
813
+ try {
814
+ if (window.localStorage) window.localStorage.setItem("piStudio.replRuntime", replRuntime);
815
+ } catch {
816
+ // Ignore storage failures.
817
+ }
818
+ }
819
+
820
+ function normalizeReplSendMode(value) {
821
+ return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "scratch";
822
+ }
823
+
824
+ function setReplSendMode(mode) {
825
+ replSendMode = normalizeReplSendMode(mode);
826
+ if (replSendModeSelect) replSendModeSelect.value = replSendMode;
827
+ try {
828
+ if (window.localStorage) window.localStorage.setItem("piStudio.replSendMode", replSendMode);
829
+ } catch {
830
+ // Ignore storage failures.
831
+ }
832
+ }
833
+
834
+ function setReplJournalCollapsed(collapsed) {
835
+ replJournalCollapsed = Boolean(collapsed);
836
+ try {
837
+ if (window.localStorage) window.localStorage.setItem("piStudio.replJournalCollapsed", replJournalCollapsed ? "true" : "false");
838
+ } catch {
839
+ // Ignore storage failures.
840
+ }
841
+ renderReplViewIfActive({ force: true });
842
+ }
843
+
844
+ function setReplSessions(sessions) {
845
+ replSessions = Array.isArray(sessions)
846
+ ? sessions.map(normalizeReplSession).filter(Boolean)
847
+ : [];
848
+ if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
849
+ replActiveSessionName = replSessions[0] ? replSessions[0].sessionName : "";
850
+ }
851
+ }
852
+
853
+ function getActiveReplSession() {
854
+ return replSessions.find((session) => session.sessionName === replActiveSessionName) || null;
855
+ }
856
+
857
+ function buildActiveReplPromptContext() {
858
+ if (rightView !== "repl") return "";
859
+ const session = getActiveReplSession();
860
+ if (!session) return "";
861
+ const runtime = session.runtime && session.runtime !== "unknown" ? session.runtime : "unknown";
862
+ return [
863
+ "[Studio active REPL]",
864
+ "The right pane is mirroring an active tmux-backed REPL session.",
865
+ "If the user refers to the active REPL, send code to this session rather than inventing a separate one.",
866
+ "Session name: " + session.sessionName,
867
+ "tmux target: " + (session.target || (session.sessionName + ":0.0")),
868
+ "runtime: " + runtime,
869
+ "Suggested shell command for direct interaction: tmux paste-buffer/send-keys targeting " + (session.target || (session.sessionName + ":0.0")),
870
+ "Prefer existing REPL tools when they target this same session; otherwise use tmux directly.",
871
+ "[/Studio active REPL]",
872
+ ].join("\n");
873
+ }
874
+
875
+ function prepareEditorTextForRunRequest(text) {
876
+ const prepared = prepareEditorTextForSend(text);
877
+ const replContext = buildActiveReplPromptContext();
878
+ return replContext ? (replContext + "\n\n" + prepared) : prepared;
879
+ }
880
+
881
+ function setActiveReplSession(sessionName) {
882
+ const name = String(sessionName || "").trim();
883
+ if (!name) {
884
+ replActiveSessionName = "";
885
+ return;
886
+ }
887
+ replActiveSessionName = name;
888
+ }
889
+
890
+ function trimReplTranscript(text) {
891
+ const value = String(text || "");
892
+ if (value.length <= REPL_TRANSCRIPT_MAX_CHARS) return value;
893
+ return "… " + formatCompactNumber(value.length - REPL_TRANSCRIPT_MAX_CHARS) + " earlier chars omitted …\n" + value.slice(value.length - REPL_TRANSCRIPT_MAX_CHARS);
894
+ }
895
+
896
+ function requestReplList() {
897
+ if (wsState === "Disconnected") return false;
898
+ return sendMessage({ type: "repl_list_request" });
899
+ }
900
+
901
+ function requestReplCapture() {
902
+ if (wsState === "Disconnected") return false;
903
+ if (replActiveSessionName) {
904
+ return sendMessage({ type: "repl_capture_request", sessionName: replActiveSessionName });
905
+ }
906
+ return sendMessage({ type: "repl_list_request" });
907
+ }
908
+
909
+ function isReplControlFocused() {
910
+ const activeEl = document.activeElement;
911
+ return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls"));
912
+ }
913
+
914
+ function renderReplViewIfActive(options) {
915
+ if (rightView !== "repl") return;
916
+ if ((!options || options.force !== true) && isReplControlFocused()) return;
917
+ if (traceRenderRaf !== null) return;
918
+ traceRenderRaf = window.requestAnimationFrame(() => {
919
+ traceRenderRaf = null;
920
+ refreshResponseUi();
921
+ });
922
+ }
923
+
924
+ function startReplPolling() {
925
+ if (rightView !== "repl") return;
926
+ if (replPollTimer !== null) return;
927
+ requestReplCapture();
928
+ replPollTimer = window.setInterval(() => {
929
+ if (rightView !== "repl") {
930
+ stopReplPolling();
931
+ return;
932
+ }
933
+ requestReplCapture();
934
+ }, REPL_POLL_INTERVAL_MS);
935
+ }
936
+
937
+ function stopReplPolling() {
938
+ if (replPollTimer !== null) {
939
+ window.clearInterval(replPollTimer);
940
+ replPollTimer = null;
941
+ }
942
+ }
943
+
944
+ function getActiveReplRuntime() {
945
+ const session = getActiveReplSession();
946
+ if (session && session.runtime && session.runtime !== "unknown") return normalizeReplRuntime(session.runtime);
947
+ return normalizeReplRuntime(replRuntime);
948
+ }
949
+
950
+ function getEditorSelectionRange() {
951
+ const raw = String(sourceTextEl.value || "");
952
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
953
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
954
+ const safeStart = Math.max(0, Math.min(start, raw.length));
955
+ const safeEnd = Math.max(safeStart, Math.min(end, raw.length));
956
+ return { raw, start: safeStart, end: safeEnd, selected: safeEnd > safeStart ? raw.slice(safeStart, safeEnd) : "" };
957
+ }
958
+
959
+ function normalizeMarkdownFenceLanguage(info) {
960
+ let value = String(info || "").trim();
961
+ if (!value) return "";
962
+ let first = "";
963
+ if (value.startsWith("{")) {
964
+ const closeIndex = value.indexOf("}");
965
+ const inner = closeIndex >= 0 ? value.slice(1, closeIndex) : value.slice(1);
966
+ first = inner.split(/[\s,]+/)[0] || "";
967
+ } else {
968
+ first = value.split(/\s+/)[0] || "";
969
+ }
970
+ first = first.replace(/^\./, "").trim().toLowerCase();
971
+ if (first === "py") return "python";
972
+ if (first === "jl") return "julia";
973
+ if (first === "sh" || first === "zsh" || first === "fish") return "shell";
974
+ if (first === "bash") return "shell";
975
+ if (first === "hs" || first === "haskell") return "ghci";
976
+ if (first === "clj" || first === "cljc") return "clojure";
977
+ return first;
978
+ }
979
+
980
+ function parseMarkdownCodeFences(markdown) {
981
+ const text = String(markdown || "");
982
+ const blocks = [];
983
+ let offset = 0;
984
+ let open = null;
985
+ while (offset <= text.length) {
986
+ const newlineIndex = text.indexOf("\n", offset);
987
+ const lineEnd = newlineIndex >= 0 ? newlineIndex : text.length;
988
+ const lineWithNewlineEnd = newlineIndex >= 0 ? newlineIndex + 1 : lineEnd;
989
+ const line = text.slice(offset, lineEnd);
990
+ if (!open) {
991
+ const openMatch = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
992
+ if (openMatch) {
993
+ const fence = openMatch[1] || "";
994
+ open = {
995
+ start: offset,
996
+ fence,
997
+ fenceChar: fence.charAt(0),
998
+ fenceLength: fence.length,
999
+ info: openMatch[2] || "",
1000
+ contentStart: lineWithNewlineEnd,
1001
+ };
1002
+ }
1003
+ } else {
1004
+ const closePattern = new RegExp("^ {0,3}" + open.fenceChar + "{" + open.fenceLength + ",}[ \\t]*$");
1005
+ if (closePattern.test(line)) {
1006
+ blocks.push({
1007
+ start: open.start,
1008
+ end: lineWithNewlineEnd,
1009
+ contentStart: open.contentStart,
1010
+ contentEnd: offset,
1011
+ info: open.info,
1012
+ language: normalizeMarkdownFenceLanguage(open.info),
1013
+ code: text.slice(open.contentStart, offset),
1014
+ });
1015
+ open = null;
1016
+ }
1017
+ }
1018
+ if (newlineIndex < 0) break;
1019
+ offset = lineWithNewlineEnd;
1020
+ }
1021
+ return blocks;
1022
+ }
1023
+
1024
+ function isFenceLanguageCompatibleWithRuntime(language, runtime) {
1025
+ const lang = normalizeMarkdownFenceLanguage(language);
1026
+ if (!lang) return true;
1027
+ const activeRuntime = normalizeReplRuntime(runtime || getActiveReplRuntime());
1028
+ if (activeRuntime === "python" || activeRuntime === "ipython") return lang === "python" || lang === "ipython";
1029
+ if (activeRuntime === "r") return lang === "r";
1030
+ if (activeRuntime === "julia") return lang === "julia";
1031
+ if (activeRuntime === "shell") return lang === "shell" || lang === "bash" || lang === "sh";
1032
+ if (activeRuntime === "ghci") return lang === "ghci" || lang === "haskell";
1033
+ if (activeRuntime === "clojure") return lang === "clojure";
1034
+ return true;
1035
+ }
1036
+
1037
+ function stripFencedBlocksFromMarkdown(markdown, blocks) {
1038
+ const text = String(markdown || "");
1039
+ const ranges = Array.isArray(blocks) ? blocks : parseMarkdownCodeFences(text);
1040
+ if (!ranges.length) return text.trim();
1041
+ let cursor = 0;
1042
+ const pieces = [];
1043
+ ranges.forEach((block) => {
1044
+ pieces.push(text.slice(cursor, block.start));
1045
+ cursor = block.end;
1046
+ });
1047
+ pieces.push(text.slice(cursor));
1048
+ return pieces.join("\n").replace(/\n{3,}/g, "\n\n").trim();
1049
+ }
1050
+
1051
+ function getCurrentMarkdownCodeFence(markdown, caretOffset) {
1052
+ const text = String(markdown || "");
1053
+ const safeCaret = Math.max(0, Math.min(Math.floor(Number(caretOffset) || 0), text.length));
1054
+ return parseMarkdownCodeFences(text).find((block) => safeCaret >= block.contentStart && safeCaret <= block.contentEnd) || null;
1055
+ }
1056
+
1057
+ function unwrapSingleMarkdownCodeFenceForReplSend(text) {
1058
+ const source = String(text || "");
1059
+ const blocks = parseMarkdownCodeFences(source);
1060
+ if (blocks.length !== 1) return null;
1061
+ const block = blocks[0];
1062
+ if (source.slice(0, block.start).trim() || source.slice(block.end).trim()) return null;
1063
+ if (!isFenceLanguageCompatibleWithRuntime(block.language, getActiveReplRuntime())) return null;
1064
+ const code = String(block.code || "").trimEnd();
1065
+ if (!code.trim()) return null;
1066
+ return {
1067
+ code,
1068
+ label: "single " + (block.language || getActiveReplRuntime()) + " chunk",
1069
+ };
1070
+ }
1071
+
1072
+ function buildScratchReplSendPayload() {
1073
+ const range = getEditorSelectionRange();
1074
+ const selected = range.selected;
1075
+ const source = selected || range.raw;
1076
+ const unwrapped = unwrapSingleMarkdownCodeFenceForReplSend(source);
1077
+ return {
1078
+ text: prepareEditorTextForSend(unwrapped ? unwrapped.code : source),
1079
+ prose: "",
1080
+ label: unwrapped ? unwrapped.label : (selected ? "selection" : "full editor"),
1081
+ mode: "scratch",
1082
+ noteOnly: false,
1083
+ skippedChunks: 0,
1084
+ };
1085
+ }
1086
+
1087
+ function buildLiterateReplSendPayload() {
1088
+ const range = getEditorSelectionRange();
1089
+ const runtime = getActiveReplRuntime();
1090
+ if (range.selected) {
1091
+ const blocks = parseMarkdownCodeFences(range.selected);
1092
+ if (blocks.length) {
1093
+ const compatibleBlocks = blocks.filter((block) => isFenceLanguageCompatibleWithRuntime(block.language, runtime));
1094
+ const code = compatibleBlocks.map((block) => String(block.code || "").trimEnd()).filter((chunk) => chunk.trim()).join("\n\n");
1095
+ const prose = stripFencedBlocksFromMarkdown(range.selected, blocks);
1096
+ if (code.trim()) {
1097
+ return {
1098
+ text: prepareEditorTextForSend(code),
1099
+ prose,
1100
+ label: "selection · " + compatibleBlocks.length + " code chunk" + (compatibleBlocks.length === 1 ? "" : "s"),
1101
+ mode: "literate",
1102
+ noteOnly: false,
1103
+ skippedChunks: Math.max(0, blocks.length - compatibleBlocks.length),
1104
+ };
1105
+ }
1106
+ if (prose.trim()) {
1107
+ return {
1108
+ text: "",
1109
+ prose,
1110
+ label: "selected prose",
1111
+ mode: "literate",
1112
+ noteOnly: true,
1113
+ skippedChunks: blocks.length,
1114
+ };
1115
+ }
1116
+ return { error: "Selected code chunks do not match the active REPL runtime." };
1117
+ }
1118
+ return {
1119
+ text: prepareEditorTextForSend(range.selected),
1120
+ prose: "",
1121
+ label: "selection",
1122
+ mode: "literate",
1123
+ noteOnly: false,
1124
+ skippedChunks: 0,
1125
+ };
1126
+ }
1127
+
1128
+ const currentBlock = getCurrentMarkdownCodeFence(range.raw, range.start);
1129
+ if (currentBlock) {
1130
+ if (!isFenceLanguageCompatibleWithRuntime(currentBlock.language, runtime)) {
1131
+ return { error: "Current code chunk is marked " + (currentBlock.language || "unknown") + ", but the active REPL is " + runtime + "." };
1132
+ }
1133
+ return {
1134
+ text: prepareEditorTextForSend(String(currentBlock.code || "").trimEnd()),
1135
+ prose: "",
1136
+ label: "current " + (currentBlock.language || runtime) + " chunk",
1137
+ mode: "literate",
1138
+ noteOnly: false,
1139
+ skippedChunks: 0,
1140
+ };
1141
+ }
1142
+
1143
+ const allBlocks = parseMarkdownCodeFences(range.raw);
1144
+ if (allBlocks.length) {
1145
+ return { error: "Place the cursor inside a code chunk, select text, or use Run all chunks. Switch send mode to Scratch to send the full editor." };
1146
+ }
1147
+
1148
+ return {
1149
+ text: prepareEditorTextForSend(range.raw),
1150
+ prose: "",
1151
+ label: "full editor",
1152
+ mode: "literate",
1153
+ noteOnly: false,
1154
+ skippedChunks: 0,
1155
+ };
1156
+ }
1157
+
1158
+ function buildAllChunksReplSendPayload() {
1159
+ const range = getEditorSelectionRange();
1160
+ const runtime = getActiveReplRuntime();
1161
+ const blocks = parseMarkdownCodeFences(range.raw);
1162
+ if (!blocks.length) return { error: "No fenced code chunks found in the editor." };
1163
+ const compatibleBlocks = blocks.filter((block) => isFenceLanguageCompatibleWithRuntime(block.language, runtime));
1164
+ const code = compatibleBlocks.map((block) => String(block.code || "").trimEnd()).filter((chunk) => chunk.trim()).join("\n\n");
1165
+ if (!code.trim()) return { error: "No code chunks match the active REPL runtime." };
1166
+ return {
1167
+ text: prepareEditorTextForSend(code),
1168
+ prose: "",
1169
+ label: "all " + runtime + " chunks · " + compatibleBlocks.length + " of " + blocks.length,
1170
+ mode: "literate",
1171
+ noteOnly: false,
1172
+ skippedChunks: Math.max(0, blocks.length - compatibleBlocks.length),
1173
+ };
1174
+ }
1175
+
1176
+ function getSelectedOrCurrentParagraphForReplNote() {
1177
+ const range = getEditorSelectionRange();
1178
+ if (range.selected.trim()) return range.selected.trim();
1179
+ const before = range.raw.lastIndexOf("\n\n", Math.max(0, range.start - 1));
1180
+ const after = range.raw.indexOf("\n\n", range.start);
1181
+ const start = before >= 0 ? before + 2 : 0;
1182
+ const end = after >= 0 ? after : range.raw.length;
1183
+ return range.raw.slice(start, end).trim();
1184
+ }
1185
+
1186
+ function trimReplJournalOutput(text) {
1187
+ const value = String(text || "").trimEnd();
1188
+ if (value.length <= REPL_JOURNAL_OUTPUT_MAX_CHARS) return value;
1189
+ return "… " + formatCompactNumber(value.length - REPL_JOURNAL_OUTPUT_MAX_CHARS) + " earlier chars omitted …\n" + value.slice(value.length - REPL_JOURNAL_OUTPUT_MAX_CHARS);
1190
+ }
1191
+
1192
+ function createReplJournalEntry(details) {
1193
+ return {
1194
+ id: "repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8),
1195
+ requestId: details.requestId || "",
1196
+ createdAt: Date.now(),
1197
+ updatedAt: Date.now(),
1198
+ sessionName: details.sessionName || "",
1199
+ runtime: details.runtime || getActiveReplRuntime(),
1200
+ label: details.label || "REPL send",
1201
+ mode: details.mode || replSendMode,
1202
+ prose: String(details.prose || ""),
1203
+ code: String(details.code || ""),
1204
+ output: String(details.output || ""),
1205
+ beforeTranscript: String(details.beforeTranscript || ""),
1206
+ status: details.status || "sent",
1207
+ skippedChunks: Math.max(0, Math.floor(Number(details.skippedChunks) || 0)),
1208
+ };
1209
+ }
1210
+
1211
+ function addReplJournalEntry(details) {
1212
+ const entry = createReplJournalEntry(details || {});
1213
+ replJournalEntries = [...replJournalEntries, entry].slice(-REPL_JOURNAL_MAX_ENTRIES);
1214
+ return entry;
1215
+ }
1216
+
1217
+ function extractReplTranscriptDelta(before, after) {
1218
+ const previous = String(before || "");
1219
+ const current = String(after || "");
1220
+ if (!current) return "";
1221
+ if (!previous) return current;
1222
+ const directIndex = current.indexOf(previous);
1223
+ if (directIndex >= 0) return current.slice(directIndex + previous.length).replace(/^\s+/, "");
1224
+ const previousLines = previous.split("\n");
1225
+ const maxSuffixLines = Math.min(24, previousLines.length);
1226
+ for (let count = maxSuffixLines; count >= 1; count -= 1) {
1227
+ const suffix = previousLines.slice(previousLines.length - count).join("\n");
1228
+ if (!suffix.trim()) continue;
1229
+ const suffixIndex = current.indexOf(suffix);
1230
+ if (suffixIndex >= 0) return current.slice(suffixIndex + suffix.length).replace(/^\s+/, "");
1231
+ }
1232
+ return current;
1233
+ }
1234
+
1235
+ function updateActiveReplJournalEntryFromTranscript(sessionName, transcript) {
1236
+ if (!activeReplJournalEntryId) return;
1237
+ const entryIndex = replJournalEntries.findIndex((entry) => entry.id === activeReplJournalEntryId);
1238
+ if (entryIndex < 0) return;
1239
+ const entry = replJournalEntries[entryIndex];
1240
+ if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return;
1241
+ const delta = trimReplJournalOutput(extractReplTranscriptDelta(entry.beforeTranscript, transcript));
1242
+ if (!delta.trim()) return;
1243
+ replJournalEntries = replJournalEntries.map((candidate) => candidate.id === entry.id
1244
+ ? { ...candidate, output: delta, status: "captured", updatedAt: Date.now() }
1245
+ : candidate);
1246
+ }
1247
+
1248
+ function getMarkdownFenceForText(text, language) {
1249
+ const value = String(text || "");
1250
+ let fence = "```";
1251
+ while (value.includes(fence)) fence += "`";
1252
+ return fence + (language ? language : "") + "\n" + value.replace(/\s+$/, "") + "\n" + fence;
1253
+ }
1254
+
1255
+ function buildReplJournalMarkdown() {
1256
+ const lines = ["# Studio REPL journal", "", "Generated: " + new Date().toLocaleString(), ""];
1257
+ if (!replJournalEntries.length) {
1258
+ lines.push("_No journal entries yet._");
1259
+ return lines.join("\n");
1260
+ }
1261
+ replJournalEntries.forEach((entry, index) => {
1262
+ lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
1263
+ lines.push("");
1264
+ lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toLocaleString());
1265
+ if (entry.runtime) lines.push("- Runtime: " + entry.runtime);
1266
+ if (entry.sessionName) lines.push("- Session: `" + entry.sessionName + "`");
1267
+ if (entry.skippedChunks) lines.push("- Skipped chunks: " + entry.skippedChunks);
1268
+ lines.push("");
1269
+ if (String(entry.prose || "").trim()) {
1270
+ lines.push(String(entry.prose).trim());
1271
+ lines.push("");
1272
+ }
1273
+ if (String(entry.code || "").trim()) {
1274
+ lines.push(getMarkdownFenceForText(entry.code, entry.runtime === "ipython" ? "python" : entry.runtime));
1275
+ lines.push("");
1276
+ }
1277
+ if (String(entry.output || "").trim()) {
1278
+ lines.push("Output:");
1279
+ lines.push("");
1280
+ lines.push(getMarkdownFenceForText(entry.output, "text"));
1281
+ lines.push("");
1282
+ }
1283
+ });
1284
+ return lines.join("\n").replace(/\n{4,}/g, "\n\n\n").trimEnd() + "\n";
1285
+ }
1286
+
1287
+ async function copyReplJournalToClipboard() {
1288
+ if (!replJournalEntries.length) {
1289
+ setStatus("No REPL journal entries to copy yet.", "warning");
1290
+ return;
1291
+ }
1292
+ if (await writeTextToClipboard(buildReplJournalMarkdown())) {
1293
+ setStatus("Copied REPL journal as Markdown.", "success");
1294
+ } else {
1295
+ setStatus("Clipboard write failed.", "warning");
1296
+ }
1297
+ }
1298
+
1299
+ function exportReplJournalMarkdown() {
1300
+ if (!replJournalEntries.length) {
1301
+ setStatus("No REPL journal entries to export yet.", "warning");
1302
+ return;
1303
+ }
1304
+ const blob = new Blob([buildReplJournalMarkdown()], { type: "text/markdown;charset=utf-8" });
1305
+ const blobUrl = URL.createObjectURL(blob);
1306
+ const link = document.createElement("a");
1307
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1308
+ link.href = blobUrl;
1309
+ link.download = "studio-repl-journal-" + stamp + ".md";
1310
+ document.body.appendChild(link);
1311
+ link.click();
1312
+ link.remove();
1313
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1314
+ setStatus("Exported REPL journal Markdown.", "success");
1315
+ }
1316
+
1317
+ function clearReplJournal() {
1318
+ replJournalEntries = [];
1319
+ activeReplJournalEntryId = "";
1320
+ setStatus("Cleared REPL journal.", "success");
1321
+ renderReplViewIfActive({ force: true });
1322
+ }
1323
+
1324
+ function loadReplJournalIntoEditor() {
1325
+ if (!replJournalEntries.length) {
1326
+ setStatus("No REPL journal entries to load yet.", "warning");
1327
+ return;
1328
+ }
1329
+ const markdown = buildReplJournalMarkdown();
1330
+ setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1331
+ setSourceState({ source: "blank", label: "REPL journal", path: null });
1332
+ setEditorLanguage("markdown");
1333
+ setStatus("Loaded REPL journal into editor.", "success");
1334
+ }
1335
+
1336
+ function addSelectedReplJournalNote() {
1337
+ const note = getSelectedOrCurrentParagraphForReplNote();
1338
+ if (!note.trim()) {
1339
+ setStatus("Select prose or place the cursor in a paragraph to journal a note.", "warning");
1340
+ return;
1341
+ }
1342
+ addReplJournalEntry({
1343
+ label: "note",
1344
+ prose: note,
1345
+ status: "note",
1346
+ mode: "literate",
1347
+ sessionName: replActiveSessionName,
1348
+ runtime: getActiveReplRuntime(),
1349
+ });
1350
+ setStatus("Added note to REPL journal.", "success");
1351
+ renderReplViewIfActive({ force: true });
1352
+ }
1353
+
1354
+ function sendReplPayload(payload) {
1355
+ const session = getActiveReplSession();
1356
+ if (!session) {
1357
+ setStatus("Start or select a REPL session first.", "warning");
1358
+ return;
1359
+ }
1360
+ if (!payload || payload.error) {
1361
+ setStatus(payload && payload.error ? payload.error : "Nothing to send to REPL.", "warning");
1362
+ return;
1363
+ }
1364
+ if (payload.noteOnly) {
1365
+ if (String(payload.prose || "").trim()) {
1366
+ addReplJournalEntry({
1367
+ label: payload.label || "note",
1368
+ prose: payload.prose,
1369
+ status: "note",
1370
+ mode: payload.mode || "literate",
1371
+ sessionName: session.sessionName,
1372
+ runtime: getActiveReplRuntime(),
1373
+ skippedChunks: payload.skippedChunks,
1374
+ });
1375
+ setStatus("Added prose to REPL journal.", "success");
1376
+ renderReplViewIfActive({ force: true });
1377
+ } else {
1378
+ setStatus("No code or prose found to send.", "warning");
1379
+ }
1380
+ return;
1381
+ }
1382
+ const text = String(payload.text || "");
1383
+ if (!text.trim()) {
1384
+ setStatus("Editor text is empty.", "warning");
1385
+ return;
1386
+ }
1387
+ const requestId = makeRequestId();
1388
+ const journalEntry = addReplJournalEntry({
1389
+ requestId,
1390
+ sessionName: session.sessionName,
1391
+ runtime: getActiveReplRuntime(),
1392
+ label: payload.label,
1393
+ mode: payload.mode,
1394
+ prose: payload.prose,
1395
+ code: text,
1396
+ beforeTranscript: replTranscript,
1397
+ status: "sending",
1398
+ skippedChunks: payload.skippedChunks,
1399
+ });
1400
+ activeReplJournalEntryId = journalEntry.id;
1401
+ replBusy = true;
1402
+ syncActionButtons();
1403
+ renderReplViewIfActive({ force: true });
1404
+ const skippedSuffix = payload.skippedChunks ? " (skipped " + payload.skippedChunks + " incompatible chunk" + (payload.skippedChunks === 1 ? "" : "s") + ")" : "";
1405
+ setStatus("Sending " + (payload.label || "editor text") + " to REPL…" + skippedSuffix, "info");
1406
+ if (!sendMessage({ type: "repl_send_request", requestId, sessionName: session.sessionName, text })) {
1407
+ replBusy = false;
1408
+ replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
1409
+ syncActionButtons();
1410
+ }
1411
+ }
1412
+
1413
+ function sendEditorTextToRepl(options) {
1414
+ const action = options && options.action ? options.action : "default";
1415
+ if (action === "all-chunks") {
1416
+ sendReplPayload(buildAllChunksReplSendPayload());
1417
+ return;
1418
+ }
1419
+ if (action === "note") {
1420
+ addSelectedReplJournalNote();
1421
+ return;
1422
+ }
1423
+ sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildScratchReplSendPayload());
1424
+ }
1425
+
748
1426
  function renderTraceViewIfActive() {
749
1427
  if (rightView !== "trace") return;
750
1428
  if (traceRenderRaf !== null) return;
@@ -1209,6 +1887,8 @@
1209
1887
  const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
1210
1888
  if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
1211
1889
  if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
1890
+ if (!isEditorOnlyMode && sendReplBtn) actionLineOneEl.appendChild(sendReplBtn);
1891
+ if (!isEditorOnlyMode && replSendModeSelect) actionLineOneEl.appendChild(replSendModeSelect);
1212
1892
  const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
1213
1893
  actionLineTwoEl.appendChild(copyDraftBtn);
1214
1894
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
@@ -2200,6 +2880,23 @@
2200
2880
  return;
2201
2881
  }
2202
2882
 
2883
+ if (
2884
+ key === "Enter"
2885
+ && (event.metaKey || event.ctrlKey)
2886
+ && !event.altKey
2887
+ && event.shiftKey
2888
+ && activePane === "left"
2889
+ && !isEditorOnlyMode
2890
+ ) {
2891
+ event.preventDefault();
2892
+ if (sendReplBtn && !sendReplBtn.hidden && !sendReplBtn.disabled) {
2893
+ sendReplBtn.click();
2894
+ } else {
2895
+ setStatus("Open REPL view and start/select a session before sending to REPL.", "warning");
2896
+ }
2897
+ return;
2898
+ }
2899
+
2203
2900
  if (
2204
2901
  key === "Enter"
2205
2902
  && (event.metaKey || event.ctrlKey)
@@ -2512,6 +3209,18 @@
2512
3209
  return;
2513
3210
  }
2514
3211
 
3212
+ if (rightView === "repl") {
3213
+ const session = getActiveReplSession();
3214
+ if (replTmuxAvailable === false) {
3215
+ referenceBadgeEl.textContent = "REPL: tmux unavailable";
3216
+ return;
3217
+ }
3218
+ referenceBadgeEl.textContent = session
3219
+ ? ("REPL: " + session.label + (replCapturedAt ? (" · updated " + formatReferenceTime(replCapturedAt)) : ""))
3220
+ : "REPL: no session selected";
3221
+ return;
3222
+ }
3223
+
2515
3224
  if (rightView === "editor-preview") {
2516
3225
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
2517
3226
  if (hasResponse) {
@@ -3702,8 +4411,13 @@
3702
4411
  }
3703
4412
 
3704
4413
  function handleTracePaneScroll() {
3705
- if (rightView !== "trace") return;
3706
- traceAutoScroll = shouldStickTraceToBottom();
4414
+ if (rightView === "trace") {
4415
+ traceAutoScroll = shouldStickTraceToBottom();
4416
+ return;
4417
+ }
4418
+ if (rightView === "repl") {
4419
+ replFollow = shouldStickTraceToBottom();
4420
+ }
3707
4421
  }
3708
4422
 
3709
4423
  async function handleTracePaneClick(event) {
@@ -3744,10 +4458,129 @@
3744
4458
  }
3745
4459
  }
3746
4460
 
4461
+ function handleReplPaneClick(event) {
4462
+ if (rightView !== "repl") return;
4463
+ const target = event.target;
4464
+ const actionBtn = target instanceof Element ? target.closest("[data-repl-action]") : null;
4465
+ if (!actionBtn) return;
4466
+ event.preventDefault();
4467
+ const action = actionBtn.getAttribute("data-repl-action") || "";
4468
+ if (action === "start" || action === "new-session") {
4469
+ const requestId = makeRequestId();
4470
+ replBusy = true;
4471
+ replError = "";
4472
+ replMessage = (action === "new-session" ? "Starting new " : "Starting ") + replRuntime + " REPL…";
4473
+ syncActionButtons();
4474
+ renderReplViewIfActive({ force: true });
4475
+ if (!sendMessage({ type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" })) {
4476
+ replBusy = false;
4477
+ syncActionButtons();
4478
+ }
4479
+ return;
4480
+ }
4481
+ if (action === "stop-session") {
4482
+ const session = getActiveReplSession();
4483
+ if (!session) {
4484
+ setStatus("Start or select a REPL session first.", "warning");
4485
+ return;
4486
+ }
4487
+ const requestId = makeRequestId();
4488
+ replBusy = true;
4489
+ replError = "";
4490
+ replMessage = "Stopping " + session.sessionName + "…";
4491
+ syncActionButtons();
4492
+ renderReplViewIfActive({ force: true });
4493
+ if (!sendMessage({ type: "repl_stop_request", requestId, sessionName: session.sessionName })) {
4494
+ replBusy = false;
4495
+ syncActionButtons();
4496
+ }
4497
+ return;
4498
+ }
4499
+ if (action === "interrupt") {
4500
+ const session = getActiveReplSession();
4501
+ if (!session) {
4502
+ setStatus("Start or select a REPL session first.", "warning");
4503
+ return;
4504
+ }
4505
+ const requestId = makeRequestId();
4506
+ replBusy = true;
4507
+ replError = "";
4508
+ replMessage = "Interrupting REPL…";
4509
+ syncActionButtons();
4510
+ renderReplViewIfActive({ force: true });
4511
+ if (!sendMessage({ type: "repl_interrupt_request", requestId, sessionName: session.sessionName })) {
4512
+ replBusy = false;
4513
+ syncActionButtons();
4514
+ }
4515
+ return;
4516
+ }
4517
+ if (action === "run-all-chunks") {
4518
+ sendEditorTextToRepl({ action: "all-chunks" });
4519
+ return;
4520
+ }
4521
+ if (action === "journal-note") {
4522
+ sendEditorTextToRepl({ action: "note" });
4523
+ return;
4524
+ }
4525
+ if (action === "journal-toggle") {
4526
+ setReplJournalCollapsed(!replJournalCollapsed);
4527
+ return;
4528
+ }
4529
+ if (action === "load-journal") {
4530
+ loadReplJournalIntoEditor();
4531
+ return;
4532
+ }
4533
+ if (action === "copy-journal") {
4534
+ void copyReplJournalToClipboard();
4535
+ return;
4536
+ }
4537
+ if (action === "export-journal") {
4538
+ exportReplJournalMarkdown();
4539
+ return;
4540
+ }
4541
+ if (action === "clear-journal") {
4542
+ clearReplJournal();
4543
+ return;
4544
+ }
4545
+ if (action === "refresh") {
4546
+ replError = "";
4547
+ replMessage = "";
4548
+ requestReplCapture();
4549
+ return;
4550
+ }
4551
+ if (action === "follow") {
4552
+ replFollow = !replFollow;
4553
+ renderReplViewIfActive({ force: true });
4554
+ }
4555
+ }
4556
+
4557
+ function handleReplPaneChange(event) {
4558
+ if (rightView !== "repl") return;
4559
+ const target = event.target;
4560
+ if (!(target instanceof Element)) return;
4561
+ const runtimeSelect = target.closest("[data-repl-runtime]");
4562
+ if (runtimeSelect && "value" in runtimeSelect) {
4563
+ setReplRuntime(runtimeSelect.value);
4564
+ renderReplViewIfActive({ force: true });
4565
+ return;
4566
+ }
4567
+ const sessionSelect = target.closest("[data-repl-session]");
4568
+ if (sessionSelect && "value" in sessionSelect) {
4569
+ setActiveReplSession(sessionSelect.value);
4570
+ replError = "";
4571
+ replMessage = "";
4572
+ replFollow = true;
4573
+ requestReplCapture();
4574
+ renderReplViewIfActive({ force: true });
4575
+ }
4576
+ }
4577
+
3747
4578
  function attachResponsePaneInteractionHandlers() {
3748
4579
  if (!critiqueViewEl) return;
3749
4580
  critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
3750
4581
  critiqueViewEl.addEventListener("click", handleTracePaneClick);
4582
+ critiqueViewEl.addEventListener("click", handleReplPaneClick);
4583
+ critiqueViewEl.addEventListener("change", handleReplPaneChange);
3751
4584
  }
3752
4585
 
3753
4586
  function replaceResponsePaneWithClone() {
@@ -3976,35 +4809,42 @@
3976
4809
  return;
3977
4810
  }
3978
4811
 
4812
+ const exportingReplJournal = rightView === "repl";
3979
4813
  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");
4814
+ if (!rightPaneShowsPreview && !exportingReplJournal) {
4815
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export PDF.", "warning");
4816
+ return;
4817
+ }
4818
+ if (exportingReplJournal && !replJournalEntries.length) {
4819
+ setStatus("No REPL journal entries to export yet.", "warning");
3982
4820
  return;
3983
4821
  }
3984
4822
 
3985
- const htmlArtifactSource = getRightPaneHtmlArtifactSource();
4823
+ const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
3986
4824
  if (htmlArtifactSource) {
3987
4825
  setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
3988
4826
  return;
3989
4827
  }
3990
4828
 
3991
- const markdown = rightView === "editor-preview"
3992
- ? prepareEditorTextForPdfExport(sourceTextEl.value)
3993
- : prepareEditorTextForPreview(latestResponseMarkdown);
4829
+ const markdown = exportingReplJournal
4830
+ ? buildReplJournalMarkdown()
4831
+ : (rightView === "editor-preview"
4832
+ ? prepareEditorTextForPdfExport(sourceTextEl.value)
4833
+ : prepareEditorTextForPreview(latestResponseMarkdown));
3994
4834
  if (!markdown || !markdown.trim()) {
3995
4835
  setStatus("Nothing to export yet.", "warning");
3996
4836
  return;
3997
4837
  }
3998
4838
 
3999
4839
  const effectivePath = getEffectiveSavePath();
4000
- const sourcePath = effectivePath || sourceState.path || "";
4840
+ const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
4001
4841
  const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
4002
4842
  const isEditorPreview = rightView === "editor-preview";
4003
4843
  const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
4004
4844
  const isLatex = isEditorPreview
4005
4845
  ? editorPdfLanguage === "latex"
4006
4846
  : /\\documentclass\b|\\begin\{document\}/.test(markdown);
4007
- let filenameHint = isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf";
4847
+ let filenameHint = exportingReplJournal ? "studio-repl-journal.pdf" : (isEditorPreview ? "studio-editor-preview.pdf" : "studio-response-preview.pdf");
4008
4848
  if (sourcePath) {
4009
4849
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
4010
4850
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -4141,31 +4981,36 @@
4141
4981
  return;
4142
4982
  }
4143
4983
 
4984
+ const exportingReplJournal = rightView === "repl";
4144
4985
  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");
4986
+ if (!rightPaneShowsPreview && !exportingReplJournal) {
4987
+ setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export HTML.", "warning");
4988
+ return;
4989
+ }
4990
+ if (exportingReplJournal && !replJournalEntries.length) {
4991
+ setStatus("No REPL journal entries to export yet.", "warning");
4147
4992
  return;
4148
4993
  }
4149
4994
 
4150
- const htmlArtifactSource = getRightPaneHtmlArtifactSource();
4151
- const markdown = htmlArtifactSource || (rightView === "editor-preview"
4995
+ const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
4996
+ const markdown = exportingReplJournal ? buildReplJournalMarkdown() : (htmlArtifactSource || (rightView === "editor-preview"
4152
4997
  ? prepareEditorTextForHtmlExport(sourceTextEl.value)
4153
- : prepareEditorTextForPreview(latestResponseMarkdown));
4998
+ : prepareEditorTextForPreview(latestResponseMarkdown)));
4154
4999
  if (!markdown || !markdown.trim()) {
4155
5000
  setStatus("Nothing to export yet.", "warning");
4156
5001
  return;
4157
5002
  }
4158
5003
 
4159
5004
  const effectivePath = getEffectiveSavePath();
4160
- const sourcePath = effectivePath || sourceState.path || "";
5005
+ const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
4161
5006
  const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
4162
5007
  const isEditorPreview = rightView === "editor-preview";
4163
5008
  const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
4164
5009
  const isLatex = htmlArtifactSource ? false : (isEditorPreview
4165
5010
  ? editorHtmlLanguage === "latex"
4166
5011
  : /\\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";
5012
+ let filenameHint = exportingReplJournal ? "studio-repl-journal.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
5013
+ let titleHint = exportingReplJournal ? "Studio REPL journal" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
4169
5014
  if (sourcePath) {
4170
5015
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
4171
5016
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -4722,6 +5567,165 @@
4722
5567
  return "<div class='trace-image-gallery'>" + cards + "</div>";
4723
5568
  }
4724
5569
 
5570
+ function getReplRuntimeHighlightLanguage(runtime) {
5571
+ const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
5572
+ if (normalized === "shell") return "bash";
5573
+ if (normalized === "ipython") return "python";
5574
+ if (normalized === "python" || normalized === "julia" || normalized === "r") return normalized;
5575
+ return "text";
5576
+ }
5577
+
5578
+ function renderHighlightedReplCode(text, runtime) {
5579
+ const source = String(text || "");
5580
+ const language = getReplRuntimeHighlightLanguage(runtime);
5581
+ if (!source.trim() || language === "text") return escapeHtml(source);
5582
+ return highlightCode(source, language, "preview");
5583
+ }
5584
+
5585
+ function renderHighlightedReplTranscriptLine(line, runtime) {
5586
+ const source = String(line || "");
5587
+ const normalized = normalizeReplRuntime(runtime || getActiveReplRuntime());
5588
+ const language = getReplRuntimeHighlightLanguage(normalized);
5589
+ let match = null;
5590
+ if (normalized === "python" || normalized === "ipython") {
5591
+ match = source.match(/^(\s*(?:>>>|\.\.\.|In \[\d+\]:|\.\.\.?:)\s?)(.*)$/);
5592
+ } else if (normalized === "r") {
5593
+ match = source.match(/^(\s*(?:>|\+)\s?)(.*)$/);
5594
+ } else if (normalized === "julia") {
5595
+ match = source.match(/^(\s*julia>\s?)(.*)$/);
5596
+ } else if (normalized === "shell") {
5597
+ match = source.match(/^(.+(?:[$%#])\s+)(.+)$/);
5598
+ } else if (normalized === "ghci") {
5599
+ match = source.match(/^(\s*(?:ghci>|[A-Za-z0-9_.]+>)\s?)(.*)$/);
5600
+ } else if (normalized === "clojure") {
5601
+ match = source.match(/^(\s*(?:[A-Za-z0-9_.-]+=>)\s?)(.*)$/);
5602
+ }
5603
+ if (!match || !String(match[2] || "").trim() || language === "text") return escapeHtml(source);
5604
+ return "<span class='repl-prompt'>" + escapeHtml(match[1] || "") + "</span>" + highlightCodeLine(match[2] || "", language, "preview");
5605
+ }
5606
+
5607
+ function renderReplTranscriptHtml(transcript, runtime) {
5608
+ const source = String(transcript || "");
5609
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
5610
+ const body = lines.map((line) => renderHighlightedReplTranscriptLine(line, runtime)).join("\n");
5611
+ return "<pre class='repl-transcript repl-transcript-highlight'>" + body + "</pre>";
5612
+ }
5613
+
5614
+ function buildReplJournalHtml() {
5615
+ const hasEntries = replJournalEntries.length > 0;
5616
+ const entryCount = replJournalEntries.length;
5617
+ const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
5618
+ const actions = "<div class='repl-journal-actions'>"
5619
+ + "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show journal" : "Hide journal") + "</button>"
5620
+ + "<button type='button' data-repl-action='load-journal'" + (hasEntries ? "" : " disabled") + ">Load in editor</button>"
5621
+ + "<button type='button' data-repl-action='copy-journal'" + (hasEntries ? "" : " disabled") + ">Copy journal</button>"
5622
+ + "<button type='button' data-repl-action='export-journal'" + (hasEntries ? "" : " disabled") + ">Export .md</button>"
5623
+ + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear</button>"
5624
+ + "</div>";
5625
+ const summaryText = hasEntries
5626
+ ? (entryCount + " journal entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
5627
+ : "Runs and notes you send from Studio will appear here.";
5628
+ if (replJournalCollapsed || !hasEntries) {
5629
+ return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
5630
+ + "<div class='repl-journal-compact-row'>"
5631
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Journal</span><span>" + escapeHtml(summaryText) + "</span></div>"
5632
+ + actions
5633
+ + "</div>"
5634
+ + "</section>";
5635
+ }
5636
+ const omitted = Math.max(0, replJournalEntries.length - 12);
5637
+ const cards = replJournalEntries.slice(-12).map((entry) => {
5638
+ const time = formatReferenceTime(entry.createdAt) || "journal";
5639
+ const code = String(entry.code || "").trim()
5640
+ ? "<div class='repl-journal-section'><div class='repl-journal-label'>Code</div><pre class='repl-journal-code response-markdown-highlight'>" + renderHighlightedReplCode(entry.code, entry.runtime) + "</pre></div>"
5641
+ : "";
5642
+ const prose = String(entry.prose || "").trim()
5643
+ ? "<div class='repl-journal-section'><div class='repl-journal-label'>Note</div><div class='repl-journal-prose'>" + escapeHtml(entry.prose) + "</div></div>"
5644
+ : "";
5645
+ const output = String(entry.output || "").trim()
5646
+ ? "<div class='repl-journal-section'><div class='repl-journal-label'>Output</div><pre class='repl-journal-output'>" + escapeHtml(trimReplJournalOutput(entry.output)) + "</pre></div>"
5647
+ : "";
5648
+ const skipped = entry.skippedChunks ? "<span class='trace-card-meta'>skipped " + escapeHtml(String(entry.skippedChunks)) + "</span>" : "";
5649
+ return "<article class='repl-journal-card'>"
5650
+ + "<div class='repl-journal-card-header'>"
5651
+ + "<span class='trace-kind-badge'>" + escapeHtml(entry.mode === "literate" ? "Literate" : (entry.status === "note" ? "Note" : "Scratch")) + "</span>"
5652
+ + "<span class='trace-card-title'>" + escapeHtml(entry.label || "REPL entry") + "</span>"
5653
+ + "<span class='trace-card-meta'>" + escapeHtml(time) + "</span>"
5654
+ + (entry.runtime ? "<span class='trace-card-meta'>" + escapeHtml(entry.runtime) + "</span>" : "")
5655
+ + skipped
5656
+ + "</div>"
5657
+ + prose
5658
+ + code
5659
+ + (output || "<div class='trace-empty-inline'>" + escapeHtml(entry.status === "note" ? "Journal note only." : "Waiting for captured output…") + "</div>")
5660
+ + "</article>";
5661
+ }).join("");
5662
+ return "<section class='repl-journal'>"
5663
+ + "<div class='repl-journal-header'><div><h3>Journal</h3><p>Side log of notes, code sends, and captured output. Separate from the live REPL transcript.</p></div>" + actions + "</div>"
5664
+ + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
5665
+ + "<div class='repl-journal-list'>" + cards + "</div>"
5666
+ + "</section>";
5667
+ }
5668
+
5669
+ function buildReplPanelHtml() {
5670
+ const runtimeOptions = [
5671
+ ["shell", "Shell"],
5672
+ ["python", "Python"],
5673
+ ["ipython", "IPython"],
5674
+ ["julia", "Julia"],
5675
+ ["r", "R"],
5676
+ ["ghci", "GHCi"],
5677
+ ["clojure", "Clojure"],
5678
+ ].map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (replRuntime === value ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
5679
+ const sessionOptions = replSessions.length
5680
+ ? replSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
5681
+ : "<option value=''>No REPL sessions</option>";
5682
+ const activeSession = getActiveReplSession();
5683
+ const statusLabel = replTmuxAvailable === false
5684
+ ? "tmux missing"
5685
+ : (activeSession ? "Mirroring" : "Idle");
5686
+ const captured = replCapturedAt ? formatReferenceTime(replCapturedAt) : "";
5687
+ const transcript = trimReplTranscript(replTranscript);
5688
+ const emptyMessage = replTmuxAvailable === false
5689
+ ? "tmux is not available. Install tmux to use Studio REPL sessions."
5690
+ : (activeSession ? "No REPL output captured yet." : "Start a REPL session, or attach to a detected pi-repl session, to mirror it here.");
5691
+ const body = transcript
5692
+ ? renderReplTranscriptHtml(transcript, activeSession ? activeSession.runtime : replRuntime)
5693
+ : "<div class='repl-empty'>" + escapeHtml(emptyMessage) + "</div>";
5694
+ const canSendToActiveSession = Boolean(activeSession) && !replBusy && replTmuxAvailable !== false;
5695
+ const canStopActiveSession = Boolean(activeSession && activeSession.source === "studio" && !replBusy && replTmuxAvailable !== false);
5696
+ return "<div class='repl-panel'>"
5697
+ + "<div class='repl-toolbar'>"
5698
+ + "<div class='repl-summary'>"
5699
+ + "<span class='trace-summary-badge'>REPL</span>"
5700
+ + "<span class='trace-summary-status trace-status-" + (activeSession ? "running" : "idle") + "'>" + escapeHtml(statusLabel) + "</span>"
5701
+ + (activeSession ? "<span class='trace-summary-meta'>" + escapeHtml(activeSession.sessionName) + "</span>" : "")
5702
+ + (captured ? "<span class='trace-summary-meta'>Updated " + escapeHtml(captured) + "</span>" : "")
5703
+ + "</div>"
5704
+ + "<div class='repl-controls'>"
5705
+ + "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
5706
+ + "<button type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or attach to the default session for this runtime.'>Start</button>"
5707
+ + "<label class='repl-control-label'>Session <select data-repl-session aria-label='REPL session'" + (replSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
5708
+ + "<details class='repl-more-controls'>"
5709
+ + "<summary title='More REPL actions'>More</summary>"
5710
+ + "<div class='repl-more-menu'>"
5711
+ + "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
5712
+ + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
5713
+ + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
5714
+ + "<button type='button' data-repl-action='run-all-chunks'" + (canSendToActiveSession ? "" : " disabled") + " title='Literate mode: send all fenced code chunks matching the active REPL runtime.'>Run all chunks</button>"
5715
+ + "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to the literate journal without sending it to the runtime.'>Journal note</button>"
5716
+ + "<button type='button' data-repl-action='refresh'>Refresh</button>"
5717
+ + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
5718
+ + "</div>"
5719
+ + "</details>"
5720
+ + "</div>"
5721
+ + "</div>"
5722
+ + (replMessage ? "<div class='repl-notice repl-notice-info'>" + escapeHtml(replMessage) + "</div>" : "")
5723
+ + (replError ? "<div class='repl-notice repl-notice-error'>" + escapeHtml(replError) + "</div>" : "")
5724
+ + body
5725
+ + buildReplJournalHtml()
5726
+ + "</div>";
5727
+ }
5728
+
4725
5729
  function buildTracePanelHtml() {
4726
5730
  const state = traceState || createEmptyTraceState();
4727
5731
  const filter = normalizeTraceFilter(traceFilter);
@@ -4856,12 +5860,32 @@
4856
5860
  scheduleResponsePaneRepaintNudge();
4857
5861
  }
4858
5862
 
5863
+ function renderReplView() {
5864
+ if (!critiqueViewEl) return;
5865
+ const shouldStick = replFollow || shouldStickTraceToBottom();
5866
+ const previousScrollTop = critiqueViewEl.scrollTop;
5867
+ finishPreviewRender(critiqueViewEl);
5868
+ critiqueViewEl.innerHTML = buildReplPanelHtml();
5869
+ critiqueViewEl.classList.remove("response-scroll-resetting");
5870
+ if (shouldStick) {
5871
+ critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
5872
+ } else {
5873
+ critiqueViewEl.scrollTop = previousScrollTop;
5874
+ }
5875
+ scheduleResponsePaneRepaintNudge();
5876
+ }
5877
+
4859
5878
  function renderActiveResult() {
4860
5879
  if (rightView === "trace") {
4861
5880
  renderTraceView();
4862
5881
  return;
4863
5882
  }
4864
5883
 
5884
+ if (rightView === "repl") {
5885
+ renderReplView();
5886
+ return;
5887
+ }
5888
+
4865
5889
  if (rightView === "editor-preview") {
4866
5890
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
4867
5891
  if (!editorText.trim()) {
@@ -4936,10 +5960,10 @@
4936
5960
  : normalizeForCompare(sourceTextEl.value);
4937
5961
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
4938
5962
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
4939
- const showingTrace = rightView === "trace";
5963
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
4940
5964
 
4941
5965
  if (responseWrapEl) {
4942
- responseWrapEl.hidden = showingTrace;
5966
+ responseWrapEl.hidden = showingAuxiliaryRightPane;
4943
5967
  }
4944
5968
 
4945
5969
  const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
@@ -4962,21 +5986,30 @@
4962
5986
  copyResponseBtn.textContent = "Copy response text";
4963
5987
 
4964
5988
  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() : "";
5989
+ const exportingReplJournal = rightView === "repl";
5990
+ const exportText = exportingReplJournal
5991
+ ? (replJournalEntries.length ? buildReplJournalMarkdown() : "")
5992
+ : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown);
5993
+ const canExportPreview = (rightPaneShowsPreview || exportingReplJournal) && Boolean(String(exportText || "").trim());
5994
+ const htmlArtifactExportSource = canExportPreview && !exportingReplJournal ? getRightPaneHtmlArtifactSource() : "";
4968
5995
  const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
4969
5996
  if (exportPdfBtn) {
4970
5997
  exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
4971
- exportPdfBtn.textContent = previewExportInProgress ? "Exporting…" : "Export right preview";
5998
+ exportPdfBtn.textContent = previewExportInProgress
5999
+ ? "Exporting…"
6000
+ : (exportingReplJournal ? "Export REPL journal" : "Export right preview");
4972
6001
  if (rightView === "trace") {
4973
6002
  exportPdfBtn.title = "Working view does not support preview export.";
6003
+ } else if (exportingReplJournal && !replJournalEntries.length) {
6004
+ exportPdfBtn.title = "No REPL journal entries to export yet.";
4974
6005
  } else if (rightView === "markdown") {
4975
- exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export.";
6006
+ exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export.";
4976
6007
  } else if (!canExportPreview) {
4977
6008
  exportPdfBtn.title = "Nothing to export yet.";
4978
6009
  } else if (isHtmlArtifactPreview) {
4979
6010
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
6011
+ } else if (exportingReplJournal) {
6012
+ exportPdfBtn.title = "Choose PDF or HTML and export the REPL journal.";
4980
6013
  } else {
4981
6014
  exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
4982
6015
  }
@@ -4985,18 +6018,20 @@
4985
6018
  exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
4986
6019
  exportPreviewPdfBtn.title = isHtmlArtifactPreview
4987
6020
  ? "Interactive HTML preview PDF export is not available yet."
4988
- : "Export the current right-pane preview as PDF.";
6021
+ : (exportingReplJournal ? "Export the REPL journal as PDF." : "Export the current right-pane preview as PDF.");
4989
6022
  }
4990
6023
  if (exportPreviewHtmlBtn) {
4991
6024
  exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
4992
6025
  exportPreviewHtmlBtn.title = isHtmlArtifactPreview
4993
6026
  ? "Export the authored HTML preview."
4994
- : "Export the current right-pane preview as standalone HTML.";
6027
+ : (exportingReplJournal ? "Export the REPL journal as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
4995
6028
  }
4996
6029
  if (exportPreviewControlsEl) {
4997
6030
  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.";
6031
+ ? (exportingReplJournal
6032
+ ? "Choose a format and export the REPL journal."
6033
+ : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
6034
+ : (exportingReplJournal ? "No REPL journal entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
5000
6035
  }
5001
6036
  if (!canExportPreview || previewExportInProgress) {
5002
6037
  closeExportPreviewMenu();
@@ -5186,6 +6221,108 @@
5186
6221
  updateOutlineUi();
5187
6222
  }
5188
6223
 
6224
+ function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
6225
+ const value = String(nextText || "");
6226
+ sourceTextEl.value = value;
6227
+ const maxIndex = value.length;
6228
+ const safeStart = Math.max(0, Math.min(Math.floor(Number(selectionStart) || 0), maxIndex));
6229
+ const safeEnd = Math.max(safeStart, Math.min(Math.floor(Number(selectionEnd) || safeStart), maxIndex));
6230
+ sourceTextEl.setSelectionRange(safeStart, safeEnd);
6231
+ sourceTextEl.dispatchEvent(new Event("input", { bubbles: true }));
6232
+ syncEditorHighlightScroll();
6233
+ if (editorView === "markdown") {
6234
+ scheduleEditorLineNumberRender();
6235
+ }
6236
+ }
6237
+
6238
+ function getSourceTextLineEditBounds(text, selectionStart, selectionEnd) {
6239
+ const source = String(text || "");
6240
+ const safeStart = Math.max(0, Math.min(Math.floor(Number(selectionStart) || 0), source.length));
6241
+ const safeEnd = Math.max(safeStart, Math.min(Math.floor(Number(selectionEnd) || safeStart), source.length));
6242
+ const rangeEnd = safeEnd > safeStart && source.charAt(safeEnd - 1) === "\n" ? safeEnd - 1 : safeEnd;
6243
+ const lineStart = source.lastIndexOf("\n", Math.max(0, safeStart - 1)) + 1;
6244
+ const nextNewline = source.indexOf("\n", Math.max(lineStart, rangeEnd));
6245
+ const lineEnd = nextNewline >= 0 ? nextNewline : source.length;
6246
+ return { lineStart, lineEnd, selectionStart: safeStart, selectionEnd: safeEnd };
6247
+ }
6248
+
6249
+ function countSourceTextLines(text) {
6250
+ if (!text) return 1;
6251
+ return String(text).split("\n").length;
6252
+ }
6253
+
6254
+ function getSourceLineUnindentLength(line) {
6255
+ const source = String(line || "");
6256
+ if (source.startsWith(EDITOR_TAB_TEXT)) return EDITOR_TAB_TEXT.length;
6257
+ if (source.startsWith("\t")) return 1;
6258
+ const spaces = source.match(/^ +/);
6259
+ return spaces ? Math.min(EDITOR_TAB_TEXT.length, spaces[0].length) : 0;
6260
+ }
6261
+
6262
+ function getRemovedIndentBeforePosition(lineStart, removeLength, position) {
6263
+ if (removeLength <= 0 || position <= lineStart) return 0;
6264
+ if (position <= lineStart + removeLength) return position - lineStart;
6265
+ return removeLength;
6266
+ }
6267
+
6268
+ function indentSourceTextSelection() {
6269
+ const source = String(sourceTextEl.value || "");
6270
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : source.length;
6271
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
6272
+ const selected = source.slice(Math.min(start, end), Math.max(start, end));
6273
+ if (start === end || !selected.includes("\n")) {
6274
+ const before = source.slice(0, Math.min(start, end));
6275
+ const after = source.slice(Math.max(start, end));
6276
+ const nextCaret = before.length + EDITOR_TAB_TEXT.length;
6277
+ applySourceTextEdit(before + EDITOR_TAB_TEXT + after, nextCaret, nextCaret);
6278
+ return;
6279
+ }
6280
+
6281
+ const bounds = getSourceTextLineEditBounds(source, start, end);
6282
+ const segment = source.slice(bounds.lineStart, bounds.lineEnd);
6283
+ const lineCount = countSourceTextLines(segment);
6284
+ const replacement = segment.replace(/^/gm, EDITOR_TAB_TEXT);
6285
+ const next = source.slice(0, bounds.lineStart) + replacement + source.slice(bounds.lineEnd);
6286
+ const nextStart = bounds.selectionStart + (bounds.lineStart < bounds.selectionStart ? EDITOR_TAB_TEXT.length : 0);
6287
+ const nextEnd = bounds.selectionEnd + (lineCount * EDITOR_TAB_TEXT.length);
6288
+ applySourceTextEdit(next, nextStart, nextEnd);
6289
+ }
6290
+
6291
+ function unindentSourceTextSelection() {
6292
+ const source = String(sourceTextEl.value || "");
6293
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : source.length;
6294
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
6295
+ const bounds = getSourceTextLineEditBounds(source, start, end);
6296
+ const segment = source.slice(bounds.lineStart, bounds.lineEnd);
6297
+ const lines = segment.split("\n");
6298
+ let absoluteLineStart = bounds.lineStart;
6299
+ let removedBeforeStart = 0;
6300
+ let removedBeforeEnd = 0;
6301
+ const nextLines = lines.map((line, index) => {
6302
+ const removeLength = getSourceLineUnindentLength(line);
6303
+ removedBeforeStart += getRemovedIndentBeforePosition(absoluteLineStart, removeLength, bounds.selectionStart);
6304
+ removedBeforeEnd += getRemovedIndentBeforePosition(absoluteLineStart, removeLength, bounds.selectionEnd);
6305
+ absoluteLineStart += line.length + (index < lines.length - 1 ? 1 : 0);
6306
+ return removeLength > 0 ? line.slice(removeLength) : line;
6307
+ });
6308
+ const replacement = nextLines.join("\n");
6309
+ if (replacement === segment) return;
6310
+ const next = source.slice(0, bounds.lineStart) + replacement + source.slice(bounds.lineEnd);
6311
+ const nextStart = Math.max(bounds.lineStart, bounds.selectionStart - removedBeforeStart);
6312
+ const nextEnd = Math.max(nextStart, bounds.selectionEnd - removedBeforeEnd);
6313
+ applySourceTextEdit(next, nextStart, nextEnd);
6314
+ }
6315
+
6316
+ function handleSourceTextTabKey(event) {
6317
+ if (!event || event.key !== "Tab" || event.metaKey || event.ctrlKey || event.altKey) return;
6318
+ event.preventDefault();
6319
+ if (event.shiftKey) {
6320
+ unindentSourceTextSelection();
6321
+ } else {
6322
+ indentSourceTextSelection();
6323
+ }
6324
+ }
6325
+
5189
6326
  function setEditorView(nextView) {
5190
6327
  editorView = nextView === "preview" ? "preview" : "markdown";
5191
6328
  editorViewSelect.value = editorView;
@@ -5227,11 +6364,19 @@
5227
6364
  ? "preview"
5228
6365
  : (nextView === "editor-preview"
5229
6366
  ? "editor-preview"
5230
- : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"));
6367
+ : (nextView === "repl"
6368
+ ? "repl"
6369
+ : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown")));
5231
6370
  rightViewSelect.value = rightView;
5232
6371
  if (rightView === "trace" && previousView !== "trace") {
5233
6372
  traceAutoScroll = true;
5234
6373
  }
6374
+ if (rightView === "repl" && previousView !== "repl") {
6375
+ replFollow = true;
6376
+ startReplPolling();
6377
+ } else if (rightView !== "repl" && previousView === "repl") {
6378
+ stopReplPolling();
6379
+ }
5235
6380
 
5236
6381
  if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
5237
6382
  window.clearTimeout(responseEditorPreviewTimer);
@@ -10679,6 +11824,14 @@
10679
11824
  queueSteerBtn.classList.remove("request-stop-active");
10680
11825
  queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
10681
11826
  }
11827
+ if (sendReplBtn) {
11828
+ sendReplBtn.hidden = true;
11829
+ sendReplBtn.disabled = true;
11830
+ }
11831
+ if (replSendModeSelect) {
11832
+ replSendModeSelect.hidden = true;
11833
+ replSendModeSelect.disabled = true;
11834
+ }
10682
11835
  if (critiqueBtn) {
10683
11836
  critiqueBtn.textContent = "Critique text";
10684
11837
  critiqueBtn.classList.remove("request-stop-active");
@@ -10693,11 +11846,14 @@
10693
11846
  sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
10694
11847
  sendRunBtn.classList.toggle("request-stop-active", directIsStop);
10695
11848
  sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
11849
+ const replHint = rightView === "repl" && getActiveReplSession()
11850
+ ? " Includes active REPL identity for prompts that refer to it."
11851
+ : "";
10696
11852
  sendRunBtn.title = directIsStop
10697
11853
  ? "Stop the active run. Shortcut: Esc."
10698
11854
  : (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.");
11855
+ ? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint
11856
+ : "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint);
10701
11857
  }
10702
11858
 
10703
11859
  if (queueSteerBtn) {
@@ -10711,6 +11867,25 @@
10711
11867
  : "Queue steering is available while Run editor text is active.";
10712
11868
  }
10713
11869
 
11870
+ if (sendReplBtn) {
11871
+ const hasSession = Boolean(getActiveReplSession());
11872
+ sendReplBtn.hidden = rightView !== "repl";
11873
+ sendReplBtn.disabled = wsState === "Disconnected" || uiBusy || replBusy || !hasSession;
11874
+ sendReplBtn.title = hasSession
11875
+ ? (replSendMode === "literate"
11876
+ ? "Literate send: selected code/prose, or the current fenced code chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
11877
+ : "Scratch send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
11878
+ : "Start or select a REPL session in the right pane first.";
11879
+ }
11880
+ if (replSendModeSelect) {
11881
+ replSendModeSelect.hidden = rightView !== "repl";
11882
+ replSendModeSelect.disabled = wsState === "Disconnected" || uiBusy || replBusy;
11883
+ replSendModeSelect.value = replSendMode;
11884
+ replSendModeSelect.title = replSendMode === "literate"
11885
+ ? "Literate send: Send to REPL uses the selection/current fenced code chunk."
11886
+ : "Scratch send: Send to REPL uses the selection, or full editor if no selection.";
11887
+ }
11888
+
10714
11889
  if (critiqueBtn) {
10715
11890
  critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique text";
10716
11891
  critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
@@ -11025,6 +12200,63 @@
11025
12200
  return;
11026
12201
  }
11027
12202
 
12203
+ if (message.type === "repl_state") {
12204
+ replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
12205
+ setReplSessions(message.sessions);
12206
+ if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
12207
+ setActiveReplSession(message.activeSessionName);
12208
+ }
12209
+ if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
12210
+ if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
12211
+ replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
12212
+ replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
12213
+ replBusy = false;
12214
+ syncActionButtons();
12215
+ renderReplViewIfActive();
12216
+ updateReferenceBadge();
12217
+ return;
12218
+ }
12219
+
12220
+ if (message.type === "repl_capture") {
12221
+ if (message.session) {
12222
+ const session = normalizeReplSession(message.session);
12223
+ if (session && !replSessions.some((candidate) => candidate.sessionName === session.sessionName)) {
12224
+ replSessions = [...replSessions, session];
12225
+ }
12226
+ }
12227
+ if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
12228
+ setActiveReplSession(message.activeSessionName);
12229
+ }
12230
+ if (typeof message.transcript === "string") {
12231
+ replTranscript = trimReplTranscript(message.transcript);
12232
+ updateActiveReplJournalEntryFromTranscript(
12233
+ typeof message.activeSessionName === "string" && message.activeSessionName.trim() ? message.activeSessionName : replActiveSessionName,
12234
+ replTranscript
12235
+ );
12236
+ }
12237
+ if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
12238
+ replError = typeof message.replError === "string" ? message.replError : "";
12239
+ if (typeof message.replMessage === "string") replMessage = message.replMessage;
12240
+ replBusy = false;
12241
+ syncActionButtons();
12242
+ renderReplViewIfActive();
12243
+ updateReferenceBadge();
12244
+ return;
12245
+ }
12246
+
12247
+ if (message.type === "repl_send_ack") {
12248
+ replBusy = false;
12249
+ replMessage = typeof message.message === "string" ? message.message : "Sent editor text to REPL.";
12250
+ replError = "";
12251
+ if (typeof message.requestId === "string") {
12252
+ replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
12253
+ }
12254
+ setStatus(replMessage, "success");
12255
+ syncActionButtons();
12256
+ renderReplViewIfActive({ force: true });
12257
+ return;
12258
+ }
12259
+
11028
12260
  if (message.type === "request_started") {
11029
12261
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
11030
12262
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
@@ -11425,6 +12657,14 @@
11425
12657
  if (typeof message.requestId === "string") {
11426
12658
  clearArmedTitleAttention(message.requestId);
11427
12659
  }
12660
+ if (replBusy) {
12661
+ replBusy = false;
12662
+ replError = typeof message.message === "string" ? message.message : "REPL request failed.";
12663
+ if (typeof message.requestId === "string") {
12664
+ replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "error", output: replError, updatedAt: Date.now() } : entry);
12665
+ }
12666
+ renderReplViewIfActive({ force: true });
12667
+ }
11428
12668
  stickyStudioKind = null;
11429
12669
  setBusy(false);
11430
12670
  setWsState("Ready");
@@ -11977,6 +13217,8 @@
11977
13217
  requestLatestResponse();
11978
13218
  });
11979
13219
 
13220
+ sourceTextEl.addEventListener("keydown", handleSourceTextTabKey);
13221
+
11980
13222
  sourceTextEl.addEventListener("input", () => {
11981
13223
  if (activePreviewCommentSelection) {
11982
13224
  clearPreviewCommentSelection();
@@ -12348,7 +13590,7 @@
12348
13590
  return;
12349
13591
  }
12350
13592
 
12351
- const prepared = prepareEditorTextForSend(sourceTextEl.value);
13593
+ const prepared = prepareEditorTextForRunRequest(sourceTextEl.value);
12352
13594
  if (!prepared.trim()) {
12353
13595
  setStatus("Editor is empty. Nothing to run.", "warning");
12354
13596
  return;
@@ -12372,7 +13614,7 @@
12372
13614
 
12373
13615
  if (queueSteerBtn) {
12374
13616
  queueSteerBtn.addEventListener("click", () => {
12375
- const prepared = prepareEditorTextForSend(sourceTextEl.value);
13617
+ const prepared = prepareEditorTextForRunRequest(sourceTextEl.value);
12376
13618
  if (!prepared.trim()) {
12377
13619
  setStatus("Editor is empty. Nothing to queue.", "warning");
12378
13620
  return;
@@ -12394,6 +13636,20 @@
12394
13636
  });
12395
13637
  }
12396
13638
 
13639
+ if (sendReplBtn) {
13640
+ sendReplBtn.addEventListener("click", () => {
13641
+ sendEditorTextToRepl();
13642
+ });
13643
+ }
13644
+
13645
+ if (replSendModeSelect) {
13646
+ replSendModeSelect.addEventListener("change", () => {
13647
+ setReplSendMode(replSendModeSelect.value);
13648
+ syncActionButtons();
13649
+ renderReplViewIfActive({ force: true });
13650
+ });
13651
+ }
13652
+
12397
13653
  copyDraftBtn.addEventListener("click", async () => {
12398
13654
  const content = sourceTextEl.value;
12399
13655
  if (!content.trim()) {
@@ -12812,6 +14068,7 @@
12812
14068
  const storedAnnotationsEnabled = readStoredAnnotationsEnabled();
12813
14069
  const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
12814
14070
  setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
14071
+ setReplSendMode(replSendMode);
12815
14072
 
12816
14073
  setEditorView(editorView);
12817
14074
  setRightView(rightView);