pi-studio 0.8.3 → 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 +24 -0
- package/README.md +5 -3
- package/client/studio-client.js +1448 -37
- package/client/studio.css +310 -0
- package/index.ts +623 -22
- 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");
|
|
@@ -214,6 +216,48 @@
|
|
|
214
216
|
const traceExpandedOutputs = new Set();
|
|
215
217
|
const TRACE_OUTPUT_PREVIEW_MAX_LINES = 50;
|
|
216
218
|
const TRACE_OUTPUT_PREVIEW_MAX_CHARS = 8000;
|
|
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
|
+
})();
|
|
217
261
|
let studioRunChainActive = false;
|
|
218
262
|
let queuedSteeringCount = 0;
|
|
219
263
|
let agentBusyFromServer = false;
|
|
@@ -287,6 +331,37 @@
|
|
|
287
331
|
: "pending";
|
|
288
332
|
}
|
|
289
333
|
|
|
334
|
+
function normalizeTraceImageMimeType(value) {
|
|
335
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isTraceImageSafeMimeType(mimeType) {
|
|
339
|
+
return TRACE_IMAGE_SAFE_MIME_TYPES.has(normalizeTraceImageMimeType(mimeType));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function normalizeTraceImage(image, fallbackIndex) {
|
|
343
|
+
if (!image || typeof image !== "object") return null;
|
|
344
|
+
const mimeType = normalizeTraceImageMimeType(image.mimeType);
|
|
345
|
+
if (!isTraceImageSafeMimeType(mimeType)) return null;
|
|
346
|
+
const data = typeof image.data === "string" ? image.data.replace(/\s+/g, "") : "";
|
|
347
|
+
if (!data || !/^[A-Za-z0-9+/]*={0,2}$/.test(data)) return null;
|
|
348
|
+
const byteLength = parseFiniteNumber(image.byteLength);
|
|
349
|
+
return {
|
|
350
|
+
id: typeof image.id === "string" && image.id.trim() ? image.id.trim() : ("trace-image-" + fallbackIndex),
|
|
351
|
+
mimeType,
|
|
352
|
+
data,
|
|
353
|
+
byteLength: byteLength == null ? estimateTraceImageByteLength(data) : byteLength,
|
|
354
|
+
label: parseNonEmptyString(image.label),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function estimateTraceImageByteLength(data) {
|
|
359
|
+
const compact = String(data || "").replace(/\s+/g, "");
|
|
360
|
+
if (!compact) return null;
|
|
361
|
+
const padding = compact.endsWith("==") ? 2 : (compact.endsWith("=") ? 1 : 0);
|
|
362
|
+
return Math.max(0, Math.floor((compact.length * 3) / 4) - padding);
|
|
363
|
+
}
|
|
364
|
+
|
|
290
365
|
function normalizeTraceEntry(entry, fallbackIndex) {
|
|
291
366
|
if (!entry || typeof entry !== "object") return null;
|
|
292
367
|
if (entry.type === "assistant") {
|
|
@@ -310,6 +385,9 @@
|
|
|
310
385
|
label: parseNonEmptyString(entry.label),
|
|
311
386
|
argsSummary: parseNonEmptyString(entry.argsSummary),
|
|
312
387
|
output: typeof entry.output === "string" ? entry.output : "",
|
|
388
|
+
images: Array.isArray(entry.images)
|
|
389
|
+
? entry.images.map((image, imageIndex) => normalizeTraceImage(image, imageIndex)).filter(Boolean)
|
|
390
|
+
: [],
|
|
313
391
|
startedAt: parseFiniteNumber(entry.startedAt) || Date.now(),
|
|
314
392
|
updatedAt: parseFiniteNumber(entry.updatedAt) || Date.now(),
|
|
315
393
|
status: normalizeTraceEntryStatus(entry.status),
|
|
@@ -542,6 +620,22 @@
|
|
|
542
620
|
});
|
|
543
621
|
}
|
|
544
622
|
|
|
623
|
+
function formatTraceImageSize(byteLength) {
|
|
624
|
+
if (typeof byteLength !== "number" || !Number.isFinite(byteLength) || byteLength < 0) return "unknown size";
|
|
625
|
+
if (byteLength < 1024) return formatNumber(byteLength) + " B";
|
|
626
|
+
if (byteLength < 1024 * 1024) return (byteLength / 1024).toFixed(byteLength >= 100 * 1024 ? 0 : 1).replace(/\.0$/, "") + " KB";
|
|
627
|
+
return (byteLength / (1024 * 1024)).toFixed(byteLength >= 100 * 1024 * 1024 ? 0 : 1).replace(/\.0$/, "") + " MB";
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function describeTraceImageForText(image) {
|
|
631
|
+
if (!image || typeof image !== "object") return "";
|
|
632
|
+
const parts = [];
|
|
633
|
+
if (image.label) parts.push(String(image.label));
|
|
634
|
+
parts.push(String(image.mimeType || "image"));
|
|
635
|
+
parts.push(formatTraceImageSize(image.byteLength));
|
|
636
|
+
return parts.filter(Boolean).join(" — ");
|
|
637
|
+
}
|
|
638
|
+
|
|
545
639
|
function buildVisibleWorkingText(filterOverride) {
|
|
546
640
|
const filter = normalizeTraceFilter(filterOverride || traceFilter);
|
|
547
641
|
const entries = getTraceEntriesForFilter(filter);
|
|
@@ -576,6 +670,12 @@
|
|
|
576
670
|
if (String(entry.output || "").trim()) {
|
|
577
671
|
parts.push("Output:\n" + String(entry.output || "").trim());
|
|
578
672
|
}
|
|
673
|
+
const imageSummaries = Array.isArray(entry.images)
|
|
674
|
+
? entry.images.map(describeTraceImageForText).filter(Boolean)
|
|
675
|
+
: [];
|
|
676
|
+
if (imageSummaries.length) {
|
|
677
|
+
parts.push("Images:\n" + imageSummaries.map((summary) => "- " + summary).join("\n"));
|
|
678
|
+
}
|
|
579
679
|
return parts.join("\n\n").trim();
|
|
580
680
|
}).filter(Boolean).join("\n\n---\n\n");
|
|
581
681
|
}
|
|
@@ -688,6 +788,641 @@
|
|
|
688
788
|
setStatus("Loaded visible working into editor.", "success");
|
|
689
789
|
}
|
|
690
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
|
+
|
|
691
1426
|
function renderTraceViewIfActive() {
|
|
692
1427
|
if (rightView !== "trace") return;
|
|
693
1428
|
if (traceRenderRaf !== null) return;
|
|
@@ -1152,6 +1887,8 @@
|
|
|
1152
1887
|
const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
1153
1888
|
if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
|
|
1154
1889
|
if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
|
|
1890
|
+
if (!isEditorOnlyMode && sendReplBtn) actionLineOneEl.appendChild(sendReplBtn);
|
|
1891
|
+
if (!isEditorOnlyMode && replSendModeSelect) actionLineOneEl.appendChild(replSendModeSelect);
|
|
1155
1892
|
const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
1156
1893
|
actionLineTwoEl.appendChild(copyDraftBtn);
|
|
1157
1894
|
if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
|
|
@@ -2143,6 +2880,23 @@
|
|
|
2143
2880
|
return;
|
|
2144
2881
|
}
|
|
2145
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
|
+
|
|
2146
2900
|
if (
|
|
2147
2901
|
key === "Enter"
|
|
2148
2902
|
&& (event.metaKey || event.ctrlKey)
|
|
@@ -2455,6 +3209,18 @@
|
|
|
2455
3209
|
return;
|
|
2456
3210
|
}
|
|
2457
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
|
+
|
|
2458
3224
|
if (rightView === "editor-preview") {
|
|
2459
3225
|
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
2460
3226
|
if (hasResponse) {
|
|
@@ -3645,8 +4411,13 @@
|
|
|
3645
4411
|
}
|
|
3646
4412
|
|
|
3647
4413
|
function handleTracePaneScroll() {
|
|
3648
|
-
if (rightView
|
|
3649
|
-
|
|
4414
|
+
if (rightView === "trace") {
|
|
4415
|
+
traceAutoScroll = shouldStickTraceToBottom();
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4418
|
+
if (rightView === "repl") {
|
|
4419
|
+
replFollow = shouldStickTraceToBottom();
|
|
4420
|
+
}
|
|
3650
4421
|
}
|
|
3651
4422
|
|
|
3652
4423
|
async function handleTracePaneClick(event) {
|
|
@@ -3687,10 +4458,129 @@
|
|
|
3687
4458
|
}
|
|
3688
4459
|
}
|
|
3689
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
|
+
|
|
3690
4578
|
function attachResponsePaneInteractionHandlers() {
|
|
3691
4579
|
if (!critiqueViewEl) return;
|
|
3692
4580
|
critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
|
|
3693
4581
|
critiqueViewEl.addEventListener("click", handleTracePaneClick);
|
|
4582
|
+
critiqueViewEl.addEventListener("click", handleReplPaneClick);
|
|
4583
|
+
critiqueViewEl.addEventListener("change", handleReplPaneChange);
|
|
3694
4584
|
}
|
|
3695
4585
|
|
|
3696
4586
|
function replaceResponsePaneWithClone() {
|
|
@@ -3919,35 +4809,42 @@
|
|
|
3919
4809
|
return;
|
|
3920
4810
|
}
|
|
3921
4811
|
|
|
4812
|
+
const exportingReplJournal = rightView === "repl";
|
|
3922
4813
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
3923
|
-
if (!rightPaneShowsPreview) {
|
|
3924
|
-
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");
|
|
3925
4820
|
return;
|
|
3926
4821
|
}
|
|
3927
4822
|
|
|
3928
|
-
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
4823
|
+
const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
|
|
3929
4824
|
if (htmlArtifactSource) {
|
|
3930
4825
|
setStatus("PDF export does not support interactive HTML previews yet. Export as HTML or use the browser print dialog inside the preview.", "warning");
|
|
3931
4826
|
return;
|
|
3932
4827
|
}
|
|
3933
4828
|
|
|
3934
|
-
const markdown =
|
|
3935
|
-
?
|
|
3936
|
-
:
|
|
4829
|
+
const markdown = exportingReplJournal
|
|
4830
|
+
? buildReplJournalMarkdown()
|
|
4831
|
+
: (rightView === "editor-preview"
|
|
4832
|
+
? prepareEditorTextForPdfExport(sourceTextEl.value)
|
|
4833
|
+
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
3937
4834
|
if (!markdown || !markdown.trim()) {
|
|
3938
4835
|
setStatus("Nothing to export yet.", "warning");
|
|
3939
4836
|
return;
|
|
3940
4837
|
}
|
|
3941
4838
|
|
|
3942
4839
|
const effectivePath = getEffectiveSavePath();
|
|
3943
|
-
const sourcePath = effectivePath || sourceState.path || "";
|
|
4840
|
+
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
3944
4841
|
const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
|
|
3945
4842
|
const isEditorPreview = rightView === "editor-preview";
|
|
3946
4843
|
const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
|
|
3947
4844
|
const isLatex = isEditorPreview
|
|
3948
4845
|
? editorPdfLanguage === "latex"
|
|
3949
4846
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
3950
|
-
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");
|
|
3951
4848
|
if (sourcePath) {
|
|
3952
4849
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
3953
4850
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
@@ -4084,31 +4981,36 @@
|
|
|
4084
4981
|
return;
|
|
4085
4982
|
}
|
|
4086
4983
|
|
|
4984
|
+
const exportingReplJournal = rightView === "repl";
|
|
4087
4985
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4088
|
-
if (!rightPaneShowsPreview) {
|
|
4089
|
-
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");
|
|
4090
4992
|
return;
|
|
4091
4993
|
}
|
|
4092
4994
|
|
|
4093
|
-
const htmlArtifactSource = getRightPaneHtmlArtifactSource();
|
|
4094
|
-
const markdown = htmlArtifactSource || (rightView === "editor-preview"
|
|
4995
|
+
const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
|
|
4996
|
+
const markdown = exportingReplJournal ? buildReplJournalMarkdown() : (htmlArtifactSource || (rightView === "editor-preview"
|
|
4095
4997
|
? prepareEditorTextForHtmlExport(sourceTextEl.value)
|
|
4096
|
-
: prepareEditorTextForPreview(latestResponseMarkdown));
|
|
4998
|
+
: prepareEditorTextForPreview(latestResponseMarkdown)));
|
|
4097
4999
|
if (!markdown || !markdown.trim()) {
|
|
4098
5000
|
setStatus("Nothing to export yet.", "warning");
|
|
4099
5001
|
return;
|
|
4100
5002
|
}
|
|
4101
5003
|
|
|
4102
5004
|
const effectivePath = getEffectiveSavePath();
|
|
4103
|
-
const sourcePath = effectivePath || sourceState.path || "";
|
|
5005
|
+
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
4104
5006
|
const resourceDir = (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "";
|
|
4105
5007
|
const isEditorPreview = rightView === "editor-preview";
|
|
4106
5008
|
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
|
|
4107
5009
|
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
4108
5010
|
? editorHtmlLanguage === "latex"
|
|
4109
5011
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
4110
|
-
let filenameHint = isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html";
|
|
4111
|
-
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");
|
|
4112
5014
|
if (sourcePath) {
|
|
4113
5015
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
4114
5016
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
@@ -4267,6 +5169,78 @@
|
|
|
4267
5169
|
return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
|
|
4268
5170
|
}
|
|
4269
5171
|
|
|
5172
|
+
function getCopyableBlockquoteText(blockEl) {
|
|
5173
|
+
const clone = blockEl && typeof blockEl.cloneNode === "function" ? blockEl.cloneNode(true) : null;
|
|
5174
|
+
const sourceEl = clone && typeof clone.querySelectorAll === "function" ? clone : blockEl;
|
|
5175
|
+
if (!sourceEl) return "";
|
|
5176
|
+
if (typeof sourceEl.querySelectorAll === "function") {
|
|
5177
|
+
Array.from(sourceEl.querySelectorAll(".studio-copy-block-btn")).forEach((buttonEl) => {
|
|
5178
|
+
if (buttonEl && buttonEl.parentNode) buttonEl.parentNode.removeChild(buttonEl);
|
|
5179
|
+
});
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
const blockTags = new Set(["ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DIV", "FIGCAPTION", "FIGURE", "FOOTER", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "LI", "OL", "P", "PRE", "SECTION", "TABLE", "TBODY", "TD", "TH", "THEAD", "TR", "UL"]);
|
|
5183
|
+
const isElementBlock = (node) => node && node.nodeType === 1 && blockTags.has(String(node.tagName || "").toUpperCase());
|
|
5184
|
+
|
|
5185
|
+
const collectInlineText = (node) => {
|
|
5186
|
+
if (!node) return "";
|
|
5187
|
+
if (node.nodeType === Node.TEXT_NODE) return node.nodeValue || "";
|
|
5188
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
|
5189
|
+
const tag = String(node.tagName || "").toUpperCase();
|
|
5190
|
+
if (tag === "SCRIPT" || tag === "STYLE" || tag === "BUTTON") return "";
|
|
5191
|
+
if (tag === "BR") return "\n";
|
|
5192
|
+
const childText = Array.from(node.childNodes || []).map(collectInlineText).join("");
|
|
5193
|
+
return isElementBlock(node) ? childText.trim() : childText;
|
|
5194
|
+
};
|
|
5195
|
+
|
|
5196
|
+
const collectBlocks = (node) => {
|
|
5197
|
+
if (!node) return [];
|
|
5198
|
+
const parts = [];
|
|
5199
|
+
let inlineBuffer = "";
|
|
5200
|
+
const flushInline = () => {
|
|
5201
|
+
const text = inlineBuffer.replace(/[ \t]+\n/g, "\n").trim();
|
|
5202
|
+
if (text) parts.push(text);
|
|
5203
|
+
inlineBuffer = "";
|
|
5204
|
+
};
|
|
5205
|
+
|
|
5206
|
+
Array.from(node.childNodes || []).forEach((child) => {
|
|
5207
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
5208
|
+
inlineBuffer += child.nodeValue || "";
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
if (child.nodeType !== Node.ELEMENT_NODE) return;
|
|
5212
|
+
const tag = String(child.tagName || "").toUpperCase();
|
|
5213
|
+
if (tag === "SCRIPT" || tag === "STYLE" || tag === "BUTTON") return;
|
|
5214
|
+
if (tag === "BR") {
|
|
5215
|
+
inlineBuffer += "\n";
|
|
5216
|
+
return;
|
|
5217
|
+
}
|
|
5218
|
+
if (isElementBlock(child)) {
|
|
5219
|
+
flushInline();
|
|
5220
|
+
if (tag === "UL" || tag === "OL") {
|
|
5221
|
+
Array.from(child.children || []).forEach((item, itemIndex) => {
|
|
5222
|
+
if (!item || String(item.tagName || "").toUpperCase() !== "LI") return;
|
|
5223
|
+
const prefix = tag === "OL" ? (String(itemIndex + 1) + ". ") : "- ";
|
|
5224
|
+
const itemText = collectInlineText(item).trim();
|
|
5225
|
+
if (itemText) parts.push(prefix + itemText);
|
|
5226
|
+
});
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
const blockText = tag === "BLOCKQUOTE"
|
|
5230
|
+
? collectBlocks(child).join("\n\n").trim()
|
|
5231
|
+
: collectInlineText(child).trim();
|
|
5232
|
+
if (blockText) parts.push(blockText);
|
|
5233
|
+
return;
|
|
5234
|
+
}
|
|
5235
|
+
inlineBuffer += collectInlineText(child);
|
|
5236
|
+
});
|
|
5237
|
+
flushInline();
|
|
5238
|
+
return parts;
|
|
5239
|
+
};
|
|
5240
|
+
|
|
5241
|
+
return normalizeCopyableBlockText(collectBlocks(sourceEl).join("\n\n")).trim();
|
|
5242
|
+
}
|
|
5243
|
+
|
|
4270
5244
|
function getCopyablePreviewBlockText(blockEl) {
|
|
4271
5245
|
if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
|
|
4272
5246
|
if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
|
|
@@ -4277,6 +5251,10 @@
|
|
|
4277
5251
|
);
|
|
4278
5252
|
}
|
|
4279
5253
|
|
|
5254
|
+
if (blockEl.matches && blockEl.matches("blockquote")) {
|
|
5255
|
+
return getCopyableBlockquoteText(blockEl);
|
|
5256
|
+
}
|
|
5257
|
+
|
|
4280
5258
|
const codeEl = typeof blockEl.querySelector === "function"
|
|
4281
5259
|
? blockEl.querySelector("pre code, code")
|
|
4282
5260
|
: null;
|
|
@@ -4334,7 +5312,7 @@
|
|
|
4334
5312
|
|
|
4335
5313
|
function decorateCopyablePreviewBlocks(targetEl) {
|
|
4336
5314
|
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
4337
|
-
const blocks = Array.from(targetEl.querySelectorAll("div.sourceCode, pre, .preview-code-lines"));
|
|
5315
|
+
const blocks = Array.from(targetEl.querySelectorAll("div.sourceCode, pre, .preview-code-lines, blockquote"));
|
|
4338
5316
|
blocks.forEach((blockEl) => {
|
|
4339
5317
|
if (!blockEl || !(blockEl instanceof Element)) return;
|
|
4340
5318
|
if (blockEl.dataset && blockEl.dataset.studioCopyDecorated === "1") return;
|
|
@@ -4572,6 +5550,182 @@
|
|
|
4572
5550
|
+ "</div>";
|
|
4573
5551
|
}
|
|
4574
5552
|
|
|
5553
|
+
function renderTraceImages(images) {
|
|
5554
|
+
const normalizedImages = Array.isArray(images)
|
|
5555
|
+
? images.map((image, index) => normalizeTraceImage(image, index)).filter(Boolean)
|
|
5556
|
+
: [];
|
|
5557
|
+
if (!normalizedImages.length) return "";
|
|
5558
|
+
const cards = normalizedImages.map((image) => {
|
|
5559
|
+
const src = "data:" + image.mimeType + ";base64," + image.data;
|
|
5560
|
+
const caption = describeTraceImageForText(image);
|
|
5561
|
+
const alt = image.label || ("Working output image: " + image.mimeType);
|
|
5562
|
+
return "<figure class='trace-image-card'>"
|
|
5563
|
+
+ "<img src='" + escapeHtml(src) + "' alt='" + escapeHtml(alt) + "' loading='lazy' decoding='async' />"
|
|
5564
|
+
+ "<figcaption class='trace-image-caption'>" + escapeHtml(caption) + "</figcaption>"
|
|
5565
|
+
+ "</figure>";
|
|
5566
|
+
}).join("");
|
|
5567
|
+
return "<div class='trace-image-gallery'>" + cards + "</div>";
|
|
5568
|
+
}
|
|
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
|
+
|
|
4575
5729
|
function buildTracePanelHtml() {
|
|
4576
5730
|
const state = traceState || createEmptyTraceState();
|
|
4577
5731
|
const filter = normalizeTraceFilter(traceFilter);
|
|
@@ -4665,8 +5819,12 @@
|
|
|
4665
5819
|
const argsSummary = entry.argsSummary
|
|
4666
5820
|
? "<div class='trace-section'><div class='trace-section-label'>Input</div>" + renderTraceOutput(entry.argsSummary, entry.id + ":input") + "</div>"
|
|
4667
5821
|
: "";
|
|
4668
|
-
const
|
|
4669
|
-
|
|
5822
|
+
const imageOutput = renderTraceImages(entry.images);
|
|
5823
|
+
const outputPieces = [];
|
|
5824
|
+
if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output"));
|
|
5825
|
+
if (imageOutput) outputPieces.push(imageOutput);
|
|
5826
|
+
const output = outputPieces.length
|
|
5827
|
+
? "<div class='trace-section'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
|
|
4670
5828
|
: "<div class='trace-empty-inline'>No output yet.</div>";
|
|
4671
5829
|
const toolStatusLabel = entry.isError
|
|
4672
5830
|
? "Error"
|
|
@@ -4702,12 +5860,32 @@
|
|
|
4702
5860
|
scheduleResponsePaneRepaintNudge();
|
|
4703
5861
|
}
|
|
4704
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
|
+
|
|
4705
5878
|
function renderActiveResult() {
|
|
4706
5879
|
if (rightView === "trace") {
|
|
4707
5880
|
renderTraceView();
|
|
4708
5881
|
return;
|
|
4709
5882
|
}
|
|
4710
5883
|
|
|
5884
|
+
if (rightView === "repl") {
|
|
5885
|
+
renderReplView();
|
|
5886
|
+
return;
|
|
5887
|
+
}
|
|
5888
|
+
|
|
4711
5889
|
if (rightView === "editor-preview") {
|
|
4712
5890
|
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
4713
5891
|
if (!editorText.trim()) {
|
|
@@ -4782,10 +5960,10 @@
|
|
|
4782
5960
|
: normalizeForCompare(sourceTextEl.value);
|
|
4783
5961
|
const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
|
|
4784
5962
|
const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
|
|
4785
|
-
const
|
|
5963
|
+
const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
|
|
4786
5964
|
|
|
4787
5965
|
if (responseWrapEl) {
|
|
4788
|
-
responseWrapEl.hidden =
|
|
5966
|
+
responseWrapEl.hidden = showingAuxiliaryRightPane;
|
|
4789
5967
|
}
|
|
4790
5968
|
|
|
4791
5969
|
const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
|
|
@@ -4808,21 +5986,30 @@
|
|
|
4808
5986
|
copyResponseBtn.textContent = "Copy response text";
|
|
4809
5987
|
|
|
4810
5988
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
4811
|
-
const
|
|
4812
|
-
const
|
|
4813
|
-
|
|
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() : "";
|
|
4814
5995
|
const isHtmlArtifactPreview = Boolean(htmlArtifactExportSource);
|
|
4815
5996
|
if (exportPdfBtn) {
|
|
4816
5997
|
exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4817
|
-
exportPdfBtn.textContent = previewExportInProgress
|
|
5998
|
+
exportPdfBtn.textContent = previewExportInProgress
|
|
5999
|
+
? "Exporting…"
|
|
6000
|
+
: (exportingReplJournal ? "Export REPL journal" : "Export right preview");
|
|
4818
6001
|
if (rightView === "trace") {
|
|
4819
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.";
|
|
4820
6005
|
} else if (rightView === "markdown") {
|
|
4821
|
-
exportPdfBtn.title = "Switch right pane to Response (Preview)
|
|
6006
|
+
exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL journal to export.";
|
|
4822
6007
|
} else if (!canExportPreview) {
|
|
4823
6008
|
exportPdfBtn.title = "Nothing to export yet.";
|
|
4824
6009
|
} else if (isHtmlArtifactPreview) {
|
|
4825
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.";
|
|
4826
6013
|
} else {
|
|
4827
6014
|
exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
|
|
4828
6015
|
}
|
|
@@ -4831,18 +6018,20 @@
|
|
|
4831
6018
|
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
4832
6019
|
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
4833
6020
|
? "Interactive HTML preview PDF export is not available yet."
|
|
4834
|
-
: "Export the current right-pane preview as PDF.";
|
|
6021
|
+
: (exportingReplJournal ? "Export the REPL journal as PDF." : "Export the current right-pane preview as PDF.");
|
|
4835
6022
|
}
|
|
4836
6023
|
if (exportPreviewHtmlBtn) {
|
|
4837
6024
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
4838
6025
|
exportPreviewHtmlBtn.title = isHtmlArtifactPreview
|
|
4839
6026
|
? "Export the authored HTML preview."
|
|
4840
|
-
: "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.");
|
|
4841
6028
|
}
|
|
4842
6029
|
if (exportPreviewControlsEl) {
|
|
4843
6030
|
exportPreviewControlsEl.title = canExportPreview
|
|
4844
|
-
? (
|
|
4845
|
-
|
|
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.");
|
|
4846
6035
|
}
|
|
4847
6036
|
if (!canExportPreview || previewExportInProgress) {
|
|
4848
6037
|
closeExportPreviewMenu();
|
|
@@ -5032,6 +6221,108 @@
|
|
|
5032
6221
|
updateOutlineUi();
|
|
5033
6222
|
}
|
|
5034
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
|
+
|
|
5035
6326
|
function setEditorView(nextView) {
|
|
5036
6327
|
editorView = nextView === "preview" ? "preview" : "markdown";
|
|
5037
6328
|
editorViewSelect.value = editorView;
|
|
@@ -5073,11 +6364,19 @@
|
|
|
5073
6364
|
? "preview"
|
|
5074
6365
|
: (nextView === "editor-preview"
|
|
5075
6366
|
? "editor-preview"
|
|
5076
|
-
: (
|
|
6367
|
+
: (nextView === "repl"
|
|
6368
|
+
? "repl"
|
|
6369
|
+
: ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown")));
|
|
5077
6370
|
rightViewSelect.value = rightView;
|
|
5078
6371
|
if (rightView === "trace" && previousView !== "trace") {
|
|
5079
6372
|
traceAutoScroll = true;
|
|
5080
6373
|
}
|
|
6374
|
+
if (rightView === "repl" && previousView !== "repl") {
|
|
6375
|
+
replFollow = true;
|
|
6376
|
+
startReplPolling();
|
|
6377
|
+
} else if (rightView !== "repl" && previousView === "repl") {
|
|
6378
|
+
stopReplPolling();
|
|
6379
|
+
}
|
|
5081
6380
|
|
|
5082
6381
|
if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
|
|
5083
6382
|
window.clearTimeout(responseEditorPreviewTimer);
|
|
@@ -10525,6 +11824,14 @@
|
|
|
10525
11824
|
queueSteerBtn.classList.remove("request-stop-active");
|
|
10526
11825
|
queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
|
|
10527
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
|
+
}
|
|
10528
11835
|
if (critiqueBtn) {
|
|
10529
11836
|
critiqueBtn.textContent = "Critique text";
|
|
10530
11837
|
critiqueBtn.classList.remove("request-stop-active");
|
|
@@ -10539,11 +11846,14 @@
|
|
|
10539
11846
|
sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
|
|
10540
11847
|
sendRunBtn.classList.toggle("request-stop-active", directIsStop);
|
|
10541
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
|
+
: "";
|
|
10542
11852
|
sendRunBtn.title = directIsStop
|
|
10543
11853
|
? "Stop the active run. Shortcut: Esc."
|
|
10544
11854
|
: (annotationsEnabled
|
|
10545
|
-
? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc."
|
|
10546
|
-
: "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);
|
|
10547
11857
|
}
|
|
10548
11858
|
|
|
10549
11859
|
if (queueSteerBtn) {
|
|
@@ -10557,6 +11867,25 @@
|
|
|
10557
11867
|
: "Queue steering is available while Run editor text is active.";
|
|
10558
11868
|
}
|
|
10559
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
|
+
|
|
10560
11889
|
if (critiqueBtn) {
|
|
10561
11890
|
critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique text";
|
|
10562
11891
|
critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
|
|
@@ -10871,6 +12200,63 @@
|
|
|
10871
12200
|
return;
|
|
10872
12201
|
}
|
|
10873
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
|
+
|
|
10874
12260
|
if (message.type === "request_started") {
|
|
10875
12261
|
pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
10876
12262
|
pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
|
|
@@ -11271,6 +12657,14 @@
|
|
|
11271
12657
|
if (typeof message.requestId === "string") {
|
|
11272
12658
|
clearArmedTitleAttention(message.requestId);
|
|
11273
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
|
+
}
|
|
11274
12668
|
stickyStudioKind = null;
|
|
11275
12669
|
setBusy(false);
|
|
11276
12670
|
setWsState("Ready");
|
|
@@ -11823,6 +13217,8 @@
|
|
|
11823
13217
|
requestLatestResponse();
|
|
11824
13218
|
});
|
|
11825
13219
|
|
|
13220
|
+
sourceTextEl.addEventListener("keydown", handleSourceTextTabKey);
|
|
13221
|
+
|
|
11826
13222
|
sourceTextEl.addEventListener("input", () => {
|
|
11827
13223
|
if (activePreviewCommentSelection) {
|
|
11828
13224
|
clearPreviewCommentSelection();
|
|
@@ -12194,7 +13590,7 @@
|
|
|
12194
13590
|
return;
|
|
12195
13591
|
}
|
|
12196
13592
|
|
|
12197
|
-
const prepared =
|
|
13593
|
+
const prepared = prepareEditorTextForRunRequest(sourceTextEl.value);
|
|
12198
13594
|
if (!prepared.trim()) {
|
|
12199
13595
|
setStatus("Editor is empty. Nothing to run.", "warning");
|
|
12200
13596
|
return;
|
|
@@ -12218,7 +13614,7 @@
|
|
|
12218
13614
|
|
|
12219
13615
|
if (queueSteerBtn) {
|
|
12220
13616
|
queueSteerBtn.addEventListener("click", () => {
|
|
12221
|
-
const prepared =
|
|
13617
|
+
const prepared = prepareEditorTextForRunRequest(sourceTextEl.value);
|
|
12222
13618
|
if (!prepared.trim()) {
|
|
12223
13619
|
setStatus("Editor is empty. Nothing to queue.", "warning");
|
|
12224
13620
|
return;
|
|
@@ -12240,6 +13636,20 @@
|
|
|
12240
13636
|
});
|
|
12241
13637
|
}
|
|
12242
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
|
+
|
|
12243
13653
|
copyDraftBtn.addEventListener("click", async () => {
|
|
12244
13654
|
const content = sourceTextEl.value;
|
|
12245
13655
|
if (!content.trim()) {
|
|
@@ -12658,6 +14068,7 @@
|
|
|
12658
14068
|
const storedAnnotationsEnabled = readStoredAnnotationsEnabled();
|
|
12659
14069
|
const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
|
|
12660
14070
|
setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
|
|
14071
|
+
setReplSendMode(replSendMode);
|
|
12661
14072
|
|
|
12662
14073
|
setEditorView(editorView);
|
|
12663
14074
|
setRightView(rightView);
|