pi-studio 0.9.11 → 0.9.13
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 +25 -0
- package/README.md +3 -1
- package/client/studio-client.js +1337 -114
- package/client/studio.css +236 -33
- package/index.ts +673 -39
- 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");
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
const saveAsBtn = document.getElementById("saveAsBtn");
|
|
103
104
|
const saveOverBtn = document.getElementById("saveOverBtn");
|
|
104
105
|
const refreshFromDiskBtn = document.getElementById("refreshFromDiskBtn");
|
|
106
|
+
const clearWorkspaceBtn = document.getElementById("clearWorkspaceBtn");
|
|
105
107
|
const sendEditorBtn = document.getElementById("sendEditorBtn");
|
|
106
108
|
const openCompanionBtn = document.getElementById("openCompanionBtn");
|
|
107
109
|
const getEditorBtn = document.getElementById("getEditorBtn");
|
|
@@ -246,6 +248,7 @@
|
|
|
246
248
|
const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
|
|
247
249
|
const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
|
|
248
250
|
const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
|
|
251
|
+
const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
249
252
|
const EDITOR_TAB_TEXT = " ";
|
|
250
253
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
251
254
|
const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
|
|
@@ -288,6 +291,15 @@
|
|
|
288
291
|
return "python";
|
|
289
292
|
}
|
|
290
293
|
})();
|
|
294
|
+
let replCommandOverrides = (() => {
|
|
295
|
+
try {
|
|
296
|
+
const raw = window.localStorage && window.localStorage.getItem("piStudio.replCommandOverrides");
|
|
297
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
298
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
299
|
+
} catch {
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
291
303
|
let replTranscript = "";
|
|
292
304
|
let replError = "";
|
|
293
305
|
let replMessage = "";
|
|
@@ -952,11 +964,26 @@
|
|
|
952
964
|
setStatus("Loaded visible working into editor.", "success");
|
|
953
965
|
}
|
|
954
966
|
|
|
955
|
-
function
|
|
967
|
+
function getKnownReplRuntime(value) {
|
|
956
968
|
const runtime = String(value || "").trim().toLowerCase();
|
|
957
969
|
return runtime === "shell" || runtime === "python" || runtime === "ipython" || runtime === "julia" || runtime === "r" || runtime === "ghci" || runtime === "clojure"
|
|
958
970
|
? runtime
|
|
959
|
-
: "
|
|
971
|
+
: "";
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function normalizeReplRuntime(value) {
|
|
975
|
+
return getKnownReplRuntime(value) || "python";
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function getReplRuntimeLabel(value) {
|
|
979
|
+
const runtime = normalizeReplRuntime(value);
|
|
980
|
+
if (runtime === "shell") return "Shell";
|
|
981
|
+
if (runtime === "python") return "Python";
|
|
982
|
+
if (runtime === "ipython") return "IPython";
|
|
983
|
+
if (runtime === "julia") return "Julia";
|
|
984
|
+
if (runtime === "r") return "R";
|
|
985
|
+
if (runtime === "ghci") return "GHCi";
|
|
986
|
+
return "Clojure";
|
|
960
987
|
}
|
|
961
988
|
|
|
962
989
|
function normalizeReplSession(session) {
|
|
@@ -981,6 +1008,47 @@
|
|
|
981
1008
|
}
|
|
982
1009
|
}
|
|
983
1010
|
|
|
1011
|
+
function normalizeReplCommandOverride(value) {
|
|
1012
|
+
return String(value || "").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function getReplCommandOverride(runtime) {
|
|
1016
|
+
const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
|
|
1017
|
+
const value = replCommandOverrides && typeof replCommandOverrides === "object"
|
|
1018
|
+
? replCommandOverrides[normalizedRuntime]
|
|
1019
|
+
: "";
|
|
1020
|
+
return normalizeReplCommandOverride(value);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function persistReplCommandOverrides() {
|
|
1024
|
+
try {
|
|
1025
|
+
if (window.localStorage) window.localStorage.setItem("piStudio.replCommandOverrides", JSON.stringify(replCommandOverrides || {}));
|
|
1026
|
+
} catch {
|
|
1027
|
+
// Ignore storage failures.
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function setReplCommandOverride(runtime, command) {
|
|
1032
|
+
const normalizedRuntime = normalizeReplRuntime(runtime || replRuntime);
|
|
1033
|
+
const normalizedCommand = normalizeReplCommandOverride(command);
|
|
1034
|
+
replCommandOverrides = replCommandOverrides && typeof replCommandOverrides === "object" ? replCommandOverrides : {};
|
|
1035
|
+
if (normalizedCommand) {
|
|
1036
|
+
replCommandOverrides[normalizedRuntime] = normalizedCommand;
|
|
1037
|
+
} else {
|
|
1038
|
+
delete replCommandOverrides[normalizedRuntime];
|
|
1039
|
+
}
|
|
1040
|
+
persistReplCommandOverrides();
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function getCurrentReplStartCommandFromDom() {
|
|
1044
|
+
if (!critiqueViewEl || typeof critiqueViewEl.querySelector !== "function") return getReplCommandOverride(replRuntime);
|
|
1045
|
+
const inputEl = critiqueViewEl.querySelector("[data-repl-command]");
|
|
1046
|
+
if (inputEl && "value" in inputEl) {
|
|
1047
|
+
return normalizeReplCommandOverride(inputEl.value);
|
|
1048
|
+
}
|
|
1049
|
+
return getReplCommandOverride(replRuntime);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
984
1052
|
function normalizeReplSendMode(value) {
|
|
985
1053
|
return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
|
|
986
1054
|
}
|
|
@@ -1056,7 +1124,10 @@
|
|
|
1056
1124
|
? sessions.map(normalizeReplSession).filter(Boolean)
|
|
1057
1125
|
: [];
|
|
1058
1126
|
if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
|
|
1059
|
-
|
|
1127
|
+
setActiveReplSession("");
|
|
1128
|
+
}
|
|
1129
|
+
if (!getActiveReplSessionForCurrentRuntime()) {
|
|
1130
|
+
selectReplSessionForRuntime(replRuntime, replActiveSessionName);
|
|
1060
1131
|
}
|
|
1061
1132
|
return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
|
|
1062
1133
|
}
|
|
@@ -1065,9 +1136,43 @@
|
|
|
1065
1136
|
return replSessions.find((session) => session.sessionName === replActiveSessionName) || null;
|
|
1066
1137
|
}
|
|
1067
1138
|
|
|
1139
|
+
function isReplSessionRuntimeCompatible(session, runtime) {
|
|
1140
|
+
if (!session) return false;
|
|
1141
|
+
const sessionRuntime = getKnownReplRuntime(session.runtime);
|
|
1142
|
+
return Boolean(sessionRuntime) && sessionRuntime === normalizeReplRuntime(runtime);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function getReplSessionsForRuntime(runtime) {
|
|
1146
|
+
const normalizedRuntime = normalizeReplRuntime(runtime);
|
|
1147
|
+
return replSessions.filter((session) => isReplSessionRuntimeCompatible(session, normalizedRuntime));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function selectReplSessionForRuntime(runtime, preferredSessionName) {
|
|
1151
|
+
const sessions = getReplSessionsForRuntime(runtime);
|
|
1152
|
+
const preferred = String(preferredSessionName || "").trim();
|
|
1153
|
+
const selected = sessions.find((session) => session.sessionName === preferred) || sessions[0] || null;
|
|
1154
|
+
setActiveReplSession(selected ? selected.sessionName : "");
|
|
1155
|
+
return selected;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function setActiveReplSessionForCurrentRuntime(sessionName) {
|
|
1159
|
+
const name = String(sessionName || "").trim();
|
|
1160
|
+
const candidate = name ? replSessions.find((session) => session.sessionName === name) : null;
|
|
1161
|
+
if (candidate && isReplSessionRuntimeCompatible(candidate, replRuntime)) {
|
|
1162
|
+
setActiveReplSession(candidate.sessionName);
|
|
1163
|
+
return candidate;
|
|
1164
|
+
}
|
|
1165
|
+
return selectReplSessionForRuntime(replRuntime, replActiveSessionName);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function getActiveReplSessionForCurrentRuntime() {
|
|
1169
|
+
const session = getActiveReplSession();
|
|
1170
|
+
return isReplSessionRuntimeCompatible(session, replRuntime) ? session : null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1068
1173
|
function buildActiveReplPromptContext() {
|
|
1069
1174
|
if (rightView !== "repl") return "";
|
|
1070
|
-
const session =
|
|
1175
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1071
1176
|
if (!session) return "";
|
|
1072
1177
|
const runtime = session.runtime && session.runtime !== "unknown" ? session.runtime : "unknown";
|
|
1073
1178
|
return [
|
|
@@ -1162,7 +1267,7 @@
|
|
|
1162
1267
|
}
|
|
1163
1268
|
|
|
1164
1269
|
function getActiveReplRuntime() {
|
|
1165
|
-
const session =
|
|
1270
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1166
1271
|
if (session && session.runtime && session.runtime !== "unknown") return normalizeReplRuntime(session.runtime);
|
|
1167
1272
|
return normalizeReplRuntime(replRuntime);
|
|
1168
1273
|
}
|
|
@@ -1502,7 +1607,7 @@
|
|
|
1502
1607
|
|
|
1503
1608
|
function stripStudioReplSubmissionEcho(delta) {
|
|
1504
1609
|
let value = String(delta || "").replace(/^\s+/, "");
|
|
1505
|
-
// The raw mirror below remains raw;
|
|
1610
|
+
// The raw mirror below remains raw; Studio record cards hide only the
|
|
1506
1611
|
// temp-file wrapper used to submit multiline snippets safely. The
|
|
1507
1612
|
// pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
|
|
1508
1613
|
const submissionEchoPatterns = [
|
|
@@ -1566,11 +1671,11 @@
|
|
|
1566
1671
|
function buildReplJournalMarkdown(entries) {
|
|
1567
1672
|
const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
|
|
1568
1673
|
const sessionName = getActiveReplJournalSessionName();
|
|
1569
|
-
const lines = ["# REPL
|
|
1674
|
+
const lines = ["# Studio REPL Record", "", "Generated: " + new Date().toLocaleString()];
|
|
1570
1675
|
if (sessionName) lines.push("Session: `" + sessionName + "`");
|
|
1571
1676
|
lines.push("");
|
|
1572
1677
|
if (!visibleEntries.length) {
|
|
1573
|
-
lines.push(sessionName ? ("_No REPL
|
|
1678
|
+
lines.push(sessionName ? ("_No Studio REPL record entries for `" + sessionName + "` yet._") : "_No Studio REPL record entries yet._");
|
|
1574
1679
|
return lines.join("\n");
|
|
1575
1680
|
}
|
|
1576
1681
|
visibleEntries.forEach((entry, index) => {
|
|
@@ -1602,11 +1707,11 @@
|
|
|
1602
1707
|
async function copyReplJournalToClipboard() {
|
|
1603
1708
|
const entries = getVisibleReplJournalEntries();
|
|
1604
1709
|
if (!entries.length) {
|
|
1605
|
-
setStatus("No REPL
|
|
1710
|
+
setStatus("No Studio REPL record entries to copy for this session yet.", "warning");
|
|
1606
1711
|
return;
|
|
1607
1712
|
}
|
|
1608
1713
|
if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
|
|
1609
|
-
setStatus("Copied REPL
|
|
1714
|
+
setStatus("Copied Studio REPL record as Markdown.", "success");
|
|
1610
1715
|
} else {
|
|
1611
1716
|
setStatus("Clipboard write failed.", "warning");
|
|
1612
1717
|
}
|
|
@@ -1615,7 +1720,7 @@
|
|
|
1615
1720
|
function exportReplJournalMarkdown() {
|
|
1616
1721
|
const entries = getVisibleReplJournalEntries();
|
|
1617
1722
|
if (!entries.length) {
|
|
1618
|
-
setStatus("No REPL
|
|
1723
|
+
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
1619
1724
|
return;
|
|
1620
1725
|
}
|
|
1621
1726
|
const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
|
|
@@ -1629,7 +1734,7 @@
|
|
|
1629
1734
|
link.click();
|
|
1630
1735
|
link.remove();
|
|
1631
1736
|
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
|
1632
|
-
setStatus("Exported REPL
|
|
1737
|
+
setStatus("Exported Studio REPL record Markdown.", "success");
|
|
1633
1738
|
}
|
|
1634
1739
|
|
|
1635
1740
|
function clearReplJournal() {
|
|
@@ -1641,27 +1746,27 @@
|
|
|
1641
1746
|
}
|
|
1642
1747
|
activeReplJournalEntryId = "";
|
|
1643
1748
|
persistReplJournalEntries();
|
|
1644
|
-
setStatus(sessionName ? "Cleared REPL
|
|
1749
|
+
setStatus(sessionName ? "Cleared Studio REPL record for this session." : "Cleared Studio REPL record.", "success");
|
|
1645
1750
|
renderReplViewIfActive({ force: true });
|
|
1646
1751
|
}
|
|
1647
1752
|
|
|
1648
1753
|
function loadReplJournalIntoEditor() {
|
|
1649
1754
|
const entries = getVisibleReplJournalEntries();
|
|
1650
1755
|
if (!entries.length) {
|
|
1651
|
-
setStatus("No REPL
|
|
1756
|
+
setStatus("No Studio REPL record entries to load for this session yet.", "warning");
|
|
1652
1757
|
return;
|
|
1653
1758
|
}
|
|
1654
1759
|
const markdown = buildReplJournalMarkdown(entries);
|
|
1655
1760
|
setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
|
|
1656
|
-
setSourceState({ source: "blank", label: "REPL
|
|
1761
|
+
setSourceState({ source: "blank", label: "Studio REPL Record", path: null });
|
|
1657
1762
|
setEditorLanguage("markdown");
|
|
1658
|
-
setStatus("Loaded REPL
|
|
1763
|
+
setStatus("Loaded Studio REPL record into editor.", "success");
|
|
1659
1764
|
}
|
|
1660
1765
|
|
|
1661
1766
|
function addSelectedReplJournalNote() {
|
|
1662
1767
|
const note = getSelectedOrCurrentParagraphForReplNote();
|
|
1663
1768
|
if (!note.trim()) {
|
|
1664
|
-
setStatus("Select prose or place the cursor in a paragraph to add a REPL
|
|
1769
|
+
setStatus("Select prose or place the cursor in a paragraph to add a Studio REPL record note.", "warning");
|
|
1665
1770
|
return;
|
|
1666
1771
|
}
|
|
1667
1772
|
addReplJournalEntry({
|
|
@@ -1672,14 +1777,14 @@
|
|
|
1672
1777
|
sessionName: replActiveSessionName,
|
|
1673
1778
|
runtime: getActiveReplRuntime(),
|
|
1674
1779
|
});
|
|
1675
|
-
setStatus("Added note to REPL
|
|
1780
|
+
setStatus("Added note to Studio REPL record.", "success");
|
|
1676
1781
|
renderReplViewIfActive({ force: true });
|
|
1677
1782
|
}
|
|
1678
1783
|
|
|
1679
1784
|
function sendReplPayload(payload) {
|
|
1680
|
-
const session =
|
|
1785
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1681
1786
|
if (!session) {
|
|
1682
|
-
setStatus("Start or select a REPL session first.", "warning");
|
|
1787
|
+
setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
|
|
1683
1788
|
return;
|
|
1684
1789
|
}
|
|
1685
1790
|
if (!payload || payload.error) {
|
|
@@ -1697,7 +1802,7 @@
|
|
|
1697
1802
|
runtime: getActiveReplRuntime(),
|
|
1698
1803
|
skippedChunks: payload.skippedChunks,
|
|
1699
1804
|
});
|
|
1700
|
-
setStatus("Added prose to REPL
|
|
1805
|
+
setStatus("Added prose to Studio REPL record.", "success");
|
|
1701
1806
|
renderReplViewIfActive({ force: true });
|
|
1702
1807
|
} else {
|
|
1703
1808
|
setStatus("No code or prose found to send.", "warning");
|
|
@@ -1771,6 +1876,13 @@
|
|
|
1771
1876
|
let fileBackedBaselineText = null;
|
|
1772
1877
|
let activePane = "left";
|
|
1773
1878
|
let paneFocusTarget = "off";
|
|
1879
|
+
let paneSplitPercent = 50;
|
|
1880
|
+
const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
|
|
1881
|
+
const PANE_SPLIT_MIN_PERCENT = 20;
|
|
1882
|
+
const PANE_SPLIT_MAX_PERCENT = 80;
|
|
1883
|
+
const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
|
|
1884
|
+
const STUDIO_WORKSPACE_STORAGE_KEY = "piStudio.workspaceState.v1";
|
|
1885
|
+
const STUDIO_WORKSPACE_MAX_TEXT_CHARS = 900_000;
|
|
1774
1886
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
|
|
1775
1887
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
1776
1888
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
@@ -1865,6 +1977,9 @@
|
|
|
1865
1977
|
let pendingReviewNoteInlineFocusId = null;
|
|
1866
1978
|
let activePreviewCommentSelection = null;
|
|
1867
1979
|
let suppressEditorSelectionComment = false;
|
|
1980
|
+
let workspacePersistenceReady = false;
|
|
1981
|
+
let workspacePersistTimer = null;
|
|
1982
|
+
let workspaceRestoredFromBrowser = false;
|
|
1868
1983
|
let suppressedEditorSelectionStart = null;
|
|
1869
1984
|
let suppressedEditorSelectionEnd = null;
|
|
1870
1985
|
const previewJumpHighlightState = new WeakMap();
|
|
@@ -2071,12 +2186,8 @@
|
|
|
2071
2186
|
if (!studioUiRefreshUi) return;
|
|
2072
2187
|
if (studioUiRefreshUi.annotationsButton) {
|
|
2073
2188
|
const inlineLabel = annotationsEnabled ? "Inline on" : "Inline hidden";
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
} else {
|
|
2077
|
-
const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
|
|
2078
|
-
setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
|
|
2079
|
-
}
|
|
2189
|
+
const headerLabel = getStudioUiRefreshAnnotationHeaderEnabled() ? "Header on" : "Header off";
|
|
2190
|
+
setStudioUiRefreshButtonText(studioUiRefreshUi.annotationsButton, "Annotations: " + inlineLabel + " · " + headerLabel);
|
|
2080
2191
|
}
|
|
2081
2192
|
if (studioUiRefreshUi.viewButton) {
|
|
2082
2193
|
const syntaxLabel = editorHighlightEnabled
|
|
@@ -2240,7 +2351,7 @@
|
|
|
2240
2351
|
const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
|
|
2241
2352
|
const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
|
|
2242
2353
|
const annotationsMenu = makeStudioUiRefreshMenu(annotationsButton, "annotations", "studio-refresh-annotations-anchor");
|
|
2243
|
-
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display",
|
|
2354
|
+
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Display", [annotationModeSelect, insertHeaderBtn]);
|
|
2244
2355
|
appendStudioUiRefreshMenuSection(annotationsMenu.menu, "Actions", [stripAnnotationsBtn, saveAnnotatedBtn]);
|
|
2245
2356
|
const viewButton = makeStudioUiRefreshElement("button", "", "View");
|
|
2246
2357
|
const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
|
|
@@ -2431,7 +2542,7 @@
|
|
|
2431
2542
|
|
|
2432
2543
|
function getTerminalBusyStatus() {
|
|
2433
2544
|
if (terminalActivityPhase === "tool") {
|
|
2434
|
-
if (terminalActivityLabel) {
|
|
2545
|
+
if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
|
|
2435
2546
|
return "Terminal: " + withEllipsis(terminalActivityLabel);
|
|
2436
2547
|
}
|
|
2437
2548
|
return terminalActivityToolName
|
|
@@ -2475,7 +2586,7 @@
|
|
|
2475
2586
|
const action = getStudioActionLabel(kind);
|
|
2476
2587
|
const queueSuffix = studioRunChainActive ? formatQueuedSteeringSuffix() : "";
|
|
2477
2588
|
if (terminalActivityPhase === "tool") {
|
|
2478
|
-
if (terminalActivityLabel) {
|
|
2589
|
+
if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
|
|
2479
2590
|
return "Studio: " + withEllipsis(terminalActivityLabel) + queueSuffix;
|
|
2480
2591
|
}
|
|
2481
2592
|
return terminalActivityToolName
|
|
@@ -2962,14 +3073,16 @@
|
|
|
2962
3073
|
// Show "Set working dir" button when not file-backed
|
|
2963
3074
|
var isFileBacked = hasRefreshableFilePath();
|
|
2964
3075
|
if (isFileBacked) {
|
|
2965
|
-
|
|
3076
|
+
var fileBackedResourceDir = getCurrentResourceDirValue() || dirnameForDisplayPath(sourceState.path);
|
|
3077
|
+
if (resourceDirInput) resourceDirInput.value = fileBackedResourceDir;
|
|
2966
3078
|
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
2967
3079
|
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
2968
3080
|
if (resourceDirLabel) resourceDirLabel.hidden = true;
|
|
2969
3081
|
if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
|
|
2970
3082
|
} else {
|
|
2971
3083
|
// Restore to label if dir is set, otherwise show button
|
|
2972
|
-
var dir =
|
|
3084
|
+
var dir = getCurrentResourceDirValue();
|
|
3085
|
+
if (resourceDirInput) resourceDirInput.value = dir;
|
|
2973
3086
|
if (dir) {
|
|
2974
3087
|
if (resourceDirBtn) resourceDirBtn.hidden = true;
|
|
2975
3088
|
if (resourceDirLabel) { resourceDirLabel.textContent = "Working dir: " + dir; resourceDirLabel.hidden = false; }
|
|
@@ -3002,6 +3115,141 @@
|
|
|
3002
3115
|
setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
|
|
3003
3116
|
}
|
|
3004
3117
|
|
|
3118
|
+
function clampPaneSplitPercent(value) {
|
|
3119
|
+
const numeric = Number(value);
|
|
3120
|
+
if (!Number.isFinite(numeric)) return 50;
|
|
3121
|
+
const clamped = Math.max(PANE_SPLIT_MIN_PERCENT, Math.min(PANE_SPLIT_MAX_PERCENT, Math.round(numeric * 10) / 10));
|
|
3122
|
+
return Math.abs(clamped - 50) <= PANE_SPLIT_SNAP_TO_CENTER_PERCENT ? 50 : clamped;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
function applyPaneSplitPercent(percent, options) {
|
|
3126
|
+
paneSplitPercent = clampPaneSplitPercent(percent);
|
|
3127
|
+
const rightPercent = Math.round((100 - paneSplitPercent) * 10) / 10;
|
|
3128
|
+
document.documentElement.style.setProperty("--studio-left-pane-fr", paneSplitPercent + "fr");
|
|
3129
|
+
document.documentElement.style.setProperty("--studio-right-pane-fr", rightPercent + "fr");
|
|
3130
|
+
if (paneResizeHandleEl) {
|
|
3131
|
+
paneResizeHandleEl.setAttribute("aria-valuemin", String(PANE_SPLIT_MIN_PERCENT));
|
|
3132
|
+
paneResizeHandleEl.setAttribute("aria-valuemax", String(PANE_SPLIT_MAX_PERCENT));
|
|
3133
|
+
paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(paneSplitPercent)));
|
|
3134
|
+
paneResizeHandleEl.setAttribute("aria-valuetext", "Editor " + Math.round(paneSplitPercent) + " percent, response " + Math.round(rightPercent) + " percent");
|
|
3135
|
+
}
|
|
3136
|
+
if (!options || options.persist !== false) {
|
|
3137
|
+
try {
|
|
3138
|
+
if (window.localStorage) window.localStorage.setItem(PANE_SPLIT_STORAGE_KEY, String(paneSplitPercent));
|
|
3139
|
+
} catch {
|
|
3140
|
+
// Ignore localStorage failures.
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function resetPaneSplitPercent() {
|
|
3146
|
+
applyPaneSplitPercent(50);
|
|
3147
|
+
setStatus("Pane split reset to 50/50.");
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function loadPaneSplitPercent() {
|
|
3151
|
+
let stored = "";
|
|
3152
|
+
try {
|
|
3153
|
+
stored = window.localStorage ? String(window.localStorage.getItem(PANE_SPLIT_STORAGE_KEY) || "") : "";
|
|
3154
|
+
} catch {
|
|
3155
|
+
stored = "";
|
|
3156
|
+
}
|
|
3157
|
+
applyPaneSplitPercent(stored ? Number(stored) : 50, { persist: false });
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
function getPaneSplitPercentFromPointerEvent(event) {
|
|
3161
|
+
const mainEl = paneResizeHandleEl && typeof paneResizeHandleEl.closest === "function"
|
|
3162
|
+
? paneResizeHandleEl.closest("main")
|
|
3163
|
+
: null;
|
|
3164
|
+
if (!mainEl || typeof mainEl.getBoundingClientRect !== "function") return paneSplitPercent;
|
|
3165
|
+
const rect = mainEl.getBoundingClientRect();
|
|
3166
|
+
if (!rect.width) return paneSplitPercent;
|
|
3167
|
+
const x = typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2);
|
|
3168
|
+
return ((x - rect.left) / rect.width) * 100;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
function setupPaneResizeHandle() {
|
|
3172
|
+
if (!paneResizeHandleEl) return;
|
|
3173
|
+
loadPaneSplitPercent();
|
|
3174
|
+
let dragging = false;
|
|
3175
|
+
let movedDuringDrag = false;
|
|
3176
|
+
let pointerStartX = 0;
|
|
3177
|
+
let activePaneResizePointerId = null;
|
|
3178
|
+
const finishDrag = () => {
|
|
3179
|
+
if (!dragging) return;
|
|
3180
|
+
dragging = false;
|
|
3181
|
+
if (document.body && document.body.classList) document.body.classList.remove("pane-resizing");
|
|
3182
|
+
try {
|
|
3183
|
+
if (typeof paneResizeHandleEl.releasePointerCapture === "function" && activePaneResizePointerId != null) {
|
|
3184
|
+
paneResizeHandleEl.releasePointerCapture(activePaneResizePointerId);
|
|
3185
|
+
}
|
|
3186
|
+
} catch {
|
|
3187
|
+
// Ignore pointer-capture cleanup failures.
|
|
3188
|
+
}
|
|
3189
|
+
activePaneResizePointerId = null;
|
|
3190
|
+
if (movedDuringDrag) {
|
|
3191
|
+
setStatus("Pane split: editor " + Math.round(paneSplitPercent) + "%, response " + Math.round(100 - paneSplitPercent) + "%.");
|
|
3192
|
+
}
|
|
3193
|
+
movedDuringDrag = false;
|
|
3194
|
+
};
|
|
3195
|
+
paneResizeHandleEl.addEventListener("pointerdown", (event) => {
|
|
3196
|
+
if (event.button != null && event.button !== 0) return;
|
|
3197
|
+
event.preventDefault();
|
|
3198
|
+
event.stopPropagation();
|
|
3199
|
+
dragging = true;
|
|
3200
|
+
movedDuringDrag = false;
|
|
3201
|
+
pointerStartX = typeof event.clientX === "number" ? event.clientX : 0;
|
|
3202
|
+
activePaneResizePointerId = event.pointerId;
|
|
3203
|
+
if (document.body && document.body.classList) document.body.classList.add("pane-resizing");
|
|
3204
|
+
try {
|
|
3205
|
+
if (typeof paneResizeHandleEl.focus === "function") paneResizeHandleEl.focus({ preventScroll: true });
|
|
3206
|
+
} catch {
|
|
3207
|
+
try { paneResizeHandleEl.focus(); } catch {}
|
|
3208
|
+
}
|
|
3209
|
+
try {
|
|
3210
|
+
if (typeof paneResizeHandleEl.setPointerCapture === "function") paneResizeHandleEl.setPointerCapture(event.pointerId);
|
|
3211
|
+
} catch {
|
|
3212
|
+
// Ignore pointer-capture failures.
|
|
3213
|
+
}
|
|
3214
|
+
});
|
|
3215
|
+
paneResizeHandleEl.addEventListener("pointermove", (event) => {
|
|
3216
|
+
if (!dragging) return;
|
|
3217
|
+
const movement = typeof event.clientX === "number" ? Math.abs(event.clientX - pointerStartX) : 0;
|
|
3218
|
+
if (!movedDuringDrag && movement < 3) return;
|
|
3219
|
+
movedDuringDrag = true;
|
|
3220
|
+
event.preventDefault();
|
|
3221
|
+
applyPaneSplitPercent(getPaneSplitPercentFromPointerEvent(event));
|
|
3222
|
+
});
|
|
3223
|
+
paneResizeHandleEl.addEventListener("pointerup", finishDrag);
|
|
3224
|
+
paneResizeHandleEl.addEventListener("pointercancel", finishDrag);
|
|
3225
|
+
paneResizeHandleEl.addEventListener("dblclick", (event) => {
|
|
3226
|
+
event.preventDefault();
|
|
3227
|
+
event.stopPropagation();
|
|
3228
|
+
resetPaneSplitPercent();
|
|
3229
|
+
});
|
|
3230
|
+
paneResizeHandleEl.addEventListener("keydown", (event) => {
|
|
3231
|
+
if (event.key === "Home") {
|
|
3232
|
+
event.preventDefault();
|
|
3233
|
+
applyPaneSplitPercent(PANE_SPLIT_MIN_PERCENT);
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
if (event.key === "End") {
|
|
3237
|
+
event.preventDefault();
|
|
3238
|
+
applyPaneSplitPercent(PANE_SPLIT_MAX_PERCENT);
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
3242
|
+
event.preventDefault();
|
|
3243
|
+
resetPaneSplitPercent();
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
3247
|
+
event.preventDefault();
|
|
3248
|
+
const step = event.shiftKey ? 10 : 5;
|
|
3249
|
+
applyPaneSplitPercent(paneSplitPercent + (event.key === "ArrowLeft" ? -step : step));
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3005
3253
|
function updatePaneFocusButtons() {
|
|
3006
3254
|
[
|
|
3007
3255
|
[leftFocusBtn, "left"],
|
|
@@ -3102,10 +3350,6 @@
|
|
|
3102
3350
|
|
|
3103
3351
|
function activatePaneFromShortcut(nextPane) {
|
|
3104
3352
|
const pane = nextPane === "right" ? "right" : "left";
|
|
3105
|
-
if (isEditorOnlyMode && pane === "right") {
|
|
3106
|
-
setStatus("Only the editor pane is available in editor-only Studio.", "warning");
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3109
3353
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3110
3354
|
setActivePane(pane);
|
|
3111
3355
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
@@ -3148,10 +3392,6 @@
|
|
|
3148
3392
|
}
|
|
3149
3393
|
|
|
3150
3394
|
function focusRightContentFromShortcut() {
|
|
3151
|
-
if (isEditorOnlyMode) {
|
|
3152
|
-
setStatus("Only the editor pane is available in editor-only Studio.", "warning");
|
|
3153
|
-
return;
|
|
3154
|
-
}
|
|
3155
3395
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3156
3396
|
setActivePane("right");
|
|
3157
3397
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
@@ -3240,6 +3480,40 @@
|
|
|
3240
3480
|
return false;
|
|
3241
3481
|
}
|
|
3242
3482
|
|
|
3483
|
+
function triggerResponseHistoryShortcut(action) {
|
|
3484
|
+
if (isEditorOnlyMode) {
|
|
3485
|
+
setStatus("Response history is unavailable in editor-only Studio.", "warning");
|
|
3486
|
+
return false;
|
|
3487
|
+
}
|
|
3488
|
+
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
3489
|
+
if (total <= 0) {
|
|
3490
|
+
setStatus("No response history available yet.", "warning");
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
if (action === "previous") {
|
|
3494
|
+
if (responseHistoryIndex <= 0) {
|
|
3495
|
+
setStatus("Already at the first response.", "warning");
|
|
3496
|
+
return false;
|
|
3497
|
+
}
|
|
3498
|
+
return selectHistoryIndex(responseHistoryIndex - 1);
|
|
3499
|
+
}
|
|
3500
|
+
if (action === "next") {
|
|
3501
|
+
if (responseHistoryIndex >= total - 1) {
|
|
3502
|
+
setStatus("Already at the latest response.", "warning");
|
|
3503
|
+
return false;
|
|
3504
|
+
}
|
|
3505
|
+
return selectHistoryIndex(responseHistoryIndex + 1);
|
|
3506
|
+
}
|
|
3507
|
+
if (action === "latest") {
|
|
3508
|
+
if (responseHistoryIndex >= total - 1) {
|
|
3509
|
+
setStatus("Already viewing the latest response.");
|
|
3510
|
+
return false;
|
|
3511
|
+
}
|
|
3512
|
+
return selectHistoryIndex(total - 1);
|
|
3513
|
+
}
|
|
3514
|
+
return false;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3243
3517
|
function isTextEntryShortcutTarget(target) {
|
|
3244
3518
|
if (!(target instanceof Element)) return false;
|
|
3245
3519
|
const editable = target.closest("input, textarea, select, [contenteditable]");
|
|
@@ -3255,6 +3529,7 @@
|
|
|
3255
3529
|
if (!event || event.defaultPrevented) return;
|
|
3256
3530
|
|
|
3257
3531
|
const key = typeof event.key === "string" ? event.key : "";
|
|
3532
|
+
const code = typeof event.code === "string" ? event.code : "";
|
|
3258
3533
|
const plainEscape = key === "Escape"
|
|
3259
3534
|
&& !event.metaKey
|
|
3260
3535
|
&& !event.ctrlKey
|
|
@@ -3355,6 +3630,24 @@
|
|
|
3355
3630
|
return;
|
|
3356
3631
|
}
|
|
3357
3632
|
|
|
3633
|
+
if (!isTextEntryShortcutTarget(event.target) && !event.metaKey && !event.ctrlKey && event.altKey && !event.shiftKey) {
|
|
3634
|
+
if (key === "ArrowLeft") {
|
|
3635
|
+
event.preventDefault();
|
|
3636
|
+
triggerResponseHistoryShortcut("previous");
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
if (key === "ArrowRight") {
|
|
3640
|
+
event.preventDefault();
|
|
3641
|
+
triggerResponseHistoryShortcut("next");
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
if (key.toLowerCase() === "l" || code === "KeyL") {
|
|
3645
|
+
event.preventDefault();
|
|
3646
|
+
triggerResponseHistoryShortcut("latest");
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3358
3651
|
const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3359
3652
|
if (isPaneSwitchShortcut) {
|
|
3360
3653
|
event.preventDefault();
|
|
@@ -4018,6 +4311,36 @@
|
|
|
4018
4311
|
+ " if (node && node.nodeType === 3) node = node.parentElement;\n"
|
|
4019
4312
|
+ " return node && typeof node.closest === 'function' ? node.closest('a[href]') : null;\n"
|
|
4020
4313
|
+ " }\n"
|
|
4314
|
+
+ " function isLocalHtmlPreviewLinkHref(value) {\n"
|
|
4315
|
+
+ " const raw = String(value || '').trim();\n"
|
|
4316
|
+
+ " if (!raw || raw.charAt(0) === '#') return false;\n"
|
|
4317
|
+
+ " if (/^\\/\\//.test(raw)) return false;\n"
|
|
4318
|
+
+ " if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;\n"
|
|
4319
|
+
+ " return true;\n"
|
|
4320
|
+
+ " }\n"
|
|
4321
|
+
+ " function postHtmlPreviewLocalLink(action, anchor, event) {\n"
|
|
4322
|
+
+ " if (!anchor || typeof anchor.getAttribute !== 'function') return false;\n"
|
|
4323
|
+
+ " if (anchor.hasAttribute('download')) return false;\n"
|
|
4324
|
+
+ " const target = String(anchor.getAttribute('target') || '').trim().toLowerCase();\n"
|
|
4325
|
+
+ " if (target && target !== '_self') return false;\n"
|
|
4326
|
+
+ " const href = String(anchor.getAttribute('href') || '').trim();\n"
|
|
4327
|
+
+ " if (!isLocalHtmlPreviewLinkHref(href)) return false;\n"
|
|
4328
|
+
+ " try { parent.postMessage({ type: 'pi-studio-html-artifact-local-link', id: PREVIEW_ID, action, href, title: String(anchor.textContent || href).trim(), clientX: event && event.clientX || 0, clientY: event && event.clientY || 0 }, '*'); } catch {}\n"
|
|
4329
|
+
+ " return true;\n"
|
|
4330
|
+
+ " }\n"
|
|
4331
|
+
+ " function handleHtmlPreviewLocalLinkClick(event) {\n"
|
|
4332
|
+
+ " if (!event || event.defaultPrevented) return;\n"
|
|
4333
|
+
+ " if (typeof event.button === 'number' && event.button !== 0) return;\n"
|
|
4334
|
+
+ " const anchor = getAnchorFromClickTarget(event.target);\n"
|
|
4335
|
+
+ " if (!postHtmlPreviewLocalLink('open', anchor, event)) return;\n"
|
|
4336
|
+
+ " event.preventDefault();\n"
|
|
4337
|
+
+ " }\n"
|
|
4338
|
+
+ " function handleHtmlPreviewLocalLinkContextMenu(event) {\n"
|
|
4339
|
+
+ " if (!event || event.defaultPrevented) return;\n"
|
|
4340
|
+
+ " const anchor = getAnchorFromClickTarget(event.target);\n"
|
|
4341
|
+
+ " if (!postHtmlPreviewLocalLink('contextmenu', anchor, event)) return;\n"
|
|
4342
|
+
+ " event.preventDefault();\n"
|
|
4343
|
+
+ " }\n"
|
|
4021
4344
|
+ " function getSameDocumentFragment(anchor) {\n"
|
|
4022
4345
|
+ " if (!anchor || typeof anchor.getAttribute !== 'function') return null;\n"
|
|
4023
4346
|
+ " if (anchor.hasAttribute('download')) return null;\n"
|
|
@@ -4205,6 +4528,60 @@
|
|
|
4205
4528
|
+ " htmlMathScanScheduled = true;\n"
|
|
4206
4529
|
+ " requestAnimationFrame(runHtmlMathRenderScan);\n"
|
|
4207
4530
|
+ " }\n"
|
|
4531
|
+
+ " const htmlResourcePlaceholders = new Map();\n"
|
|
4532
|
+
+ " let htmlResourceSerial = 0;\n"
|
|
4533
|
+
+ " let htmlResourceScanScheduled = false;\n"
|
|
4534
|
+
+ " function shouldResolveHtmlPreviewResourceUrl(value) {\n"
|
|
4535
|
+
+ " const raw = String(value || '').trim();\n"
|
|
4536
|
+
+ " if (!raw || raw.charAt(0) === '#') return false;\n"
|
|
4537
|
+
+ " if (/^(?:data|blob|http|https|about|javascript|mailto):/i.test(raw)) return false;\n"
|
|
4538
|
+
+ " if (/^\\/\\//.test(raw)) return false;\n"
|
|
4539
|
+
+ " return /\\.(?:png|jpe?g|gif|webp)(?:[?#].*)?$/i.test(raw);\n"
|
|
4540
|
+
+ " }\n"
|
|
4541
|
+
+ " function scanHtmlPreviewResources() {\n"
|
|
4542
|
+
+ " htmlResourceScanScheduled = false;\n"
|
|
4543
|
+
+ " if (!document.body) return;\n"
|
|
4544
|
+
+ " const items = [];\n"
|
|
4545
|
+
+ " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
|
|
4546
|
+
+ " images.forEach((image) => {\n"
|
|
4547
|
+
+ " if (!image || !image.getAttribute) return;\n"
|
|
4548
|
+
+ " if (image.getAttribute('data-pi-studio-html-resource-resolved') === 'true') return;\n"
|
|
4549
|
+
+ " const raw = String(image.getAttribute('src') || '').trim();\n"
|
|
4550
|
+
+ " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
|
|
4551
|
+
+ " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
|
|
4552
|
+
+ " if (!resourceId) {\n"
|
|
4553
|
+
+ " resourceId = PREVIEW_ID + '_resource_' + (++htmlResourceSerial).toString(36);\n"
|
|
4554
|
+
+ " image.setAttribute('data-pi-studio-html-resource-id', resourceId);\n"
|
|
4555
|
+
+ " }\n"
|
|
4556
|
+
+ " htmlResourcePlaceholders.set(resourceId, image);\n"
|
|
4557
|
+
+ " items.push({ resourceId, url: raw });\n"
|
|
4558
|
+
+ " });\n"
|
|
4559
|
+
+ " if (items.length > 0) {\n"
|
|
4560
|
+
+ " try { parent.postMessage({ type: 'pi-studio-html-artifact-resolve-resources', id: PREVIEW_ID, resources: items.slice(0, 100) }, '*'); } catch {}\n"
|
|
4561
|
+
+ " }\n"
|
|
4562
|
+
+ " }\n"
|
|
4563
|
+
+ " function scheduleHtmlPreviewResourceScan() {\n"
|
|
4564
|
+
+ " if (htmlResourceScanScheduled) return;\n"
|
|
4565
|
+
+ " htmlResourceScanScheduled = true;\n"
|
|
4566
|
+
+ " requestAnimationFrame(scanHtmlPreviewResources);\n"
|
|
4567
|
+
+ " }\n"
|
|
4568
|
+
+ " function applyResolvedHtmlPreviewResources(results) {\n"
|
|
4569
|
+
+ " if (!Array.isArray(results)) return;\n"
|
|
4570
|
+
+ " results.forEach((result) => {\n"
|
|
4571
|
+
+ " if (!result || typeof result !== 'object') return;\n"
|
|
4572
|
+
+ " const resourceId = typeof result.resourceId === 'string' ? result.resourceId : '';\n"
|
|
4573
|
+
+ " const image = resourceId ? htmlResourcePlaceholders.get(resourceId) : null;\n"
|
|
4574
|
+
+ " if (!image || !image.isConnected) return;\n"
|
|
4575
|
+
+ " if (result.ok === true && typeof result.dataUrl === 'string' && result.dataUrl) {\n"
|
|
4576
|
+
+ " image.setAttribute('src', result.dataUrl);\n"
|
|
4577
|
+
+ " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
|
|
4578
|
+
+ " } else if (typeof result.error === 'string' && result.error) {\n"
|
|
4579
|
+
+ " image.setAttribute('title', result.error);\n"
|
|
4580
|
+
+ " }\n"
|
|
4581
|
+
+ " htmlResourcePlaceholders.delete(resourceId);\n"
|
|
4582
|
+
+ " });\n"
|
|
4583
|
+
+ " scheduleHeight();\n"
|
|
4584
|
+
+ " }\n"
|
|
4208
4585
|
+ " window.addEventListener('message', (event) => {\n"
|
|
4209
4586
|
+ " const data = event && event.data;\n"
|
|
4210
4587
|
+ " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
|
|
@@ -4214,15 +4591,21 @@
|
|
|
4214
4591
|
+ " }\n"
|
|
4215
4592
|
+ " if (data.type === 'pi-studio-html-artifact-math-rendered') {\n"
|
|
4216
4593
|
+ " applyRenderedHtmlMath(data.results);\n"
|
|
4594
|
+
+ " return;\n"
|
|
4595
|
+
+ " }\n"
|
|
4596
|
+
+ " if (data.type === 'pi-studio-html-artifact-resources-resolved') {\n"
|
|
4597
|
+
+ " applyResolvedHtmlPreviewResources(data.results);\n"
|
|
4217
4598
|
+ " }\n"
|
|
4218
4599
|
+ " });\n"
|
|
4219
4600
|
+ " document.addEventListener('click', handleFragmentAnchorClick);\n"
|
|
4220
|
-
+ " document.addEventListener('
|
|
4601
|
+
+ " document.addEventListener('click', handleHtmlPreviewLocalLinkClick);\n"
|
|
4602
|
+
+ " document.addEventListener('contextmenu', handleHtmlPreviewLocalLinkContextMenu);\n"
|
|
4603
|
+
+ " document.addEventListener('DOMContentLoaded', () => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
4221
4604
|
+ " window.addEventListener('hashchange', () => {\n"
|
|
4222
4605
|
+ " const hash = String(window.location && window.location.hash || '');\n"
|
|
4223
4606
|
+ " if (hash) scrollFragmentIntoView(hash.slice(1), { smooth: false });\n"
|
|
4224
4607
|
+ " });\n"
|
|
4225
|
-
+ " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
|
|
4608
|
+
+ " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
4226
4609
|
+ " window.addEventListener('resize', scheduleHeight);\n"
|
|
4227
4610
|
+ " if (typeof ResizeObserver === 'function') {\n"
|
|
4228
4611
|
+ " const observer = new ResizeObserver(scheduleHeight);\n"
|
|
@@ -4230,15 +4613,15 @@
|
|
|
4230
4613
|
+ " if (document.body) observer.observe(document.body);\n"
|
|
4231
4614
|
+ " }\n"
|
|
4232
4615
|
+ " if (typeof MutationObserver === 'function') {\n"
|
|
4233
|
-
+ " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
|
|
4616
|
+
+ " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); });\n"
|
|
4234
4617
|
+ " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
|
|
4235
4618
|
+ " }\n"
|
|
4236
4619
|
+ " scheduleHeight();\n"
|
|
4237
4620
|
+ " setTimeout(scheduleHeight, 80);\n"
|
|
4238
4621
|
+ " setTimeout(scheduleHeight, 350);\n"
|
|
4239
|
-
+ " setTimeout(scheduleHtmlMathRenderScan, 0);\n"
|
|
4240
|
-
+ " setTimeout(scheduleHtmlMathRenderScan, 120);\n"
|
|
4241
|
-
+ " setTimeout(scheduleHtmlMathRenderScan, 500);\n"
|
|
4622
|
+
+ " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 0);\n"
|
|
4623
|
+
+ " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 120);\n"
|
|
4624
|
+
+ " setTimeout(() => { scheduleHtmlMathRenderScan(); scheduleHtmlPreviewResourceScan(); }, 500);\n"
|
|
4242
4625
|
+ "})();\n"
|
|
4243
4626
|
+ "<\/script>";
|
|
4244
4627
|
}
|
|
@@ -4467,9 +4850,154 @@
|
|
|
4467
4850
|
void renderHtmlArtifactMathItems(record, items);
|
|
4468
4851
|
}
|
|
4469
4852
|
|
|
4853
|
+
function normalizeHtmlArtifactResourceItems(rawItems) {
|
|
4854
|
+
if (!Array.isArray(rawItems)) return [];
|
|
4855
|
+
return rawItems.slice(0, 100).map((item) => {
|
|
4856
|
+
const raw = item && typeof item === "object" ? item : null;
|
|
4857
|
+
const resourceId = raw && typeof raw.resourceId === "string" ? raw.resourceId : "";
|
|
4858
|
+
const url = raw && typeof raw.url === "string" ? raw.url : "";
|
|
4859
|
+
if (!resourceId || !url.trim()) return null;
|
|
4860
|
+
return { resourceId, url };
|
|
4861
|
+
}).filter(Boolean);
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
function buildHtmlArtifactResourceFetchUrl(record, resourceUrl) {
|
|
4865
|
+
const token = getToken();
|
|
4866
|
+
if (!token) return "";
|
|
4867
|
+
const params = new URLSearchParams({ token, path: String(resourceUrl || "") });
|
|
4868
|
+
if (record && record.sourcePath) {
|
|
4869
|
+
params.set("sourcePath", record.sourcePath);
|
|
4870
|
+
}
|
|
4871
|
+
if (record && record.resourceDir) {
|
|
4872
|
+
params.set("resourceDir", record.resourceDir);
|
|
4873
|
+
}
|
|
4874
|
+
return "/html-preview-resource?" + params.toString();
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
function postHtmlArtifactResourceResults(record, results) {
|
|
4878
|
+
if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
|
|
4879
|
+
try {
|
|
4880
|
+
record.iframe.contentWindow.postMessage({
|
|
4881
|
+
type: "pi-studio-html-artifact-resources-resolved",
|
|
4882
|
+
id: record.id || "",
|
|
4883
|
+
results: Array.isArray(results) ? results : [],
|
|
4884
|
+
}, "*");
|
|
4885
|
+
} catch {
|
|
4886
|
+
// Ignore iframe postMessage failures.
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
async function fetchHtmlArtifactResource(record, item) {
|
|
4891
|
+
const resourceId = item && item.resourceId ? item.resourceId : "";
|
|
4892
|
+
try {
|
|
4893
|
+
const fetchUrl = buildHtmlArtifactResourceFetchUrl(record, item.url);
|
|
4894
|
+
if (!fetchUrl) throw new Error("Missing Studio token in URL.");
|
|
4895
|
+
const response = await fetchWithTimeout(fetchUrl, { method: "GET" }, HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS, "HTML preview resource load");
|
|
4896
|
+
const payload = await response.json().catch(() => null);
|
|
4897
|
+
if (!response.ok || !payload || payload.ok !== true || typeof payload.dataUrl !== "string") {
|
|
4898
|
+
const message = payload && typeof payload.error === "string" ? payload.error : "HTML preview resource load failed with HTTP " + response.status + ".";
|
|
4899
|
+
throw new Error(message);
|
|
4900
|
+
}
|
|
4901
|
+
return { resourceId, ok: true, dataUrl: payload.dataUrl };
|
|
4902
|
+
} catch (error) {
|
|
4903
|
+
return { resourceId, ok: false, error: error && error.message ? error.message : String(error || "HTML preview resource load failed.") };
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
async function resolveHtmlArtifactResources(record, items) {
|
|
4908
|
+
if (!record || !Array.isArray(items) || items.length === 0) return;
|
|
4909
|
+
if (record.detail) record.detail.textContent = "HTML preview · loading local images";
|
|
4910
|
+
const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
|
|
4911
|
+
postHtmlArtifactResourceResults(record, results);
|
|
4912
|
+
if (record.detail) record.detail.textContent = "HTML preview";
|
|
4913
|
+
}
|
|
4914
|
+
|
|
4915
|
+
function handleHtmlArtifactFrameResourceMessage(event) {
|
|
4916
|
+
const data = event && event.data;
|
|
4917
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-resolve-resources") return;
|
|
4918
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
4919
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
4920
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
4921
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
4922
|
+
return;
|
|
4923
|
+
}
|
|
4924
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
4925
|
+
const items = normalizeHtmlArtifactResourceItems(data.resources);
|
|
4926
|
+
if (items.length === 0) return;
|
|
4927
|
+
record.resourceResolveBatchCount = Math.max(0, Number(record.resourceResolveBatchCount) || 0) + 1;
|
|
4928
|
+
record.resourceResolveItemCount = Math.max(0, Number(record.resourceResolveItemCount) || 0) + items.length;
|
|
4929
|
+
if (record.resourceResolveBatchCount > 12 || record.resourceResolveItemCount > 300) {
|
|
4930
|
+
postHtmlArtifactResourceResults(record, items.map((item) => ({ resourceId: item.resourceId, ok: false, error: "HTML preview local image load limit reached." })));
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4933
|
+
void resolveHtmlArtifactResources(record, items);
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
function getHtmlArtifactLocalLinkContext(record, data) {
|
|
4937
|
+
return {
|
|
4938
|
+
href: typeof data.href === "string" ? data.href : "",
|
|
4939
|
+
title: typeof data.title === "string" && data.title.trim() ? data.title.trim() : (typeof data.href === "string" ? data.href : "local link"),
|
|
4940
|
+
sourcePath: record && record.sourcePath ? String(record.sourcePath) : "",
|
|
4941
|
+
resourceDir: record && record.resourceDir ? String(record.resourceDir) : "",
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
function getHtmlArtifactLocalLinkClientPoint(record, data) {
|
|
4946
|
+
const iframe = record && record.iframe;
|
|
4947
|
+
const rect = iframe && typeof iframe.getBoundingClientRect === "function"
|
|
4948
|
+
? iframe.getBoundingClientRect()
|
|
4949
|
+
: { left: 0, top: 0 };
|
|
4950
|
+
return {
|
|
4951
|
+
clientX: rect.left + (Number(data.clientX) || 0),
|
|
4952
|
+
clientY: rect.top + (Number(data.clientY) || 0),
|
|
4953
|
+
};
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
function handleHtmlArtifactFrameLocalLinkMessage(event) {
|
|
4957
|
+
const data = event && event.data;
|
|
4958
|
+
if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-local-link") return;
|
|
4959
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
4960
|
+
const record = id ? htmlArtifactFramesById.get(id) : null;
|
|
4961
|
+
if (!record || !record.iframe || !record.iframe.isConnected) {
|
|
4962
|
+
if (id) htmlArtifactFramesById.delete(id);
|
|
4963
|
+
return;
|
|
4964
|
+
}
|
|
4965
|
+
if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
|
|
4966
|
+
const context = getHtmlArtifactLocalLinkContext(record, data);
|
|
4967
|
+
if (!isStudioLocalPreviewHref(context.href)) return;
|
|
4968
|
+
const action = typeof data.action === "string" ? data.action : "open";
|
|
4969
|
+
if (action === "contextmenu") {
|
|
4970
|
+
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
4971
|
+
showPreviewLinkMenu(null, point, context);
|
|
4972
|
+
return;
|
|
4973
|
+
}
|
|
4974
|
+
const kind = getPreviewLocalLinkKind(context.href);
|
|
4975
|
+
if (kind === "pdf") {
|
|
4976
|
+
openPreviewPdfLink(context.href, context.title, context);
|
|
4977
|
+
return;
|
|
4978
|
+
}
|
|
4979
|
+
if (kind === "image") {
|
|
4980
|
+
const pendingWindow = window.open("", "_blank");
|
|
4981
|
+
void openPreviewImageLink(context.href, context.title, context, pendingWindow).catch((error) => {
|
|
4982
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
4983
|
+
});
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
if (kind === "text") {
|
|
4987
|
+
const pendingWindow = window.open("", "_blank");
|
|
4988
|
+
void openPreviewDocumentInNewEditor(context.href, pendingWindow, context).catch((error) => {
|
|
4989
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
4990
|
+
});
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
setStatus("Right-click this local HTML preview link for file actions.", "warning");
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4470
4996
|
window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
|
|
4471
4997
|
window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
|
|
4472
4998
|
window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
|
|
4999
|
+
window.addEventListener("message", handleHtmlArtifactFrameResourceMessage);
|
|
5000
|
+
window.addEventListener("message", handleHtmlArtifactFrameLocalLinkMessage);
|
|
4473
5001
|
|
|
4474
5002
|
function isStudioHtmlFocusOpen() {
|
|
4475
5003
|
return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
|
|
@@ -4777,7 +5305,19 @@
|
|
|
4777
5305
|
iframe.addEventListener("load", () => { postArtifactZoom(); });
|
|
4778
5306
|
iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
|
|
4779
5307
|
shell.appendChild(iframe);
|
|
4780
|
-
htmlArtifactFramesById.set(previewId, {
|
|
5308
|
+
htmlArtifactFramesById.set(previewId, {
|
|
5309
|
+
id: previewId,
|
|
5310
|
+
iframe,
|
|
5311
|
+
shell,
|
|
5312
|
+
detail,
|
|
5313
|
+
zoomControls,
|
|
5314
|
+
sourcePath: options && options.sourcePath ? String(options.sourcePath) : "",
|
|
5315
|
+
resourceDir: options && options.resourceDir ? String(options.resourceDir) : "",
|
|
5316
|
+
mathRenderBatchCount: 0,
|
|
5317
|
+
mathRenderItemCount: 0,
|
|
5318
|
+
resourceResolveBatchCount: 0,
|
|
5319
|
+
resourceResolveItemCount: 0,
|
|
5320
|
+
});
|
|
4781
5321
|
|
|
4782
5322
|
targetEl.appendChild(shell);
|
|
4783
5323
|
|
|
@@ -5010,13 +5550,16 @@
|
|
|
5010
5550
|
if (!token) return "";
|
|
5011
5551
|
const pdfPath = String(options && options.path ? options.path : "").trim();
|
|
5012
5552
|
if (!pdfPath) return "";
|
|
5553
|
+
const explicitSourcePath = options && typeof options.sourcePath === "string" ? options.sourcePath.trim() : "";
|
|
5554
|
+
const explicitResourceDir = options && typeof options.resourceDir === "string" ? normalizeStudioResourceDirValue(options.resourceDir) : "";
|
|
5013
5555
|
const effectivePath = getEffectiveSavePath();
|
|
5014
|
-
const sourcePath = useEditorResourceContext ? (effectivePath || sourceState.path || "") : "";
|
|
5015
|
-
const resourceDir =
|
|
5556
|
+
const sourcePath = explicitSourcePath || (useEditorResourceContext ? (effectivePath || sourceState.path || "") : "");
|
|
5557
|
+
const resourceDir = explicitResourceDir || getCurrentResourceDirValue();
|
|
5016
5558
|
const params = new URLSearchParams({ token, path: pdfPath });
|
|
5017
5559
|
if (sourcePath) {
|
|
5018
5560
|
params.set("sourcePath", sourcePath);
|
|
5019
|
-
}
|
|
5561
|
+
}
|
|
5562
|
+
if (resourceDir) {
|
|
5020
5563
|
params.set("resourceDir", resourceDir);
|
|
5021
5564
|
}
|
|
5022
5565
|
return "/pdf-resource?" + params.toString();
|
|
@@ -5892,12 +6435,16 @@
|
|
|
5892
6435
|
const action = actionBtn.getAttribute("data-repl-action") || "";
|
|
5893
6436
|
if (action === "start" || action === "new-session") {
|
|
5894
6437
|
const requestId = makeRequestId();
|
|
6438
|
+
const command = getCurrentReplStartCommandFromDom();
|
|
6439
|
+
setReplCommandOverride(replRuntime, command);
|
|
5895
6440
|
replBusy = true;
|
|
5896
6441
|
replError = "";
|
|
5897
|
-
replMessage = (action === "new-session" ? "Starting new " : "Starting ") + replRuntime + "
|
|
6442
|
+
replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
|
|
5898
6443
|
syncActionButtons();
|
|
5899
6444
|
renderReplViewIfActive({ force: true });
|
|
5900
|
-
|
|
6445
|
+
const message = { type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" };
|
|
6446
|
+
if (command) message.command = command;
|
|
6447
|
+
if (!sendMessage(message)) {
|
|
5901
6448
|
replBusy = false;
|
|
5902
6449
|
syncActionButtons();
|
|
5903
6450
|
}
|
|
@@ -6001,13 +6548,29 @@
|
|
|
6001
6548
|
if (!(target instanceof Element)) return;
|
|
6002
6549
|
const runtimeSelect = target.closest("[data-repl-runtime]");
|
|
6003
6550
|
if (runtimeSelect && "value" in runtimeSelect) {
|
|
6551
|
+
const previousActive = replActiveSessionName;
|
|
6004
6552
|
setReplRuntime(runtimeSelect.value);
|
|
6553
|
+
selectReplSessionForRuntime(replRuntime, previousActive);
|
|
6554
|
+
replError = "";
|
|
6555
|
+
replMessage = "";
|
|
6556
|
+
if (replActiveSessionName) {
|
|
6557
|
+
requestReplCapture();
|
|
6558
|
+
} else {
|
|
6559
|
+
replTranscript = "";
|
|
6560
|
+
replCapturedAt = 0;
|
|
6561
|
+
syncActionButtons();
|
|
6562
|
+
}
|
|
6005
6563
|
renderReplViewIfActive({ force: true });
|
|
6006
6564
|
return;
|
|
6007
6565
|
}
|
|
6566
|
+
const commandInput = target.closest("[data-repl-command]");
|
|
6567
|
+
if (commandInput && "value" in commandInput) {
|
|
6568
|
+
setReplCommandOverride(replRuntime, commandInput.value);
|
|
6569
|
+
return;
|
|
6570
|
+
}
|
|
6008
6571
|
const sessionSelect = target.closest("[data-repl-session]");
|
|
6009
6572
|
if (sessionSelect && "value" in sessionSelect) {
|
|
6010
|
-
|
|
6573
|
+
setActiveReplSessionForCurrentRuntime(sessionSelect.value);
|
|
6011
6574
|
replError = "";
|
|
6012
6575
|
replMessage = "";
|
|
6013
6576
|
replFollow = true;
|
|
@@ -6190,7 +6753,7 @@
|
|
|
6190
6753
|
const payload = {
|
|
6191
6754
|
markdown: String(markdown || ""),
|
|
6192
6755
|
sourcePath: sourcePath,
|
|
6193
|
-
resourceDir: (!sourcePath && resourceDirInput) ?
|
|
6756
|
+
resourceDir: (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "",
|
|
6194
6757
|
};
|
|
6195
6758
|
if (previewOptions.includeEditorLanguage) {
|
|
6196
6759
|
payload.editorLanguage = String(editorLanguage || "");
|
|
@@ -6291,12 +6854,12 @@
|
|
|
6291
6854
|
const exportingReplJournal = rightView === "repl";
|
|
6292
6855
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
6293
6856
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
6294
|
-
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL
|
|
6857
|
+
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export PDF.", "warning");
|
|
6295
6858
|
return;
|
|
6296
6859
|
}
|
|
6297
6860
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
6298
6861
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
6299
|
-
setStatus("No REPL
|
|
6862
|
+
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
6300
6863
|
return;
|
|
6301
6864
|
}
|
|
6302
6865
|
|
|
@@ -6318,7 +6881,7 @@
|
|
|
6318
6881
|
|
|
6319
6882
|
const effectivePath = getEffectiveSavePath();
|
|
6320
6883
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
6321
|
-
const resourceDir = (!sourcePath && resourceDirInput) ?
|
|
6884
|
+
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
6322
6885
|
const isEditorPreview = rightView === "editor-preview";
|
|
6323
6886
|
const editorPdfLanguage = isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "";
|
|
6324
6887
|
const isLatex = isEditorPreview
|
|
@@ -6464,12 +7027,12 @@
|
|
|
6464
7027
|
const exportingReplJournal = rightView === "repl";
|
|
6465
7028
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
6466
7029
|
if (!rightPaneShowsPreview && !exportingReplJournal) {
|
|
6467
|
-
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL
|
|
7030
|
+
setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL to export HTML.", "warning");
|
|
6468
7031
|
return;
|
|
6469
7032
|
}
|
|
6470
7033
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
6471
7034
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
6472
|
-
setStatus("No REPL
|
|
7035
|
+
setStatus("No Studio REPL record entries to export for this session yet.", "warning");
|
|
6473
7036
|
return;
|
|
6474
7037
|
}
|
|
6475
7038
|
|
|
@@ -6484,14 +7047,14 @@
|
|
|
6484
7047
|
|
|
6485
7048
|
const effectivePath = getEffectiveSavePath();
|
|
6486
7049
|
const sourcePath = exportingReplJournal ? "" : (effectivePath || sourceState.path || "");
|
|
6487
|
-
const resourceDir = (!sourcePath && resourceDirInput) ?
|
|
7050
|
+
const resourceDir = (!sourcePath && resourceDirInput) ? getCurrentResourceDirValue() : "";
|
|
6488
7051
|
const isEditorPreview = rightView === "editor-preview";
|
|
6489
7052
|
const editorHtmlLanguage = htmlArtifactSource ? "html" : (isEditorPreview ? normalizeFenceLanguage(editorLanguage || "") : "");
|
|
6490
7053
|
const isLatex = htmlArtifactSource ? false : (isEditorPreview
|
|
6491
7054
|
? editorHtmlLanguage === "latex"
|
|
6492
7055
|
: /\\documentclass\b|\\begin\{document\}/.test(markdown));
|
|
6493
7056
|
let filenameHint = exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : "studio-response-preview.html");
|
|
6494
|
-
let titleHint = exportingReplJournal ? "REPL
|
|
7057
|
+
let titleHint = exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview");
|
|
6495
7058
|
if (sourcePath) {
|
|
6496
7059
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
6497
7060
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
@@ -6873,7 +7436,7 @@
|
|
|
6873
7436
|
decorateCopyablePreviewBlocks(targetEl);
|
|
6874
7437
|
|
|
6875
7438
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
6876
|
-
if (!sourceState.path && !(
|
|
7439
|
+
if (!sourceState.path && !getCurrentResourceDirValue()) {
|
|
6877
7440
|
var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
|
|
6878
7441
|
var hasLatexImages = /\\includegraphics/.test(markdown || "");
|
|
6879
7442
|
if (hasRelativeImages || hasLatexImages) {
|
|
@@ -6907,7 +7470,7 @@
|
|
|
6907
7470
|
if (editorView !== "preview") return;
|
|
6908
7471
|
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
6909
7472
|
if (isHtmlArtifactPreviewText(text, editorLanguage)) {
|
|
6910
|
-
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview" });
|
|
7473
|
+
renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
6911
7474
|
return;
|
|
6912
7475
|
}
|
|
6913
7476
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
@@ -7190,10 +7753,9 @@
|
|
|
7190
7753
|
const visibleEntries = getVisibleReplJournalEntries();
|
|
7191
7754
|
const hasEntries = visibleEntries.length > 0;
|
|
7192
7755
|
const entryCount = visibleEntries.length;
|
|
7193
|
-
const hiddenEntryCount = getHiddenReplJournalEntryCount();
|
|
7194
7756
|
const sessionName = getActiveReplJournalSessionName();
|
|
7195
7757
|
const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
|
|
7196
|
-
const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show
|
|
7758
|
+
const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
|
|
7197
7759
|
const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
|
|
7198
7760
|
const summaryText = hasEntries
|
|
7199
7761
|
? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
|
|
@@ -7201,7 +7763,7 @@
|
|
|
7201
7763
|
if (replJournalCollapsed) {
|
|
7202
7764
|
return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
|
|
7203
7765
|
+ "<div class='repl-journal-compact-row'>"
|
|
7204
|
-
+ "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>REPL
|
|
7766
|
+
+ "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Studio REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
|
|
7205
7767
|
+ "<div class='repl-journal-actions'>" + toggleButton + "</div>"
|
|
7206
7768
|
+ "</div>"
|
|
7207
7769
|
+ "</section>";
|
|
@@ -7239,14 +7801,14 @@
|
|
|
7239
7801
|
}).join("");
|
|
7240
7802
|
const emptyText = sessionName
|
|
7241
7803
|
? (String(transcript || "").trim()
|
|
7242
|
-
? "No REPL
|
|
7243
|
-
: "No REPL
|
|
7244
|
-
: "No REPL
|
|
7804
|
+
? "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."
|
|
7805
|
+
: "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
|
|
7806
|
+
: "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
|
|
7245
7807
|
const terminalContent = banner
|
|
7246
7808
|
+ (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
|
|
7247
7809
|
return "<section class='repl-journal'>"
|
|
7248
|
-
+ "<div class='repl-journal-header'><
|
|
7249
|
-
+
|
|
7810
|
+
+ "<div class='repl-journal-header'><h3>Studio REPL Record</h3>" + toggleActions + "</div>"
|
|
7811
|
+
+ "<p class='repl-journal-description'>Clean record for the selected tmux session. Raw tmux mirror below.</p>"
|
|
7250
7812
|
+ (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
|
|
7251
7813
|
+ "<div class='repl-journal-list'>" + terminalContent + "</div>"
|
|
7252
7814
|
+ "</section>";
|
|
@@ -7270,7 +7832,7 @@
|
|
|
7270
7832
|
+ "</section>";
|
|
7271
7833
|
}
|
|
7272
7834
|
return "<section class='repl-mirror'>"
|
|
7273
|
-
+ "<div class='repl-journal-header'><div><h3>Raw REPL
|
|
7835
|
+
+ "<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>"
|
|
7274
7836
|
+ body
|
|
7275
7837
|
+ "</section>";
|
|
7276
7838
|
}
|
|
@@ -7290,10 +7852,13 @@
|
|
|
7290
7852
|
["ghci", "GHCi"],
|
|
7291
7853
|
["clojure", "Clojure"],
|
|
7292
7854
|
].map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (replRuntime === value ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
|
|
7293
|
-
const
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7855
|
+
const runtimeLabel = getReplRuntimeLabel(replRuntime);
|
|
7856
|
+
const visibleSessions = getReplSessionsForRuntime(replRuntime);
|
|
7857
|
+
const sessionOptions = visibleSessions.length
|
|
7858
|
+
? visibleSessions.map((session) => "<option value='" + escapeHtml(session.sessionName) + "'" + (session.sessionName === replActiveSessionName ? " selected" : "") + ">" + escapeHtml(session.label || session.sessionName) + "</option>").join("")
|
|
7859
|
+
: "<option value=''>None</option>";
|
|
7860
|
+
const replCommand = getReplCommandOverride(replRuntime);
|
|
7861
|
+
const activeSession = getActiveReplSessionForCurrentRuntime();
|
|
7297
7862
|
const transcript = trimReplTranscript(replTranscript);
|
|
7298
7863
|
const emptyMessage = replTmuxAvailable === false
|
|
7299
7864
|
? "tmux is not available. Install tmux to use Studio REPL sessions."
|
|
@@ -7307,17 +7872,17 @@
|
|
|
7307
7872
|
+ "<div class='repl-toolbar'>"
|
|
7308
7873
|
+ "<div class='repl-controls'>"
|
|
7309
7874
|
+ "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
|
|
7310
|
-
+ "<
|
|
7311
|
-
+ "<
|
|
7875
|
+
+ "<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>"
|
|
7876
|
+
+ "<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>"
|
|
7877
|
+
+ "<label class='repl-control-label repl-session-label'>Session <select data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
|
|
7312
7878
|
+ "<details class='repl-more-controls'>"
|
|
7313
7879
|
+ "<summary title='More REPL actions'>More</summary>"
|
|
7314
7880
|
+ "<div class='repl-more-menu'>"
|
|
7315
|
-
+ "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
|
|
7316
7881
|
+ "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
|
|
7317
7882
|
+ "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
|
|
7318
7883
|
+ "<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>"
|
|
7319
7884
|
+ "<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>"
|
|
7320
|
-
+ "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to
|
|
7885
|
+
+ "<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>"
|
|
7321
7886
|
+ "<button type='button' data-repl-action='refresh'>Refresh</button>"
|
|
7322
7887
|
+ "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
|
|
7323
7888
|
+ "</div>"
|
|
@@ -7501,7 +8066,7 @@
|
|
|
7501
8066
|
return;
|
|
7502
8067
|
}
|
|
7503
8068
|
if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
|
|
7504
|
-
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview" });
|
|
8069
|
+
renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
7505
8070
|
return;
|
|
7506
8071
|
}
|
|
7507
8072
|
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
@@ -7525,7 +8090,7 @@
|
|
|
7525
8090
|
|
|
7526
8091
|
if (rightView === "preview") {
|
|
7527
8092
|
if (isHtmlArtifactPreviewText(markdown, "")) {
|
|
7528
|
-
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview" });
|
|
8093
|
+
renderHtmlArtifactPreview(critiqueViewEl, markdown, "response", { title: "Response HTML preview", ...getHtmlPreviewResourceContextOptions() });
|
|
7529
8094
|
return;
|
|
7530
8095
|
}
|
|
7531
8096
|
const nonce = ++responsePreviewRenderNonce;
|
|
@@ -7604,19 +8169,19 @@
|
|
|
7604
8169
|
exportPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
7605
8170
|
exportPdfBtn.textContent = previewExportInProgress
|
|
7606
8171
|
? "Exporting…"
|
|
7607
|
-
: (exportingReplJournal ? "Export
|
|
8172
|
+
: (exportingReplJournal ? "Export record" : "Export right preview");
|
|
7608
8173
|
if (rightView === "trace") {
|
|
7609
8174
|
exportPdfBtn.title = "Working view does not support preview export.";
|
|
7610
8175
|
} else if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
7611
|
-
exportPdfBtn.title = "No REPL
|
|
8176
|
+
exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
|
|
7612
8177
|
} else if (rightView === "markdown") {
|
|
7613
|
-
exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL
|
|
8178
|
+
exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL to export.";
|
|
7614
8179
|
} else if (!canExportPreview) {
|
|
7615
8180
|
exportPdfBtn.title = "Nothing to export yet.";
|
|
7616
8181
|
} else if (isHtmlArtifactPreview) {
|
|
7617
8182
|
exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
|
|
7618
8183
|
} else if (exportingReplJournal) {
|
|
7619
|
-
exportPdfBtn.title = "Choose PDF or HTML and export REPL
|
|
8184
|
+
exportPdfBtn.title = "Choose PDF or HTML and export the Studio REPL record.";
|
|
7620
8185
|
} else {
|
|
7621
8186
|
exportPdfBtn.title = "Choose PDF or HTML and export the current right-pane preview.";
|
|
7622
8187
|
}
|
|
@@ -7625,20 +8190,20 @@
|
|
|
7625
8190
|
exportPreviewPdfBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview || isHtmlArtifactPreview;
|
|
7626
8191
|
exportPreviewPdfBtn.title = isHtmlArtifactPreview
|
|
7627
8192
|
? "Interactive HTML preview PDF export is not available yet."
|
|
7628
|
-
: (exportingReplJournal ? "Export
|
|
8193
|
+
: (exportingReplJournal ? "Export the Studio REPL record as PDF." : "Export the current right-pane preview as PDF.");
|
|
7629
8194
|
}
|
|
7630
8195
|
if (exportPreviewHtmlBtn) {
|
|
7631
8196
|
exportPreviewHtmlBtn.disabled = uiBusy || previewExportInProgress || !canExportPreview;
|
|
7632
8197
|
exportPreviewHtmlBtn.title = isHtmlArtifactPreview
|
|
7633
8198
|
? "Export the authored HTML preview."
|
|
7634
|
-
: (exportingReplJournal ? "Export
|
|
8199
|
+
: (exportingReplJournal ? "Export the Studio REPL record as standalone HTML." : "Export the current right-pane preview as standalone HTML.");
|
|
7635
8200
|
}
|
|
7636
8201
|
if (exportPreviewControlsEl) {
|
|
7637
8202
|
exportPreviewControlsEl.title = canExportPreview
|
|
7638
8203
|
? (exportingReplJournal
|
|
7639
|
-
? "Choose a format and export REPL
|
|
8204
|
+
? "Choose a format and export the Studio REPL record."
|
|
7640
8205
|
: (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
|
|
7641
|
-
: (exportingReplJournal ? "No REPL
|
|
8206
|
+
: (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
|
|
7642
8207
|
}
|
|
7643
8208
|
if (!canExportPreview || previewExportInProgress) {
|
|
7644
8209
|
closeExportPreviewMenu();
|
|
@@ -7658,17 +8223,57 @@
|
|
|
7658
8223
|
updateResultActionButtons();
|
|
7659
8224
|
}
|
|
7660
8225
|
|
|
8226
|
+
function normalizeStudioResourceDirValue(value) {
|
|
8227
|
+
let text = String(value || "").trim();
|
|
8228
|
+
if (text.length >= 2) {
|
|
8229
|
+
const first = text.charAt(0);
|
|
8230
|
+
const last = text.charAt(text.length - 1);
|
|
8231
|
+
if ((first === "\"" && last === "\"") || (first === "'" && last === "'")) {
|
|
8232
|
+
text = text.slice(1, -1).trim();
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
if (/^file:\/\//i.test(text)) {
|
|
8236
|
+
try {
|
|
8237
|
+
text = decodeURIComponent(new URL(text).pathname || text).trim();
|
|
8238
|
+
} catch {}
|
|
8239
|
+
}
|
|
8240
|
+
const markers = ["/Users/", "/home/", "/Volumes/", "/private/", "/tmp/", "/var/", "/opt/", "/Applications/"];
|
|
8241
|
+
let embeddedAbsoluteIndex = -1;
|
|
8242
|
+
for (const marker of markers) {
|
|
8243
|
+
const index = text.lastIndexOf(marker);
|
|
8244
|
+
if (index > 0) embeddedAbsoluteIndex = Math.max(embeddedAbsoluteIndex, index);
|
|
8245
|
+
}
|
|
8246
|
+
const windowsMatch = text.match(/.*([A-Za-z]:[\\/].*)$/);
|
|
8247
|
+
if (windowsMatch && windowsMatch[1]) return windowsMatch[1].trim();
|
|
8248
|
+
if (embeddedAbsoluteIndex > 0) text = text.slice(embeddedAbsoluteIndex).trim();
|
|
8249
|
+
return text;
|
|
8250
|
+
}
|
|
8251
|
+
|
|
8252
|
+
function getCurrentResourceDirValue() {
|
|
8253
|
+
return resourceDirInput ? normalizeStudioResourceDirValue(resourceDirInput.value) : "";
|
|
8254
|
+
}
|
|
8255
|
+
|
|
7661
8256
|
function getEffectiveSavePath() {
|
|
7662
8257
|
// File-backed: use the original path
|
|
7663
8258
|
if (sourceState.path) return sourceState.path;
|
|
7664
8259
|
// Upload with working dir + filename: derive path
|
|
7665
|
-
|
|
8260
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
8261
|
+
if (sourceState.source === "upload" && sourceState.label && resourceDir) {
|
|
7666
8262
|
var name = sourceState.label.replace(/^upload:\s*/i, "");
|
|
7667
|
-
if (name) return
|
|
8263
|
+
if (name) return resourceDir.replace(/\/$/, "") + "/" + name;
|
|
7668
8264
|
}
|
|
7669
8265
|
return null;
|
|
7670
8266
|
}
|
|
7671
8267
|
|
|
8268
|
+
function getHtmlPreviewResourceContextOptions() {
|
|
8269
|
+
const sourcePath = getEffectiveSavePath() || sourceState.path || "";
|
|
8270
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
8271
|
+
return {
|
|
8272
|
+
sourcePath,
|
|
8273
|
+
resourceDir,
|
|
8274
|
+
};
|
|
8275
|
+
}
|
|
8276
|
+
|
|
7672
8277
|
function buildAnnotatedSaveSuggestion() {
|
|
7673
8278
|
const effectivePath = getEffectiveSavePath() || sourceState.path || "";
|
|
7674
8279
|
if (effectivePath) {
|
|
@@ -7681,8 +8286,8 @@
|
|
|
7681
8286
|
|
|
7682
8287
|
const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
|
|
7683
8288
|
const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
|
|
7684
|
-
const suggestedDir =
|
|
7685
|
-
?
|
|
8289
|
+
const suggestedDir = getCurrentResourceDirValue()
|
|
8290
|
+
? getCurrentResourceDirValue().replace(/\/$/, "") + "/"
|
|
7686
8291
|
: "./";
|
|
7687
8292
|
return suggestedDir + stem + ".annotated.md";
|
|
7688
8293
|
}
|
|
@@ -7719,6 +8324,7 @@
|
|
|
7719
8324
|
saveAsBtn.disabled = uiBusy;
|
|
7720
8325
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
7721
8326
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
8327
|
+
if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy;
|
|
7722
8328
|
sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
|
|
7723
8329
|
if (getEditorBtn) getEditorBtn.disabled = uiBusy;
|
|
7724
8330
|
if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
|
|
@@ -7735,7 +8341,7 @@
|
|
|
7735
8341
|
rightViewSelect.disabled = isEditorOnlyMode;
|
|
7736
8342
|
followSelect.disabled = isEditorOnlyMode || uiBusy;
|
|
7737
8343
|
if (responseHighlightSelect) responseHighlightSelect.disabled = isEditorOnlyMode || rightView !== "markdown";
|
|
7738
|
-
insertHeaderBtn.disabled = uiBusy
|
|
8344
|
+
insertHeaderBtn.disabled = uiBusy;
|
|
7739
8345
|
lensSelect.disabled = uiBusy || isEditorOnlyMode;
|
|
7740
8346
|
updateSaveFileTooltip();
|
|
7741
8347
|
updateRefreshFromDiskTooltip();
|
|
@@ -7777,6 +8383,197 @@
|
|
|
7777
8383
|
previousDescriptor: previousDescriptor,
|
|
7778
8384
|
carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
|
|
7779
8385
|
});
|
|
8386
|
+
scheduleWorkspacePersistence();
|
|
8387
|
+
}
|
|
8388
|
+
|
|
8389
|
+
function normalizeWorkspaceSourceState(value) {
|
|
8390
|
+
const raw = value && typeof value === "object" ? value : {};
|
|
8391
|
+
const path = typeof raw.path === "string" && raw.path.trim() ? raw.path.trim() : null;
|
|
8392
|
+
return {
|
|
8393
|
+
source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "blank",
|
|
8394
|
+
label: typeof raw.label === "string" && raw.label.trim() ? raw.label.trim() : "blank",
|
|
8395
|
+
path,
|
|
8396
|
+
draftId: path ? null : (typeof raw.draftId === "string" && raw.draftId.trim() ? raw.draftId.trim() : null),
|
|
8397
|
+
};
|
|
8398
|
+
}
|
|
8399
|
+
|
|
8400
|
+
function getWorkspaceStateIdentity(state) {
|
|
8401
|
+
const normalized = normalizeWorkspaceSourceState(state);
|
|
8402
|
+
if (normalized.path) return "file:" + normalized.path;
|
|
8403
|
+
if (normalized.draftId) return "draft:" + normalized.draftId;
|
|
8404
|
+
return "source:" + normalized.source + ":" + normalized.label;
|
|
8405
|
+
}
|
|
8406
|
+
|
|
8407
|
+
function readPersistedWorkspaceState() {
|
|
8408
|
+
try {
|
|
8409
|
+
const raw = window.localStorage ? window.localStorage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
|
|
8410
|
+
if (!raw) return null;
|
|
8411
|
+
const parsed = JSON.parse(raw);
|
|
8412
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return null;
|
|
8413
|
+
if (typeof parsed.text !== "string") return null;
|
|
8414
|
+
return parsed;
|
|
8415
|
+
} catch {
|
|
8416
|
+
return null;
|
|
8417
|
+
}
|
|
8418
|
+
}
|
|
8419
|
+
|
|
8420
|
+
function shouldRestorePersistedWorkspaceState(state) {
|
|
8421
|
+
if (!state || typeof state.text !== "string") return false;
|
|
8422
|
+
const storedSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
8423
|
+
const initialIdentity = getWorkspaceStateIdentity(initialSourceState);
|
|
8424
|
+
const storedIdentity = getWorkspaceStateIdentity(storedSourceState);
|
|
8425
|
+
if (storedIdentity === initialIdentity) return true;
|
|
8426
|
+
if (!explicitDocumentIdentityFromUrl && initialSourceState.source === "blank" && !initialSourceState.path) return true;
|
|
8427
|
+
return false;
|
|
8428
|
+
}
|
|
8429
|
+
|
|
8430
|
+
function buildWorkspacePersistencePayload() {
|
|
8431
|
+
return {
|
|
8432
|
+
version: 1,
|
|
8433
|
+
savedAt: Date.now(),
|
|
8434
|
+
sourceState: normalizeWorkspaceSourceState(sourceState),
|
|
8435
|
+
resourceDir: getCurrentResourceDirValue(),
|
|
8436
|
+
editorView,
|
|
8437
|
+
rightView,
|
|
8438
|
+
editorLanguage,
|
|
8439
|
+
followLatest,
|
|
8440
|
+
responseHistoryIndex,
|
|
8441
|
+
selectionStart: typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0,
|
|
8442
|
+
selectionEnd: typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : 0,
|
|
8443
|
+
scrollTop: typeof sourceTextEl.scrollTop === "number" ? sourceTextEl.scrollTop : 0,
|
|
8444
|
+
text: String(sourceTextEl.value || ""),
|
|
8445
|
+
};
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
function persistWorkspaceStateNow() {
|
|
8449
|
+
if (!workspacePersistenceReady) return;
|
|
8450
|
+
try {
|
|
8451
|
+
if (!window.localStorage) return;
|
|
8452
|
+
const payload = buildWorkspacePersistencePayload();
|
|
8453
|
+
if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
|
|
8454
|
+
window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8455
|
+
return;
|
|
8456
|
+
}
|
|
8457
|
+
window.localStorage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
|
|
8458
|
+
} catch {
|
|
8459
|
+
// Ignore browser storage failures and quota limits.
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
|
|
8463
|
+
function scheduleWorkspacePersistence() {
|
|
8464
|
+
if (!workspacePersistenceReady) return;
|
|
8465
|
+
if (workspacePersistTimer !== null) window.clearTimeout(workspacePersistTimer);
|
|
8466
|
+
workspacePersistTimer = window.setTimeout(() => {
|
|
8467
|
+
workspacePersistTimer = null;
|
|
8468
|
+
persistWorkspaceStateNow();
|
|
8469
|
+
}, 160);
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8472
|
+
function flushWorkspacePersistence() {
|
|
8473
|
+
if (workspacePersistTimer !== null) {
|
|
8474
|
+
window.clearTimeout(workspacePersistTimer);
|
|
8475
|
+
workspacePersistTimer = null;
|
|
8476
|
+
}
|
|
8477
|
+
persistWorkspaceStateNow();
|
|
8478
|
+
}
|
|
8479
|
+
|
|
8480
|
+
function clearPersistedWorkspaceState() {
|
|
8481
|
+
if (workspacePersistTimer !== null) {
|
|
8482
|
+
window.clearTimeout(workspacePersistTimer);
|
|
8483
|
+
workspacePersistTimer = null;
|
|
8484
|
+
}
|
|
8485
|
+
try {
|
|
8486
|
+
if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8487
|
+
} catch {}
|
|
8488
|
+
}
|
|
8489
|
+
|
|
8490
|
+
function applyPersistedWorkspaceState(state) {
|
|
8491
|
+
if (!shouldRestorePersistedWorkspaceState(state)) return false;
|
|
8492
|
+
const nextSourceState = normalizeWorkspaceSourceState(state.sourceState);
|
|
8493
|
+
const nextResourceDir = normalizeStudioResourceDirValue(typeof state.resourceDir === "string" ? state.resourceDir : "");
|
|
8494
|
+
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
8495
|
+
setEditorText(state.text, { preserveScroll: false, preserveSelection: false });
|
|
8496
|
+
setSourceState(nextSourceState);
|
|
8497
|
+
if (resourceDirInput && nextResourceDir) {
|
|
8498
|
+
resourceDirInput.value = nextResourceDir;
|
|
8499
|
+
updateSourceBadge();
|
|
8500
|
+
}
|
|
8501
|
+
if (typeof state.editorLanguage === "string" && state.editorLanguage.trim()) {
|
|
8502
|
+
setEditorLanguage(state.editorLanguage.trim());
|
|
8503
|
+
}
|
|
8504
|
+
editorView = state.editorView === "preview" ? "preview" : "markdown";
|
|
8505
|
+
rightView = state.rightView === "preview"
|
|
8506
|
+
? "preview"
|
|
8507
|
+
: (state.rightView === "editor-preview"
|
|
8508
|
+
? "editor-preview"
|
|
8509
|
+
: (state.rightView === "repl" ? "repl" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")));
|
|
8510
|
+
if (typeof state.followLatest === "boolean") {
|
|
8511
|
+
followLatest = state.followLatest;
|
|
8512
|
+
}
|
|
8513
|
+
if (followSelect) followSelect.value = followLatest ? "on" : "off";
|
|
8514
|
+
if (typeof state.responseHistoryIndex === "number" && Number.isFinite(state.responseHistoryIndex)) {
|
|
8515
|
+
responseHistoryIndex = Math.max(-1, Math.floor(state.responseHistoryIndex));
|
|
8516
|
+
}
|
|
8517
|
+
const maxIndex = String(sourceTextEl.value || "").length;
|
|
8518
|
+
const start = Math.max(0, Math.min(Math.floor(Number(state.selectionStart) || 0), maxIndex));
|
|
8519
|
+
const end = Math.max(start, Math.min(Math.floor(Number(state.selectionEnd) || start), maxIndex));
|
|
8520
|
+
try { sourceTextEl.setSelectionRange(start, end); } catch {}
|
|
8521
|
+
if (typeof state.scrollTop === "number" && Number.isFinite(state.scrollTop)) {
|
|
8522
|
+
sourceTextEl.scrollTop = Math.max(0, state.scrollTop);
|
|
8523
|
+
}
|
|
8524
|
+
workspaceRestoredFromBrowser = true;
|
|
8525
|
+
initialDocumentApplied = true;
|
|
8526
|
+
return true;
|
|
8527
|
+
}
|
|
8528
|
+
|
|
8529
|
+
function clearStudioWorkspace() {
|
|
8530
|
+
if (uiBusy) {
|
|
8531
|
+
setStatus("Studio is busy.", "warning");
|
|
8532
|
+
return;
|
|
8533
|
+
}
|
|
8534
|
+
const confirmed = window.confirm("Clear the current editor draft in this browser tab? Saved files and responses are not changed.");
|
|
8535
|
+
if (!confirmed) return;
|
|
8536
|
+
const preservedResponseState = {
|
|
8537
|
+
responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
|
|
8538
|
+
responseHistoryIndex,
|
|
8539
|
+
queuedLatestResponse,
|
|
8540
|
+
followLatest,
|
|
8541
|
+
latestResponseMarkdown,
|
|
8542
|
+
latestResponseThinking,
|
|
8543
|
+
latestResponseTimestamp,
|
|
8544
|
+
latestResponseKind,
|
|
8545
|
+
latestResponseIsStructuredCritique,
|
|
8546
|
+
latestResponseHasContent,
|
|
8547
|
+
latestResponseNormalized,
|
|
8548
|
+
latestResponseThinkingNormalized,
|
|
8549
|
+
latestCritiqueNotes,
|
|
8550
|
+
latestCritiqueNotesNormalized,
|
|
8551
|
+
};
|
|
8552
|
+
clearPersistedWorkspaceState();
|
|
8553
|
+
if (resourceDirInput) resourceDirInput.value = "";
|
|
8554
|
+
if (resourceDirLabel) resourceDirLabel.textContent = "";
|
|
8555
|
+
setEditorText("", { preserveScroll: false, preserveSelection: false });
|
|
8556
|
+
setSourceState({ source: "blank", label: "blank", path: null, draftId: makeStudioDraftId() });
|
|
8557
|
+
setEditorLanguage("markdown");
|
|
8558
|
+
setEditorView("markdown");
|
|
8559
|
+
responseHistory = preservedResponseState.responseHistory;
|
|
8560
|
+
responseHistoryIndex = preservedResponseState.responseHistoryIndex;
|
|
8561
|
+
queuedLatestResponse = preservedResponseState.queuedLatestResponse;
|
|
8562
|
+
followLatest = preservedResponseState.followLatest;
|
|
8563
|
+
latestResponseMarkdown = preservedResponseState.latestResponseMarkdown;
|
|
8564
|
+
latestResponseThinking = preservedResponseState.latestResponseThinking;
|
|
8565
|
+
latestResponseTimestamp = preservedResponseState.latestResponseTimestamp;
|
|
8566
|
+
latestResponseKind = preservedResponseState.latestResponseKind;
|
|
8567
|
+
latestResponseIsStructuredCritique = preservedResponseState.latestResponseIsStructuredCritique;
|
|
8568
|
+
latestResponseHasContent = preservedResponseState.latestResponseHasContent;
|
|
8569
|
+
latestResponseNormalized = preservedResponseState.latestResponseNormalized;
|
|
8570
|
+
latestResponseThinkingNormalized = preservedResponseState.latestResponseThinkingNormalized;
|
|
8571
|
+
latestCritiqueNotes = preservedResponseState.latestCritiqueNotes;
|
|
8572
|
+
latestCritiqueNotesNormalized = preservedResponseState.latestCritiqueNotesNormalized;
|
|
8573
|
+
if (followSelect) followSelect.value = followLatest ? "on" : "off";
|
|
8574
|
+
refreshResponseUi();
|
|
8575
|
+
persistWorkspaceStateNow();
|
|
8576
|
+
setStatus("Editor cleared. Saved files and responses were not changed.", "success");
|
|
7780
8577
|
}
|
|
7781
8578
|
|
|
7782
8579
|
function setEditorText(nextText, options) {
|
|
@@ -7826,6 +8623,7 @@
|
|
|
7826
8623
|
}
|
|
7827
8624
|
updateEditorSelectionCommentUi();
|
|
7828
8625
|
updateOutlineUi();
|
|
8626
|
+
scheduleWorkspacePersistence();
|
|
7829
8627
|
}
|
|
7830
8628
|
|
|
7831
8629
|
function applySourceTextEdit(nextText, selectionStart, selectionEnd) {
|
|
@@ -7963,6 +8761,7 @@
|
|
|
7963
8761
|
updateReviewNotesUi();
|
|
7964
8762
|
updateEditorSelectionCommentUi();
|
|
7965
8763
|
updateOutlineUi();
|
|
8764
|
+
scheduleWorkspacePersistence();
|
|
7966
8765
|
}
|
|
7967
8766
|
|
|
7968
8767
|
function setRightView(nextView) {
|
|
@@ -7995,6 +8794,7 @@
|
|
|
7995
8794
|
|
|
7996
8795
|
refreshResponseUi();
|
|
7997
8796
|
syncActionButtons();
|
|
8797
|
+
scheduleWorkspacePersistence();
|
|
7998
8798
|
}
|
|
7999
8799
|
|
|
8000
8800
|
function lineNumbersShouldBeVisible() {
|
|
@@ -8303,6 +9103,383 @@
|
|
|
8303
9103
|
}
|
|
8304
9104
|
}
|
|
8305
9105
|
|
|
9106
|
+
const PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS = new Set([
|
|
9107
|
+
".md", ".markdown", ".mdx", ".qmd", ".txt", ".tex", ".latex", ".rst", ".adoc",
|
|
9108
|
+
".html", ".htm", ".css", ".xml", ".yaml", ".yml", ".toml", ".json", ".jsonc", ".json5", ".csv", ".tsv", ".log",
|
|
9109
|
+
".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx",
|
|
9110
|
+
".py", ".pyw", ".sh", ".bash", ".zsh", ".rs", ".c", ".h", ".cpp", ".cxx", ".cc", ".hpp", ".hxx",
|
|
9111
|
+
".jl", ".f90", ".f95", ".f03", ".f", ".for", ".r", ".m", ".java", ".go", ".rb", ".swift", ".lua",
|
|
9112
|
+
".diff", ".patch",
|
|
9113
|
+
]);
|
|
9114
|
+
const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
9115
|
+
let previewLinkMenuEl = null;
|
|
9116
|
+
let activePreviewLinkContext = null;
|
|
9117
|
+
|
|
9118
|
+
function stripPreviewLocalLinkUrlSuffix(href) {
|
|
9119
|
+
const raw = String(href || "").trim();
|
|
9120
|
+
const hashIndex = raw.indexOf("#");
|
|
9121
|
+
const queryIndex = raw.indexOf("?");
|
|
9122
|
+
let end = raw.length;
|
|
9123
|
+
if (queryIndex >= 0) end = Math.min(end, queryIndex);
|
|
9124
|
+
if (hashIndex >= 0) end = Math.min(end, hashIndex);
|
|
9125
|
+
return raw.slice(0, end);
|
|
9126
|
+
}
|
|
9127
|
+
|
|
9128
|
+
function parsePreviewLocalLinkPage(href) {
|
|
9129
|
+
const raw = String(href || "");
|
|
9130
|
+
const parts = [];
|
|
9131
|
+
const queryIndex = raw.indexOf("?");
|
|
9132
|
+
if (queryIndex >= 0) {
|
|
9133
|
+
const queryEnd = raw.indexOf("#", queryIndex);
|
|
9134
|
+
parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
|
|
9135
|
+
}
|
|
9136
|
+
const hashIndex = raw.indexOf("#");
|
|
9137
|
+
if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
|
|
9138
|
+
for (const part of parts) {
|
|
9139
|
+
try {
|
|
9140
|
+
const params = new URLSearchParams(part);
|
|
9141
|
+
const value = params.get("page") || params.get("p");
|
|
9142
|
+
if (value) {
|
|
9143
|
+
const page = Number.parseInt(value, 10);
|
|
9144
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
9145
|
+
}
|
|
9146
|
+
} catch {}
|
|
9147
|
+
const match = String(part || "").match(/(?:^|[&;])page=(\d+)/i) || String(part || "").match(/^page=(\d+)$/i);
|
|
9148
|
+
if (match && match[1]) {
|
|
9149
|
+
const page = Number.parseInt(match[1], 10);
|
|
9150
|
+
if (Number.isFinite(page) && page > 0) return page;
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
return 0;
|
|
9154
|
+
}
|
|
9155
|
+
|
|
9156
|
+
function getPreviewLocalLinkExtension(href) {
|
|
9157
|
+
const path = stripPreviewLocalLinkUrlSuffix(href);
|
|
9158
|
+
const match = path.match(/\.([A-Za-z0-9_+-]+)$/);
|
|
9159
|
+
return match ? ("." + match[1].toLowerCase()) : "";
|
|
9160
|
+
}
|
|
9161
|
+
|
|
9162
|
+
function getPreviewLocalLinkKind(href) {
|
|
9163
|
+
const ext = getPreviewLocalLinkExtension(href);
|
|
9164
|
+
if (ext === ".pdf") return "pdf";
|
|
9165
|
+
if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext)) return "text";
|
|
9166
|
+
if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
|
|
9167
|
+
return "other";
|
|
9168
|
+
}
|
|
9169
|
+
|
|
9170
|
+
function isStudioLocalPreviewHref(href) {
|
|
9171
|
+
const raw = String(href || "").trim();
|
|
9172
|
+
if (!raw || raw.charAt(0) === "#") return false;
|
|
9173
|
+
if (/^\/\//.test(raw)) return false;
|
|
9174
|
+
if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
|
|
9175
|
+
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|local-preview-link|reveal-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
9176
|
+
return true;
|
|
9177
|
+
}
|
|
9178
|
+
|
|
9179
|
+
function getEffectivePreviewLinkContext(contextOverride) {
|
|
9180
|
+
const fallback = getHtmlPreviewResourceContextOptions();
|
|
9181
|
+
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : null;
|
|
9182
|
+
return {
|
|
9183
|
+
sourcePath: context && context.sourcePath ? String(context.sourcePath) : (fallback.sourcePath || ""),
|
|
9184
|
+
resourceDir: context && context.resourceDir ? String(context.resourceDir) : (fallback.resourceDir || ""),
|
|
9185
|
+
};
|
|
9186
|
+
}
|
|
9187
|
+
|
|
9188
|
+
function getPreviewLinkResourceQuery(path, contextOverride) {
|
|
9189
|
+
const context = getEffectivePreviewLinkContext(contextOverride);
|
|
9190
|
+
const query = { path: String(path || "") };
|
|
9191
|
+
if (context.sourcePath) query.sourcePath = String(context.sourcePath);
|
|
9192
|
+
if (context.resourceDir) query.resourceDir = String(context.resourceDir);
|
|
9193
|
+
return query;
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9196
|
+
function getPreviewLinkAnchorFromEvent(event) {
|
|
9197
|
+
const target = event && event.target;
|
|
9198
|
+
const anchor = target instanceof Element ? target.closest("#sourcePreview a[href], #critiqueView a[href]") : null;
|
|
9199
|
+
if (!anchor) return null;
|
|
9200
|
+
if (anchor.closest(".studio-pdf-card, .studio-html-artifact-toolbar, .studio-copy-block-btn")) return null;
|
|
9201
|
+
const href = String(anchor.getAttribute("href") || "").trim();
|
|
9202
|
+
if (!isStudioLocalPreviewHref(href)) return null;
|
|
9203
|
+
return anchor;
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9206
|
+
function closePreviewLinkMenu() {
|
|
9207
|
+
activePreviewLinkContext = null;
|
|
9208
|
+
if (previewLinkMenuEl) previewLinkMenuEl.hidden = true;
|
|
9209
|
+
}
|
|
9210
|
+
|
|
9211
|
+
function ensurePreviewLinkMenu() {
|
|
9212
|
+
if (previewLinkMenuEl) return previewLinkMenuEl;
|
|
9213
|
+
const menu = document.createElement("div");
|
|
9214
|
+
menu.className = "studio-preview-link-menu";
|
|
9215
|
+
menu.hidden = true;
|
|
9216
|
+
menu.setAttribute("role", "menu");
|
|
9217
|
+
document.body.appendChild(menu);
|
|
9218
|
+
previewLinkMenuEl = menu;
|
|
9219
|
+
return menu;
|
|
9220
|
+
}
|
|
9221
|
+
|
|
9222
|
+
function appendPreviewLinkMenuButton(menu, label, action) {
|
|
9223
|
+
const button = document.createElement("button");
|
|
9224
|
+
button.type = "button";
|
|
9225
|
+
button.setAttribute("role", "menuitem");
|
|
9226
|
+
button.dataset.previewLinkAction = action;
|
|
9227
|
+
button.textContent = label;
|
|
9228
|
+
menu.appendChild(button);
|
|
9229
|
+
}
|
|
9230
|
+
|
|
9231
|
+
function positionPreviewLinkMenu(menu, clientX, clientY) {
|
|
9232
|
+
const margin = 8;
|
|
9233
|
+
menu.style.left = "0px";
|
|
9234
|
+
menu.style.top = "0px";
|
|
9235
|
+
menu.hidden = false;
|
|
9236
|
+
const rect = menu.getBoundingClientRect();
|
|
9237
|
+
const x = Math.max(margin, Math.min(window.innerWidth - rect.width - margin, Number(clientX) || margin));
|
|
9238
|
+
const y = Math.max(margin, Math.min(window.innerHeight - rect.height - margin, Number(clientY) || margin));
|
|
9239
|
+
menu.style.left = x + "px";
|
|
9240
|
+
menu.style.top = y + "px";
|
|
9241
|
+
}
|
|
9242
|
+
|
|
9243
|
+
function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
9244
|
+
const href = String(anchor && anchor.getAttribute ? anchor.getAttribute("href") || "" : (contextOverride && contextOverride.href ? contextOverride.href : "")).trim();
|
|
9245
|
+
if (!isStudioLocalPreviewHref(href)) return false;
|
|
9246
|
+
const kind = getPreviewLocalLinkKind(href);
|
|
9247
|
+
const menu = ensurePreviewLinkMenu();
|
|
9248
|
+
menu.innerHTML = "";
|
|
9249
|
+
const linkContext = getEffectivePreviewLinkContext(contextOverride);
|
|
9250
|
+
activePreviewLinkContext = {
|
|
9251
|
+
href,
|
|
9252
|
+
title: String((contextOverride && contextOverride.title) || (anchor && anchor.textContent) || href || "local link").trim() || href,
|
|
9253
|
+
sourcePath: linkContext.sourcePath,
|
|
9254
|
+
resourceDir: linkContext.resourceDir,
|
|
9255
|
+
};
|
|
9256
|
+
if (kind === "pdf") {
|
|
9257
|
+
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
9258
|
+
} else if (kind === "text") {
|
|
9259
|
+
appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
|
|
9260
|
+
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
9261
|
+
} else if (kind === "image") {
|
|
9262
|
+
appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
|
|
9263
|
+
}
|
|
9264
|
+
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
9265
|
+
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
9266
|
+
positionPreviewLinkMenu(menu, event && event.clientX, event && event.clientY);
|
|
9267
|
+
const firstButton = menu.querySelector("button");
|
|
9268
|
+
if (firstButton && typeof firstButton.focus === "function") {
|
|
9269
|
+
window.setTimeout(() => firstButton.focus({ preventScroll: true }), 0);
|
|
9270
|
+
}
|
|
9271
|
+
return true;
|
|
9272
|
+
}
|
|
9273
|
+
|
|
9274
|
+
async function fetchPreviewLocalLink(action, href, contextOverride) {
|
|
9275
|
+
return fetchStudioJson("/local-preview-link", {
|
|
9276
|
+
query: { ...getPreviewLinkResourceQuery(href, contextOverride), action },
|
|
9277
|
+
});
|
|
9278
|
+
}
|
|
9279
|
+
|
|
9280
|
+
function getPreviewPdfViewerUrl(href, contextOverride) {
|
|
9281
|
+
const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
|
|
9282
|
+
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
|
|
9283
|
+
const resourceUrl = buildStudioPdfResourceUrl({ path: cleanPath, sourcePath: context.sourcePath || "", resourceDir: context.resourceDir || "" }, true);
|
|
9284
|
+
const page = parsePreviewLocalLinkPage(href);
|
|
9285
|
+
return resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
9286
|
+
}
|
|
9287
|
+
|
|
9288
|
+
function openPreviewPdfLink(href, title, contextOverride) {
|
|
9289
|
+
const viewerUrl = getPreviewPdfViewerUrl(href, contextOverride);
|
|
9290
|
+
if (!viewerUrl) {
|
|
9291
|
+
setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
|
|
9292
|
+
return false;
|
|
9293
|
+
}
|
|
9294
|
+
openStudioPdfFocusViewer(viewerUrl, title || href);
|
|
9295
|
+
return true;
|
|
9296
|
+
}
|
|
9297
|
+
|
|
9298
|
+
async function openPreviewImageLink(href, title, contextOverride, pendingWindow) {
|
|
9299
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
9300
|
+
try {
|
|
9301
|
+
if (popup && popup.document && popup.document.body) {
|
|
9302
|
+
popup.document.title = "Opening image…";
|
|
9303
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening image…</p>";
|
|
9304
|
+
}
|
|
9305
|
+
} catch {}
|
|
9306
|
+
try {
|
|
9307
|
+
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
9308
|
+
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
9309
|
+
});
|
|
9310
|
+
const dataUrl = payload && typeof payload.dataUrl === "string" ? payload.dataUrl : "";
|
|
9311
|
+
if (!dataUrl) throw new Error("Studio did not return image data.");
|
|
9312
|
+
const safeTitle = escapeHtml(String(title || href || "Local image"));
|
|
9313
|
+
const safeSrc = escapeHtml(dataUrl);
|
|
9314
|
+
const html = "<!doctype html><html><head><meta charset='utf-8'><title>" + safeTitle + "</title>"
|
|
9315
|
+
+ "<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee;font:13px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}img{max-width:100vw;max-height:100vh;object-fit:contain;}header{position:fixed;left:0;right:0;top:0;padding:8px 10px;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);}</style>"
|
|
9316
|
+
+ "</head><body><header>" + safeTitle + "</header><img src='" + safeSrc + "' alt='" + safeTitle + "'></body></html>";
|
|
9317
|
+
if (popup && !popup.closed && popup.document) {
|
|
9318
|
+
popup.document.open();
|
|
9319
|
+
popup.document.write(html);
|
|
9320
|
+
popup.document.close();
|
|
9321
|
+
setStatus("Opened local image preview.", "success");
|
|
9322
|
+
return;
|
|
9323
|
+
}
|
|
9324
|
+
const opened = window.open(dataUrl, "_blank");
|
|
9325
|
+
if (!opened) throw new Error("Popup blocked while opening image preview.");
|
|
9326
|
+
setStatus("Opened local image preview.", "success");
|
|
9327
|
+
} catch (error) {
|
|
9328
|
+
if (popup && !popup.closed) {
|
|
9329
|
+
try { popup.close(); } catch {}
|
|
9330
|
+
}
|
|
9331
|
+
throw error;
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
|
|
9335
|
+
function editorHasPotentialUnsavedContent() {
|
|
9336
|
+
const text = String(sourceTextEl.value || "");
|
|
9337
|
+
if (!text.trim()) return false;
|
|
9338
|
+
if (hasRefreshableFilePath()) return editorDiffersFromFileBackedBaseline();
|
|
9339
|
+
return true;
|
|
9340
|
+
}
|
|
9341
|
+
|
|
9342
|
+
async function openPreviewDocumentHere(href, contextOverride) {
|
|
9343
|
+
if (editorHasPotentialUnsavedContent()) {
|
|
9344
|
+
const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
|
|
9345
|
+
if (!confirmed) return;
|
|
9346
|
+
}
|
|
9347
|
+
const payload = await fetchPreviewLocalLink("document", href, contextOverride);
|
|
9348
|
+
if (typeof payload.text !== "string") throw new Error("Studio did not return document text.");
|
|
9349
|
+
const path = typeof payload.path === "string" ? payload.path : "";
|
|
9350
|
+
const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
|
|
9351
|
+
const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
|
|
9352
|
+
if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
|
|
9353
|
+
setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
|
|
9354
|
+
setSourceState({ source: "file", label, path });
|
|
9355
|
+
markFileBackedBaseline(payload.text);
|
|
9356
|
+
const detected = detectLanguageFromName(path || label);
|
|
9357
|
+
if (detected) setEditorLanguage(detected);
|
|
9358
|
+
setEditorView("markdown");
|
|
9359
|
+
setActivePane("left");
|
|
9360
|
+
setStatus("Opened linked file in editor: " + label, "success");
|
|
9361
|
+
}
|
|
9362
|
+
|
|
9363
|
+
async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
|
|
9364
|
+
const popup = pendingWindow || window.open("", "_blank");
|
|
9365
|
+
try {
|
|
9366
|
+
if (popup && popup.document && popup.document.body) {
|
|
9367
|
+
popup.document.title = "Opening linked file…";
|
|
9368
|
+
popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening linked file…</p>";
|
|
9369
|
+
}
|
|
9370
|
+
} catch {}
|
|
9371
|
+
try {
|
|
9372
|
+
const payload = await fetchPreviewLocalLink("editor-url", href, contextOverride);
|
|
9373
|
+
const targetUrl = payload && typeof payload.relativeUrl === "string"
|
|
9374
|
+
? new URL(payload.relativeUrl, window.location.href).href
|
|
9375
|
+
: (payload && typeof payload.url === "string" ? payload.url : "");
|
|
9376
|
+
if (!targetUrl) throw new Error("Studio did not return an editor URL.");
|
|
9377
|
+
if (popup && !popup.closed) {
|
|
9378
|
+
try {
|
|
9379
|
+
popup.opener = null;
|
|
9380
|
+
popup.location.href = targetUrl;
|
|
9381
|
+
setStatus("Opening linked file in a new editor.", "success");
|
|
9382
|
+
return;
|
|
9383
|
+
} catch {}
|
|
9384
|
+
}
|
|
9385
|
+
window.open(targetUrl, "_blank", "noopener");
|
|
9386
|
+
setStatus("Opening linked file in a new editor.", "success");
|
|
9387
|
+
} catch (error) {
|
|
9388
|
+
if (popup && !popup.closed) {
|
|
9389
|
+
try { popup.close(); } catch {}
|
|
9390
|
+
}
|
|
9391
|
+
throw error;
|
|
9392
|
+
}
|
|
9393
|
+
}
|
|
9394
|
+
|
|
9395
|
+
async function copyPreviewLocalLinkPath(href, contextOverride) {
|
|
9396
|
+
const payload = await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
9397
|
+
const path = typeof payload.path === "string" ? payload.path : "";
|
|
9398
|
+
if (!path) throw new Error("Studio did not return a file path.");
|
|
9399
|
+
const ok = await writeTextToClipboard(path);
|
|
9400
|
+
if (!ok) throw new Error("Clipboard write failed.");
|
|
9401
|
+
setStatus("Copied local path.", "success");
|
|
9402
|
+
}
|
|
9403
|
+
|
|
9404
|
+
async function revealPreviewLocalLink(href, contextOverride) {
|
|
9405
|
+
const query = getPreviewLinkResourceQuery(href, contextOverride);
|
|
9406
|
+
const payload = await fetchStudioJson("/reveal-local-resource", {
|
|
9407
|
+
method: "POST",
|
|
9408
|
+
body: JSON.stringify(query),
|
|
9409
|
+
});
|
|
9410
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Opened file manager.", "success");
|
|
9411
|
+
}
|
|
9412
|
+
|
|
9413
|
+
async function runPreviewLinkAction(action, context) {
|
|
9414
|
+
const href = context && context.href ? context.href : "";
|
|
9415
|
+
if (!href) return;
|
|
9416
|
+
try {
|
|
9417
|
+
if (action === "open-pdf") {
|
|
9418
|
+
openPreviewPdfLink(href, context.title || href, context);
|
|
9419
|
+
return;
|
|
9420
|
+
}
|
|
9421
|
+
if (action === "open-new") {
|
|
9422
|
+
await openPreviewDocumentInNewEditor(href, null, context);
|
|
9423
|
+
return;
|
|
9424
|
+
}
|
|
9425
|
+
if (action === "open-here") {
|
|
9426
|
+
await openPreviewDocumentHere(href, context);
|
|
9427
|
+
return;
|
|
9428
|
+
}
|
|
9429
|
+
if (action === "open-image") {
|
|
9430
|
+
await openPreviewImageLink(href, context.title || href, context);
|
|
9431
|
+
return;
|
|
9432
|
+
}
|
|
9433
|
+
if (action === "copy-path") {
|
|
9434
|
+
await copyPreviewLocalLinkPath(href, context);
|
|
9435
|
+
return;
|
|
9436
|
+
}
|
|
9437
|
+
if (action === "reveal") {
|
|
9438
|
+
await revealPreviewLocalLink(href, context);
|
|
9439
|
+
}
|
|
9440
|
+
} catch (error) {
|
|
9441
|
+
setStatus((error && error.message) ? error.message : String(error || "Local link action failed."), "warning");
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
|
|
9445
|
+
function handlePreviewLocalLinkClick(event) {
|
|
9446
|
+
const anchor = getPreviewLinkAnchorFromEvent(event);
|
|
9447
|
+
if (!anchor) return;
|
|
9448
|
+
const href = String(anchor.getAttribute("href") || "").trim();
|
|
9449
|
+
const kind = getPreviewLocalLinkKind(href);
|
|
9450
|
+
event.preventDefault();
|
|
9451
|
+
event.stopPropagation();
|
|
9452
|
+
closePreviewLinkMenu();
|
|
9453
|
+
const title = String(anchor.textContent || href).trim() || href;
|
|
9454
|
+
if (kind === "pdf") {
|
|
9455
|
+
openPreviewPdfLink(href, title);
|
|
9456
|
+
return;
|
|
9457
|
+
}
|
|
9458
|
+
if (kind === "image") {
|
|
9459
|
+
const pendingWindow = window.open("", "_blank");
|
|
9460
|
+
void openPreviewImageLink(href, title, null, pendingWindow).catch((error) => {
|
|
9461
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked image."), "warning");
|
|
9462
|
+
});
|
|
9463
|
+
return;
|
|
9464
|
+
}
|
|
9465
|
+
if (kind === "text") {
|
|
9466
|
+
const pendingWindow = window.open("", "_blank");
|
|
9467
|
+
void openPreviewDocumentInNewEditor(href, pendingWindow).catch((error) => {
|
|
9468
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
9469
|
+
});
|
|
9470
|
+
return;
|
|
9471
|
+
}
|
|
9472
|
+
setStatus("Right-click this local link for file actions.", "warning");
|
|
9473
|
+
}
|
|
9474
|
+
|
|
9475
|
+
function handlePreviewLocalLinkContextMenu(event) {
|
|
9476
|
+
const anchor = getPreviewLinkAnchorFromEvent(event);
|
|
9477
|
+
if (!anchor) return;
|
|
9478
|
+
event.preventDefault();
|
|
9479
|
+
event.stopPropagation();
|
|
9480
|
+
showPreviewLinkMenu(anchor, event);
|
|
9481
|
+
}
|
|
9482
|
+
|
|
8306
9483
|
function makeRequestId() {
|
|
8307
9484
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
8308
9485
|
return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -8520,10 +9697,6 @@
|
|
|
8520
9697
|
return index > 0 ? value.slice(0, index) : "";
|
|
8521
9698
|
}
|
|
8522
9699
|
|
|
8523
|
-
function getCurrentResourceDirValue() {
|
|
8524
|
-
return resourceDirInput ? String(resourceDirInput.value || "").trim() : "";
|
|
8525
|
-
}
|
|
8526
|
-
|
|
8527
9700
|
function getDefaultQuizContextPath(scope) {
|
|
8528
9701
|
const normalizedScope = normalizeQuizScope(scope);
|
|
8529
9702
|
const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
|
|
@@ -14174,6 +15347,7 @@
|
|
|
14174
15347
|
scheduleSourcePreviewRender(0);
|
|
14175
15348
|
}
|
|
14176
15349
|
updateOutlineUi();
|
|
15350
|
+
scheduleWorkspacePersistence();
|
|
14177
15351
|
}
|
|
14178
15352
|
|
|
14179
15353
|
function setEditorHighlightMode(mode) {
|
|
@@ -14262,7 +15436,7 @@
|
|
|
14262
15436
|
sendRunBtn.classList.toggle("request-stop-active", directIsStop);
|
|
14263
15437
|
sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
|
|
14264
15438
|
sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
|
|
14265
|
-
const replHint = rightView === "repl" &&
|
|
15439
|
+
const replHint = rightView === "repl" && getActiveReplSessionForCurrentRuntime()
|
|
14266
15440
|
? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
|
|
14267
15441
|
: "";
|
|
14268
15442
|
sendRunBtn.title = directIsStop
|
|
@@ -14283,7 +15457,7 @@
|
|
|
14283
15457
|
: "Queue steering is available while Run editor text is active.";
|
|
14284
15458
|
}
|
|
14285
15459
|
|
|
14286
|
-
const hasReplSession = Boolean(
|
|
15460
|
+
const hasReplSession = Boolean(getActiveReplSessionForCurrentRuntime());
|
|
14287
15461
|
if (sendReplBtn) {
|
|
14288
15462
|
const showReplSend = rightView === "repl";
|
|
14289
15463
|
sendReplBtn.hidden = !showReplSend;
|
|
@@ -14651,7 +15825,7 @@
|
|
|
14651
15825
|
replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
|
|
14652
15826
|
const sessionsChanged = setReplSessions(message.sessions);
|
|
14653
15827
|
if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
|
|
14654
|
-
|
|
15828
|
+
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
14655
15829
|
}
|
|
14656
15830
|
const journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
14657
15831
|
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
@@ -14704,7 +15878,7 @@
|
|
|
14704
15878
|
}
|
|
14705
15879
|
}
|
|
14706
15880
|
if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
|
|
14707
|
-
|
|
15881
|
+
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
14708
15882
|
}
|
|
14709
15883
|
let journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
14710
15884
|
if (typeof message.transcript === "string") {
|
|
@@ -14902,6 +16076,10 @@
|
|
|
14902
16076
|
stickyStudioKind = null;
|
|
14903
16077
|
}
|
|
14904
16078
|
if (message.path) {
|
|
16079
|
+
const savedResourceDir = typeof message.resourceDir === "string" && message.resourceDir.trim()
|
|
16080
|
+
? normalizeStudioResourceDirValue(message.resourceDir)
|
|
16081
|
+
: dirnameForDisplayPath(message.path);
|
|
16082
|
+
if (resourceDirInput) resourceDirInput.value = savedResourceDir;
|
|
14905
16083
|
setSourceState({
|
|
14906
16084
|
source: "file",
|
|
14907
16085
|
label: message.label || message.path,
|
|
@@ -14977,6 +16155,10 @@
|
|
|
14977
16155
|
? nextDoc.path
|
|
14978
16156
|
: null;
|
|
14979
16157
|
|
|
16158
|
+
const nextResourceDir = typeof nextDoc.resourceDir === "string" && nextDoc.resourceDir.trim()
|
|
16159
|
+
? normalizeStudioResourceDirValue(nextDoc.resourceDir)
|
|
16160
|
+
: (nextPath ? dirnameForDisplayPath(nextPath) : "");
|
|
16161
|
+
if (resourceDirInput) resourceDirInput.value = nextResourceDir;
|
|
14980
16162
|
setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
|
|
14981
16163
|
setSourceState({
|
|
14982
16164
|
source: nextSource,
|
|
@@ -15519,6 +16701,8 @@
|
|
|
15519
16701
|
rightPaneEl.addEventListener("focusin", (event) => activatePaneFromInteraction("right", event));
|
|
15520
16702
|
}
|
|
15521
16703
|
|
|
16704
|
+
setupPaneResizeHandle();
|
|
16705
|
+
|
|
15522
16706
|
if (leftFocusBtn) {
|
|
15523
16707
|
leftFocusBtn.addEventListener("click", () => {
|
|
15524
16708
|
if (paneFocusTarget === "left") {
|
|
@@ -15543,6 +16727,7 @@
|
|
|
15543
16727
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
15544
16728
|
window.addEventListener("beforeunload", () => {
|
|
15545
16729
|
stopFooterSpinner();
|
|
16730
|
+
flushWorkspacePersistence();
|
|
15546
16731
|
flushScratchpadPersistence();
|
|
15547
16732
|
flushReviewNotesPersistence();
|
|
15548
16733
|
});
|
|
@@ -15559,6 +16744,7 @@
|
|
|
15559
16744
|
|
|
15560
16745
|
followSelect.addEventListener("change", () => {
|
|
15561
16746
|
followLatest = followSelect.value !== "off";
|
|
16747
|
+
scheduleWorkspacePersistence();
|
|
15562
16748
|
if (followLatest && queuedLatestResponse) {
|
|
15563
16749
|
if (responseHistory.length > 0) {
|
|
15564
16750
|
selectHistoryIndex(responseHistory.length - 1, { silent: true });
|
|
@@ -15722,6 +16908,7 @@
|
|
|
15722
16908
|
renderReviewNotesList();
|
|
15723
16909
|
updateReviewNotesUi();
|
|
15724
16910
|
}
|
|
16911
|
+
scheduleWorkspacePersistence();
|
|
15725
16912
|
});
|
|
15726
16913
|
|
|
15727
16914
|
sourceTextEl.addEventListener("select", () => {
|
|
@@ -15898,7 +17085,10 @@
|
|
|
15898
17085
|
closeExportPreviewMenu();
|
|
15899
17086
|
});
|
|
15900
17087
|
document.addEventListener("keydown", (event) => {
|
|
15901
|
-
if (event.key === "Escape")
|
|
17088
|
+
if (event.key === "Escape") {
|
|
17089
|
+
closeExportPreviewMenu();
|
|
17090
|
+
closePreviewLinkMenu();
|
|
17091
|
+
}
|
|
15902
17092
|
});
|
|
15903
17093
|
|
|
15904
17094
|
saveAsBtn.addEventListener("click", () => {
|
|
@@ -15909,7 +17099,7 @@
|
|
|
15909
17099
|
}
|
|
15910
17100
|
|
|
15911
17101
|
var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
|
|
15912
|
-
var suggestedDir =
|
|
17102
|
+
var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
|
|
15913
17103
|
const suggested = sourceState.path || (suggestedDir + suggestedName);
|
|
15914
17104
|
const path = window.prompt("Save editor content as:", suggested);
|
|
15915
17105
|
if (!path) return;
|
|
@@ -15988,6 +17178,12 @@
|
|
|
15988
17178
|
});
|
|
15989
17179
|
}
|
|
15990
17180
|
|
|
17181
|
+
if (clearWorkspaceBtn) {
|
|
17182
|
+
clearWorkspaceBtn.addEventListener("click", () => {
|
|
17183
|
+
clearStudioWorkspace();
|
|
17184
|
+
});
|
|
17185
|
+
}
|
|
17186
|
+
|
|
15991
17187
|
sendEditorBtn.addEventListener("click", () => {
|
|
15992
17188
|
const content = sourceTextEl.value;
|
|
15993
17189
|
if (!content.trim()) {
|
|
@@ -16025,9 +17221,7 @@
|
|
|
16025
17221
|
content,
|
|
16026
17222
|
label: sourceState && sourceState.label ? sourceState.label : "current editor",
|
|
16027
17223
|
path: sourceState && sourceState.path ? sourceState.path : undefined,
|
|
16028
|
-
resourceDir:
|
|
16029
|
-
? resourceDirInput.value.trim()
|
|
16030
|
-
: undefined,
|
|
17224
|
+
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
16031
17225
|
});
|
|
16032
17226
|
|
|
16033
17227
|
if (!sent) {
|
|
@@ -16067,9 +17261,7 @@
|
|
|
16067
17261
|
type: "load_git_diff_request",
|
|
16068
17262
|
requestId,
|
|
16069
17263
|
sourcePath: effectivePath || sourceState.path || undefined,
|
|
16070
|
-
resourceDir:
|
|
16071
|
-
? resourceDirInput.value.trim()
|
|
16072
|
-
: undefined,
|
|
17264
|
+
resourceDir: getCurrentResourceDirValue() || undefined,
|
|
16073
17265
|
});
|
|
16074
17266
|
|
|
16075
17267
|
if (!sent) {
|
|
@@ -16288,6 +17480,27 @@
|
|
|
16288
17480
|
void handleCopyPreviewBlockButtonClick(event);
|
|
16289
17481
|
}, true);
|
|
16290
17482
|
|
|
17483
|
+
document.addEventListener("click", (event) => {
|
|
17484
|
+
const target = event.target;
|
|
17485
|
+
const menuButton = target instanceof Element ? target.closest(".studio-preview-link-menu [data-preview-link-action]") : null;
|
|
17486
|
+
if (menuButton) {
|
|
17487
|
+
event.preventDefault();
|
|
17488
|
+
event.stopPropagation();
|
|
17489
|
+
const action = String(menuButton.getAttribute("data-preview-link-action") || "");
|
|
17490
|
+
const context = activePreviewLinkContext;
|
|
17491
|
+
closePreviewLinkMenu();
|
|
17492
|
+
void runPreviewLinkAction(action, context);
|
|
17493
|
+
return;
|
|
17494
|
+
}
|
|
17495
|
+
if (target instanceof Element && target.closest(".studio-preview-link-menu")) return;
|
|
17496
|
+
closePreviewLinkMenu();
|
|
17497
|
+
handlePreviewLocalLinkClick(event);
|
|
17498
|
+
}, true);
|
|
17499
|
+
|
|
17500
|
+
document.addEventListener("contextmenu", (event) => {
|
|
17501
|
+
handlePreviewLocalLinkContextMenu(event);
|
|
17502
|
+
}, true);
|
|
17503
|
+
|
|
16291
17504
|
document.addEventListener("pointerup", (event) => {
|
|
16292
17505
|
const target = event.target;
|
|
16293
17506
|
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|
|
@@ -16478,7 +17691,8 @@
|
|
|
16478
17691
|
if (resourceDirLabel) resourceDirLabel.hidden = state !== "label";
|
|
16479
17692
|
}
|
|
16480
17693
|
function applyResourceDir() {
|
|
16481
|
-
var dir =
|
|
17694
|
+
var dir = getCurrentResourceDirValue();
|
|
17695
|
+
if (resourceDirInput) resourceDirInput.value = dir;
|
|
16482
17696
|
if (dir) {
|
|
16483
17697
|
if (resourceDirLabel) resourceDirLabel.textContent = "Working dir: " + dir;
|
|
16484
17698
|
showResourceDirState("label");
|
|
@@ -16488,6 +17702,7 @@
|
|
|
16488
17702
|
updateSaveFileTooltip();
|
|
16489
17703
|
syncActionButtons();
|
|
16490
17704
|
renderSourcePreview();
|
|
17705
|
+
scheduleWorkspacePersistence();
|
|
16491
17706
|
}
|
|
16492
17707
|
if (sourceBadgeEl) {
|
|
16493
17708
|
sourceBadgeEl.addEventListener("click", () => {
|
|
@@ -16513,7 +17728,7 @@
|
|
|
16513
17728
|
applyResourceDir();
|
|
16514
17729
|
} else if (e.key === "Escape") {
|
|
16515
17730
|
e.preventDefault();
|
|
16516
|
-
var dir =
|
|
17731
|
+
var dir = getCurrentResourceDirValue();
|
|
16517
17732
|
if (dir) {
|
|
16518
17733
|
showResourceDirState("label");
|
|
16519
17734
|
} else {
|
|
@@ -16530,6 +17745,7 @@
|
|
|
16530
17745
|
updateSaveFileTooltip();
|
|
16531
17746
|
syncActionButtons();
|
|
16532
17747
|
renderSourcePreview();
|
|
17748
|
+
scheduleWorkspacePersistence();
|
|
16533
17749
|
});
|
|
16534
17750
|
}
|
|
16535
17751
|
|
|
@@ -16578,7 +17794,7 @@
|
|
|
16578
17794
|
setResponseFontSize(initialResponseFontSize, { persist: false });
|
|
16579
17795
|
|
|
16580
17796
|
if (resourceDirInput && initialResourceDir) {
|
|
16581
|
-
resourceDirInput.value = initialResourceDir;
|
|
17797
|
+
resourceDirInput.value = normalizeStudioResourceDirValue(initialResourceDir);
|
|
16582
17798
|
}
|
|
16583
17799
|
setSourceState(initialSourceState);
|
|
16584
17800
|
refreshResponseUi();
|
|
@@ -16606,9 +17822,16 @@
|
|
|
16606
17822
|
setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
|
|
16607
17823
|
setReplSendMode(replSendMode);
|
|
16608
17824
|
|
|
17825
|
+
const persistedWorkspaceState = readPersistedWorkspaceState();
|
|
17826
|
+
applyPersistedWorkspaceState(persistedWorkspaceState);
|
|
17827
|
+
|
|
16609
17828
|
setEditorView(editorView);
|
|
16610
17829
|
setRightView(rightView);
|
|
16611
17830
|
renderSourcePreview();
|
|
17831
|
+
workspacePersistenceReady = true;
|
|
17832
|
+
if (workspaceRestoredFromBrowser) {
|
|
17833
|
+
setStatus("Restored editor workspace from this browser tab. Use Clear editor to discard it.", "success");
|
|
17834
|
+
}
|
|
16612
17835
|
connect();
|
|
16613
17836
|
} catch (error) {
|
|
16614
17837
|
hardFail("Studio UI init failed", error);
|