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.
- package/CHANGELOG.md +18 -0
- package/README.md +3 -1
- package/client/studio-client.js +1291 -34
- package/client/studio.css +264 -0
- package/index.ts +455 -0
- package/package.json +2 -2
package/client/studio-client.js
CHANGED
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
|
|
108
108
|
const sendRunBtn = document.getElementById("sendRunBtn");
|
|
109
109
|
const queueSteerBtn = document.getElementById("queueSteerBtn");
|
|
110
|
+
const sendReplBtn = document.getElementById("sendReplBtn");
|
|
111
|
+
const replSendModeSelect = document.getElementById("replSendModeSelect");
|
|
110
112
|
const copyDraftBtn = document.getElementById("copyDraftBtn");
|
|
111
113
|
const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
|
|
112
114
|
const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
|
|
@@ -215,6 +217,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
|
|
3706
|
-
|
|
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)
|
|
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 =
|
|
3992
|
-
?
|
|
3993
|
-
:
|
|
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)
|
|
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
|
|
5963
|
+
const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
|
|
4940
5964
|
|
|
4941
5965
|
if (responseWrapEl) {
|
|
4942
|
-
responseWrapEl.hidden =
|
|
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
|
|
4966
|
-
const
|
|
4967
|
-
|
|
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
|
|
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)
|
|
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
|
-
? (
|
|
4999
|
-
|
|
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
|
-
: (
|
|
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 =
|
|
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 =
|
|
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);
|