pi-studio 0.9.10 → 0.9.12
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 +22 -0
- package/README.md +1 -1
- package/client/studio-client.js +981 -70
- package/client/studio.css +224 -32
- package/index.ts +505 -32
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
const editorSelectionJumpBtn = document.getElementById("editorSelectionJumpBtn");
|
|
62
62
|
const leftPaneEl = document.getElementById("leftPane");
|
|
63
63
|
const rightPaneEl = document.getElementById("rightPane");
|
|
64
|
+
const paneResizeHandleEl = document.getElementById("paneResizeHandle");
|
|
64
65
|
const sourceBadgeEl = document.getElementById("sourceBadge");
|
|
65
66
|
const syncBadgeEl = document.getElementById("syncBadge");
|
|
66
67
|
let critiqueViewEl = document.getElementById("critiqueView");
|
|
@@ -245,6 +246,8 @@
|
|
|
245
246
|
const REPL_JOURNAL_MAX_ENTRIES = 80;
|
|
246
247
|
const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
|
|
247
248
|
const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
|
|
249
|
+
const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
|
|
250
|
+
const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
248
251
|
const EDITOR_TAB_TEXT = " ";
|
|
249
252
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
250
253
|
const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
|
|
@@ -287,6 +290,15 @@
|
|
|
287
290
|
return "python";
|
|
288
291
|
}
|
|
289
292
|
})();
|
|
293
|
+
let replCommandOverrides = (() => {
|
|
294
|
+
try {
|
|
295
|
+
const raw = window.localStorage && window.localStorage.getItem("piStudio.replCommandOverrides");
|
|
296
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
297
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
298
|
+
} catch {
|
|
299
|
+
return {};
|
|
300
|
+
}
|
|
301
|
+
})();
|
|
290
302
|
let replTranscript = "";
|
|
291
303
|
let replError = "";
|
|
292
304
|
let replMessage = "";
|
|
@@ -951,11 +963,26 @@
|
|
|
951
963
|
setStatus("Loaded visible working into editor.", "success");
|
|
952
964
|
}
|
|
953
965
|
|
|
954
|
-
function
|
|
966
|
+
function getKnownReplRuntime(value) {
|
|
955
967
|
const runtime = String(value || "").trim().toLowerCase();
|
|
956
968
|
return runtime === "shell" || runtime === "python" || runtime === "ipython" || runtime === "julia" || runtime === "r" || runtime === "ghci" || runtime === "clojure"
|
|
957
969
|
? runtime
|
|
958
|
-
: "
|
|
970
|
+
: "";
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function normalizeReplRuntime(value) {
|
|
974
|
+
return getKnownReplRuntime(value) || "python";
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function getReplRuntimeLabel(value) {
|
|
978
|
+
const runtime = normalizeReplRuntime(value);
|
|
979
|
+
if (runtime === "shell") return "Shell";
|
|
980
|
+
if (runtime === "python") return "Python";
|
|
981
|
+
if (runtime === "ipython") return "IPython";
|
|
982
|
+
if (runtime === "julia") return "Julia";
|
|
983
|
+
if (runtime === "r") return "R";
|
|
984
|
+
if (runtime === "ghci") return "GHCi";
|
|
985
|
+
return "Clojure";
|
|
959
986
|
}
|
|
960
987
|
|
|
961
988
|
function normalizeReplSession(session) {
|
|
@@ -980,6 +1007,47 @@
|
|
|
980
1007
|
}
|
|
981
1008
|
}
|
|
982
1009
|
|
|
1010
|
+
function normalizeReplCommandOverride(value) {
|
|
1011
|
+
return String(value || "").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function getReplCommandOverride(runtime) {
|
|
1015
|
+
const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
|
|
1016
|
+
const value = replCommandOverrides && typeof replCommandOverrides === "object"
|
|
1017
|
+
? replCommandOverrides[normalizedRuntime]
|
|
1018
|
+
: "";
|
|
1019
|
+
return normalizeReplCommandOverride(value);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function persistReplCommandOverrides() {
|
|
1023
|
+
try {
|
|
1024
|
+
if (window.localStorage) window.localStorage.setItem("piStudio.replCommandOverrides", JSON.stringify(replCommandOverrides || {}));
|
|
1025
|
+
} catch {
|
|
1026
|
+
// Ignore storage failures.
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function setReplCommandOverride(runtime, command) {
|
|
1031
|
+
const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
|
|
1032
|
+
const normalizedCommand = normalizeReplCommandOverride(command);
|
|
1033
|
+
replCommandOverrides = replCommandOverrides && typeof replCommandOverrides === "object" ? replCommandOverrides : {};
|
|
1034
|
+
if (normalizedCommand) {
|
|
1035
|
+
replCommandOverrides[normalizedRuntime] = normalizedCommand;
|
|
1036
|
+
} else {
|
|
1037
|
+
delete replCommandOverrides[normalizedRuntime];
|
|
1038
|
+
}
|
|
1039
|
+
persistReplCommandOverrides();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function getCurrentReplStartCommandFromDom() {
|
|
1043
|
+
if (!critiqueViewEl || typeof critiqueViewEl.querySelector !== "function") return getReplCommandOverride(replRuntime);
|
|
1044
|
+
const inputEl = critiqueViewEl.querySelector("[data-repl-command]");
|
|
1045
|
+
if (inputEl && "value" in inputEl) {
|
|
1046
|
+
return normalizeReplCommandOverride(inputEl.value);
|
|
1047
|
+
}
|
|
1048
|
+
return getReplCommandOverride(replRuntime);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
983
1051
|
function normalizeReplSendMode(value) {
|
|
984
1052
|
return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
|
|
985
1053
|
}
|
|
@@ -1055,7 +1123,10 @@
|
|
|
1055
1123
|
? sessions.map(normalizeReplSession).filter(Boolean)
|
|
1056
1124
|
: [];
|
|
1057
1125
|
if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
|
|
1058
|
-
|
|
1126
|
+
setActiveReplSession("");
|
|
1127
|
+
}
|
|
1128
|
+
if (!getActiveReplSessionForCurrentRuntime()) {
|
|
1129
|
+
selectReplSessionForRuntime(replRuntime, replActiveSessionName);
|
|
1059
1130
|
}
|
|
1060
1131
|
return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
|
|
1061
1132
|
}
|
|
@@ -1064,9 +1135,43 @@
|
|
|
1064
1135
|
return replSessions.find((session) => session.sessionName === replActiveSessionName) || null;
|
|
1065
1136
|
}
|
|
1066
1137
|
|
|
1138
|
+
function isReplSessionRuntimeCompatible(session, runtime) {
|
|
1139
|
+
if (!session) return false;
|
|
1140
|
+
const sessionRuntime = getKnownReplRuntime(session.runtime);
|
|
1141
|
+
return Boolean(sessionRuntime) && sessionRuntime === normalizeReplRuntime(runtime);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function getReplSessionsForRuntime(runtime) {
|
|
1145
|
+
const normalizedRuntime = normalizeReplRuntime(runtime);
|
|
1146
|
+
return replSessions.filter((session) => isReplSessionRuntimeCompatible(session, normalizedRuntime));
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function selectReplSessionForRuntime(runtime, preferredSessionName) {
|
|
1150
|
+
const sessions = getReplSessionsForRuntime(runtime);
|
|
1151
|
+
const preferred = String(preferredSessionName || "").trim();
|
|
1152
|
+
const selected = sessions.find((session) => session.sessionName === preferred) || sessions[0] || null;
|
|
1153
|
+
setActiveReplSession(selected ? selected.sessionName : "");
|
|
1154
|
+
return selected;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function setActiveReplSessionForCurrentRuntime(sessionName) {
|
|
1158
|
+
const name = String(sessionName || "").trim();
|
|
1159
|
+
const candidate = name ? replSessions.find((session) => session.sessionName === name) : null;
|
|
1160
|
+
if (candidate && isReplSessionRuntimeCompatible(candidate, replRuntime)) {
|
|
1161
|
+
setActiveReplSession(candidate.sessionName);
|
|
1162
|
+
return candidate;
|
|
1163
|
+
}
|
|
1164
|
+
return selectReplSessionForRuntime(replRuntime, replActiveSessionName);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function getActiveReplSessionForCurrentRuntime() {
|
|
1168
|
+
const session = getActiveReplSession();
|
|
1169
|
+
return isReplSessionRuntimeCompatible(session, replRuntime) ? session : null;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1067
1172
|
function buildActiveReplPromptContext() {
|
|
1068
1173
|
if (rightView !== "repl") return "";
|
|
1069
|
-
const session =
|
|
1174
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1070
1175
|
if (!session) return "";
|
|
1071
1176
|
const runtime = session.runtime && session.runtime !== "unknown" ? session.runtime : "unknown";
|
|
1072
1177
|
return [
|
|
@@ -1161,7 +1266,7 @@
|
|
|
1161
1266
|
}
|
|
1162
1267
|
|
|
1163
1268
|
function getActiveReplRuntime() {
|
|
1164
|
-
const session =
|
|
1269
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1165
1270
|
if (session && session.runtime && session.runtime !== "unknown") return normalizeReplRuntime(session.runtime);
|
|
1166
1271
|
return normalizeReplRuntime(replRuntime);
|
|
1167
1272
|
}
|
|
@@ -1501,7 +1606,7 @@
|
|
|
1501
1606
|
|
|
1502
1607
|
function stripStudioReplSubmissionEcho(delta) {
|
|
1503
1608
|
let value = String(delta || "").replace(/^\s+/, "");
|
|
1504
|
-
// The raw mirror below remains raw;
|
|
1609
|
+
// The raw mirror below remains raw; Studio record cards hide only the
|
|
1505
1610
|
// temp-file wrapper used to submit multiline snippets safely. The
|
|
1506
1611
|
// pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
|
|
1507
1612
|
const submissionEchoPatterns = [
|
|
@@ -1565,11 +1670,11 @@
|
|
|
1565
1670
|
function buildReplJournalMarkdown(entries) {
|
|
1566
1671
|
const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
|
|
1567
1672
|
const sessionName = getActiveReplJournalSessionName();
|
|
1568
|
-
const lines = ["# REPL
|
|
1673
|
+
const lines = ["# Studio REPL Record", "", "Generated: " + new Date().toLocaleString()];
|
|
1569
1674
|
if (sessionName) lines.push("Session: `" + sessionName + "`");
|
|
1570
1675
|
lines.push("");
|
|
1571
1676
|
if (!visibleEntries.length) {
|
|
1572
|
-
lines.push(sessionName ? ("_No REPL
|
|
1677
|
+
lines.push(sessionName ? ("_No Studio REPL record entries for `" + sessionName + "` yet._") : "_No Studio REPL record entries yet._");
|
|
1573
1678
|
return lines.join("\n");
|
|
1574
1679
|
}
|
|
1575
1680
|
visibleEntries.forEach((entry, index) => {
|
|
@@ -1601,11 +1706,11 @@
|
|
|
1601
1706
|
async function copyReplJournalToClipboard() {
|
|
1602
1707
|
const entries = getVisibleReplJournalEntries();
|
|
1603
1708
|
if (!entries.length) {
|
|
1604
|
-
setStatus("No REPL
|
|
1709
|
+
setStatus("No Studio REPL record entries to copy for this session yet.", "warning");
|
|
1605
1710
|
return;
|
|
1606
1711
|
}
|
|
1607
1712
|
if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
|
|
1608
|
-
setStatus("Copied REPL
|
|
1713
|
+
setStatus("Copied Studio REPL record as Markdown.", "success");
|
|
1609
1714
|
} else {
|
|
1610
1715
|
setStatus("Clipboard write failed.", "warning");
|
|
1611
1716
|
}
|
|
@@ -1614,7 +1719,7 @@
|
|
|
1614
1719
|
function exportReplJournalMarkdown() {
|
|
1615
1720
|
const entries = getVisibleReplJournalEntries();
|
|
1616
1721
|
if (!entries.length) {
|
|
1617
|
-
setStatus("No REPL
|
|
1722
|
+
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
1618
1723
|
return;
|
|
1619
1724
|
}
|
|
1620
1725
|
const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
|
|
@@ -1628,7 +1733,7 @@
|
|
|
1628
1733
|
link.click();
|
|
1629
1734
|
link.remove();
|
|
1630
1735
|
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
|
1631
|
-
setStatus("Exported REPL
|
|
1736
|
+
setStatus("Exported Studio REPL record Markdown.", "success");
|
|
1632
1737
|
}
|
|
1633
1738
|
|
|
1634
1739
|
function clearReplJournal() {
|
|
@@ -1640,27 +1745,27 @@
|
|
|
1640
1745
|
}
|
|
1641
1746
|
activeReplJournalEntryId = "";
|
|
1642
1747
|
persistReplJournalEntries();
|
|
1643
|
-
setStatus(sessionName ? "Cleared REPL
|
|
1748
|
+
setStatus(sessionName ? "Cleared Studio REPL record for this session." : "Cleared Studio REPL record.", "success");
|
|
1644
1749
|
renderReplViewIfActive({ force: true });
|
|
1645
1750
|
}
|
|
1646
1751
|
|
|
1647
1752
|
function loadReplJournalIntoEditor() {
|
|
1648
1753
|
const entries = getVisibleReplJournalEntries();
|
|
1649
1754
|
if (!entries.length) {
|
|
1650
|
-
setStatus("No REPL
|
|
1755
|
+
setStatus("No Studio REPL record entries to load for this session yet.", "warning");
|
|
1651
1756
|
return;
|
|
1652
1757
|
}
|
|
1653
1758
|
const markdown = buildReplJournalMarkdown(entries);
|
|
1654
1759
|
setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
|
|
1655
|
-
setSourceState({ source: "blank", label: "REPL
|
|
1760
|
+
setSourceState({ source: "blank", label: "Studio REPL Record", path: null });
|
|
1656
1761
|
setEditorLanguage("markdown");
|
|
1657
|
-
setStatus("Loaded REPL
|
|
1762
|
+
setStatus("Loaded Studio REPL record into editor.", "success");
|
|
1658
1763
|
}
|
|
1659
1764
|
|
|
1660
1765
|
function addSelectedReplJournalNote() {
|
|
1661
1766
|
const note = getSelectedOrCurrentParagraphForReplNote();
|
|
1662
1767
|
if (!note.trim()) {
|
|
1663
|
-
setStatus("Select prose or place the cursor in a paragraph to add a REPL
|
|
1768
|
+
setStatus("Select prose or place the cursor in a paragraph to add a Studio REPL record note.", "warning");
|
|
1664
1769
|
return;
|
|
1665
1770
|
}
|
|
1666
1771
|
addReplJournalEntry({
|
|
@@ -1671,14 +1776,14 @@
|
|
|
1671
1776
|
sessionName: replActiveSessionName,
|
|
1672
1777
|
runtime: getActiveReplRuntime(),
|
|
1673
1778
|
});
|
|
1674
|
-
setStatus("Added note to REPL
|
|
1779
|
+
setStatus("Added note to Studio REPL record.", "success");
|
|
1675
1780
|
renderReplViewIfActive({ force: true });
|
|
1676
1781
|
}
|
|
1677
1782
|
|
|
1678
1783
|
function sendReplPayload(payload) {
|
|
1679
|
-
const session =
|
|
1784
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1680
1785
|
if (!session) {
|
|
1681
|
-
setStatus("Start or select a REPL session first.", "warning");
|
|
1786
|
+
setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
|
|
1682
1787
|
return;
|
|
1683
1788
|
}
|
|
1684
1789
|
if (!payload || payload.error) {
|
|
@@ -1696,7 +1801,7 @@
|
|
|
1696
1801
|
runtime: getActiveReplRuntime(),
|
|
1697
1802
|
skippedChunks: payload.skippedChunks,
|
|
1698
1803
|
});
|
|
1699
|
-
setStatus("Added prose to REPL
|
|
1804
|
+
setStatus("Added prose to Studio REPL record.", "success");
|
|
1700
1805
|
renderReplViewIfActive({ force: true });
|
|
1701
1806
|
} else {
|
|
1702
1807
|
setStatus("No code or prose found to send.", "warning");
|
|
@@ -1770,6 +1875,11 @@
|
|
|
1770
1875
|
let fileBackedBaselineText = null;
|
|
1771
1876
|
let activePane = "left";
|
|
1772
1877
|
let paneFocusTarget = "off";
|
|
1878
|
+
let paneSplitPercent = 50;
|
|
1879
|
+
const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
|
|
1880
|
+
const PANE_SPLIT_MIN_PERCENT = 20;
|
|
1881
|
+
const PANE_SPLIT_MAX_PERCENT = 80;
|
|
1882
|
+
const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
|
|
1773
1883
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
|
|
1774
1884
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
1775
1885
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
@@ -2430,7 +2540,7 @@
|
|
|
2430
2540
|
|
|
2431
2541
|
function getTerminalBusyStatus() {
|
|
2432
2542
|
if (terminalActivityPhase === "tool") {
|
|
2433
|
-
if (terminalActivityLabel) {
|
|
2543
|
+
if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
|
|
2434
2544
|
return "Terminal: " + withEllipsis(terminalActivityLabel);
|
|
2435
2545
|
}
|
|
2436
2546
|
return terminalActivityToolName
|
|
@@ -2474,7 +2584,7 @@
|
|
|
2474
2584
|
const action = getStudioActionLabel(kind);
|
|
2475
2585
|
const queueSuffix = studioRunChainActive ? formatQueuedSteeringSuffix() : "";
|
|
2476
2586
|
if (terminalActivityPhase === "tool") {
|
|
2477
|
-
if (terminalActivityLabel) {
|
|
2587
|
+
if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
|
|
2478
2588
|
return "Studio: " + withEllipsis(terminalActivityLabel) + queueSuffix;
|
|
2479
2589
|
}
|
|
2480
2590
|
return terminalActivityToolName
|
|
@@ -3001,6 +3111,142 @@
|
|
|
3001
3111
|
setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
|
|
3002
3112
|
}
|
|
3003
3113
|
|
|
3114
|
+
function clampPaneSplitPercent(value) {
|
|
3115
|
+
const numeric = Number(value);
|
|
3116
|
+
if (!Number.isFinite(numeric)) return 50;
|
|
3117
|
+
const clamped = Math.max(PANE_SPLIT_MIN_PERCENT, Math.min(PANE_SPLIT_MAX_PERCENT, Math.round(numeric * 10) / 10));
|
|
3118
|
+
return Math.abs(clamped - 50) <= PANE_SPLIT_SNAP_TO_CENTER_PERCENT ? 50 : clamped;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
function applyPaneSplitPercent(percent, options) {
|
|
3122
|
+
paneSplitPercent = clampPaneSplitPercent(percent);
|
|
3123
|
+
const rightPercent = Math.round((100 - paneSplitPercent) * 10) / 10;
|
|
3124
|
+
document.documentElement.style.setProperty("--studio-left-pane-fr", paneSplitPercent + "fr");
|
|
3125
|
+
document.documentElement.style.setProperty("--studio-right-pane-fr", rightPercent + "fr");
|
|
3126
|
+
if (paneResizeHandleEl) {
|
|
3127
|
+
paneResizeHandleEl.setAttribute("aria-valuemin", String(PANE_SPLIT_MIN_PERCENT));
|
|
3128
|
+
paneResizeHandleEl.setAttribute("aria-valuemax", String(PANE_SPLIT_MAX_PERCENT));
|
|
3129
|
+
paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(paneSplitPercent)));
|
|
3130
|
+
paneResizeHandleEl.setAttribute("aria-valuetext", "Editor " + Math.round(paneSplitPercent) + " percent, response " + Math.round(rightPercent) + " percent");
|
|
3131
|
+
}
|
|
3132
|
+
if (!options || options.persist !== false) {
|
|
3133
|
+
try {
|
|
3134
|
+
if (window.localStorage) window.localStorage.setItem(PANE_SPLIT_STORAGE_KEY, String(paneSplitPercent));
|
|
3135
|
+
} catch {
|
|
3136
|
+
// Ignore localStorage failures.
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
function resetPaneSplitPercent() {
|
|
3142
|
+
applyPaneSplitPercent(50);
|
|
3143
|
+
setStatus("Pane split reset to 50/50.");
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function loadPaneSplitPercent() {
|
|
3147
|
+
if (isEditorOnlyMode) return;
|
|
3148
|
+
let stored = "";
|
|
3149
|
+
try {
|
|
3150
|
+
stored = window.localStorage ? String(window.localStorage.getItem(PANE_SPLIT_STORAGE_KEY) || "") : "";
|
|
3151
|
+
} catch {
|
|
3152
|
+
stored = "";
|
|
3153
|
+
}
|
|
3154
|
+
applyPaneSplitPercent(stored ? Number(stored) : 50, { persist: false });
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
function getPaneSplitPercentFromPointerEvent(event) {
|
|
3158
|
+
const mainEl = paneResizeHandleEl && typeof paneResizeHandleEl.closest === "function"
|
|
3159
|
+
? paneResizeHandleEl.closest("main")
|
|
3160
|
+
: null;
|
|
3161
|
+
if (!mainEl || typeof mainEl.getBoundingClientRect !== "function") return paneSplitPercent;
|
|
3162
|
+
const rect = mainEl.getBoundingClientRect();
|
|
3163
|
+
if (!rect.width) return paneSplitPercent;
|
|
3164
|
+
const x = typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2);
|
|
3165
|
+
return ((x - rect.left) / rect.width) * 100;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
function setupPaneResizeHandle() {
|
|
3169
|
+
if (!paneResizeHandleEl || isEditorOnlyMode) return;
|
|
3170
|
+
loadPaneSplitPercent();
|
|
3171
|
+
let dragging = false;
|
|
3172
|
+
let movedDuringDrag = false;
|
|
3173
|
+
let pointerStartX = 0;
|
|
3174
|
+
let activePaneResizePointerId = null;
|
|
3175
|
+
const finishDrag = () => {
|
|
3176
|
+
if (!dragging) return;
|
|
3177
|
+
dragging = false;
|
|
3178
|
+
if (document.body && document.body.classList) document.body.classList.remove("pane-resizing");
|
|
3179
|
+
try {
|
|
3180
|
+
if (typeof paneResizeHandleEl.releasePointerCapture === "function" && activePaneResizePointerId != null) {
|
|
3181
|
+
paneResizeHandleEl.releasePointerCapture(activePaneResizePointerId);
|
|
3182
|
+
}
|
|
3183
|
+
} catch {
|
|
3184
|
+
// Ignore pointer-capture cleanup failures.
|
|
3185
|
+
}
|
|
3186
|
+
activePaneResizePointerId = null;
|
|
3187
|
+
if (movedDuringDrag) {
|
|
3188
|
+
setStatus("Pane split: editor " + Math.round(paneSplitPercent) + "%, response " + Math.round(100 - paneSplitPercent) + "%.");
|
|
3189
|
+
}
|
|
3190
|
+
movedDuringDrag = false;
|
|
3191
|
+
};
|
|
3192
|
+
paneResizeHandleEl.addEventListener("pointerdown", (event) => {
|
|
3193
|
+
if (event.button != null && event.button !== 0) return;
|
|
3194
|
+
event.preventDefault();
|
|
3195
|
+
event.stopPropagation();
|
|
3196
|
+
dragging = true;
|
|
3197
|
+
movedDuringDrag = false;
|
|
3198
|
+
pointerStartX = typeof event.clientX === "number" ? event.clientX : 0;
|
|
3199
|
+
activePaneResizePointerId = event.pointerId;
|
|
3200
|
+
if (document.body && document.body.classList) document.body.classList.add("pane-resizing");
|
|
3201
|
+
try {
|
|
3202
|
+
if (typeof paneResizeHandleEl.focus === "function") paneResizeHandleEl.focus({ preventScroll: true });
|
|
3203
|
+
} catch {
|
|
3204
|
+
try { paneResizeHandleEl.focus(); } catch {}
|
|
3205
|
+
}
|
|
3206
|
+
try {
|
|
3207
|
+
if (typeof paneResizeHandleEl.setPointerCapture === "function") paneResizeHandleEl.setPointerCapture(event.pointerId);
|
|
3208
|
+
} catch {
|
|
3209
|
+
// Ignore pointer-capture failures.
|
|
3210
|
+
}
|
|
3211
|
+
});
|
|
3212
|
+
paneResizeHandleEl.addEventListener("pointermove", (event) => {
|
|
3213
|
+
if (!dragging) return;
|
|
3214
|
+
const movement = typeof event.clientX === "number" ? Math.abs(event.clientX - pointerStartX) : 0;
|
|
3215
|
+
if (!movedDuringDrag && movement < 3) return;
|
|
3216
|
+
movedDuringDrag = true;
|
|
3217
|
+
event.preventDefault();
|
|
3218
|
+
applyPaneSplitPercent(getPaneSplitPercentFromPointerEvent(event));
|
|
3219
|
+
});
|
|
3220
|
+
paneResizeHandleEl.addEventListener("pointerup", finishDrag);
|
|
3221
|
+
paneResizeHandleEl.addEventListener("pointercancel", finishDrag);
|
|
3222
|
+
paneResizeHandleEl.addEventListener("dblclick", (event) => {
|
|
3223
|
+
event.preventDefault();
|
|
3224
|
+
event.stopPropagation();
|
|
3225
|
+
resetPaneSplitPercent();
|
|
3226
|
+
});
|
|
3227
|
+
paneResizeHandleEl.addEventListener("keydown", (event) => {
|
|
3228
|
+
if (event.key === "Home") {
|
|
3229
|
+
event.preventDefault();
|
|
3230
|
+
applyPaneSplitPercent(PANE_SPLIT_MIN_PERCENT);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
if (event.key === "End") {
|
|
3234
|
+
event.preventDefault();
|
|
3235
|
+
applyPaneSplitPercent(PANE_SPLIT_MAX_PERCENT);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
3239
|
+
event.preventDefault();
|
|
3240
|
+
resetPaneSplitPercent();
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
3244
|
+
event.preventDefault();
|
|
3245
|
+
const step = event.shiftKey ? 10 : 5;
|
|
3246
|
+
applyPaneSplitPercent(paneSplitPercent + (event.key === "ArrowLeft" ? -step : step));
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3004
3250
|
function updatePaneFocusButtons() {
|
|
3005
3251
|
[
|
|
3006
3252
|
[leftFocusBtn, "left"],
|
|
@@ -3239,6 +3485,40 @@
|
|
|
3239
3485
|
return false;
|
|
3240
3486
|
}
|
|
3241
3487
|
|
|
3488
|
+
function triggerResponseHistoryShortcut(action) {
|
|
3489
|
+
if (isEditorOnlyMode) {
|
|
3490
|
+
setStatus("Response history is unavailable in editor-only Studio.", "warning");
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3494
|
+
if (total <= 0) {
|
|
3495
|
+
setStatus("No response history available yet.", "warning");
|
|
3496
|
+
return false;
|
|
3497
|
+
}
|
|
3498
|
+
if (action === "previous") {
|
|
3499
|
+
if (responseHistoryIndex <= 0) {
|
|
3500
|
+
setStatus("Already at the first response.", "warning");
|
|
3501
|
+
return false;
|
|
3502
|
+
}
|
|
3503
|
+
return selectHistoryIndex(responseHistoryIndex - 1);
|
|
3504
|
+
}
|
|
3505
|
+
if (action === "next") {
|
|
3506
|
+
if (responseHistoryIndex >= total - 1) {
|
|
3507
|
+
setStatus("Already at the latest response.", "warning");
|
|
3508
|
+
return false;
|
|
3509
|
+
}
|
|
3510
|
+
return selectHistoryIndex(responseHistoryIndex + 1);
|
|
3511
|
+
}
|
|
3512
|
+
if (action === "latest") {
|
|
3513
|
+
if (responseHistoryIndex >= total - 1) {
|
|
3514
|
+
setStatus("Already viewing the latest response.");
|
|
3515
|
+
return false;
|
|
3516
|
+
}
|
|
3517
|
+
return selectHistoryIndex(total - 1);
|
|
3518
|
+
}
|
|
3519
|
+
return false;
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3242
3522
|
function isTextEntryShortcutTarget(target) {
|
|
3243
3523
|
if (!(target instanceof Element)) return false;
|
|
3244
3524
|
const editable = target.closest("input, textarea, select, [contenteditable]");
|
|
@@ -3254,6 +3534,7 @@
|
|
|
3254
3534
|
if (!event || event.defaultPrevented) return;
|
|
3255
3535
|
|
|
3256
3536
|
const key = typeof event.key === "string" ? event.key : "";
|
|
3537
|
+
const code = typeof event.code === "string" ? event.code : "";
|
|
3257
3538
|
const plainEscape = key === "Escape"
|
|
3258
3539
|
&& !event.metaKey
|
|
3259
3540
|
&& !event.ctrlKey
|
|
@@ -3354,6 +3635,24 @@
|
|
|
3354
3635
|
return;
|
|
3355
3636
|
}
|
|
3356
3637
|
|
|
3638
|
+
if (!isTextEntryShortcutTarget(event.target) && !event.metaKey && !event.ctrlKey && event.altKey && !event.shiftKey) {
|
|
3639
|
+
if (key === "ArrowLeft") {
|
|
3640
|
+
event.preventDefault();
|
|
3641
|
+
triggerResponseHistoryShortcut("previous");
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
if (key === "ArrowRight") {
|
|
3645
|
+
event.preventDefault();
|
|
3646
|
+
triggerResponseHistoryShortcut("next");
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
if (key.toLowerCase() === "l" || code === "KeyL") {
|
|
3650
|
+
event.preventDefault();
|
|
3651
|
+
triggerResponseHistoryShortcut("latest");
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3357
3656
|
const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3358
3657
|
if (isPaneSwitchShortcut) {
|
|
3359
3658
|
event.preventDefault();
|
|
@@ -3968,6 +4267,11 @@
|
|
|
3968
4267
|
+ " root ? root.offsetHeight : 0\n"
|
|
3969
4268
|
+ " ));\n"
|
|
3970
4269
|
+ " }\n"
|
|
4270
|
+
+ " function getScrollTop() {\n"
|
|
4271
|
+
+ " const body = document.body;\n"
|
|
4272
|
+
+ " const root = document.documentElement;\n"
|
|
4273
|
+
+ " return window.scrollY || (root ? root.scrollTop : 0) || (body ? body.scrollTop : 0) || 0;\n"
|
|
4274
|
+
+ " }\n"
|
|
3971
4275
|
+ " function sendHeight() {\n"
|
|
3972
4276
|
+ " scheduled = false;\n"
|
|
3973
4277
|
+ " const height = measureHeight();\n"
|
|
@@ -3980,13 +4284,301 @@
|
|
|
3980
4284
|
+ " scheduled = true;\n"
|
|
3981
4285
|
+ " requestAnimationFrame(sendHeight);\n"
|
|
3982
4286
|
+ " }\n"
|
|
4287
|
+
+ " function decodeFragment(value) {\n"
|
|
4288
|
+
+ " const text = String(value || '').replace(/^#/, '');\n"
|
|
4289
|
+
+ " try { return decodeURIComponent(text); } catch { return text; }\n"
|
|
4290
|
+
+ " }\n"
|
|
4291
|
+
+ " function findNamedFragmentTarget(fragment) {\n"
|
|
4292
|
+
+ " const decoded = decodeFragment(fragment);\n"
|
|
4293
|
+
+ " if (!decoded) return document.documentElement || document.body;\n"
|
|
4294
|
+
+ " return document.getElementById(decoded) || document.getElementsByName(decoded)[0] || null;\n"
|
|
4295
|
+
+ " }\n"
|
|
4296
|
+
+ " function postFragmentScroll(target) {\n"
|
|
4297
|
+
+ " if (!target || typeof target.getBoundingClientRect !== 'function') return;\n"
|
|
4298
|
+
+ " const rect = target.getBoundingClientRect();\n"
|
|
4299
|
+
+ " const scrollTop = getScrollTop();\n"
|
|
4300
|
+
+ " try {\n"
|
|
4301
|
+
+ " parent.postMessage({ type: 'pi-studio-html-artifact-fragment', id: PREVIEW_ID, targetTop: Math.max(0, rect.top + scrollTop), scrollTop, viewportHeight: window.innerHeight || 0, documentHeight: measureHeight() }, '*');\n"
|
|
4302
|
+
+ " } catch {}\n"
|
|
4303
|
+
+ " }\n"
|
|
4304
|
+
+ " function scrollFragmentIntoView(fragment, options) {\n"
|
|
4305
|
+
+ " const target = findNamedFragmentTarget(fragment);\n"
|
|
4306
|
+
+ " if (!target) return false;\n"
|
|
4307
|
+
+ " const behavior = options && options.smooth === false ? 'auto' : 'smooth';\n"
|
|
4308
|
+
+ " try { target.scrollIntoView({ block: 'start', inline: 'nearest', behavior }); } catch { try { target.scrollIntoView(true); } catch {} }\n"
|
|
4309
|
+
+ " postFragmentScroll(target);\n"
|
|
4310
|
+
+ " setTimeout(() => postFragmentScroll(target), 80);\n"
|
|
4311
|
+
+ " setTimeout(() => postFragmentScroll(target), 300);\n"
|
|
4312
|
+
+ " return true;\n"
|
|
4313
|
+
+ " }\n"
|
|
4314
|
+
+ " function getAnchorFromClickTarget(target) {\n"
|
|
4315
|
+
+ " let node = target;\n"
|
|
4316
|
+
+ " if (node && node.nodeType === 3) node = node.parentElement;\n"
|
|
4317
|
+
+ " return node && typeof node.closest === 'function' ? node.closest('a[href]') : null;\n"
|
|
4318
|
+
+ " }\n"
|
|
4319
|
+
+ " function getSameDocumentFragment(anchor) {\n"
|
|
4320
|
+
+ " if (!anchor || typeof anchor.getAttribute !== 'function') return null;\n"
|
|
4321
|
+
+ " if (anchor.hasAttribute('download')) return null;\n"
|
|
4322
|
+
+ " const target = String(anchor.getAttribute('target') || '').trim().toLowerCase();\n"
|
|
4323
|
+
+ " if (target && target !== '_self') return null;\n"
|
|
4324
|
+
+ " const rawHref = String(anchor.getAttribute('href') || '').trim();\n"
|
|
4325
|
+
+ " if (!rawHref) return null;\n"
|
|
4326
|
+
+ " if (rawHref.charAt(0) === '#') return rawHref.slice(1);\n"
|
|
4327
|
+
+ " const hashIndex = rawHref.indexOf('#');\n"
|
|
4328
|
+
+ " if (hashIndex < 0) return null;\n"
|
|
4329
|
+
+ " const beforeHash = rawHref.slice(0, hashIndex);\n"
|
|
4330
|
+
+ " const currentWithoutHash = String(window.location && window.location.href || '').split('#')[0];\n"
|
|
4331
|
+
+ " if (!beforeHash || beforeHash === currentWithoutHash || beforeHash === 'about:srcdoc') return rawHref.slice(hashIndex + 1);\n"
|
|
4332
|
+
+ " return null;\n"
|
|
4333
|
+
+ " }\n"
|
|
4334
|
+
+ " function writeFragmentHistory(fragment) {\n"
|
|
4335
|
+
+ " try {\n"
|
|
4336
|
+
+ " if (history && typeof history.pushState === 'function') {\n"
|
|
4337
|
+
+ " history.pushState(null, '', fragment ? '#' + encodeURIComponent(decodeFragment(fragment)) : '#');\n"
|
|
4338
|
+
+ " }\n"
|
|
4339
|
+
+ " } catch {}\n"
|
|
4340
|
+
+ " }\n"
|
|
4341
|
+
+ " function handleFragmentAnchorClick(event) {\n"
|
|
4342
|
+
+ " if (!event || event.defaultPrevented) return;\n"
|
|
4343
|
+
+ " if (typeof event.button === 'number' && event.button !== 0) return;\n"
|
|
4344
|
+
+ " if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n"
|
|
4345
|
+
+ " const anchor = getAnchorFromClickTarget(event.target);\n"
|
|
4346
|
+
+ " const fragment = getSameDocumentFragment(anchor);\n"
|
|
4347
|
+
+ " if (fragment == null) return;\n"
|
|
4348
|
+
+ " if (!scrollFragmentIntoView(fragment)) return;\n"
|
|
4349
|
+
+ " event.preventDefault();\n"
|
|
4350
|
+
+ " writeFragmentHistory(fragment);\n"
|
|
4351
|
+
+ " }\n"
|
|
4352
|
+
+ " const htmlMathPlaceholders = new Map();\n"
|
|
4353
|
+
+ " let htmlMathSerial = 0;\n"
|
|
4354
|
+
+ " let htmlMathScanScheduled = false;\n"
|
|
4355
|
+
+ " function delimiterListIncludes(list, start, end) {\n"
|
|
4356
|
+
+ " if (!Array.isArray(list)) return false;\n"
|
|
4357
|
+
+ " return list.some((entry) => Array.isArray(entry) && entry[0] === start && entry[1] === end);\n"
|
|
4358
|
+
+ " }\n"
|
|
4359
|
+
+ " function getHtmlMathDelimiterConfig() {\n"
|
|
4360
|
+
+ " const mathJax = window.MathJax && typeof window.MathJax === 'object' ? window.MathJax : null;\n"
|
|
4361
|
+
+ " const tex = mathJax && mathJax.tex && typeof mathJax.tex === 'object' ? mathJax.tex : null;\n"
|
|
4362
|
+
+ " return {\n"
|
|
4363
|
+
+ " inlineDollar: Boolean(tex && delimiterListIncludes(tex.inlineMath, '$', '$')),\n"
|
|
4364
|
+
+ " displayDollar: Boolean(tex && delimiterListIncludes(tex.displayMath, '$$', '$$')),\n"
|
|
4365
|
+
+ " };\n"
|
|
4366
|
+
+ " }\n"
|
|
4367
|
+
+ " function isEscapedAt(text, index) {\n"
|
|
4368
|
+
+ " let count = 0;\n"
|
|
4369
|
+
+ " let pos = index - 1;\n"
|
|
4370
|
+
+ " while (pos >= 0 && text.charAt(pos) === '\\\\') { count += 1; pos -= 1; }\n"
|
|
4371
|
+
+ " return count % 2 === 1;\n"
|
|
4372
|
+
+ " }\n"
|
|
4373
|
+
+ " function findUnescapedDelimiter(text, delimiter, fromIndex) {\n"
|
|
4374
|
+
+ " let index = Math.max(0, Number(fromIndex) || 0);\n"
|
|
4375
|
+
+ " while (index < text.length) {\n"
|
|
4376
|
+
+ " index = text.indexOf(delimiter, index);\n"
|
|
4377
|
+
+ " if (index < 0) return -1;\n"
|
|
4378
|
+
+ " if (!isEscapedAt(text, index)) return index;\n"
|
|
4379
|
+
+ " index += Math.max(1, delimiter.length);\n"
|
|
4380
|
+
+ " }\n"
|
|
4381
|
+
+ " return -1;\n"
|
|
4382
|
+
+ " }\n"
|
|
4383
|
+
+ " function textMightContainMath(text, config) {\n"
|
|
4384
|
+
+ " if (!text) return false;\n"
|
|
4385
|
+
+ " if (text.indexOf('\\\\(') !== -1 || text.indexOf('\\\\[') !== -1) return true;\n"
|
|
4386
|
+
+ " return Boolean((config.inlineDollar || config.displayDollar) && text.indexOf('$') !== -1);\n"
|
|
4387
|
+
+ " }\n"
|
|
4388
|
+
+ " function findNextMathSegment(text, startIndex, config) {\n"
|
|
4389
|
+
+ " for (let index = startIndex; index < text.length; index += 1) {\n"
|
|
4390
|
+
+ " if (text.startsWith('\\\\(', index)) {\n"
|
|
4391
|
+
+ " const end = findUnescapedDelimiter(text, '\\\\)', index + 2);\n"
|
|
4392
|
+
+ " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: false };\n"
|
|
4393
|
+
+ " }\n"
|
|
4394
|
+
+ " if (text.startsWith('\\\\[', index)) {\n"
|
|
4395
|
+
+ " const end = findUnescapedDelimiter(text, '\\\\]', index + 2);\n"
|
|
4396
|
+
+ " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: true };\n"
|
|
4397
|
+
+ " }\n"
|
|
4398
|
+
+ " if (config.displayDollar && text.startsWith('$$', index) && !isEscapedAt(text, index)) {\n"
|
|
4399
|
+
+ " const end = findUnescapedDelimiter(text, '$$', index + 2);\n"
|
|
4400
|
+
+ " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: true };\n"
|
|
4401
|
+
+ " }\n"
|
|
4402
|
+
+ " if (config.inlineDollar && text.charAt(index) === '$' && text.charAt(index + 1) !== '$' && !isEscapedAt(text, index)) {\n"
|
|
4403
|
+
+ " const end = findUnescapedDelimiter(text, '$', index + 1);\n"
|
|
4404
|
+
+ " if (end > index + 1) return { start: index, end: end + 1, tex: text.slice(index + 1, end).trim(), display: false };\n"
|
|
4405
|
+
+ " }\n"
|
|
4406
|
+
+ " }\n"
|
|
4407
|
+
+ " return null;\n"
|
|
4408
|
+
+ " }\n"
|
|
4409
|
+
+ " function parseHtmlMathSegments(text, config, maxCount) {\n"
|
|
4410
|
+
+ " const segments = [];\n"
|
|
4411
|
+
+ " let index = 0;\n"
|
|
4412
|
+
+ " const maxSegments = Math.max(1, Number(maxCount) || 1);\n"
|
|
4413
|
+
+ " while (index < text.length && segments.length < maxSegments) {\n"
|
|
4414
|
+
+ " const segment = findNextMathSegment(text, index, config);\n"
|
|
4415
|
+
+ " if (!segment) break;\n"
|
|
4416
|
+
+ " if (segment.tex) segments.push(segment);\n"
|
|
4417
|
+
+ " index = Math.max(segment.end, segment.start + 1);\n"
|
|
4418
|
+
+ " }\n"
|
|
4419
|
+
+ " return segments;\n"
|
|
4420
|
+
+ " }\n"
|
|
4421
|
+
+ " function shouldSkipHtmlMathTextNode(node) {\n"
|
|
4422
|
+
+ " let el = node && node.parentElement;\n"
|
|
4423
|
+
+ " while (el) {\n"
|
|
4424
|
+
+ " const tag = el.tagName ? el.tagName.toLowerCase() : '';\n"
|
|
4425
|
+
+ " if (['script', 'style', 'textarea', 'pre', 'code', 'math', 'svg', 'mjx-container'].indexOf(tag) !== -1) return true;\n"
|
|
4426
|
+
+ " if (el.classList && (el.classList.contains('pi-studio-html-math') || el.classList.contains('MathJax'))) return true;\n"
|
|
4427
|
+
+ " el = el.parentElement;\n"
|
|
4428
|
+
+ " }\n"
|
|
4429
|
+
+ " return false;\n"
|
|
4430
|
+
+ " }\n"
|
|
4431
|
+
+ " function replaceTextNodeWithHtmlMathPlaceholders(node, segments) {\n"
|
|
4432
|
+
+ " if (!node || !node.parentNode || !segments || segments.length === 0) return [];\n"
|
|
4433
|
+
+ " const text = String(node.nodeValue || '');\n"
|
|
4434
|
+
+ " const fragment = document.createDocumentFragment();\n"
|
|
4435
|
+
+ " const items = [];\n"
|
|
4436
|
+
+ " let index = 0;\n"
|
|
4437
|
+
+ " segments.forEach((segment) => {\n"
|
|
4438
|
+
+ " if (segment.start > index) fragment.appendChild(document.createTextNode(text.slice(index, segment.start)));\n"
|
|
4439
|
+
+ " const mathId = PREVIEW_ID + '_math_' + (++htmlMathSerial).toString(36);\n"
|
|
4440
|
+
+ " const span = document.createElement('span');\n"
|
|
4441
|
+
+ " span.className = 'pi-studio-html-math pi-studio-html-math-' + (segment.display ? 'display' : 'inline');\n"
|
|
4442
|
+
+ " span.setAttribute('data-pi-studio-html-math-id', mathId);\n"
|
|
4443
|
+
+ " span.setAttribute('aria-busy', 'true');\n"
|
|
4444
|
+
+ " span.textContent = text.slice(segment.start, segment.end);\n"
|
|
4445
|
+
+ " htmlMathPlaceholders.set(mathId, span);\n"
|
|
4446
|
+
+ " items.push({ mathId, tex: segment.tex, display: Boolean(segment.display) });\n"
|
|
4447
|
+
+ " fragment.appendChild(span);\n"
|
|
4448
|
+
+ " index = segment.end;\n"
|
|
4449
|
+
+ " });\n"
|
|
4450
|
+
+ " if (index < text.length) fragment.appendChild(document.createTextNode(text.slice(index)));\n"
|
|
4451
|
+
+ " node.parentNode.replaceChild(fragment, node);\n"
|
|
4452
|
+
+ " return items;\n"
|
|
4453
|
+
+ " }\n"
|
|
4454
|
+
+ " function applyRenderedHtmlMath(results) {\n"
|
|
4455
|
+
+ " if (!Array.isArray(results)) return;\n"
|
|
4456
|
+
+ " results.forEach((result) => {\n"
|
|
4457
|
+
+ " if (!result || typeof result !== 'object') return;\n"
|
|
4458
|
+
+ " const mathId = typeof result.mathId === 'string' ? result.mathId : '';\n"
|
|
4459
|
+
+ " const placeholder = mathId ? htmlMathPlaceholders.get(mathId) : null;\n"
|
|
4460
|
+
+ " if (!placeholder || !placeholder.isConnected) return;\n"
|
|
4461
|
+
+ " placeholder.removeAttribute('aria-busy');\n"
|
|
4462
|
+
+ " if (result.ok === true && typeof result.html === 'string' && result.html.trim()) {\n"
|
|
4463
|
+
+ " placeholder.innerHTML = result.html;\n"
|
|
4464
|
+
+ " placeholder.classList.add('pi-studio-html-math-rendered');\n"
|
|
4465
|
+
+ " } else {\n"
|
|
4466
|
+
+ " placeholder.classList.add('pi-studio-html-math-failed');\n"
|
|
4467
|
+
+ " if (typeof result.error === 'string' && result.error) placeholder.title = result.error;\n"
|
|
4468
|
+
+ " }\n"
|
|
4469
|
+
+ " htmlMathPlaceholders.delete(mathId);\n"
|
|
4470
|
+
+ " });\n"
|
|
4471
|
+
+ " scheduleHeight();\n"
|
|
4472
|
+
+ " }\n"
|
|
4473
|
+
+ " function runHtmlMathRenderScan() {\n"
|
|
4474
|
+
+ " htmlMathScanScheduled = false;\n"
|
|
4475
|
+
+ " if (!document.body || typeof document.createTreeWalker !== 'function') return;\n"
|
|
4476
|
+
+ " const nodeFilterApi = typeof NodeFilter !== 'undefined' ? NodeFilter : { SHOW_TEXT: 4, FILTER_ACCEPT: 1, FILTER_REJECT: 2 };\n"
|
|
4477
|
+
+ " const config = getHtmlMathDelimiterConfig();\n"
|
|
4478
|
+
+ " const nodes = [];\n"
|
|
4479
|
+
+ " const walker = document.createTreeWalker(document.body, nodeFilterApi.SHOW_TEXT, {\n"
|
|
4480
|
+
+ " acceptNode(node) {\n"
|
|
4481
|
+
+ " const text = String(node && node.nodeValue || '');\n"
|
|
4482
|
+
+ " if (!textMightContainMath(text, config)) return nodeFilterApi.FILTER_REJECT;\n"
|
|
4483
|
+
+ " if (shouldSkipHtmlMathTextNode(node)) return nodeFilterApi.FILTER_REJECT;\n"
|
|
4484
|
+
+ " return nodeFilterApi.FILTER_ACCEPT;\n"
|
|
4485
|
+
+ " }\n"
|
|
4486
|
+
+ " });\n"
|
|
4487
|
+
+ " while (walker.nextNode()) nodes.push(walker.currentNode);\n"
|
|
4488
|
+
+ " const items = [];\n"
|
|
4489
|
+
+ " for (const node of nodes) {\n"
|
|
4490
|
+
+ " const remaining = 250 - items.length;\n"
|
|
4491
|
+
+ " if (remaining <= 0) break;\n"
|
|
4492
|
+
+ " const text = String(node && node.nodeValue || '');\n"
|
|
4493
|
+
+ " const segments = parseHtmlMathSegments(text, config, remaining);\n"
|
|
4494
|
+
+ " if (segments.length === 0) continue;\n"
|
|
4495
|
+
+ " items.push(...replaceTextNodeWithHtmlMathPlaceholders(node, segments));\n"
|
|
4496
|
+
+ " }\n"
|
|
4497
|
+
+ " if (items.length > 0) {\n"
|
|
4498
|
+
+ " try { parent.postMessage({ type: 'pi-studio-html-artifact-render-math', id: PREVIEW_ID, items }, '*'); } catch {}\n"
|
|
4499
|
+
+ " }\n"
|
|
4500
|
+
+ " }\n"
|
|
4501
|
+
+ " function scheduleHtmlMathRenderScan() {\n"
|
|
4502
|
+
+ " if (htmlMathScanScheduled) return;\n"
|
|
4503
|
+
+ " htmlMathScanScheduled = true;\n"
|
|
4504
|
+
+ " requestAnimationFrame(runHtmlMathRenderScan);\n"
|
|
4505
|
+
+ " }\n"
|
|
4506
|
+
+ " const htmlResourcePlaceholders = new Map();\n"
|
|
4507
|
+
+ " let htmlResourceSerial = 0;\n"
|
|
4508
|
+
+ " let htmlResourceScanScheduled = false;\n"
|
|
4509
|
+
+ " function shouldResolveHtmlPreviewResourceUrl(value) {\n"
|
|
4510
|
+
+ " const raw = String(value || '').trim();\n"
|
|
4511
|
+
+ " if (!raw || raw.charAt(0) === '#') return false;\n"
|
|
4512
|
+
+ " if (/^(?:data|blob|http|https|about|javascript|mailto):/i.test(raw)) return false;\n"
|
|
4513
|
+
+ " if (/^\\/\\//.test(raw)) return false;\n"
|
|
4514
|
+
+ " return /\\.(?:png|jpe?g|gif|webp)(?:[?#].*)?$/i.test(raw);\n"
|
|
4515
|
+
+ " }\n"
|
|
4516
|
+
+ " function scanHtmlPreviewResources() {\n"
|
|
4517
|
+
+ " htmlResourceScanScheduled = false;\n"
|
|
4518
|
+
+ " if (!document.body) return;\n"
|
|
4519
|
+
+ " const items = [];\n"
|
|
4520
|
+
+ " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
|
|
4521
|
+
+ " images.forEach((image) => {\n"
|
|
4522
|
+
+ " if (!image || !image.getAttribute) return;\n"
|
|
4523
|
+
+ " if (image.getAttribute('data-pi-studio-html-resource-resolved') === 'true') return;\n"
|
|
4524
|
+
+ " const raw = String(image.getAttribute('src') || '').trim();\n"
|
|
4525
|
+
+ " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
|
|
4526
|
+
+ " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
|
|
4527
|
+
+ " if (!resourceId) {\n"
|
|
4528
|
+
+ " resourceId = PREVIEW_ID + '_resource_' + (++htmlResourceSerial).toString(36);\n"
|
|
4529
|
+
+ " image.setAttribute('data-pi-studio-html-resource-id', resourceId);\n"
|
|
4530
|
+
+ " }\n"
|
|
4531
|
+
+ " htmlResourcePlaceholders.set(resourceId, image);\n"
|
|
4532
|
+
+ " items.push({ resourceId, url: raw });\n"
|
|
4533
|
+
+ " });\n"
|
|
4534
|
+
+ " if (items.length > 0) {\n"
|
|
4535
|
+
+ " try { parent.postMessage({ type: 'pi-studio-html-artifact-resolve-resources', id: PREVIEW_ID, resources: items.slice(0, 100) }, '*'); } catch {}\n"
|
|
4536
|
+
+ " }\n"
|
|
4537
|
+
+ " }\n"
|
|
4538
|
+
+ " function scheduleHtmlPreviewResourceScan() {\n"
|
|
4539
|
+
+ " if (htmlResourceScanScheduled) return;\n"
|
|
4540
|
+
+ " htmlResourceScanScheduled = true;\n"
|
|
4541
|
+
+ " requestAnimationFrame(scanHtmlPreviewResources);\n"
|
|
4542
|
+
+ " }\n"
|
|
4543
|
+
+ " function applyResolvedHtmlPreviewResources(results) {\n"
|
|
4544
|
+
+ " if (!Array.isArray(results)) return;\n"
|
|
4545
|
+
+ " results.forEach((result) => {\n"
|
|
4546
|
+
+ " if (!result || typeof result !== 'object') return;\n"
|
|
4547
|
+
+ " const resourceId = typeof result.resourceId === 'string' ? result.resourceId : '';\n"
|
|
4548
|
+
+ " const image = resourceId ? htmlResourcePlaceholders.get(resourceId) : null;\n"
|
|
4549
|
+
+ " if (!image || !image.isConnected) return;\n"
|
|
4550
|
+
+ " if (result.ok === true && typeof result.dataUrl === 'string' && result.dataUrl) {\n"
|
|
4551
|
+
+ " image.setAttribute('src', result.dataUrl);\n"
|
|
4552
|
+
+ " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
|
|
4553
|
+
+ " } else if (typeof result.error === 'string' && result.error) {\n"
|
|
4554
|
+
+ " image.setAttribute('title', result.error);\n"
|
|
4555
|
+
+ " }\n"
|
|
4556
|
+
+ " htmlResourcePlaceholders.delete(resourceId);\n"
|
|
4557
|
+
+ " });\n"
|
|
4558
|
+
+ " scheduleHeight();\n"
|
|
4559
|
+
+ " }\n"
|
|
3983
4560
|
+ " window.addEventListener('message', (event) => {\n"
|
|
3984
4561
|
+ " const data = event && event.data;\n"
|
|
3985
|
-
+ " if (!data || typeof data !== 'object') return;\n"
|
|
3986
|
-
+ " if (data.type
|
|
3987
|
-
+ "
|
|
4562
|
+
+ " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
|
|
4563
|
+
+ " if (data.type === 'pi-studio-html-artifact-zoom') {\n"
|
|
4564
|
+
+ " applyZoom(data.zoom);\n"
|
|
4565
|
+
+ " return;\n"
|
|
4566
|
+
+ " }\n"
|
|
4567
|
+
+ " if (data.type === 'pi-studio-html-artifact-math-rendered') {\n"
|
|
4568
|
+
+ " applyRenderedHtmlMath(data.results);\n"
|
|
4569
|
+
+ " return;\n"
|
|
4570
|
+
+ " }\n"
|
|
4571
|
+
+ " if (data.type === 'pi-studio-html-artifact-resources-resolved') {\n"
|
|
4572
|
+
+ " applyResolvedHtmlPreviewResources(data.results);\n"
|
|
4573
|
+
+ " }\n"
|
|
3988
4574
|
+ " });\n"
|
|
3989
|
-
+ "
|
|
4575
|
+
+ " document.addEventListener('click', handleFragmentAnchorClick);\n"
|
|
4576
|
+
+ " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
4577
|
+
+ " window.addEventListener('hashchange', () => {\n"
|
|
4578
|
+
+ " const hash = String(window.location && window.location.hash || '');\n"
|
|
4579
|
+
+ " if (hash) scrollFragmentIntoView(hash.slice(1), { smooth: false });\n"
|
|
4580
|
+
+ " });\n"
|
|
4581
|
+
+ " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
3990
4582
|
+ " window.addEventListener('resize', scheduleHeight);\n"
|
|
3991
4583
|
+ " if (typeof ResizeObserver === 'function') {\n"
|
|
3992
4584
|
+ " const observer = new ResizeObserver(scheduleHeight);\n"
|
|
@@ -3994,20 +4586,32 @@
|
|
|
3994
4586
|
+ " if (document.body) observer.observe(document.body);\n"
|
|
3995
4587
|
+ " }\n"
|
|
3996
4588
|
+ " if (typeof MutationObserver === 'function') {\n"
|
|
3997
|
-
+ " const observer = new MutationObserver(scheduleHeight);\n"
|
|
4589
|
+
+ " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
3998
4590
|
+ " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
|
|
3999
4591
|
+ " }\n"
|
|
4000
4592
|
+ " scheduleHeight();\n"
|
|
4001
4593
|
+ " setTimeout(scheduleHeight, 80);\n"
|
|
4002
4594
|
+ " setTimeout(scheduleHeight, 350);\n"
|
|
4595
|
+
+ " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 0);\n"
|
|
4596
|
+
+ " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 120);\n"
|
|
4597
|
+
+ " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 500);\n"
|
|
4003
4598
|
+ "})();\n"
|
|
4004
4599
|
+ "<\/script>";
|
|
4005
4600
|
}
|
|
4006
4601
|
|
|
4602
|
+
function buildHtmlArtifactPreviewMathStyle() {
|
|
4603
|
+
return "<style data-pi-studio-html-preview-math>\n"
|
|
4604
|
+
+ ".pi-studio-html-math-display{display:block;margin:0.75em 0;overflow-x:auto;text-align:center;}\n"
|
|
4605
|
+
+ ".pi-studio-html-math-display>math{display:block;margin:0 auto;}\n"
|
|
4606
|
+
+ ".pi-studio-html-math-inline>math{vertical-align:-0.15em;}\n"
|
|
4607
|
+
+ "</style>\n";
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4007
4610
|
function buildHtmlArtifactPreviewHeadMarkup(previewId) {
|
|
4008
4611
|
return "<meta charset=\"utf-8\">\n"
|
|
4009
4612
|
+ "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
|
|
4010
4613
|
+ "<meta http-equiv=\"Content-Security-Policy\" content=\"" + escapeHtml(HTML_ARTIFACT_PREVIEW_CSP) + "\">\n"
|
|
4614
|
+
+ buildHtmlArtifactPreviewMathStyle()
|
|
4011
4615
|
+ buildHtmlArtifactPreviewResizeScript(previewId);
|
|
4012
4616
|
}
|
|
4013
4617
|
|
|
@@ -4067,7 +4671,244 @@
|
|
|
4067
4671
|
}
|
|
4068
4672
|
}
|
|
4069
4673
|
|
|
4674
|
+
function handleHtmlArtifactFrameFragmentMessage(event) {
|
|
4675
|
+
const data = event && event.data;
|
|
4676
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-fragment") return;
|
|
4677
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
4678
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
4679
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
4680
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
4684
|
+
if (record.shell && record.shell.classList && record.shell.classList.contains("is-focused")) return;
|
|
4685
|
+
|
|
4686
|
+
const scrollContainer = record.shell && typeof record.shell.closest === "function"
|
|
4687
|
+
? record.shell.closest(".panel-scroll")
|
|
4688
|
+
: null;
|
|
4689
|
+
const isCapped = Boolean(record.iframe.classList && record.iframe.classList.contains("is-height-capped"));
|
|
4690
|
+
const documentHeight = Number(data.documentHeight);
|
|
4691
|
+
const viewportHeight = Number(data.viewportHeight);
|
|
4692
|
+
const isInternallyScrollable = isCapped
|
|
4693
|
+
|| (Number.isFinite(documentHeight) && Number.isFinite(viewportHeight) && documentHeight > viewportHeight + 2);
|
|
4694
|
+
if (!scrollContainer || isInternallyScrollable) {
|
|
4695
|
+
if (typeof record.iframe.scrollIntoView === "function") {
|
|
4696
|
+
try {
|
|
4697
|
+
record.iframe.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
|
|
4698
|
+
} catch {
|
|
4699
|
+
record.iframe.scrollIntoView(false);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
const rawTargetTop = Number(data.targetTop);
|
|
4706
|
+
const offsetInFrame = Number.isFinite(rawTargetTop) && rawTargetTop > 0 ? rawTargetTop : 0;
|
|
4707
|
+
const iframeRect = record.iframe.getBoundingClientRect();
|
|
4708
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
4709
|
+
const topPadding = 12;
|
|
4710
|
+
const nextTop = Math.max(
|
|
4711
|
+
0,
|
|
4712
|
+
scrollContainer.scrollTop + iframeRect.top - containerRect.top + offsetInFrame - topPadding,
|
|
4713
|
+
);
|
|
4714
|
+
try {
|
|
4715
|
+
scrollContainer.scrollTo({ top: nextTop, behavior: "smooth" });
|
|
4716
|
+
} catch {
|
|
4717
|
+
scrollContainer.scrollTop = nextTop;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
function normalizeHtmlArtifactMathRenderItems(rawItems) {
|
|
4722
|
+
if (!Array.isArray(rawItems)) return [];
|
|
4723
|
+
return rawItems.slice(0, 250).map((item) => {
|
|
4724
|
+
const raw = item && typeof item === "object" ? item : null;
|
|
4725
|
+
const mathId = raw && typeof raw.mathId === "string" ? raw.mathId : "";
|
|
4726
|
+
const tex = raw && typeof raw.tex === "string" ? raw.tex : "";
|
|
4727
|
+
if (!mathId || !tex.trim()) return null;
|
|
4728
|
+
return {
|
|
4729
|
+
mathId,
|
|
4730
|
+
tex,
|
|
4731
|
+
display: Boolean(raw.display),
|
|
4732
|
+
};
|
|
4733
|
+
}).filter(Boolean);
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
async function fetchRenderedHtmlArtifactMath(items) {
|
|
4737
|
+
const token = getToken();
|
|
4738
|
+
if (!token) {
|
|
4739
|
+
throw new Error("Missing Studio token in URL.");
|
|
4740
|
+
}
|
|
4741
|
+
const response = await fetchWithTimeout("/render-math?token=" + encodeURIComponent(token), {
|
|
4742
|
+
method: "POST",
|
|
4743
|
+
headers: {
|
|
4744
|
+
"Content-Type": "application/json",
|
|
4745
|
+
},
|
|
4746
|
+
body: JSON.stringify({ items }),
|
|
4747
|
+
}, HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS, "HTML preview math render");
|
|
4748
|
+
|
|
4749
|
+
const rawBody = await response.text();
|
|
4750
|
+
let payload = null;
|
|
4751
|
+
try {
|
|
4752
|
+
payload = rawBody ? JSON.parse(rawBody) : null;
|
|
4753
|
+
} catch {
|
|
4754
|
+
payload = null;
|
|
4755
|
+
}
|
|
4756
|
+
if (!response.ok) {
|
|
4757
|
+
const message = payload && typeof payload.error === "string"
|
|
4758
|
+
? payload.error
|
|
4759
|
+
: "HTML preview math render failed with HTTP " + response.status + ".";
|
|
4760
|
+
throw new Error(message);
|
|
4761
|
+
}
|
|
4762
|
+
if (!payload || payload.ok !== true || !Array.isArray(payload.results)) {
|
|
4763
|
+
throw new Error("HTML preview math renderer returned an invalid payload.");
|
|
4764
|
+
}
|
|
4765
|
+
return payload.results;
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
function postHtmlArtifactMathResults(record, results) {
|
|
4769
|
+
if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
|
|
4770
|
+
try {
|
|
4771
|
+
record.iframe.contentWindow.postMessage({
|
|
4772
|
+
type: "pi-studio-html-artifact-math-rendered",
|
|
4773
|
+
id: record.id || "",
|
|
4774
|
+
results: Array.isArray(results) ? results : [],
|
|
4775
|
+
}, "*");
|
|
4776
|
+
} catch {
|
|
4777
|
+
// Ignore iframe postMessage failures.
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
async function renderHtmlArtifactMathItems(record, items) {
|
|
4782
|
+
if (!record || !Array.isArray(items) || items.length === 0) return;
|
|
4783
|
+
if (record.detail) record.detail.textContent = "HTML preview · rendering math";
|
|
4784
|
+
try {
|
|
4785
|
+
const results = await fetchRenderedHtmlArtifactMath(items);
|
|
4786
|
+
postHtmlArtifactMathResults(record, results);
|
|
4787
|
+
} catch (error) {
|
|
4788
|
+
console.error("HTML preview math render failed:", error);
|
|
4789
|
+
postHtmlArtifactMathResults(record, items.map((item) => ({
|
|
4790
|
+
mathId: item.mathId,
|
|
4791
|
+
ok: false,
|
|
4792
|
+
error: error && error.message ? error.message : String(error || "HTML preview math render failed."),
|
|
4793
|
+
})));
|
|
4794
|
+
} finally {
|
|
4795
|
+
if (record.detail) record.detail.textContent = "HTML preview";
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
function handleHtmlArtifactFrameMathRenderMessage(event) {
|
|
4800
|
+
const data = event && event.data;
|
|
4801
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-render-math") return;
|
|
4802
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
4803
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
4804
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
4805
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
4809
|
+
const items = normalizeHtmlArtifactMathRenderItems(data.items);
|
|
4810
|
+
if (items.length === 0) return;
|
|
4811
|
+
|
|
4812
|
+
record.mathRenderBatchCount = Math.max(0, Number(record.mathRenderBatchCount) || 0) + 1;
|
|
4813
|
+
record.mathRenderItemCount = Math.max(0, Number(record.mathRenderItemCount) || 0) + items.length;
|
|
4814
|
+
if (record.mathRenderBatchCount > 24 || record.mathRenderItemCount > 1000) {
|
|
4815
|
+
postHtmlArtifactMathResults(record, items.map((item) => ({
|
|
4816
|
+
mathId: item.mathId,
|
|
4817
|
+
ok: false,
|
|
4818
|
+
error: "HTML preview math render limit reached.",
|
|
4819
|
+
})));
|
|
4820
|
+
return;
|
|
4821
|
+
}
|
|
4822
|
+
|
|
4823
|
+
void renderHtmlArtifactMathItems(record, items);
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
function normalizeHtmlArtifactResourceItems(rawItems) {
|
|
4827
|
+
if (!Array.isArray(rawItems)) return [];
|
|
4828
|
+
return rawItems.slice(0, 100).map((item) => {
|
|
4829
|
+
const raw = item && typeof item === "object" ? item : null;
|
|
4830
|
+
const resourceId = raw && typeof raw.resourceId === "string" ? raw.resourceId : "";
|
|
4831
|
+
const url = raw && typeof raw.url === "string" ? raw.url : "";
|
|
4832
|
+
if (!resourceId || !url.trim()) return null;
|
|
4833
|
+
return { resourceId, url };
|
|
4834
|
+
}).filter(Boolean);
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
function buildHtmlArtifactResourceFetchUrl(record, resourceUrl) {
|
|
4838
|
+
const token = getToken();
|
|
4839
|
+
if (!token) return "";
|
|
4840
|
+
const params = new URLSearchParams({ token, path: String(resourceUrl || "") });
|
|
4841
|
+
if (record && record.sourcePath) {
|
|
4842
|
+
params.set("sourcePath", record.sourcePath);
|
|
4843
|
+
} else if (record && record.resourceDir) {
|
|
4844
|
+
params.set("resourceDir", record.resourceDir);
|
|
4845
|
+
}
|
|
4846
|
+
return "/html-preview-resource?" + params.toString();
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
function postHtmlArtifactResourceResults(record, results) {
|
|
4850
|
+
if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
|
|
4851
|
+
try {
|
|
4852
|
+
record.iframe.contentWindow.postMessage({
|
|
4853
|
+
type: "pi-studio-html-artifact-resources-resolved",
|
|
4854
|
+
id: record.id || "",
|
|
4855
|
+
results: Array.isArray(results) ? results : [],
|
|
4856
|
+
}, "*");
|
|
4857
|
+
} catch {
|
|
4858
|
+
// Ignore iframe postMessage failures.
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
|
|
4862
|
+
async function fetchHtmlArtifactResource(record, item) {
|
|
4863
|
+
const resourceId = item && item.resourceId ? item.resourceId : "";
|
|
4864
|
+
try {
|
|
4865
|
+
const fetchUrl = buildHtmlArtifactResourceFetchUrl(record, item.url);
|
|
4866
|
+
if (!fetchUrl) throw new Error("Missing Studio token in URL.");
|
|
4867
|
+
const response = await fetchWithTimeout(fetchUrl, { method: "GET" }, HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS, "HTML preview resource load");
|
|
4868
|
+
const payload = await response.json().catch(() => null);
|
|
4869
|
+
if (!response.ok || !payload || payload.ok !== true || typeof payload.dataUrl !== "string") {
|
|
4870
|
+
const message = payload && typeof payload.error === "string" ? payload.error : "HTML preview resource load failed with HTTP " + response.status + ".";
|
|
4871
|
+
throw new Error(message);
|
|
4872
|
+
}
|
|
4873
|
+
return { resourceId, ok: true, dataUrl: payload.dataUrl };
|
|
4874
|
+
} catch (error) {
|
|
4875
|
+
return { resourceId, ok: false, error: error && error.message ? error.message : String(error || "HTML preview resource load failed.") };
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
async function resolveHtmlArtifactResources(record, items) {
|
|
4880
|
+
if (!record || !Array.isArray(items) || items.length === 0) return;
|
|
4881
|
+
if (record.detail) record.detail.textContent = "HTML preview · loading local images";
|
|
4882
|
+
const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
|
|
4883
|
+
postHtmlArtifactResourceResults(record, results);
|
|
4884
|
+
if (record.detail) record.detail.textContent = "HTML preview";
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
function handleHtmlArtifactFrameResourceMessage(event) {
|
|
4888
|
+
const data = event && event.data;
|
|
4889
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-resolve-resources") return;
|
|
4890
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
4891
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
4892
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
4893
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
4897
|
+
const items = normalizeHtmlArtifactResourceItems(data.resources);
|
|
4898
|
+
if (items.length === 0) return;
|
|
4899
|
+
record.resourceResolveBatchCount = Math.max(0, Number(record.resourceResolveBatchCount) || 0) + 1;
|
|
4900
|
+
record.resourceResolveItemCount = Math.max(0, Number(record.resourceResolveItemCount) || 0) + items.length;
|
|
4901
|
+
if (record.resourceResolveBatchCount > 12 || record.resourceResolveItemCount > 300) {
|
|
4902
|
+
postHtmlArtifactResourceResults(record, items.map((item) => ({ resourceId: item.resourceId, ok: false, error: "HTML preview local image load limit reached." })));
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4905
|
+
void resolveHtmlArtifactResources(record, items);
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4070
4908
|
window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
|
|
4909
|
+
window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
|
|
4910
|
+
window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
|
|
4911
|
+
window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
|
|
4071
4912
|
|
|
4072
4913
|
function isStudioHtmlFocusOpen() {
|
|
4073
4914
|
return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
|
|
@@ -4375,7 +5216,19 @@
|
|
|
4375
5216
|
iframe.addEventListener("load", () => { postArtifactZoom(); });
|
|
4376
5217
|
iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
|
|
4377
5218
|
shell.appendChild(iframe);
|
|
4378
|
-
htmlArtifactFramesById.set(previewId, {
|
|
5219
|
+
htmlArtifactFramesById.set(previewId, {
|
|
5220
|
+
id: previewId,
|
|
5221
|
+
iframe,
|
|
5222
|
+
shell,
|
|
5223
|
+
detail,
|
|
5224
|
+
zoomControls,
|
|
5225
|
+
sourcePath: options && options.sourcePath ? String(options.sourcePath) : "",
|
|
5226
|
+
resourceDir: options && options.resourceDir ? String(options.resourceDir) : "",
|
|
5227
|
+
mathRenderBatchCount: 0,
|
|
5228
|
+
mathRenderItemCount: 0,
|
|
5229
|
+
resourceResolveBatchCount: 0,
|
|
5230
|
+
resourceResolveItemCount: 0,
|
|
5231
|
+
});
|
|
4379
5232
|
|
|
4380
5233
|
targetEl.appendChild(shell);
|
|
4381
5234
|
|
|
@@ -5490,12 +6343,16 @@
|
|
|
5490
6343
|
const action = actionBtn.getAttribute("data-repl-action") || "";
|
|
5491
6344
|
if (action === "start" || action === "new-session") {
|
|
5492
6345
|
const requestId = makeRequestId();
|
|
6346
|
+
const command = getCurrentReplStartCommandFromDom();
|
|
6347
|
+
setReplCommandOverride(replRuntime, command);
|
|
5493
6348
|
replBusy = true;
|
|
5494
6349
|
replError = "";
|
|
5495
|
-
replMessage = (action === "new-session" ? "Starting new " : "Starting ") + replRuntime + "
|
|
6350
|
+
replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
|
|
5496
6351
|
syncActionButtons();
|
|
5497
6352
|
renderReplViewIfActive({ force: true });
|
|
5498
|
-
|
|
6353
|
+
const message = { type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" };
|
|
6354
|
+
if (command) message.command = command;
|
|
6355
|
+
if (!sendMessage(message)) {
|
|
5499
6356
|
replBusy = false;
|
|
5500
6357
|
syncActionButtons();
|
|
5501
6358
|
}
|
|
@@ -5599,13 +6456,29 @@
|
|
|
5599
6456
|
if (!(target instanceof Element)) return;
|
|
5600
6457
|
const runtimeSelect = target.closest("[data-repl-runtime]");
|
|
5601
6458
|
if (runtimeSelect && "value" in runtimeSelect) {
|
|
6459
|
+
const previousActive = replActiveSessionName;
|
|
5602
6460
|
setReplRuntime(runtimeSelect.value);
|
|
6461
|
+
selectReplSessionForRuntime(replRuntime, previousActive);
|
|
6462
|
+
replError = "";
|
|
6463
|
+
replMessage = "";
|
|
6464
|
+
if (replActiveSessionName) {
|
|
6465
|
+
requestReplCapture();
|
|
6466
|
+
} else {
|
|
6467
|
+
replTranscript = "";
|
|
6468
|
+
replCapturedAt = 0;
|
|
6469
|
+
syncActionButtons();
|
|
6470
|
+
}
|
|
5603
6471
|
renderReplViewIfActive({ force: true });
|
|
5604
6472
|
return;
|
|
5605
6473
|
}
|
|
6474
|
+
const commandInput = target.closest("[data-repl-command]");
|
|
6475
|
+
if (commandInput && "value" in commandInput) {
|
|
6476
|
+
setReplCommandOverride(replRuntime, commandInput.value);
|
|
6477
|
+
return;
|
|
6478
|
+
}
|
|
5606
6479
|
const sessionSelect = target.closest("[data-repl-session]");
|
|
5607
6480
|
if (sessionSelect && "value" in sessionSelect) {
|
|
5608
|
-
|
|
6481
|
+
setActiveReplSessionForCurrentRuntime(sessionSelect.value);
|
|
5609
6482
|
replError = "";
|
|
5610
6483
|
replMessage = "";
|
|
5611
6484
|
replFollow = true;
|
|
@@ -5719,12 +6592,34 @@
|
|
|
5719
6592
|
const source = codeEl ? codeEl.textContent : preEl.textContent;
|
|
5720
6593
|
|
|
5721
6594
|
const wrapper = document.createElement("div");
|
|
5722
|
-
wrapper.className = "mermaid-container";
|
|
6595
|
+
wrapper.className = "mermaid-container studio-copyable-block";
|
|
6596
|
+
if (wrapper.dataset) {
|
|
6597
|
+
wrapper.dataset.mermaidSource = source || "";
|
|
6598
|
+
wrapper.dataset.studioCopyDecorated = "1";
|
|
6599
|
+
}
|
|
5723
6600
|
|
|
5724
6601
|
const diagramEl = document.createElement("div");
|
|
5725
6602
|
diagramEl.className = "mermaid";
|
|
5726
6603
|
diagramEl.textContent = source || "";
|
|
5727
6604
|
|
|
6605
|
+
const copyBtn = document.createElement("button");
|
|
6606
|
+
copyBtn.type = "button";
|
|
6607
|
+
copyBtn.className = "studio-copy-block-btn studio-copy-mermaid-source-btn";
|
|
6608
|
+
copyBtn.textContent = "Copy source";
|
|
6609
|
+
copyBtn.title = "Copy this Mermaid source to the clipboard.";
|
|
6610
|
+
copyBtn.setAttribute("aria-label", "Copy Mermaid source to the clipboard");
|
|
6611
|
+
copyBtn.addEventListener("pointerdown", (event) => {
|
|
6612
|
+
event.stopPropagation();
|
|
6613
|
+
});
|
|
6614
|
+
copyBtn.addEventListener("mousedown", (event) => {
|
|
6615
|
+
event.stopPropagation();
|
|
6616
|
+
});
|
|
6617
|
+
|
|
6618
|
+
const toolbarEl = document.createElement("div");
|
|
6619
|
+
toolbarEl.className = "mermaid-source-toolbar";
|
|
6620
|
+
toolbarEl.appendChild(copyBtn);
|
|
6621
|
+
|
|
6622
|
+
wrapper.appendChild(toolbarEl);
|
|
5728
6623
|
wrapper.appendChild(diagramEl);
|
|
5729
6624
|
preEl.replaceWith(wrapper);
|
|
5730
6625
|
});
|
|
@@ -5867,12 +6762,12 @@
|
|
|
5867
6762
|
const exportingReplJournal = rightView === "repl";
|
|
5868
6763
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
5869
6764
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
5870
|
-
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL
|
|
6765
|
+
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
|
|
5871
6766
|
return;
|
|
5872
6767
|
}
|
|
5873
6768
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
5874
6769
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
5875
|
-
setStatus("No REPL
|
|
6770
|
+
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
5876
6771
|
return;
|
|
5877
6772
|
}
|
|
5878
6773
|
|
|
@@ -6040,12 +6935,12 @@
|
|
|
6040
6935
|
const exportingReplJournal = rightView === "repl";
|
|
6041
6936
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
6042
6937
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
6043
|
-
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL
|
|
6938
|
+
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
|
|
6044
6939
|
return;
|
|
6045
6940
|
}
|
|
6046
6941
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
6047
6942
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
6048
|
-
setStatus("No REPL
|
|
6943
|
+
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
6049
6944
|
return;
|
|
6050
6945
|
}
|
|
6051
6946
|
|
|
@@ -6067,7 +6962,7 @@
|
|
|
6067
6962
|
? editorHtmlLanguage === "latex"
|
|
6068
6963
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
6069
6964
|
let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
|
|
6070
|
-
let titleHint = exportingReplJournal ? "REPL
|
|
6965
|
+
let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
|
|
6071
6966
|
if (sourcePath) {
|
|
6072
6967
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
6073
6968
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
@@ -6300,6 +7195,9 @@
|
|
|
6300
7195
|
|
|
6301
7196
|
function getCopyablePreviewBlockText(blockEl) {
|
|
6302
7197
|
if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
|
|
7198
|
+
if (blockEl.classList && blockEl.classList.contains("mermaid-container") && blockEl.dataset && typeof blockEl.dataset.mermaidSource === "string") {
|
|
7199
|
+
return normalizeCopyableBlockText(blockEl.dataset.mermaidSource);
|
|
7200
|
+
}
|
|
6303
7201
|
if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
|
|
6304
7202
|
return normalizeCopyableBlockText(
|
|
6305
7203
|
Array.from(blockEl.querySelectorAll(".preview-code-line-content"))
|
|
@@ -6480,7 +7378,7 @@
|
|
|
6480
7378
|
if (editorView !== "preview") return;
|
|
6481
7379
|
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
6482
7380
|
if (isHtmlArtifactPreviewText(text, editorLanguage)) {
|
|
6483
|
-
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview" });
|
|
7381
|
+
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
6484
7382
|
return;
|
|
6485
7383
|
}
|
|
6486
7384
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
@@ -6763,10 +7661,9 @@
|
|
|
6763
7661
|
const visibleEntries = getVisibleReplJournalEntries();
|
|
6764
7662
|
const hasEntries = visibleEntries.length > 0;
|
|
6765
7663
|
const entryCount = visibleEntries.length;
|
|
6766
|
-
const hiddenEntryCount = getHiddenReplJournalEntryCount();
|
|
6767
7664
|
const sessionName = getActiveReplJournalSessionName();
|
|
6768
7665
|
const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
|
|
6769
|
-
const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show
|
|
7666
|
+
const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
|
|
6770
7667
|
const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
|
|
6771
7668
|
const summaryText = hasEntries
|
|
6772
7669
|
? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
|
|
@@ -6774,7 +7671,7 @@
|
|
|
6774
7671
|
if (replJournalCollapsed) {
|
|
6775
7672
|
return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
|
|
6776
7673
|
+ "<div class='repl-journal-compact-row'>"
|
|
6777
|
-
+ "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>REPL
|
|
7674
|
+
+ "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Studio REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
|
|
6778
7675
|
+ "<div class='repl-journal-actions'>" + toggleButton + "</div>"
|
|
6779
7676
|
+ "</div>"
|
|
6780
7677
|
+ "</section>";
|
|
@@ -6812,14 +7709,14 @@
|
|
|
6812
7709
|
}).join("");
|
|
6813
7710
|
const emptyText = sessionName
|
|
6814
7711
|
? (String(transcript || "").trim()
|
|
6815
|
-
? "No REPL
|
|
6816
|
-
: "No REPL
|
|
6817
|
-
: "No REPL
|
|
7712
|
+
? "No Studio REPL record entries yet. The raw tmux mirror below still has this session's history; send code from Studio to build a clean record."
|
|
7713
|
+
: "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
|
|
7714
|
+
: "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
|
|
6818
7715
|
const terminalContent = banner
|
|
6819
7716
|
+ (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
|
|
6820
7717
|
return "<section class='repl-journal'>"
|
|
6821
|
-
+ "<div class='repl-journal-header'><
|
|
6822
|
-
+
|
|
7718
|
+
+ "<div class='repl-journal-header'><h3>Studio REPL Record</h3>" + toggleActions + "</div>"
|
|
7719
|
+
+ "<p class='repl-journal-description'>Clean record for the selected tmux session. Raw tmux mirror below.</p>"
|
|
6823
7720
|
+ (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
|
|
6824
7721
|
+ "<div class='repl-journal-list'>" + terminalContent + "</div>"
|
|
6825
7722
|
+ "</section>";
|
|
@@ -6843,7 +7740,7 @@
|
|
|
6843
7740
|
+ "</section>";
|
|
6844
7741
|
}
|
|
6845
7742
|
return "<section class='repl-mirror'>"
|
|
6846
|
-
+ "<div class='repl-journal-header'><div><h3>Raw REPL
|
|
7743
|
+
+ "<div class='repl-journal-header'><div><h3>Raw REPL Mirror</h3><p>Best-effort tmux pane mirror. Useful for directly typed commands and debugging; the Studio record above is cleaner.</p></div>" + actions + "</div>"
|
|
6847
7744
|
+ body
|
|
6848
7745
|
+ "</section>";
|
|
6849
7746
|
}
|
|
@@ -6863,10 +7760,13 @@
|
|
|
6863
7760
|
["ghci", "GHCi"],
|
|
6864
7761
|
["clojure", "Clojure"],
|
|
6865
7762
|
].map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (replRuntime === value ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
|
|
6866
|
-
const
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
7763
|
+
const runtimeLabel = getReplRuntimeLabel(replRuntime);
|
|
7764
|
+
const visibleSessions = getReplSessionsForRuntime(replRuntime);
|
|
7765
|
+
const sessionOptions = visibleSessions.length
|
|
7766
|
+
? visibleSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
|
|
7767
|
+
: "<option value=''>None</option>";
|
|
7768
|
+
const replCommand = getReplCommandOverride(replRuntime);
|
|
7769
|
+
const activeSession = getActiveReplSessionForCurrentRuntime();
|
|
6870
7770
|
const transcript = trimReplTranscript(replTranscript);
|
|
6871
7771
|
const emptyMessage = replTmuxAvailable === false
|
|
6872
7772
|
? "tmux is not available. Install tmux to use Studio REPL sessions."
|
|
@@ -6880,17 +7780,17 @@
|
|
|
6880
7780
|
+ "<div class='repl-toolbar'>"
|
|
6881
7781
|
+ "<div class='repl-controls'>"
|
|
6882
7782
|
+ "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
|
|
6883
|
-
+ "<
|
|
6884
|
-
+ "<
|
|
7783
|
+
+ "<label class='repl-control-label repl-command-label'>Start cmd <input data-repl-command type='text' value='" + escapeHtml(replCommand) + "' placeholder='default' aria-label='REPL start command' title='Command used by Start for this runtime. Leave blank for the default; use this for envs, e.g. .venv/bin/python, uv run python, or conda run --no-capture-output -n env python.'></label>"
|
|
7784
|
+
+ "<button class='repl-start-btn' type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or switch to a " + escapeHtml(runtimeLabel) + " session using the selected runtime and start command.'>Start</button>"
|
|
7785
|
+
+ "<label class='repl-control-label repl-session-label'>Session <select data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
|
|
6885
7786
|
+ "<details class='repl-more-controls'>"
|
|
6886
7787
|
+ "<summary title='More REPL actions'>More</summary>"
|
|
6887
7788
|
+ "<div class='repl-more-menu'>"
|
|
6888
|
-
+ "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
|
|
6889
7789
|
+ "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
|
|
6890
7790
|
+ "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
|
|
6891
7791
|
+ "<button type='button' data-repl-action='copy-attach-command'" + (activeSession ? "" : " disabled") + " title='Copy command for attaching to this tmux session in a terminal.'>Copy attach command</button>"
|
|
6892
7792
|
+ "<button type='button' data-repl-action='run-all-chunks'" + (canSendToActiveSession ? "" : " disabled") + " title='Literate send: send all fenced code chunks matching the active REPL runtime.'>Run all chunks</button>"
|
|
6893
|
-
+ "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to
|
|
7793
|
+
+ "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to the Studio REPL record (Literate send) without sending it to the runtime.'>Add note</button>"
|
|
6894
7794
|
+ "<button type='button' data-repl-action='refresh'>Refresh</button>"
|
|
6895
7795
|
+ "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
|
|
6896
7796
|
+ "</div>"
|
|
@@ -7074,7 +7974,7 @@
|
|
|
7074
7974
|
return;
|
|
7075
7975
|
}
|
|
7076
7976
|
if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
|
|
7077
|
-
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview" });
|
|
7977
|
+
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
7078
7978
|
return;
|
|
7079
7979
|
}
|
|
7080
7980
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
@@ -7098,7 +7998,7 @@
|
|
|
7098
7998
|
|
|
7099
7999
|
if (rightView === "preview") {
|
|
7100
8000
|
if (isHtmlArtifactPreviewText(markdown, "")) {
|
|
7101
|
-
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview" });
|
|
8001
|
+
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
7102
8002
|
return;
|
|
7103
8003
|
}
|
|
7104
8004
|
const nonce = ++responsePreviewRenderNonce;
|
|
@@ -7177,19 +8077,19 @@
|
|
|
7177
8077
|
exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
7178
8078
|
exportPdfBtn.textContent = previewExportInProgress
|
|
7179
8079
|
? "Exporting…"
|
|
7180
|
-
: (exportingReplJournal ? "Export
|
|
8080
|
+
: (exportingReplJournal ? "Export record" : "Export right preview");
|
|
7181
8081
|
if (rightView === "trace") {
|
|
7182
8082
|
exportPdfBtn.title = "Working view does not support preview export.";
|
|
7183
8083
|
} else if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
7184
|
-
exportPdfBtn.title = "No REPL
|
|
8084
|
+
exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
|
|
7185
8085
|
} else if (rightView === "markdown") {
|
|
7186
|
-
exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL
|
|
8086
|
+
exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL to export.";
|
|
7187
8087
|
} else if (!canExportPreview) {
|
|
7188
8088
|
exportPdfBtn.title = "Nothing to export yet.";
|
|
7189
8089
|
} else if (isHtmlArtifactPreview) {
|
|
7190
8090
|
exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
|
|
7191
8091
|
} else if (exportingReplJournal) {
|
|
7192
|
-
exportPdfBtn.title = "Choose PDF or HTML and export REPL
|
|
8092
|
+
exportPdfBtn.title = "Choose PDF or HTML and export the Studio REPL record.";
|
|
7193
8093
|
} else {
|
|
7194
8094
|
exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
|
|
7195
8095
|
}
|
|
@@ -7198,20 +8098,20 @@
|
|
|
7198
8098
|
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
7199
8099
|
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
7200
8100
|
? "Interactive HTML preview PDF export is not available yet."
|
|
7201
|
-
: (exportingReplJournal ? "Export
|
|
8101
|
+
: (exportingReplJournal ? "Export the Studio REPL record as PDF." : "Export the current right-pane preview as PDF.");
|
|
7202
8102
|
}
|
|
7203
8103
|
if (exportPreviewHtmlBtn) {
|
|
7204
8104
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
7205
8105
|
exportPreviewHtmlBtn.title = isHtmlArtifactPreview
|
|
7206
8106
|
? "Export the authored HTML preview."
|
|
7207
|
-
: (exportingReplJournal ? "Export
|
|
8107
|
+
: (exportingReplJournal ? "Export the Studio REPL record as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
|
|
7208
8108
|
}
|
|
7209
8109
|
if (exportPreviewControlsEl) {
|
|
7210
8110
|
exportPreviewControlsEl.title = canExportPreview
|
|
7211
8111
|
? (exportingReplJournal
|
|
7212
|
-
? "Choose a format and export REPL
|
|
8112
|
+
? "Choose a format and export the Studio REPL record."
|
|
7213
8113
|
: (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
|
|
7214
|
-
: (exportingReplJournal ? "No REPL
|
|
8114
|
+
: (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
|
|
7215
8115
|
}
|
|
7216
8116
|
if (!canExportPreview || previewExportInProgress) {
|
|
7217
8117
|
closeExportPreviewMenu();
|
|
@@ -7242,6 +8142,15 @@
|
|
|
7242
8142
|
return null;
|
|
7243
8143
|
}
|
|
7244
8144
|
|
|
8145
|
+
function getHtmlPreviewResourceContextOptions() {
|
|
8146
|
+
const sourcePath = getEffectiveSavePath() || sourceState.path || "";
|
|
8147
|
+
const resourceDir = resourceDirInput && resourceDirInput.value.trim() ? resourceDirInput.value.trim() : "";
|
|
8148
|
+
return {
|
|
8149
|
+
sourcePath,
|
|
8150
|
+
resourceDir: sourcePath ? "" : resourceDir,
|
|
8151
|
+
};
|
|
8152
|
+
}
|
|
8153
|
+
|
|
7245
8154
|
function buildAnnotatedSaveSuggestion() {
|
|
7246
8155
|
const effectivePath = getEffectiveSavePath() || sourceState.path || "";
|
|
7247
8156
|
if (effectivePath) {
|
|
@@ -13835,7 +14744,7 @@
|
|
|
13835
14744
|
sendRunBtn.classList.toggle("request-stop-active", directIsStop);
|
|
13836
14745
|
sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
|
|
13837
14746
|
sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
|
|
13838
|
-
const replHint = rightView === "repl" &&
|
|
14747
|
+
const replHint = rightView === "repl" && getActiveReplSessionForCurrentRuntime()
|
|
13839
14748
|
? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
|
|
13840
14749
|
: "";
|
|
13841
14750
|
sendRunBtn.title = directIsStop
|
|
@@ -13856,7 +14765,7 @@
|
|
|
13856
14765
|
: "Queue steering is available while Run editor text is active.";
|
|
13857
14766
|
}
|
|
13858
14767
|
|
|
13859
|
-
const hasReplSession = Boolean(
|
|
14768
|
+
const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
|
|
13860
14769
|
if (sendReplBtn) {
|
|
13861
14770
|
const showReplSend = rightView === "repl";
|
|
13862
14771
|
sendReplBtn.hidden = !showReplSend;
|
|
@@ -14224,7 +15133,7 @@
|
|
|
14224
15133
|
replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
|
|
14225
15134
|
const sessionsChanged = setReplSessions(message.sessions);
|
|
14226
15135
|
if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
|
|
14227
|
-
|
|
15136
|
+
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
14228
15137
|
}
|
|
14229
15138
|
const journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
14230
15139
|
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
@@ -14277,7 +15186,7 @@
|
|
|
14277
15186
|
}
|
|
14278
15187
|
}
|
|
14279
15188
|
if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
|
|
14280
|
-
|
|
15189
|
+
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
14281
15190
|
}
|
|
14282
15191
|
let journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
14283
15192
|
if (typeof message.transcript === "string") {
|
|
@@ -15092,6 +16001,8 @@
|
|
|
15092
16001
|
rightPaneEl.addEventListener("focusin", (event) => activatePaneFromInteraction("right", event));
|
|
15093
16002
|
}
|
|
15094
16003
|
|
|
16004
|
+
setupPaneResizeHandle();
|
|
16005
|
+
|
|
15095
16006
|
if (leftFocusBtn) {
|
|
15096
16007
|
leftFocusBtn.addEventListener("click", () => {
|
|
15097
16008
|
if (paneFocusTarget === "left") {
|