pi-studio 0.5.43 → 0.5.45
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 +21 -0
- package/README.md +2 -1
- package/client/studio-client.js +1509 -68
- package/client/studio.css +372 -4
- package/index.ts +481 -51
- package/package.json +9 -2
package/client/studio-client.js
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
const statusSpinnerEl = document.getElementById("statusSpinner");
|
|
5
5
|
const footerMetaEl = document.getElementById("footerMeta");
|
|
6
6
|
const footerMetaTextEl = document.getElementById("footerMetaText");
|
|
7
|
+
let faviconLinkEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
|
|
8
|
+
if (!faviconLinkEl) {
|
|
9
|
+
faviconLinkEl = document.createElement("link");
|
|
10
|
+
faviconLinkEl.rel = "icon";
|
|
11
|
+
document.head.appendChild(faviconLinkEl);
|
|
12
|
+
}
|
|
7
13
|
const BRAILLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
8
14
|
let spinnerTimer = null;
|
|
9
15
|
let spinnerFrameIndex = 0;
|
|
@@ -41,6 +47,11 @@
|
|
|
41
47
|
const sourceEditorWrapEl = document.getElementById("sourceEditorWrap");
|
|
42
48
|
const sourceTextEl = document.getElementById("sourceText");
|
|
43
49
|
const sourceHighlightEl = document.getElementById("sourceHighlight");
|
|
50
|
+
const reviewNoteGutterEl = document.getElementById("reviewNoteGutter");
|
|
51
|
+
const reviewNoteGutterContentEl = document.getElementById("reviewNoteGutterContent");
|
|
52
|
+
const lineNumberGutterEl = document.getElementById("lineNumberGutter");
|
|
53
|
+
const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
|
|
54
|
+
const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
|
|
44
55
|
const sourcePreviewEl = document.getElementById("sourcePreview");
|
|
45
56
|
const leftPaneEl = document.getElementById("leftPane");
|
|
46
57
|
const rightPaneEl = document.getElementById("rightPane");
|
|
@@ -84,11 +95,12 @@
|
|
|
84
95
|
const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
|
|
85
96
|
const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
|
|
86
97
|
const highlightSelect = document.getElementById("highlightSelect");
|
|
87
|
-
const
|
|
98
|
+
const lineNumbersSelect = document.getElementById("lineNumbersSelect");
|
|
88
99
|
const annotationModeSelect = document.getElementById("annotationModeSelect");
|
|
89
100
|
const compactBtn = document.getElementById("compactBtn");
|
|
90
101
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
91
102
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
103
|
+
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
92
104
|
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
93
105
|
const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
|
|
94
106
|
const scratchpadDialogEl = document.getElementById("scratchpadDialog");
|
|
@@ -99,16 +111,35 @@
|
|
|
99
111
|
const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
|
|
100
112
|
const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
|
|
101
113
|
const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
|
|
114
|
+
const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
|
|
115
|
+
const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
|
|
116
|
+
const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
|
|
117
|
+
const reviewNotesListEl = document.getElementById("reviewNotesList");
|
|
118
|
+
const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
|
|
119
|
+
const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
|
|
120
|
+
const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
|
|
121
|
+
const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
|
|
122
|
+
const reviewNotesDoneBtn = document.getElementById("reviewNotesDoneBtn");
|
|
102
123
|
|
|
103
124
|
const studioMode = (document.body && document.body.dataset && document.body.dataset.studioMode) === "editor-only"
|
|
104
125
|
? "editor-only"
|
|
105
126
|
: "full";
|
|
106
127
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
107
128
|
|
|
129
|
+
const initialQueryParams = new URLSearchParams(window.location.search || "");
|
|
130
|
+
const explicitDocumentIdentityFromUrl = initialQueryParams.has("docSource")
|
|
131
|
+
|| initialQueryParams.has("docLabel")
|
|
132
|
+
|| initialQueryParams.has("docPath")
|
|
133
|
+
|| initialQueryParams.has("draftId");
|
|
108
134
|
const initialSourceState = {
|
|
109
|
-
source: (
|
|
110
|
-
|
|
111
|
-
|
|
135
|
+
source: initialQueryParams.get("docSource")
|
|
136
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialSource) || "blank"),
|
|
137
|
+
label: initialQueryParams.get("docLabel")
|
|
138
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialLabel) || "blank"),
|
|
139
|
+
path: initialQueryParams.get("docPath")
|
|
140
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialPath) || null),
|
|
141
|
+
draftId: initialQueryParams.get("draftId")
|
|
142
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialDraftId) || null),
|
|
112
143
|
};
|
|
113
144
|
|
|
114
145
|
let ws = null;
|
|
@@ -158,6 +189,7 @@
|
|
|
158
189
|
let titleAttentionMessage = "";
|
|
159
190
|
let titleAttentionRequestId = null;
|
|
160
191
|
let titleAttentionRequestKind = null;
|
|
192
|
+
let lastRenderedFaviconHref = "";
|
|
161
193
|
|
|
162
194
|
function parseFiniteNumber(value) {
|
|
163
195
|
if (value == null || value === "") return null;
|
|
@@ -196,6 +228,7 @@
|
|
|
196
228
|
source: initialSourceState.source,
|
|
197
229
|
label: initialSourceState.label,
|
|
198
230
|
path: initialSourceState.path,
|
|
231
|
+
draftId: initialSourceState.draftId,
|
|
199
232
|
};
|
|
200
233
|
let fileBackedBaselineText = null;
|
|
201
234
|
let activePane = "left";
|
|
@@ -203,6 +236,7 @@
|
|
|
203
236
|
const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
|
|
204
237
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
205
238
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
239
|
+
const EDITOR_LINE_NUMBERS_STORAGE_KEY = "piStudio.editorLineNumbersEnabled";
|
|
206
240
|
// Single source of truth: language -> file extensions (and display label)
|
|
207
241
|
var LANG_EXT_MAP = {
|
|
208
242
|
markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
|
|
@@ -244,7 +278,6 @@
|
|
|
244
278
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
245
279
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
246
280
|
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
247
|
-
const SCRATCHPAD_STORAGE_KEY = "piStudio.scratchpad";
|
|
248
281
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
249
282
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
250
283
|
const previewPendingTimers = new WeakMap();
|
|
@@ -258,9 +291,19 @@
|
|
|
258
291
|
let editorLanguage = "markdown";
|
|
259
292
|
let responseHighlightEnabled = false;
|
|
260
293
|
let editorHighlightRenderRaf = null;
|
|
294
|
+
let lineNumbersEnabled = false;
|
|
295
|
+
let lineNumbersRenderRaf = null;
|
|
261
296
|
let annotationsEnabled = true;
|
|
262
297
|
let scratchpadText = "";
|
|
263
298
|
let scratchpadReturnFocusEl = null;
|
|
299
|
+
let scratchpadPersistTimer = null;
|
|
300
|
+
let scratchpadLoadNonce = 0;
|
|
301
|
+
let reviewNotes = [];
|
|
302
|
+
let reviewNotesReturnFocusEl = null;
|
|
303
|
+
let reviewNotesPersistTimer = null;
|
|
304
|
+
let reviewNotesLoadNonce = 0;
|
|
305
|
+
let pendingReviewNoteFocusId = null;
|
|
306
|
+
let pendingReviewNoteInlineFocusId = null;
|
|
264
307
|
const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
|
|
265
308
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
266
309
|
if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
|
|
@@ -664,14 +707,134 @@
|
|
|
664
707
|
updateDocumentTitle();
|
|
665
708
|
}
|
|
666
709
|
|
|
710
|
+
function truncateTitleSegment(text, maxLength) {
|
|
711
|
+
const normalized = normalizeActivityLabel(text);
|
|
712
|
+
if (!normalized) return "";
|
|
713
|
+
if (!Number.isFinite(maxLength) || maxLength <= 1 || normalized.length <= maxLength) {
|
|
714
|
+
return normalized;
|
|
715
|
+
}
|
|
716
|
+
return normalized.slice(0, maxLength - 1).trimEnd() + "…";
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function readThemeColor(variableName, fallback) {
|
|
720
|
+
try {
|
|
721
|
+
const value = window.getComputedStyle(document.documentElement).getPropertyValue(variableName);
|
|
722
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
723
|
+
return trimmed || fallback;
|
|
724
|
+
} catch {
|
|
725
|
+
return fallback;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function getTitleActionMessage(kind) {
|
|
730
|
+
if (kind === "annotation") return "Replying…";
|
|
731
|
+
if (kind === "critique") return "Critiquing…";
|
|
732
|
+
if (kind === "direct") return "Running…";
|
|
733
|
+
if (kind === "compact") return "Compacting…";
|
|
734
|
+
if (kind === "send_to_editor") return "Sending to editor…";
|
|
735
|
+
if (kind === "get_from_editor") return "Loading from editor…";
|
|
736
|
+
if (kind === "load_git_diff") return "Loading git diff…";
|
|
737
|
+
if (kind === "refresh_from_disk") return "Refreshing from disk…";
|
|
738
|
+
if (kind === "save_as" || kind === "save_over") return "Saving…";
|
|
739
|
+
return "Working…";
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function getTitleBusyMessage() {
|
|
743
|
+
const activeKind = pendingKind || (agentBusyFromServer ? stickyStudioKind : null);
|
|
744
|
+
const hasStudioOwnedBusyState = uiBusy
|
|
745
|
+
|| Boolean(pendingRequestId)
|
|
746
|
+
|| Boolean(pendingKind)
|
|
747
|
+
|| compactInProgress
|
|
748
|
+
|| Boolean(agentBusyFromServer && stickyStudioKind)
|
|
749
|
+
|| Boolean(agentBusyFromServer && studioRunChainActive);
|
|
750
|
+
|
|
751
|
+
if (!hasStudioOwnedBusyState) return "";
|
|
752
|
+
|
|
753
|
+
if (
|
|
754
|
+
pendingKind === "compact"
|
|
755
|
+
|| compactInProgress
|
|
756
|
+
|| (agentBusyFromServer && stickyStudioKind === "compact")
|
|
757
|
+
) {
|
|
758
|
+
return "Compacting…";
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (terminalActivityPhase === "tool") {
|
|
762
|
+
if (terminalActivityLabel && !isGenericToolLabel(terminalActivityLabel)) {
|
|
763
|
+
return truncateTitleSegment(withEllipsis(terminalActivityLabel), 34);
|
|
764
|
+
}
|
|
765
|
+
if (activeKind) return getTitleActionMessage(activeKind);
|
|
766
|
+
if (agentBusyFromServer && studioRunChainActive) return "Running…";
|
|
767
|
+
return "Working…";
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (terminalActivityPhase === "responding") {
|
|
771
|
+
if (activeKind === "critique") return "Critiquing…";
|
|
772
|
+
if (activeKind === "annotation") return "Replying…";
|
|
773
|
+
if (activeKind === "direct") return "Thinking…";
|
|
774
|
+
return "Working…";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (activeKind) return getTitleActionMessage(activeKind);
|
|
778
|
+
if (uiBusy || (agentBusyFromServer && studioRunChainActive)) return "Running…";
|
|
779
|
+
return "";
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function getDynamicTitlePrefix() {
|
|
783
|
+
if (titleAttentionMessage) return titleAttentionMessage;
|
|
784
|
+
if (wsState === "Connecting") return reconnectAttempt > 0 ? "Reconnecting…" : "Connecting…";
|
|
785
|
+
if (wsState === "Disconnected") return "Disconnected";
|
|
786
|
+
return getTitleBusyMessage();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function buildStudioFaviconHref() {
|
|
790
|
+
const idleColor = readThemeColor("--text", "#111111");
|
|
791
|
+
const accent = readThemeColor("--accent", "#2563eb");
|
|
792
|
+
const ok = readThemeColor("--ok", "#16a34a");
|
|
793
|
+
const warn = readThemeColor("--warn", "#d97706");
|
|
794
|
+
const error = readThemeColor("--error", "#dc2626");
|
|
795
|
+
|
|
796
|
+
let piColor = idleColor;
|
|
797
|
+
if (titleAttentionMessage) {
|
|
798
|
+
piColor = ok;
|
|
799
|
+
} else if (wsState === "Disconnected") {
|
|
800
|
+
piColor = error;
|
|
801
|
+
} else if (wsState === "Connecting") {
|
|
802
|
+
piColor = accent;
|
|
803
|
+
} else if (getTitleBusyMessage()) {
|
|
804
|
+
piColor = warn;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const svg = [
|
|
808
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">',
|
|
809
|
+
`<text x="32" y="35" text-anchor="middle" dominant-baseline="middle" font-size="50" font-weight="700" font-family="ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" fill="${piColor}">π</text>`,
|
|
810
|
+
"</svg>",
|
|
811
|
+
].join("");
|
|
812
|
+
return "data:image/svg+xml," + encodeURIComponent(svg);
|
|
813
|
+
}
|
|
814
|
+
|
|
667
815
|
function updateDocumentTitle() {
|
|
668
816
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
669
817
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
670
818
|
const titleParts = ["pi Studio"];
|
|
671
819
|
if (terminalText && terminalText !== "unknown") titleParts.push(terminalText);
|
|
672
820
|
if (modelText && modelText !== "none") titleParts.push(modelText);
|
|
673
|
-
|
|
674
|
-
|
|
821
|
+
|
|
822
|
+
const titlePrefix = getDynamicTitlePrefix();
|
|
823
|
+
if (titlePrefix) titleParts.unshift(titlePrefix);
|
|
824
|
+
|
|
825
|
+
const nextTitle = titleParts.join(" · ");
|
|
826
|
+
if (document.title !== nextTitle) {
|
|
827
|
+
document.title = nextTitle;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (faviconLinkEl) {
|
|
831
|
+
const nextFaviconHref = buildStudioFaviconHref();
|
|
832
|
+
if (nextFaviconHref !== lastRenderedFaviconHref) {
|
|
833
|
+
faviconLinkEl.href = nextFaviconHref;
|
|
834
|
+
faviconLinkEl.type = "image/svg+xml";
|
|
835
|
+
lastRenderedFaviconHref = nextFaviconHref;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
675
838
|
}
|
|
676
839
|
|
|
677
840
|
function updateFooterMeta() {
|
|
@@ -705,7 +868,7 @@
|
|
|
705
868
|
function startFooterSpinner() {
|
|
706
869
|
if (spinnerTimer) return;
|
|
707
870
|
spinnerTimer = window.setInterval(() => {
|
|
708
|
-
spinnerFrameIndex
|
|
871
|
+
spinnerFrameIndex += 1;
|
|
709
872
|
renderStatus();
|
|
710
873
|
}, 80);
|
|
711
874
|
}
|
|
@@ -803,6 +966,12 @@
|
|
|
803
966
|
function updateSourceBadge() {
|
|
804
967
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
805
968
|
sourceBadgeEl.textContent = "Editor origin: " + label;
|
|
969
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
970
|
+
if (sourceBadgeEl) {
|
|
971
|
+
sourceBadgeEl.title = descriptor.fileBacked
|
|
972
|
+
? ("Editor origin: " + label + "\nClick to reset origin and detach the current editor text into a new draft. The file on disk will not be changed.")
|
|
973
|
+
: ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes.");
|
|
974
|
+
}
|
|
806
975
|
// Show "Set working dir" button when not file-backed
|
|
807
976
|
var isFileBacked = hasRefreshableFilePath();
|
|
808
977
|
if (isFileBacked) {
|
|
@@ -826,6 +995,26 @@
|
|
|
826
995
|
}
|
|
827
996
|
}
|
|
828
997
|
|
|
998
|
+
function resetEditorOrigin() {
|
|
999
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
1000
|
+
const message = descriptor.fileBacked
|
|
1001
|
+
? ("Reset editor origin and detach the current text from\n\n" + descriptor.label + "\n\ninto a new draft? The file on disk will not be changed, and the current scratchpad/review notes will carry into the new draft.")
|
|
1002
|
+
: ("Reset editor origin and start a new independent draft? The current editor text, scratchpad, and review notes will carry into the new draft.");
|
|
1003
|
+
if (!window.confirm(message)) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const nextLabel = String(sourceTextEl.value || "").trim() ? "draft" : "blank";
|
|
1007
|
+
setSourceState({
|
|
1008
|
+
source: "blank",
|
|
1009
|
+
label: nextLabel,
|
|
1010
|
+
path: null,
|
|
1011
|
+
draftId: makeStudioDraftId(),
|
|
1012
|
+
}, {
|
|
1013
|
+
carryCurrentMetadataToNewDocument: true,
|
|
1014
|
+
});
|
|
1015
|
+
setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
829
1018
|
function updatePaneFocusButtons() {
|
|
830
1019
|
[
|
|
831
1020
|
[leftFocusBtn, "left"],
|
|
@@ -927,6 +1116,12 @@
|
|
|
927
1116
|
&& typeof scratchpadDialogEl.contains === "function"
|
|
928
1117
|
&& scratchpadDialogEl.contains(event.target)
|
|
929
1118
|
);
|
|
1119
|
+
const reviewNotesOwnsEvent = Boolean(
|
|
1120
|
+
reviewNotesDialogEl
|
|
1121
|
+
&& event.target
|
|
1122
|
+
&& typeof reviewNotesDialogEl.contains === "function"
|
|
1123
|
+
&& reviewNotesDialogEl.contains(event.target)
|
|
1124
|
+
);
|
|
930
1125
|
|
|
931
1126
|
if (isScratchpadOpen() && plainEscape) {
|
|
932
1127
|
event.preventDefault();
|
|
@@ -934,7 +1129,13 @@
|
|
|
934
1129
|
return;
|
|
935
1130
|
}
|
|
936
1131
|
|
|
937
|
-
if (
|
|
1132
|
+
if (isReviewNotesOpen() && plainEscape) {
|
|
1133
|
+
event.preventDefault();
|
|
1134
|
+
closeReviewNotes();
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent) {
|
|
938
1139
|
return;
|
|
939
1140
|
}
|
|
940
1141
|
|
|
@@ -2410,6 +2611,9 @@
|
|
|
2410
2611
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
2411
2612
|
scheduleEditorHighlightRender();
|
|
2412
2613
|
}
|
|
2614
|
+
if (editorView === "markdown") {
|
|
2615
|
+
scheduleEditorLineNumberRender();
|
|
2616
|
+
}
|
|
2413
2617
|
if (rightView === "editor-preview") {
|
|
2414
2618
|
scheduleResponseEditorPreviewRender(previewDelayMs);
|
|
2415
2619
|
}
|
|
@@ -2634,6 +2838,7 @@
|
|
|
2634
2838
|
const canRefreshFromDisk = hasRefreshableFilePath();
|
|
2635
2839
|
|
|
2636
2840
|
fileInput.disabled = uiBusy;
|
|
2841
|
+
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
2637
2842
|
saveAsBtn.disabled = uiBusy;
|
|
2638
2843
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
2639
2844
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
@@ -2643,7 +2848,7 @@
|
|
|
2643
2848
|
syncRunAndCritiqueButtons();
|
|
2644
2849
|
copyDraftBtn.disabled = uiBusy;
|
|
2645
2850
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
2646
|
-
if (
|
|
2851
|
+
if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
|
|
2647
2852
|
if (annotationModeSelect) annotationModeSelect.disabled = uiBusy;
|
|
2648
2853
|
if (saveAnnotatedBtn) saveAnnotatedBtn.disabled = uiBusy;
|
|
2649
2854
|
if (stripAnnotationsBtn) stripAnnotationsBtn.disabled = uiBusy || !hasAnnotationMarkers(sourceTextEl.value);
|
|
@@ -2667,17 +2872,33 @@
|
|
|
2667
2872
|
syncActionButtons();
|
|
2668
2873
|
}
|
|
2669
2874
|
|
|
2670
|
-
function setSourceState(next) {
|
|
2875
|
+
function setSourceState(next, options) {
|
|
2876
|
+
const previousDescriptor = getCurrentStudioDocumentDescriptor();
|
|
2877
|
+
const nextPath = next && next.path ? next.path : null;
|
|
2671
2878
|
sourceState = {
|
|
2672
2879
|
source: next && next.source ? next.source : "blank",
|
|
2673
2880
|
label: next && next.label ? next.label : "blank",
|
|
2674
|
-
path:
|
|
2881
|
+
path: nextPath,
|
|
2882
|
+
draftId: nextPath
|
|
2883
|
+
? null
|
|
2884
|
+
: (next && next.draftId ? next.draftId : makeStudioDraftId()),
|
|
2675
2885
|
};
|
|
2676
2886
|
if (!sourceState.path) {
|
|
2677
2887
|
clearFileBackedBaseline();
|
|
2678
2888
|
}
|
|
2889
|
+
updateStudioDocumentUrlState(sourceState);
|
|
2679
2890
|
updateSourceBadge();
|
|
2680
2891
|
syncActionButtons();
|
|
2892
|
+
updateScratchpadUi();
|
|
2893
|
+
updateReviewNotesUi();
|
|
2894
|
+
loadScratchpadForCurrentDocument({
|
|
2895
|
+
previousDescriptor: previousDescriptor,
|
|
2896
|
+
carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
|
|
2897
|
+
});
|
|
2898
|
+
void loadReviewNotesForCurrentDocument({
|
|
2899
|
+
previousDescriptor: previousDescriptor,
|
|
2900
|
+
carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
|
|
2901
|
+
});
|
|
2681
2902
|
}
|
|
2682
2903
|
|
|
2683
2904
|
function setEditorText(nextText, options) {
|
|
@@ -2710,6 +2931,9 @@
|
|
|
2710
2931
|
schedule(() => {
|
|
2711
2932
|
syncEditorHighlightScroll();
|
|
2712
2933
|
});
|
|
2934
|
+
if (editorView === "markdown") {
|
|
2935
|
+
scheduleEditorLineNumberRender();
|
|
2936
|
+
}
|
|
2713
2937
|
|
|
2714
2938
|
updateAnnotatedReplyHeaderButton();
|
|
2715
2939
|
|
|
@@ -2745,7 +2969,12 @@
|
|
|
2745
2969
|
}
|
|
2746
2970
|
|
|
2747
2971
|
updateEditorHighlightState();
|
|
2748
|
-
|
|
2972
|
+
syncHighlightSelectUi();
|
|
2973
|
+
updateLineNumberGutterVisibility();
|
|
2974
|
+
if (!showPreview) {
|
|
2975
|
+
scheduleEditorLineNumberRender();
|
|
2976
|
+
}
|
|
2977
|
+
updateReviewNotesUi();
|
|
2749
2978
|
}
|
|
2750
2979
|
|
|
2751
2980
|
function setRightView(nextView) {
|
|
@@ -2765,12 +2994,311 @@
|
|
|
2765
2994
|
syncActionButtons();
|
|
2766
2995
|
}
|
|
2767
2996
|
|
|
2997
|
+
function lineNumbersShouldBeVisible() {
|
|
2998
|
+
return Boolean(
|
|
2999
|
+
lineNumbersEnabled
|
|
3000
|
+
&& editorView === "markdown"
|
|
3001
|
+
&& sourceEditorWrapEl
|
|
3002
|
+
&& lineNumberGutterEl
|
|
3003
|
+
&& lineNumberGutterContentEl
|
|
3004
|
+
&& lineNumberMeasureEl,
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
function reviewNoteGutterShouldBeVisible() {
|
|
3009
|
+
return Boolean(
|
|
3010
|
+
editorView === "markdown"
|
|
3011
|
+
&& sourceEditorWrapEl
|
|
3012
|
+
&& reviewNoteGutterEl
|
|
3013
|
+
&& reviewNoteGutterContentEl
|
|
3014
|
+
&& lineNumberMeasureEl
|
|
3015
|
+
&& Array.isArray(reviewNotes)
|
|
3016
|
+
&& reviewNotes.length > 0,
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
function getEditorLineNumberGutterWidthCss(lineCount) {
|
|
3021
|
+
const digits = Math.max(2, String(Math.max(1, lineCount || 0)).length);
|
|
3022
|
+
return "calc(" + digits + "ch + 18px)";
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function updateLineNumberGutterVisibility() {
|
|
3026
|
+
const lineNumbersVisible = lineNumbersShouldBeVisible();
|
|
3027
|
+
const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
|
|
3028
|
+
const anyVisible = lineNumbersVisible || reviewMarkersVisible;
|
|
3029
|
+
if (sourceEditorWrapEl) {
|
|
3030
|
+
sourceEditorWrapEl.classList.toggle("line-numbers-enabled", lineNumbersVisible);
|
|
3031
|
+
sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
|
|
3032
|
+
sourceEditorWrapEl.style.setProperty(
|
|
3033
|
+
"--editor-line-number-gutter-width",
|
|
3034
|
+
lineNumbersVisible
|
|
3035
|
+
? getEditorLineNumberGutterWidthCss(Math.max(1, String(sourceTextEl.value || "").replace(/\r\n/g, "\n").split("\n").length))
|
|
3036
|
+
: "0px",
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
if (reviewNoteGutterEl) {
|
|
3040
|
+
reviewNoteGutterEl.hidden = !reviewMarkersVisible;
|
|
3041
|
+
}
|
|
3042
|
+
if (lineNumberGutterEl) {
|
|
3043
|
+
lineNumberGutterEl.hidden = !lineNumbersVisible;
|
|
3044
|
+
}
|
|
3045
|
+
if (!reviewMarkersVisible && reviewNoteGutterContentEl) {
|
|
3046
|
+
reviewNoteGutterContentEl.innerHTML = "";
|
|
3047
|
+
}
|
|
3048
|
+
if (!lineNumbersVisible && lineNumberGutterContentEl) {
|
|
3049
|
+
lineNumberGutterContentEl.innerHTML = "";
|
|
3050
|
+
}
|
|
3051
|
+
if (!anyVisible && lineNumberMeasureEl) {
|
|
3052
|
+
lineNumberMeasureEl.innerHTML = "";
|
|
3053
|
+
}
|
|
3054
|
+
return anyVisible;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
function renderEditorLineNumbersNow() {
|
|
3058
|
+
if (!updateLineNumberGutterVisibility()) return;
|
|
3059
|
+
|
|
3060
|
+
const text = String(sourceTextEl.value || "").replace(/\r\n/g, "\n");
|
|
3061
|
+
const lines = text.split("\n");
|
|
3062
|
+
const lineCount = Math.max(1, lines.length);
|
|
3063
|
+
const lineNumbersVisible = lineNumbersShouldBeVisible();
|
|
3064
|
+
const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
|
|
3065
|
+
|
|
3066
|
+
if (sourceEditorWrapEl) {
|
|
3067
|
+
sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
|
|
3068
|
+
sourceEditorWrapEl.style.setProperty(
|
|
3069
|
+
"--editor-line-number-gutter-width",
|
|
3070
|
+
lineNumbersVisible ? getEditorLineNumberGutterWidthCss(lineCount) : "0px",
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
const styles = window.getComputedStyle(sourceTextEl);
|
|
3075
|
+
const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
|
|
3076
|
+
const paddingTop = parseFloat(styles.paddingTop) || 0;
|
|
3077
|
+
const paddingRight = parseFloat(styles.paddingRight) || 0;
|
|
3078
|
+
const paddingBottom = parseFloat(styles.paddingBottom) || 0;
|
|
3079
|
+
const paddingLeft = parseFloat(styles.paddingLeft) || 0;
|
|
3080
|
+
const contentWidth = Math.max(1, sourceTextEl.clientWidth - paddingLeft - paddingRight);
|
|
3081
|
+
|
|
3082
|
+
if (lineNumberGutterContentEl) {
|
|
3083
|
+
lineNumberGutterContentEl.style.paddingTop = paddingTop + "px";
|
|
3084
|
+
lineNumberGutterContentEl.style.paddingBottom = paddingBottom + "px";
|
|
3085
|
+
}
|
|
3086
|
+
if (reviewNoteGutterContentEl) {
|
|
3087
|
+
reviewNoteGutterContentEl.style.paddingTop = paddingTop + "px";
|
|
3088
|
+
reviewNoteGutterContentEl.style.paddingBottom = paddingBottom + "px";
|
|
3089
|
+
}
|
|
3090
|
+
lineNumberMeasureEl.style.width = contentWidth + "px";
|
|
3091
|
+
lineNumberMeasureEl.innerHTML = lines
|
|
3092
|
+
.map((line) => "<div class='editor-line-number-measure-line'>" + (line.length ? escapeHtml(line) : "​") + "</div>")
|
|
3093
|
+
.join("");
|
|
3094
|
+
|
|
3095
|
+
const measureLines = Array.from(lineNumberMeasureEl.children);
|
|
3096
|
+
const reviewNoteLineMap = reviewMarkersVisible ? buildReviewNoteLineMap(text) : null;
|
|
3097
|
+
|
|
3098
|
+
if (lineNumbersVisible && lineNumberGutterContentEl) {
|
|
3099
|
+
lineNumberGutterContentEl.innerHTML = measureLines
|
|
3100
|
+
.map((lineEl, index) => {
|
|
3101
|
+
const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
|
|
3102
|
+
return "<div class='editor-line-number-row' style='height:" + height.toFixed(2) + "px'>" + (index + 1) + "</div>";
|
|
3103
|
+
})
|
|
3104
|
+
.join("");
|
|
3105
|
+
} else if (lineNumberGutterContentEl) {
|
|
3106
|
+
lineNumberGutterContentEl.innerHTML = "";
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
if (reviewMarkersVisible && reviewNoteGutterContentEl && reviewNoteLineMap) {
|
|
3110
|
+
reviewNoteGutterContentEl.innerHTML = measureLines
|
|
3111
|
+
.map((lineEl, index) => {
|
|
3112
|
+
const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
|
|
3113
|
+
const lineNumber = index + 1;
|
|
3114
|
+
const notesForLine = reviewNoteLineMap.get(lineNumber) || [];
|
|
3115
|
+
const count = notesForLine.length;
|
|
3116
|
+
if (count <= 0) {
|
|
3117
|
+
return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'></div>";
|
|
3118
|
+
}
|
|
3119
|
+
const title = count === 1
|
|
3120
|
+
? ("1 local comment on line " + lineNumber + ". Open comments.")
|
|
3121
|
+
: (count + " local comments on line " + lineNumber + ". Open comments.");
|
|
3122
|
+
const markerLabel = count > 9 ? "9+" : (count > 1 ? String(count) : "•");
|
|
3123
|
+
return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'><button type='button' class='editor-review-note-marker"
|
|
3124
|
+
+ (count > 1 ? " has-multiple" : "")
|
|
3125
|
+
+ "' data-review-note-id='" + escapeHtml(notesForLine[0].id) + "' title='" + escapeHtml(title) + "' aria-label='" + escapeHtml(title) + "'>"
|
|
3126
|
+
+ escapeHtml(markerLabel)
|
|
3127
|
+
+ "</button></div>";
|
|
3128
|
+
})
|
|
3129
|
+
.join("");
|
|
3130
|
+
} else if (reviewNoteGutterContentEl) {
|
|
3131
|
+
reviewNoteGutterContentEl.innerHTML = "";
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
syncEditorHighlightScroll();
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
function scrollEditorRangeIntoView(range) {
|
|
3138
|
+
if (!range || editorView !== "markdown") return;
|
|
3139
|
+
renderEditorLineNumbersNow();
|
|
3140
|
+
|
|
3141
|
+
const text = String(sourceTextEl.value || "");
|
|
3142
|
+
const startLine = getLineNumberAtOffset(text, range.start);
|
|
3143
|
+
const endLine = getLineNumberAtOffset(text, Math.max(range.start, range.end > range.start ? range.end - 1 : range.end));
|
|
3144
|
+
const styles = window.getComputedStyle(sourceTextEl);
|
|
3145
|
+
const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
|
|
3146
|
+
const paddingTop = parseFloat(styles.paddingTop) || 0;
|
|
3147
|
+
const paddingBottom = parseFloat(styles.paddingBottom) || 0;
|
|
3148
|
+
const measureLines = lineNumberMeasureEl ? Array.from(lineNumberMeasureEl.children) : [];
|
|
3149
|
+
|
|
3150
|
+
function getLineTop(lineNumber) {
|
|
3151
|
+
let top = paddingTop;
|
|
3152
|
+
for (let i = 0; i < lineNumber - 1; i += 1) {
|
|
3153
|
+
const lineEl = measureLines[i];
|
|
3154
|
+
top += Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
|
|
3155
|
+
}
|
|
3156
|
+
return top;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function getLineBottom(lineNumber) {
|
|
3160
|
+
const lineEl = measureLines[Math.max(0, lineNumber - 1)];
|
|
3161
|
+
return getLineTop(lineNumber) + Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
const rangeTop = getLineTop(startLine);
|
|
3165
|
+
const rangeBottom = getLineBottom(endLine);
|
|
3166
|
+
const viewportTop = sourceTextEl.scrollTop;
|
|
3167
|
+
const viewportBottom = viewportTop + sourceTextEl.clientHeight;
|
|
3168
|
+
const margin = Math.max(18, Math.round(sourceTextEl.clientHeight * 0.12));
|
|
3169
|
+
|
|
3170
|
+
let nextScrollTop = viewportTop;
|
|
3171
|
+
if (rangeTop - margin < viewportTop) {
|
|
3172
|
+
nextScrollTop = Math.max(0, rangeTop - margin);
|
|
3173
|
+
} else if (rangeBottom + margin > viewportBottom) {
|
|
3174
|
+
nextScrollTop = Math.max(0, rangeBottom - sourceTextEl.clientHeight + margin + paddingBottom);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
if (Math.abs(nextScrollTop - viewportTop) > 1) {
|
|
3178
|
+
sourceTextEl.scrollTop = nextScrollTop;
|
|
3179
|
+
syncEditorHighlightScroll();
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
function scheduleEditorLineNumberRender() {
|
|
3184
|
+
if (lineNumbersRenderRaf !== null) {
|
|
3185
|
+
if (typeof window.cancelAnimationFrame === "function") {
|
|
3186
|
+
window.cancelAnimationFrame(lineNumbersRenderRaf);
|
|
3187
|
+
} else {
|
|
3188
|
+
window.clearTimeout(lineNumbersRenderRaf);
|
|
3189
|
+
}
|
|
3190
|
+
lineNumbersRenderRaf = null;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3194
|
+
? window.requestAnimationFrame.bind(window)
|
|
3195
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
3196
|
+
|
|
3197
|
+
lineNumbersRenderRaf = schedule(() => {
|
|
3198
|
+
lineNumbersRenderRaf = null;
|
|
3199
|
+
renderEditorLineNumbersNow();
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
function readStoredEditorLineNumbersEnabled() {
|
|
3204
|
+
return readStoredToggle(EDITOR_LINE_NUMBERS_STORAGE_KEY);
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function persistEditorLineNumbersEnabled(enabled) {
|
|
3208
|
+
persistStoredToggle(EDITOR_LINE_NUMBERS_STORAGE_KEY, enabled);
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
function setLineNumbersEnabled(enabled) {
|
|
3212
|
+
lineNumbersEnabled = Boolean(enabled);
|
|
3213
|
+
persistEditorLineNumbersEnabled(lineNumbersEnabled);
|
|
3214
|
+
if (lineNumbersSelect) {
|
|
3215
|
+
lineNumbersSelect.value = lineNumbersEnabled ? "on" : "off";
|
|
3216
|
+
}
|
|
3217
|
+
updateLineNumberGutterVisibility();
|
|
3218
|
+
scheduleEditorLineNumberRender();
|
|
3219
|
+
if (editorHighlightEnabled && editorView === "markdown") {
|
|
3220
|
+
scheduleEditorHighlightRender();
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
|
|
2768
3224
|
function getToken() {
|
|
2769
3225
|
const query = new URLSearchParams(window.location.search || "");
|
|
2770
3226
|
const hash = new URLSearchParams((window.location.hash || "").replace(/^#/, ""));
|
|
2771
3227
|
return query.get("token") || hash.get("token") || "";
|
|
2772
3228
|
}
|
|
2773
3229
|
|
|
3230
|
+
function buildAuthedStudioUrl(pathname, extraParams) {
|
|
3231
|
+
const token = getToken();
|
|
3232
|
+
if (!token) {
|
|
3233
|
+
throw new Error("Missing Studio token in URL.");
|
|
3234
|
+
}
|
|
3235
|
+
const params = new URLSearchParams(extraParams || {});
|
|
3236
|
+
params.set("token", token);
|
|
3237
|
+
return pathname + "?" + params.toString();
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
function updateStudioDocumentUrlState(state) {
|
|
3241
|
+
try {
|
|
3242
|
+
const currentUrl = new URL(window.location.href);
|
|
3243
|
+
const params = currentUrl.searchParams;
|
|
3244
|
+
const nextState = state && typeof state === "object" ? state : sourceState;
|
|
3245
|
+
const nextSource = nextState && nextState.source ? String(nextState.source) : "blank";
|
|
3246
|
+
const nextLabel = nextState && nextState.label ? String(nextState.label) : "blank";
|
|
3247
|
+
const nextPath = nextState && nextState.path ? String(nextState.path) : "";
|
|
3248
|
+
const nextDraftId = nextState && nextState.draftId ? String(nextState.draftId) : "";
|
|
3249
|
+
if (nextSource) params.set("docSource", nextSource);
|
|
3250
|
+
else params.delete("docSource");
|
|
3251
|
+
if (nextLabel) params.set("docLabel", nextLabel);
|
|
3252
|
+
else params.delete("docLabel");
|
|
3253
|
+
if (nextPath) params.set("docPath", nextPath);
|
|
3254
|
+
else params.delete("docPath");
|
|
3255
|
+
if (nextDraftId) params.set("draftId", nextDraftId);
|
|
3256
|
+
else params.delete("draftId");
|
|
3257
|
+
window.history.replaceState(null, "", currentUrl.toString());
|
|
3258
|
+
} catch {
|
|
3259
|
+
// Ignore URL-state update failures.
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
async function fetchStudioJson(pathname, options) {
|
|
3264
|
+
const init = options || {};
|
|
3265
|
+
const headers = new Headers(init.headers || undefined);
|
|
3266
|
+
const method = String(init.method || "GET").toUpperCase();
|
|
3267
|
+
if (init.body != null && !headers.has("Content-Type")) {
|
|
3268
|
+
headers.set("Content-Type", "application/json");
|
|
3269
|
+
}
|
|
3270
|
+
const response = await fetch(buildAuthedStudioUrl(pathname, init.query), {
|
|
3271
|
+
method,
|
|
3272
|
+
headers,
|
|
3273
|
+
body: init.body,
|
|
3274
|
+
cache: "no-store",
|
|
3275
|
+
});
|
|
3276
|
+
let payload = null;
|
|
3277
|
+
try {
|
|
3278
|
+
payload = await response.json();
|
|
3279
|
+
} catch {
|
|
3280
|
+
payload = null;
|
|
3281
|
+
}
|
|
3282
|
+
if (!response.ok || !payload || payload.ok === false) {
|
|
3283
|
+
const message = payload && typeof payload.error === "string"
|
|
3284
|
+
? payload.error
|
|
3285
|
+
: (response.status + " " + response.statusText).trim();
|
|
3286
|
+
throw new Error(message || (method + " " + pathname + " failed."));
|
|
3287
|
+
}
|
|
3288
|
+
return payload;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
function trySendStudioJsonBeacon(pathname, payload, extraParams) {
|
|
3292
|
+
try {
|
|
3293
|
+
if (!navigator.sendBeacon || typeof navigator.sendBeacon !== "function") return false;
|
|
3294
|
+
const body = JSON.stringify(payload || {});
|
|
3295
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
3296
|
+
return navigator.sendBeacon(buildAuthedStudioUrl(pathname, extraParams), blob);
|
|
3297
|
+
} catch {
|
|
3298
|
+
return false;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
|
|
2774
3302
|
function makeRequestId() {
|
|
2775
3303
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
2776
3304
|
return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -2778,6 +3306,10 @@
|
|
|
2778
3306
|
return "req_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
|
|
2779
3307
|
}
|
|
2780
3308
|
|
|
3309
|
+
function makeStudioDraftId() {
|
|
3310
|
+
return "draft_" + makeRequestId();
|
|
3311
|
+
}
|
|
3312
|
+
|
|
2781
3313
|
function escapeHtml(text) {
|
|
2782
3314
|
return text
|
|
2783
3315
|
.replace(/&/g, "&")
|
|
@@ -3291,9 +3823,16 @@
|
|
|
3291
3823
|
}
|
|
3292
3824
|
|
|
3293
3825
|
function syncEditorHighlightScroll() {
|
|
3294
|
-
if (
|
|
3295
|
-
|
|
3296
|
-
|
|
3826
|
+
if (sourceHighlightEl) {
|
|
3827
|
+
sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
|
|
3828
|
+
sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
|
|
3829
|
+
}
|
|
3830
|
+
if (reviewNoteGutterEl) {
|
|
3831
|
+
reviewNoteGutterEl.scrollTop = sourceTextEl.scrollTop;
|
|
3832
|
+
}
|
|
3833
|
+
if (lineNumberGutterEl) {
|
|
3834
|
+
lineNumberGutterEl.scrollTop = sourceTextEl.scrollTop;
|
|
3835
|
+
}
|
|
3297
3836
|
}
|
|
3298
3837
|
|
|
3299
3838
|
function runEditorMetaUpdateNow() {
|
|
@@ -3370,51 +3909,823 @@
|
|
|
3370
3909
|
persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
|
|
3371
3910
|
}
|
|
3372
3911
|
|
|
3373
|
-
function
|
|
3374
|
-
|
|
3912
|
+
function isScratchpadOpen() {
|
|
3913
|
+
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
function isReviewNotesOpen() {
|
|
3917
|
+
return Boolean(reviewNotesOverlayEl && !reviewNotesOverlayEl.hidden);
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
function syncModalOpenState() {
|
|
3921
|
+
document.body.classList.toggle("scratchpad-open", isScratchpadOpen());
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
function describeStudioDocument(state) {
|
|
3925
|
+
const currentState = state && typeof state === "object" ? state : sourceState;
|
|
3926
|
+
const source = currentState && currentState.source ? String(currentState.source) : "blank";
|
|
3927
|
+
const label = currentState && currentState.label ? String(currentState.label) : "blank";
|
|
3928
|
+
const path = currentState && currentState.path ? String(currentState.path) : "";
|
|
3929
|
+
const draftId = currentState && currentState.draftId ? String(currentState.draftId) : "";
|
|
3930
|
+
if (path) {
|
|
3931
|
+
return {
|
|
3932
|
+
key: "file:" + path,
|
|
3933
|
+
label: path,
|
|
3934
|
+
fileBacked: true,
|
|
3935
|
+
draftBacked: false,
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
const normalizedLabel = label.trim().replace(/\s+/g, " ") || source;
|
|
3939
|
+
if (draftId) {
|
|
3940
|
+
return {
|
|
3941
|
+
key: "draft:" + draftId,
|
|
3942
|
+
label: normalizedLabel,
|
|
3943
|
+
fileBacked: false,
|
|
3944
|
+
draftBacked: true,
|
|
3945
|
+
};
|
|
3946
|
+
}
|
|
3947
|
+
return {
|
|
3948
|
+
key: "doc:" + source + ":" + normalizedLabel,
|
|
3949
|
+
label: normalizedLabel,
|
|
3950
|
+
fileBacked: false,
|
|
3951
|
+
draftBacked: false,
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
function getCurrentStudioDocumentDescriptor() {
|
|
3956
|
+
return describeStudioDocument(sourceState);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
async function fetchScratchpadTextForDocumentKey(documentKey) {
|
|
3960
|
+
const payload = await fetchStudioJson("/scratchpad-state", {
|
|
3961
|
+
query: { documentKey: documentKey },
|
|
3962
|
+
});
|
|
3963
|
+
return payload && typeof payload.text === "string" ? payload.text : "";
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
function flushScratchpadPersistence(documentKeyOverride, textOverride) {
|
|
3967
|
+
const descriptor = documentKeyOverride
|
|
3968
|
+
? { key: String(documentKeyOverride || "").trim() }
|
|
3969
|
+
: getCurrentStudioDocumentDescriptor();
|
|
3970
|
+
const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
|
|
3971
|
+
if (!key) return;
|
|
3972
|
+
if (scratchpadPersistTimer !== null) {
|
|
3973
|
+
window.clearTimeout(scratchpadPersistTimer);
|
|
3974
|
+
scratchpadPersistTimer = null;
|
|
3975
|
+
}
|
|
3976
|
+
const snapshot = String(arguments.length >= 2 ? textOverride : scratchpadText || "");
|
|
3977
|
+
if (trySendStudioJsonBeacon("/scratchpad-state", { documentKey: key, text: snapshot })) {
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
void fetchStudioJson("/scratchpad-state", {
|
|
3981
|
+
method: "POST",
|
|
3982
|
+
body: JSON.stringify({ documentKey: key, text: snapshot }),
|
|
3983
|
+
}).catch(() => {
|
|
3984
|
+
// Ignore scratchpad persistence failures for now.
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function scheduleScratchpadPersistence(text, documentKey) {
|
|
3989
|
+
if (scratchpadPersistTimer !== null) {
|
|
3990
|
+
window.clearTimeout(scratchpadPersistTimer);
|
|
3991
|
+
}
|
|
3992
|
+
const snapshot = String(text || "");
|
|
3993
|
+
const key = String(documentKey || "").trim();
|
|
3994
|
+
if (!key) return;
|
|
3995
|
+
scratchpadPersistTimer = window.setTimeout(() => {
|
|
3996
|
+
scratchpadPersistTimer = null;
|
|
3997
|
+
flushScratchpadPersistence(key, snapshot);
|
|
3998
|
+
}, 180);
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
async function loadScratchpadForDocumentKey(documentKey) {
|
|
4002
|
+
const key = String(documentKey || "").trim();
|
|
4003
|
+
const loadNonce = ++scratchpadLoadNonce;
|
|
4004
|
+
if (!key) {
|
|
4005
|
+
setScratchpadText("", { persist: false });
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
3375
4008
|
try {
|
|
3376
|
-
const
|
|
3377
|
-
|
|
4009
|
+
const serverText = await fetchScratchpadTextForDocumentKey(key);
|
|
4010
|
+
if (loadNonce !== scratchpadLoadNonce) return;
|
|
4011
|
+
if (key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4012
|
+
setScratchpadText(serverText, { persist: false });
|
|
3378
4013
|
} catch {
|
|
3379
|
-
return
|
|
4014
|
+
if (loadNonce !== scratchpadLoadNonce) return;
|
|
4015
|
+
if (key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4016
|
+
setScratchpadText("", { persist: false });
|
|
3380
4017
|
}
|
|
3381
4018
|
}
|
|
3382
4019
|
|
|
3383
|
-
function
|
|
3384
|
-
if (!
|
|
4020
|
+
async function maybeCarryScratchpadToNewDocument(previousDescriptor, nextDescriptor) {
|
|
4021
|
+
if (!previousDescriptor || !nextDescriptor || previousDescriptor.key === nextDescriptor.key) return;
|
|
4022
|
+
const snapshot = String(scratchpadText || "");
|
|
4023
|
+
if (!snapshot.trim()) return;
|
|
3385
4024
|
try {
|
|
3386
|
-
|
|
4025
|
+
const existing = await fetchScratchpadTextForDocumentKey(nextDescriptor.key);
|
|
4026
|
+
if (String(existing || "").trim()) return;
|
|
4027
|
+
await fetchStudioJson("/scratchpad-state", {
|
|
4028
|
+
method: "POST",
|
|
4029
|
+
body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot }),
|
|
4030
|
+
});
|
|
3387
4031
|
} catch {
|
|
3388
|
-
//
|
|
4032
|
+
// Ignore carry-over failures and just fall back to normal scope loading.
|
|
3389
4033
|
}
|
|
3390
4034
|
}
|
|
3391
4035
|
|
|
3392
|
-
function
|
|
3393
|
-
|
|
4036
|
+
function loadScratchpadForCurrentDocument(options) {
|
|
4037
|
+
const previousDescriptor = options && options.previousDescriptor ? options.previousDescriptor : null;
|
|
4038
|
+
const shouldCarryToNewDocument = Boolean(options && options.carryCurrentMetadataToNewDocument);
|
|
4039
|
+
const currentDescriptor = getCurrentStudioDocumentDescriptor();
|
|
4040
|
+
void (async () => {
|
|
4041
|
+
if (shouldCarryToNewDocument && previousDescriptor) {
|
|
4042
|
+
await maybeCarryScratchpadToNewDocument(previousDescriptor, currentDescriptor);
|
|
4043
|
+
}
|
|
4044
|
+
await loadScratchpadForDocumentKey(currentDescriptor.key);
|
|
4045
|
+
})();
|
|
3394
4046
|
}
|
|
3395
4047
|
|
|
3396
|
-
function
|
|
3397
|
-
|
|
4048
|
+
function persistScratchpadText(value) {
|
|
4049
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4050
|
+
scheduleScratchpadPersistence(value, descriptor.key);
|
|
3398
4051
|
}
|
|
3399
4052
|
|
|
3400
|
-
function
|
|
3401
|
-
|
|
4053
|
+
function normalizeReviewNote(note) {
|
|
4054
|
+
if (!note || typeof note !== "object") return null;
|
|
4055
|
+
const id = typeof note.id === "string" && note.id.trim() ? note.id : makeRequestId();
|
|
4056
|
+
const text = typeof note.text === "string" ? note.text : "";
|
|
4057
|
+
const createdAt = typeof note.createdAt === "number" && Number.isFinite(note.createdAt)
|
|
4058
|
+
? note.createdAt
|
|
4059
|
+
: Date.now();
|
|
4060
|
+
const updatedAt = typeof note.updatedAt === "number" && Number.isFinite(note.updatedAt)
|
|
4061
|
+
? note.updatedAt
|
|
4062
|
+
: createdAt;
|
|
4063
|
+
const selectionStart = typeof note.selectionStart === "number" && Number.isFinite(note.selectionStart)
|
|
4064
|
+
? Math.max(0, Math.floor(note.selectionStart))
|
|
4065
|
+
: 0;
|
|
4066
|
+
const selectionEnd = typeof note.selectionEnd === "number" && Number.isFinite(note.selectionEnd)
|
|
4067
|
+
? Math.max(selectionStart, Math.floor(note.selectionEnd))
|
|
4068
|
+
: selectionStart;
|
|
4069
|
+
const lineStart = typeof note.lineStart === "number" && Number.isFinite(note.lineStart)
|
|
4070
|
+
? Math.max(1, Math.floor(note.lineStart))
|
|
4071
|
+
: 1;
|
|
4072
|
+
const lineEnd = typeof note.lineEnd === "number" && Number.isFinite(note.lineEnd)
|
|
4073
|
+
? Math.max(lineStart, Math.floor(note.lineEnd))
|
|
4074
|
+
: lineStart;
|
|
4075
|
+
return {
|
|
4076
|
+
id,
|
|
4077
|
+
text,
|
|
4078
|
+
createdAt,
|
|
4079
|
+
updatedAt,
|
|
4080
|
+
selectionStart,
|
|
4081
|
+
selectionEnd,
|
|
4082
|
+
lineStart,
|
|
4083
|
+
lineEnd,
|
|
4084
|
+
selectedText: typeof note.selectedText === "string" ? note.selectedText : "",
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
function cloneReviewNotes(notes) {
|
|
4089
|
+
return Array.isArray(notes)
|
|
4090
|
+
? notes
|
|
4091
|
+
.map((note) => normalizeReviewNote(note))
|
|
4092
|
+
.filter(Boolean)
|
|
4093
|
+
.map((note) => ({ ...note }))
|
|
4094
|
+
: [];
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
async function fetchReviewNotesForDocumentKey(documentKey) {
|
|
4098
|
+
const payload = await fetchStudioJson("/review-notes", {
|
|
4099
|
+
query: { documentKey: documentKey },
|
|
4100
|
+
});
|
|
4101
|
+
return cloneReviewNotes(payload && Array.isArray(payload.notes) ? payload.notes : []);
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
function flushReviewNotesPersistence(documentKeyOverride, notesOverride) {
|
|
4105
|
+
const descriptor = documentKeyOverride
|
|
4106
|
+
? { key: String(documentKeyOverride || "").trim() }
|
|
4107
|
+
: getCurrentStudioDocumentDescriptor();
|
|
4108
|
+
const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
|
|
4109
|
+
if (!key) return;
|
|
4110
|
+
if (reviewNotesPersistTimer !== null) {
|
|
4111
|
+
window.clearTimeout(reviewNotesPersistTimer);
|
|
4112
|
+
reviewNotesPersistTimer = null;
|
|
4113
|
+
}
|
|
4114
|
+
const snapshot = cloneReviewNotes(arguments.length >= 2 ? notesOverride : reviewNotes);
|
|
4115
|
+
if (trySendStudioJsonBeacon("/review-notes", { documentKey: key, notes: snapshot })) {
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
void fetchStudioJson("/review-notes", {
|
|
4119
|
+
method: "POST",
|
|
4120
|
+
body: JSON.stringify({ documentKey: key, notes: snapshot }),
|
|
4121
|
+
}).catch(() => {
|
|
4122
|
+
// Ignore persistence failures; the in-memory notes list remains available for this session.
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
function scheduleReviewNotesPersistence() {
|
|
4127
|
+
if (reviewNotesPersistTimer !== null) {
|
|
4128
|
+
window.clearTimeout(reviewNotesPersistTimer);
|
|
4129
|
+
}
|
|
4130
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4131
|
+
const snapshot = cloneReviewNotes(reviewNotes);
|
|
4132
|
+
reviewNotesPersistTimer = window.setTimeout(() => {
|
|
4133
|
+
reviewNotesPersistTimer = null;
|
|
4134
|
+
flushReviewNotesPersistence(descriptor.key, snapshot);
|
|
4135
|
+
}, 180);
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
async function maybeCarryReviewNotesToNewDocument(previousDescriptor, nextDescriptor) {
|
|
4139
|
+
if (!previousDescriptor || !nextDescriptor || previousDescriptor.key === nextDescriptor.key) return;
|
|
4140
|
+
const snapshot = cloneReviewNotes(reviewNotes);
|
|
4141
|
+
if (!snapshot.length) return;
|
|
4142
|
+
try {
|
|
4143
|
+
const existing = await fetchReviewNotesForDocumentKey(nextDescriptor.key);
|
|
4144
|
+
if (existing.length > 0) return;
|
|
4145
|
+
await fetchStudioJson("/review-notes", {
|
|
4146
|
+
method: "POST",
|
|
4147
|
+
body: JSON.stringify({ documentKey: nextDescriptor.key, notes: snapshot }),
|
|
4148
|
+
});
|
|
4149
|
+
} catch {
|
|
4150
|
+
// Ignore carry-over failures and just fall back to normal scope loading.
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
async function loadReviewNotesForCurrentDocument(options) {
|
|
4155
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4156
|
+
const previousDescriptor = options && options.previousDescriptor ? options.previousDescriptor : null;
|
|
4157
|
+
const shouldCarryToNewDocument = Boolean(options && options.carryCurrentMetadataToNewDocument);
|
|
4158
|
+
const loadNonce = ++reviewNotesLoadNonce;
|
|
4159
|
+
try {
|
|
4160
|
+
if (shouldCarryToNewDocument && previousDescriptor) {
|
|
4161
|
+
await maybeCarryReviewNotesToNewDocument(previousDescriptor, descriptor);
|
|
4162
|
+
}
|
|
4163
|
+
const notes = await fetchReviewNotesForDocumentKey(descriptor.key);
|
|
4164
|
+
if (loadNonce !== reviewNotesLoadNonce) return;
|
|
4165
|
+
if (descriptor.key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4166
|
+
reviewNotes = notes;
|
|
4167
|
+
} catch {
|
|
4168
|
+
if (loadNonce !== reviewNotesLoadNonce) return;
|
|
4169
|
+
if (descriptor.key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4170
|
+
reviewNotes = [];
|
|
4171
|
+
}
|
|
4172
|
+
updateReviewNotesUi();
|
|
4173
|
+
renderReviewNotesList();
|
|
4174
|
+
if (editorView === "markdown") {
|
|
4175
|
+
scheduleEditorLineNumberRender();
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
function formatReviewNoteTimestamp(timestamp) {
|
|
4180
|
+
if (!Number.isFinite(timestamp)) return "Saved locally";
|
|
4181
|
+
try {
|
|
4182
|
+
return "Updated " + new Date(timestamp).toLocaleString();
|
|
4183
|
+
} catch {
|
|
4184
|
+
return "Saved locally";
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
function summarizeReviewNoteAnchor(note) {
|
|
4189
|
+
const start = Math.max(1, Number(note && note.lineStart) || 1);
|
|
4190
|
+
const end = Math.max(start, Number(note && note.lineEnd) || start);
|
|
4191
|
+
return start === end ? "Line " + start : ("Lines " + start + "–" + end);
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
function summarizeReviewNoteQuote(note) {
|
|
4195
|
+
const normalized = String(note && note.selectedText ? note.selectedText : "")
|
|
4196
|
+
.replace(/\s+/g, " ")
|
|
4197
|
+
.trim();
|
|
4198
|
+
if (!normalized) return "Anchor: current line / empty selection";
|
|
4199
|
+
return normalized.length > 140 ? normalized.slice(0, 137) + "…" : normalized;
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
function getLineNumberAtOffset(text, offset) {
|
|
4203
|
+
const source = String(text || "");
|
|
4204
|
+
const safeOffset = Math.max(0, Math.min(Number(offset) || 0, source.length));
|
|
4205
|
+
let line = 1;
|
|
4206
|
+
for (let i = 0; i < safeOffset; i += 1) {
|
|
4207
|
+
if (source[i] === "\n") line += 1;
|
|
4208
|
+
}
|
|
4209
|
+
return line;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
function getLineRangeAtOffset(text, offset) {
|
|
4213
|
+
const source = String(text || "");
|
|
4214
|
+
const safeOffset = Math.max(0, Math.min(Number(offset) || 0, source.length));
|
|
4215
|
+
let start = safeOffset;
|
|
4216
|
+
while (start > 0 && source[start - 1] !== "\n") start -= 1;
|
|
4217
|
+
let end = safeOffset;
|
|
4218
|
+
while (end < source.length && source[end] !== "\n") end += 1;
|
|
4219
|
+
return {
|
|
4220
|
+
start,
|
|
4221
|
+
end,
|
|
4222
|
+
lineNumber: getLineNumberAtOffset(source, safeOffset),
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
function getLineRangeForNumbers(text, lineStart, lineEnd) {
|
|
4227
|
+
const lines = String(text || "").split("\n");
|
|
4228
|
+
const safeLineStart = Math.max(1, Math.min(Math.floor(lineStart || 1), Math.max(1, lines.length)));
|
|
4229
|
+
const safeLineEnd = Math.max(safeLineStart, Math.min(Math.floor(lineEnd || safeLineStart), Math.max(1, lines.length)));
|
|
4230
|
+
let start = 0;
|
|
4231
|
+
for (let i = 0; i < safeLineStart - 1; i += 1) {
|
|
4232
|
+
start += lines[i].length + 1;
|
|
4233
|
+
}
|
|
4234
|
+
let end = start;
|
|
4235
|
+
for (let i = safeLineStart - 1; i < safeLineEnd; i += 1) {
|
|
4236
|
+
end += lines[i].length;
|
|
4237
|
+
if (i < safeLineEnd - 1) end += 1;
|
|
4238
|
+
}
|
|
4239
|
+
return { start, end };
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
function getEditorAnchorForReviewNote() {
|
|
4243
|
+
const current = String(sourceTextEl.value || "");
|
|
4244
|
+
const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
|
|
4245
|
+
const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
|
|
4246
|
+
const safeStart = Math.max(0, Math.min(start, current.length));
|
|
4247
|
+
const safeEnd = Math.max(safeStart, Math.min(end, current.length));
|
|
4248
|
+
if (safeStart !== safeEnd) {
|
|
4249
|
+
return {
|
|
4250
|
+
selectionStart: safeStart,
|
|
4251
|
+
selectionEnd: safeEnd,
|
|
4252
|
+
lineStart: getLineNumberAtOffset(current, safeStart),
|
|
4253
|
+
lineEnd: getLineNumberAtOffset(current, Math.max(safeStart, safeEnd - 1)),
|
|
4254
|
+
selectedText: current.slice(safeStart, safeEnd),
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
const lineRange = getLineRangeAtOffset(current, safeStart);
|
|
4258
|
+
return {
|
|
4259
|
+
selectionStart: lineRange.start,
|
|
4260
|
+
selectionEnd: lineRange.end,
|
|
4261
|
+
lineStart: lineRange.lineNumber,
|
|
4262
|
+
lineEnd: lineRange.lineNumber,
|
|
4263
|
+
selectedText: current.slice(lineRange.start, lineRange.end),
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
function resolveReviewNoteRange(note, text) {
|
|
4268
|
+
const source = String(text || "");
|
|
4269
|
+
const safeStart = Math.max(0, Math.min(Number(note && note.selectionStart) || 0, source.length));
|
|
4270
|
+
const safeEnd = Math.max(safeStart, Math.min(Number(note && note.selectionEnd) || safeStart, source.length));
|
|
4271
|
+
const selectedText = String(note && note.selectedText ? note.selectedText : "");
|
|
4272
|
+
if (selectedText && source.slice(safeStart, safeEnd) === selectedText) {
|
|
4273
|
+
return { start: safeStart, end: safeEnd };
|
|
4274
|
+
}
|
|
4275
|
+
if (!selectedText && safeEnd >= safeStart) {
|
|
4276
|
+
return { start: safeStart, end: safeEnd };
|
|
4277
|
+
}
|
|
4278
|
+
if (selectedText) {
|
|
4279
|
+
const foundIndex = source.indexOf(selectedText);
|
|
4280
|
+
if (foundIndex >= 0) {
|
|
4281
|
+
return { start: foundIndex, end: foundIndex + selectedText.length };
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
return getLineRangeForNumbers(source, note && note.lineStart, note && note.lineEnd);
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
function getResolvedReviewNoteLineBounds(note, text) {
|
|
4288
|
+
const source = String(text || "");
|
|
4289
|
+
const range = resolveReviewNoteRange(note, source);
|
|
4290
|
+
if (!range) return null;
|
|
4291
|
+
const startLine = getLineNumberAtOffset(source, range.start);
|
|
4292
|
+
const endLookupOffset = range.end > range.start ? range.end - 1 : range.start;
|
|
4293
|
+
const endLine = getLineNumberAtOffset(source, endLookupOffset);
|
|
4294
|
+
return {
|
|
4295
|
+
start: range.start,
|
|
4296
|
+
end: range.end,
|
|
4297
|
+
lineStart: startLine,
|
|
4298
|
+
lineEnd: Math.max(startLine, endLine),
|
|
4299
|
+
};
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
function buildReviewNoteLineMap(text) {
|
|
4303
|
+
const source = String(text || "");
|
|
4304
|
+
const lineMap = new Map();
|
|
4305
|
+
for (const note of reviewNotes) {
|
|
4306
|
+
const bounds = getResolvedReviewNoteLineBounds(note, source);
|
|
4307
|
+
if (!bounds) continue;
|
|
4308
|
+
for (let line = bounds.lineStart; line <= bounds.lineEnd; line += 1) {
|
|
4309
|
+
const notesForLine = lineMap.get(line) || [];
|
|
4310
|
+
notesForLine.push(note);
|
|
4311
|
+
lineMap.set(line, notesForLine);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
return lineMap;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
function getDisplayReviewNotes() {
|
|
4318
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
4319
|
+
return reviewNotes.slice().sort((left, right) => {
|
|
4320
|
+
const leftBounds = getResolvedReviewNoteLineBounds(left, source);
|
|
4321
|
+
const rightBounds = getResolvedReviewNoteLineBounds(right, source);
|
|
4322
|
+
const leftLine = leftBounds ? leftBounds.lineStart : Math.max(1, Number(left && left.lineStart) || 1);
|
|
4323
|
+
const rightLine = rightBounds ? rightBounds.lineStart : Math.max(1, Number(right && right.lineStart) || 1);
|
|
4324
|
+
if (leftLine !== rightLine) return leftLine - rightLine;
|
|
4325
|
+
|
|
4326
|
+
const leftStart = leftBounds ? leftBounds.start : Math.max(0, Number(left && left.selectionStart) || 0);
|
|
4327
|
+
const rightStart = rightBounds ? rightBounds.start : Math.max(0, Number(right && right.selectionStart) || 0);
|
|
4328
|
+
if (leftStart !== rightStart) return leftStart - rightStart;
|
|
4329
|
+
|
|
4330
|
+
const leftCreated = Number(left && left.createdAt) || 0;
|
|
4331
|
+
const rightCreated = Number(right && right.createdAt) || 0;
|
|
4332
|
+
if (leftCreated !== rightCreated) return leftCreated - rightCreated;
|
|
4333
|
+
|
|
4334
|
+
return String(left && left.id ? left.id : "").localeCompare(String(right && right.id ? right.id : ""));
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
function focusReviewNoteInPanel(noteId) {
|
|
4339
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
4340
|
+
if (!note) return;
|
|
4341
|
+
pendingReviewNoteFocusId = note.id;
|
|
4342
|
+
openReviewNotes();
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
function escapeReviewNoteAnnotationText(text) {
|
|
4346
|
+
return String(text || "")
|
|
4347
|
+
.replace(/\\/g, "\\\\")
|
|
4348
|
+
.replace(/\]/g, "\\]")
|
|
4349
|
+
.trim();
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
function getReviewNoteInlineState(note, text) {
|
|
4353
|
+
const source = String(text || "");
|
|
4354
|
+
const annotationBody = escapeReviewNoteAnnotationText(note && note.text);
|
|
4355
|
+
if (!annotationBody) {
|
|
4356
|
+
return {
|
|
4357
|
+
annotationBody: "",
|
|
4358
|
+
range: null,
|
|
4359
|
+
markerText: "",
|
|
4360
|
+
exists: false,
|
|
4361
|
+
canToggle: false,
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
const range = resolveReviewNoteRange(note, source);
|
|
4365
|
+
if (!range) {
|
|
4366
|
+
return {
|
|
4367
|
+
annotationBody,
|
|
4368
|
+
range: null,
|
|
4369
|
+
markerText: "",
|
|
4370
|
+
exists: false,
|
|
4371
|
+
canToggle: false,
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
const markerText = (range.start === range.end ? "" : " ") + "[an: " + annotationBody + "]";
|
|
4375
|
+
const exists = source.slice(range.end, range.end + markerText.length) === markerText;
|
|
4376
|
+
return {
|
|
4377
|
+
annotationBody,
|
|
4378
|
+
range,
|
|
4379
|
+
markerText,
|
|
4380
|
+
exists,
|
|
4381
|
+
canToggle: true,
|
|
4382
|
+
};
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
function setReviewNotes(nextNotes, options) {
|
|
4386
|
+
reviewNotes = cloneReviewNotes(nextNotes);
|
|
4387
|
+
updateReviewNotesUi();
|
|
4388
|
+
renderReviewNotesList();
|
|
4389
|
+
if (editorView === "markdown") {
|
|
4390
|
+
scheduleEditorLineNumberRender();
|
|
4391
|
+
}
|
|
4392
|
+
if (!options || options.persist !== false) {
|
|
4393
|
+
scheduleReviewNotesPersistence();
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
function updateReviewNotesUi() {
|
|
4398
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4399
|
+
const count = reviewNotes.length;
|
|
4400
|
+
const hasNotes = count > 0;
|
|
4401
|
+
const isOpen = isReviewNotesOpen();
|
|
4402
|
+
if (reviewNotesBtn) {
|
|
4403
|
+
reviewNotesBtn.textContent = hasNotes ? "Comments •" : "Comments";
|
|
4404
|
+
reviewNotesBtn.classList.toggle("has-content", hasNotes);
|
|
4405
|
+
reviewNotesBtn.classList.toggle("is-active", isOpen);
|
|
4406
|
+
reviewNotesBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
|
|
4407
|
+
reviewNotesBtn.title = isOpen
|
|
4408
|
+
? "Hide local comments."
|
|
4409
|
+
: (hasNotes
|
|
4410
|
+
? (count + " local comment" + (count === 1 ? "" : "s") + " for " + descriptor.label + ". Open the side-by-side comments rail.")
|
|
4411
|
+
: "Open local comments beside the current editor document or draft. Comments stay outside the document text and can later be converted into [an: ...] annotations.");
|
|
4412
|
+
}
|
|
4413
|
+
if (reviewNotesMetaEl) {
|
|
4414
|
+
const scopeLabel = descriptor.fileBacked
|
|
4415
|
+
? "file-backed"
|
|
4416
|
+
: (descriptor.draftBacked ? "draft-backed" : "local buffer");
|
|
4417
|
+
reviewNotesMetaEl.textContent = hasNotes
|
|
4418
|
+
? (count + " comment" + (count === 1 ? "" : "s") + " · " + scopeLabel + " · " + descriptor.label)
|
|
4419
|
+
: ("No comments yet · " + scopeLabel);
|
|
4420
|
+
}
|
|
4421
|
+
if (reviewNotesAddBtn) {
|
|
4422
|
+
reviewNotesAddBtn.disabled = editorView !== "markdown";
|
|
4423
|
+
reviewNotesAddBtn.title = editorView === "markdown"
|
|
4424
|
+
? "Create a new local comment from the current editor selection, or from the current line if nothing is selected."
|
|
4425
|
+
: "Switch to Editor (Raw) to anchor a comment to the current selection or line.";
|
|
4426
|
+
}
|
|
4427
|
+
if (reviewNotesInlineAllBtn) {
|
|
4428
|
+
const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
4429
|
+
const toggleCandidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, currentText).canToggle);
|
|
4430
|
+
const allInline = toggleCandidates.length > 0 && toggleCandidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
|
|
4431
|
+
reviewNotesInlineAllBtn.disabled = uiBusy || toggleCandidates.length === 0;
|
|
4432
|
+
reviewNotesInlineAllBtn.textContent = allInline ? "All inline: On" : "All inline: Off";
|
|
4433
|
+
reviewNotesInlineAllBtn.setAttribute("aria-pressed", allInline ? "true" : "false");
|
|
4434
|
+
reviewNotesInlineAllBtn.title = allInline
|
|
4435
|
+
? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
|
|
4436
|
+
: "Inline annotations derived from all non-empty comments are currently off. Click to add them.";
|
|
4437
|
+
}
|
|
4438
|
+
if (reviewNotesDoneBtn) {
|
|
4439
|
+
reviewNotesDoneBtn.disabled = !isOpen;
|
|
4440
|
+
}
|
|
4441
|
+
if (reviewNotesEmptyStateEl) {
|
|
4442
|
+
reviewNotesEmptyStateEl.hidden = hasNotes;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
function renderReviewNotesList() {
|
|
4447
|
+
if (!reviewNotesListEl) return;
|
|
4448
|
+
reviewNotesListEl.innerHTML = "";
|
|
4449
|
+
for (const note of getDisplayReviewNotes()) {
|
|
4450
|
+
const card = document.createElement("article");
|
|
4451
|
+
card.className = "review-note-card";
|
|
4452
|
+
|
|
4453
|
+
const header = document.createElement("div");
|
|
4454
|
+
header.className = "review-note-card-header";
|
|
4455
|
+
|
|
4456
|
+
const titleWrap = document.createElement("div");
|
|
4457
|
+
titleWrap.className = "review-note-card-title";
|
|
4458
|
+
|
|
4459
|
+
const anchor = document.createElement("span");
|
|
4460
|
+
anchor.className = "review-note-anchor";
|
|
4461
|
+
anchor.textContent = summarizeReviewNoteAnchor(note);
|
|
4462
|
+
titleWrap.appendChild(anchor);
|
|
4463
|
+
|
|
4464
|
+
const quote = document.createElement("div");
|
|
4465
|
+
quote.className = "review-note-quote";
|
|
4466
|
+
quote.textContent = summarizeReviewNoteQuote(note);
|
|
4467
|
+
titleWrap.appendChild(quote);
|
|
4468
|
+
header.appendChild(titleWrap);
|
|
4469
|
+
|
|
4470
|
+
card.appendChild(header);
|
|
4471
|
+
|
|
4472
|
+
const textarea = document.createElement("textarea");
|
|
4473
|
+
textarea.value = String(note.text || "");
|
|
4474
|
+
textarea.placeholder = "Write a local comment here…";
|
|
4475
|
+
textarea.title = "Write a local comment. Press Enter to finish editing, or Shift+Enter for a new line.";
|
|
4476
|
+
card.appendChild(textarea);
|
|
4477
|
+
|
|
4478
|
+
const footer = document.createElement("div");
|
|
4479
|
+
footer.className = "review-note-card-footer";
|
|
4480
|
+
|
|
4481
|
+
const timestamp = document.createElement("span");
|
|
4482
|
+
timestamp.className = "review-note-timestamp";
|
|
4483
|
+
timestamp.textContent = formatReviewNoteTimestamp(note.updatedAt);
|
|
4484
|
+
|
|
4485
|
+
const actions = document.createElement("div");
|
|
4486
|
+
actions.className = "review-note-card-actions";
|
|
4487
|
+
|
|
4488
|
+
const jumpBtn = document.createElement("button");
|
|
4489
|
+
jumpBtn.type = "button";
|
|
4490
|
+
jumpBtn.textContent = "Jump";
|
|
4491
|
+
jumpBtn.title = "Jump to this comment's anchored location in the editor.";
|
|
4492
|
+
jumpBtn.addEventListener("click", () => {
|
|
4493
|
+
jumpToReviewNote(note.id);
|
|
4494
|
+
});
|
|
4495
|
+
actions.appendChild(jumpBtn);
|
|
4496
|
+
|
|
4497
|
+
const inlineState = getReviewNoteInlineState(note, sourceTextEl.value || "");
|
|
4498
|
+
const convertBtn = document.createElement("button");
|
|
4499
|
+
convertBtn.type = "button";
|
|
4500
|
+
convertBtn.className = "review-note-inline-btn";
|
|
4501
|
+
convertBtn.textContent = inlineState.exists ? "Inline: On" : "Inline: Off";
|
|
4502
|
+
convertBtn.setAttribute("aria-pressed", inlineState.exists ? "true" : "false");
|
|
4503
|
+
convertBtn.disabled = !inlineState.canToggle || uiBusy;
|
|
4504
|
+
convertBtn.title = inlineState.exists
|
|
4505
|
+
? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
|
|
4506
|
+
: "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
|
|
4507
|
+
convertBtn.addEventListener("click", () => {
|
|
4508
|
+
convertReviewNoteToAnnotation(note.id);
|
|
4509
|
+
});
|
|
4510
|
+
actions.appendChild(convertBtn);
|
|
4511
|
+
|
|
4512
|
+
const deleteBtn = document.createElement("button");
|
|
4513
|
+
deleteBtn.type = "button";
|
|
4514
|
+
deleteBtn.className = "review-note-delete-btn";
|
|
4515
|
+
deleteBtn.textContent = "Delete";
|
|
4516
|
+
deleteBtn.title = "Delete this local comment.";
|
|
4517
|
+
deleteBtn.addEventListener("click", () => {
|
|
4518
|
+
deleteReviewNote(note.id);
|
|
4519
|
+
});
|
|
4520
|
+
actions.appendChild(deleteBtn);
|
|
4521
|
+
|
|
4522
|
+
footer.appendChild(timestamp);
|
|
4523
|
+
footer.appendChild(actions);
|
|
4524
|
+
card.appendChild(footer);
|
|
4525
|
+
|
|
4526
|
+
textarea.addEventListener("input", () => {
|
|
4527
|
+
note.text = textarea.value;
|
|
4528
|
+
note.updatedAt = Date.now();
|
|
4529
|
+
timestamp.textContent = formatReviewNoteTimestamp(note.updatedAt);
|
|
4530
|
+
const nextInlineState = getReviewNoteInlineState(note, sourceTextEl.value || "");
|
|
4531
|
+
convertBtn.disabled = !nextInlineState.canToggle || uiBusy;
|
|
4532
|
+
convertBtn.textContent = nextInlineState.exists ? "Inline: On" : "Inline: Off";
|
|
4533
|
+
convertBtn.setAttribute("aria-pressed", nextInlineState.exists ? "true" : "false");
|
|
4534
|
+
convertBtn.title = nextInlineState.exists
|
|
4535
|
+
? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
|
|
4536
|
+
: "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
|
|
4537
|
+
scheduleReviewNotesPersistence();
|
|
4538
|
+
updateReviewNotesUi();
|
|
4539
|
+
});
|
|
4540
|
+
|
|
4541
|
+
textarea.addEventListener("keydown", (event) => {
|
|
4542
|
+
if (
|
|
4543
|
+
event.key === "Enter"
|
|
4544
|
+
&& !event.shiftKey
|
|
4545
|
+
&& !event.altKey
|
|
4546
|
+
&& !event.ctrlKey
|
|
4547
|
+
&& !event.metaKey
|
|
4548
|
+
) {
|
|
4549
|
+
event.preventDefault();
|
|
4550
|
+
textarea.blur();
|
|
4551
|
+
if (!convertBtn.disabled) {
|
|
4552
|
+
convertBtn.focus();
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
});
|
|
4556
|
+
|
|
4557
|
+
reviewNotesListEl.appendChild(card);
|
|
4558
|
+
|
|
4559
|
+
if (pendingReviewNoteInlineFocusId && pendingReviewNoteInlineFocusId === note.id && isReviewNotesOpen()) {
|
|
4560
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
4561
|
+
? window.requestAnimationFrame.bind(window)
|
|
4562
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
4563
|
+
schedule(() => {
|
|
4564
|
+
card.scrollIntoView({ block: "nearest" });
|
|
4565
|
+
if (!convertBtn.disabled) convertBtn.focus();
|
|
4566
|
+
});
|
|
4567
|
+
} else if (pendingReviewNoteFocusId && pendingReviewNoteFocusId === note.id && isReviewNotesOpen()) {
|
|
4568
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
4569
|
+
? window.requestAnimationFrame.bind(window)
|
|
4570
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
4571
|
+
schedule(() => {
|
|
4572
|
+
card.scrollIntoView({ block: "nearest" });
|
|
4573
|
+
textarea.focus();
|
|
4574
|
+
const end = textarea.value.length;
|
|
4575
|
+
textarea.setSelectionRange(end, end);
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
pendingReviewNoteFocusId = null;
|
|
4580
|
+
pendingReviewNoteInlineFocusId = null;
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
function addReviewNoteFromEditorSelection() {
|
|
4584
|
+
if (editorView !== "markdown") {
|
|
4585
|
+
setStatus("Switch to Editor (Raw) before adding an anchored comment.", "warning");
|
|
4586
|
+
return;
|
|
4587
|
+
}
|
|
4588
|
+
const anchor = getEditorAnchorForReviewNote();
|
|
4589
|
+
const note = normalizeReviewNote({
|
|
4590
|
+
id: makeRequestId(),
|
|
4591
|
+
text: "",
|
|
4592
|
+
createdAt: Date.now(),
|
|
4593
|
+
updatedAt: Date.now(),
|
|
4594
|
+
selectionStart: anchor.selectionStart,
|
|
4595
|
+
selectionEnd: anchor.selectionEnd,
|
|
4596
|
+
lineStart: anchor.lineStart,
|
|
4597
|
+
lineEnd: anchor.lineEnd,
|
|
4598
|
+
selectedText: anchor.selectedText,
|
|
4599
|
+
});
|
|
4600
|
+
if (!note) return;
|
|
4601
|
+
pendingReviewNoteFocusId = note.id;
|
|
4602
|
+
setReviewNotes(reviewNotes.concat([note]));
|
|
4603
|
+
if (!isReviewNotesOpen()) {
|
|
4604
|
+
openReviewNotes();
|
|
4605
|
+
}
|
|
4606
|
+
setStatus("Added local comment.", "success");
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
function jumpToReviewNote(noteId) {
|
|
4610
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
4611
|
+
if (!note) return;
|
|
4612
|
+
const current = String(sourceTextEl.value || "");
|
|
4613
|
+
const range = resolveReviewNoteRange(note, current);
|
|
4614
|
+
if (!range) {
|
|
4615
|
+
setStatus("Could not find the anchored location for this comment.", "warning");
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4618
|
+
setEditorView("markdown");
|
|
4619
|
+
setActivePane("left");
|
|
4620
|
+
sourceTextEl.focus();
|
|
4621
|
+
sourceTextEl.setSelectionRange(range.start, range.end);
|
|
4622
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
4623
|
+
? window.requestAnimationFrame.bind(window)
|
|
4624
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
4625
|
+
schedule(() => {
|
|
4626
|
+
scrollEditorRangeIntoView(range);
|
|
4627
|
+
});
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
function deleteReviewNote(noteId) {
|
|
4631
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
4632
|
+
if (!note) return;
|
|
4633
|
+
const confirmed = window.confirm("Delete this local comment?");
|
|
4634
|
+
if (!confirmed) return;
|
|
4635
|
+
setReviewNotes(reviewNotes.filter((entry) => entry && entry.id !== noteId));
|
|
4636
|
+
setStatus("Deleted local comment.", "success");
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
function convertReviewNoteToAnnotation(noteId) {
|
|
4640
|
+
if (uiBusy) {
|
|
4641
|
+
setStatus("Wait until the current Studio action finishes before toggling inline annotation state.", "warning");
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
4645
|
+
if (!note) return;
|
|
4646
|
+
const current = String(sourceTextEl.value || "");
|
|
4647
|
+
const inlineState = getReviewNoteInlineState(note, current);
|
|
4648
|
+
if (!inlineState.annotationBody) {
|
|
4649
|
+
setStatus("Comment is empty. Add some text before toggling inline annotation state.", "warning");
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
if (!inlineState.range || !inlineState.canToggle) {
|
|
4653
|
+
setStatus("Could not find the anchored location for this comment.", "warning");
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
const next = inlineState.exists
|
|
4657
|
+
? current.slice(0, inlineState.range.end) + current.slice(inlineState.range.end + inlineState.markerText.length)
|
|
4658
|
+
: current.slice(0, inlineState.range.end) + inlineState.markerText + current.slice(inlineState.range.end);
|
|
4659
|
+
setEditorView("markdown");
|
|
4660
|
+
setEditorText(next, { preserveScroll: true, preserveSelection: true });
|
|
4661
|
+
pendingReviewNoteInlineFocusId = note.id;
|
|
4662
|
+
renderReviewNotesList();
|
|
4663
|
+
updateReviewNotesUi();
|
|
4664
|
+
setStatus(inlineState.exists ? "Removed inline annotation from local comment." : "Added inline annotation from local comment.", "success");
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
function toggleAllReviewNotesInlineAnnotations() {
|
|
4668
|
+
if (uiBusy) {
|
|
4669
|
+
setStatus("Wait until the current Studio action finishes before toggling inline annotations.", "warning");
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
const candidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, sourceTextEl.value || "").canToggle);
|
|
4673
|
+
if (candidates.length === 0) {
|
|
4674
|
+
setStatus("No non-empty comments are ready to toggle inline.", "warning");
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
let currentText = String(sourceTextEl.value || "");
|
|
4678
|
+
const shouldRemoveAll = candidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
|
|
4679
|
+
const ordered = candidates
|
|
4680
|
+
.map((note) => ({ note, state: getReviewNoteInlineState(note, currentText) }))
|
|
4681
|
+
.filter((entry) => entry.state.range)
|
|
4682
|
+
.sort((left, right) => (right.state.range ? right.state.range.end : 0) - (left.state.range ? left.state.range.end : 0));
|
|
4683
|
+
|
|
4684
|
+
let changed = false;
|
|
4685
|
+
for (const entry of ordered) {
|
|
4686
|
+
const liveState = getReviewNoteInlineState(entry.note, currentText);
|
|
4687
|
+
if (!liveState.range || !liveState.canToggle) continue;
|
|
4688
|
+
if (shouldRemoveAll) {
|
|
4689
|
+
if (!liveState.exists) continue;
|
|
4690
|
+
currentText = currentText.slice(0, liveState.range.end) + currentText.slice(liveState.range.end + liveState.markerText.length);
|
|
4691
|
+
changed = true;
|
|
4692
|
+
} else {
|
|
4693
|
+
if (liveState.exists) continue;
|
|
4694
|
+
currentText = currentText.slice(0, liveState.range.end) + liveState.markerText + currentText.slice(liveState.range.end);
|
|
4695
|
+
changed = true;
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
if (!changed) {
|
|
4700
|
+
setStatus(shouldRemoveAll ? "No inline annotations were removed." : "No inline annotations were added.", "warning");
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
setEditorView("markdown");
|
|
4705
|
+
setEditorText(currentText, { preserveScroll: true, preserveSelection: true });
|
|
4706
|
+
renderReviewNotesList();
|
|
4707
|
+
updateReviewNotesUi();
|
|
4708
|
+
if (reviewNotesInlineAllBtn && typeof reviewNotesInlineAllBtn.focus === "function") {
|
|
4709
|
+
reviewNotesInlineAllBtn.focus();
|
|
4710
|
+
}
|
|
4711
|
+
setStatus(shouldRemoveAll ? "Removed inline annotations from all comments." : "Added inline annotations from all comments.", "success");
|
|
3402
4712
|
}
|
|
3403
4713
|
|
|
3404
4714
|
function updateScratchpadUi() {
|
|
3405
4715
|
const normalized = String(scratchpadText || "");
|
|
3406
4716
|
const hasContent = Boolean(normalized.trim());
|
|
4717
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
3407
4718
|
if (scratchpadBtn) {
|
|
3408
4719
|
scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
|
|
3409
4720
|
scratchpadBtn.classList.toggle("has-content", hasContent);
|
|
3410
4721
|
scratchpadBtn.title = hasContent
|
|
3411
|
-
? "Open
|
|
3412
|
-
: "Open a local persistent scratchpad for
|
|
4722
|
+
? ("Open the local persistent scratchpad for this document/draft. Scope: " + descriptor.label + ". File-backed docs come back across Pi restarts; unsaved drafts stay with this draft instance until saved or cleared.")
|
|
4723
|
+
: ("Open a local persistent scratchpad for this document/draft. Scope: " + descriptor.label + ". File-backed docs come back across Pi restarts; unsaved drafts stay with this draft instance until saved or cleared.");
|
|
3413
4724
|
}
|
|
3414
4725
|
if (scratchpadMetaEl) {
|
|
3415
4726
|
scratchpadMetaEl.textContent = hasContent
|
|
3416
|
-
? "Saved locally
|
|
3417
|
-
: "Empty · local
|
|
4727
|
+
? ("Saved locally for this document/draft · " + normalized.length + " chars")
|
|
4728
|
+
: "Empty · local to this document/draft";
|
|
3418
4729
|
}
|
|
3419
4730
|
if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
|
|
3420
4731
|
if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
|
|
@@ -3435,7 +4746,7 @@
|
|
|
3435
4746
|
function closeScratchpad(options) {
|
|
3436
4747
|
if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
|
|
3437
4748
|
scratchpadOverlayEl.hidden = true;
|
|
3438
|
-
|
|
4749
|
+
syncModalOpenState();
|
|
3439
4750
|
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
3440
4751
|
? options.focusTarget
|
|
3441
4752
|
: (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
|
|
@@ -3450,11 +4761,14 @@
|
|
|
3450
4761
|
|
|
3451
4762
|
function openScratchpad() {
|
|
3452
4763
|
if (!scratchpadOverlayEl) return;
|
|
4764
|
+
if (isReviewNotesOpen()) {
|
|
4765
|
+
closeReviewNotes({ focusTarget: null });
|
|
4766
|
+
}
|
|
3453
4767
|
scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
3454
4768
|
? document.activeElement
|
|
3455
4769
|
: sourceTextEl;
|
|
3456
4770
|
scratchpadOverlayEl.hidden = false;
|
|
3457
|
-
|
|
4771
|
+
syncModalOpenState();
|
|
3458
4772
|
if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
|
|
3459
4773
|
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3460
4774
|
? window.requestAnimationFrame.bind(window)
|
|
@@ -3469,6 +4783,49 @@
|
|
|
3469
4783
|
}
|
|
3470
4784
|
}
|
|
3471
4785
|
|
|
4786
|
+
function closeReviewNotes(options) {
|
|
4787
|
+
if (!reviewNotesOverlayEl || reviewNotesOverlayEl.hidden) return;
|
|
4788
|
+
reviewNotesOverlayEl.hidden = true;
|
|
4789
|
+
updateReviewNotesUi();
|
|
4790
|
+
if (editorView === "markdown") {
|
|
4791
|
+
scheduleEditorLineNumberRender();
|
|
4792
|
+
}
|
|
4793
|
+
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
4794
|
+
? options.focusTarget
|
|
4795
|
+
: (reviewNotesReturnFocusEl || reviewNotesBtn || sourceTextEl);
|
|
4796
|
+
reviewNotesReturnFocusEl = null;
|
|
4797
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
4798
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
4799
|
+
? window.requestAnimationFrame.bind(window)
|
|
4800
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
4801
|
+
schedule(() => focusTarget.focus());
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
function openReviewNotes() {
|
|
4806
|
+
if (!reviewNotesOverlayEl) return;
|
|
4807
|
+
if (isScratchpadOpen()) {
|
|
4808
|
+
closeScratchpad({ focusTarget: null });
|
|
4809
|
+
}
|
|
4810
|
+
reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
4811
|
+
? document.activeElement
|
|
4812
|
+
: sourceTextEl;
|
|
4813
|
+
reviewNotesOverlayEl.hidden = false;
|
|
4814
|
+
renderReviewNotesList();
|
|
4815
|
+
updateReviewNotesUi();
|
|
4816
|
+
if (editorView === "markdown") {
|
|
4817
|
+
scheduleEditorLineNumberRender();
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
function toggleReviewNotes() {
|
|
4822
|
+
if (isReviewNotesOpen()) {
|
|
4823
|
+
closeReviewNotes({ focusTarget: reviewNotesBtn || sourceTextEl });
|
|
4824
|
+
} else {
|
|
4825
|
+
openReviewNotes();
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
|
|
3472
4829
|
function insertScratchpadIntoEditor() {
|
|
3473
4830
|
const content = String(scratchpadText || "");
|
|
3474
4831
|
if (!content.trim()) {
|
|
@@ -3521,14 +4878,22 @@
|
|
|
3521
4878
|
syncEditorHighlightScroll();
|
|
3522
4879
|
}
|
|
3523
4880
|
|
|
4881
|
+
function syncHighlightSelectUi() {
|
|
4882
|
+
if (!highlightSelect) return;
|
|
4883
|
+
if (!editorHighlightEnabled) {
|
|
4884
|
+
highlightSelect.value = "off";
|
|
4885
|
+
return;
|
|
4886
|
+
}
|
|
4887
|
+
highlightSelect.value = (editorLanguage && SUPPORTED_LANGUAGES.indexOf(editorLanguage) !== -1)
|
|
4888
|
+
? editorLanguage
|
|
4889
|
+
: "markdown";
|
|
4890
|
+
}
|
|
4891
|
+
|
|
3524
4892
|
function setEditorHighlightEnabled(enabled) {
|
|
3525
4893
|
editorHighlightEnabled = Boolean(enabled);
|
|
3526
4894
|
persistEditorHighlightEnabled(editorHighlightEnabled);
|
|
3527
|
-
|
|
3528
|
-
highlightSelect.value = editorHighlightEnabled ? "on" : "off";
|
|
3529
|
-
}
|
|
4895
|
+
syncHighlightSelectUi();
|
|
3530
4896
|
updateEditorHighlightState();
|
|
3531
|
-
updateLangSelectVisibility();
|
|
3532
4897
|
}
|
|
3533
4898
|
|
|
3534
4899
|
function readStoredEditorLanguage() {
|
|
@@ -3552,9 +4917,7 @@
|
|
|
3552
4917
|
function setEditorLanguage(lang) {
|
|
3553
4918
|
editorLanguage = (lang && SUPPORTED_LANGUAGES.indexOf(lang) !== -1) ? lang : "markdown";
|
|
3554
4919
|
persistEditorLanguage(editorLanguage);
|
|
3555
|
-
|
|
3556
|
-
langSelect.value = editorLanguage;
|
|
3557
|
-
}
|
|
4920
|
+
syncHighlightSelectUi();
|
|
3558
4921
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
3559
4922
|
scheduleEditorHighlightRender();
|
|
3560
4923
|
}
|
|
@@ -3563,11 +4926,13 @@
|
|
|
3563
4926
|
}
|
|
3564
4927
|
}
|
|
3565
4928
|
|
|
3566
|
-
function
|
|
3567
|
-
if (
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
4929
|
+
function setEditorHighlightMode(mode) {
|
|
4930
|
+
if (mode === "off") {
|
|
4931
|
+
setEditorHighlightEnabled(false);
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
setEditorLanguage(mode);
|
|
4935
|
+
setEditorHighlightEnabled(true);
|
|
3571
4936
|
}
|
|
3572
4937
|
|
|
3573
4938
|
function setResponseHighlightEnabled(enabled) {
|
|
@@ -3618,7 +4983,7 @@
|
|
|
3618
4983
|
queueSteerBtn.title = "Queue steering is unavailable in editor-only mode.";
|
|
3619
4984
|
}
|
|
3620
4985
|
if (critiqueBtn) {
|
|
3621
|
-
critiqueBtn.textContent = "Critique
|
|
4986
|
+
critiqueBtn.textContent = "Critique text";
|
|
3622
4987
|
critiqueBtn.classList.remove("request-stop-active");
|
|
3623
4988
|
critiqueBtn.disabled = true;
|
|
3624
4989
|
critiqueBtn.title = "Critique is unavailable in editor-only mode.";
|
|
@@ -3649,7 +5014,7 @@
|
|
|
3649
5014
|
}
|
|
3650
5015
|
|
|
3651
5016
|
if (critiqueBtn) {
|
|
3652
|
-
critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique
|
|
5017
|
+
critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique text";
|
|
3653
5018
|
critiqueBtn.classList.toggle("request-stop-active", critiqueIsStop);
|
|
3654
5019
|
critiqueBtn.disabled = critiqueIsStop ? wsState === "Disconnected" : (uiBusy || canQueueSteering);
|
|
3655
5020
|
critiqueBtn.title = critiqueIsStop
|
|
@@ -3657,8 +5022,8 @@
|
|
|
3657
5022
|
: (canQueueSteering
|
|
3658
5023
|
? "Critique queueing is not supported while Run editor text is active."
|
|
3659
5024
|
: (annotationsEnabled
|
|
3660
|
-
? "Critique
|
|
3661
|
-
: "Critique
|
|
5025
|
+
? "Critique text as-is (includes [an: ...] markers)."
|
|
5026
|
+
: "Critique text with [an: ...] markers stripped."));
|
|
3662
5027
|
}
|
|
3663
5028
|
}
|
|
3664
5029
|
|
|
@@ -3666,8 +5031,8 @@
|
|
|
3666
5031
|
if (annotationModeSelect) {
|
|
3667
5032
|
annotationModeSelect.value = annotationsEnabled ? "on" : "off";
|
|
3668
5033
|
annotationModeSelect.title = annotationsEnabled
|
|
3669
|
-
? "
|
|
3670
|
-
: "
|
|
5034
|
+
? "Inline annotations On: keep and send [an: ...] markers."
|
|
5035
|
+
: "Inline annotations Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.";
|
|
3671
5036
|
}
|
|
3672
5037
|
|
|
3673
5038
|
syncRunAndCritiqueButtons();
|
|
@@ -3844,6 +5209,7 @@
|
|
|
3844
5209
|
|
|
3845
5210
|
let loadedInitialDocument = false;
|
|
3846
5211
|
if (
|
|
5212
|
+
!explicitDocumentIdentityFromUrl &&
|
|
3847
5213
|
!initialDocumentApplied &&
|
|
3848
5214
|
message.initialDocument &&
|
|
3849
5215
|
typeof message.initialDocument.text === "string"
|
|
@@ -3855,6 +5221,9 @@
|
|
|
3855
5221
|
source: message.initialDocument.source || "blank",
|
|
3856
5222
|
label: message.initialDocument.label || "blank",
|
|
3857
5223
|
path: message.initialDocument.path || null,
|
|
5224
|
+
draftId: typeof message.initialDocument.draftId === "string" && message.initialDocument.draftId.trim()
|
|
5225
|
+
? message.initialDocument.draftId.trim()
|
|
5226
|
+
: (initialSourceState.draftId || null),
|
|
3858
5227
|
});
|
|
3859
5228
|
if (message.initialDocument.path) {
|
|
3860
5229
|
markFileBackedBaseline(message.initialDocument.text);
|
|
@@ -4067,6 +5436,8 @@
|
|
|
4067
5436
|
source: "file",
|
|
4068
5437
|
label: message.label || message.path,
|
|
4069
5438
|
path: message.path,
|
|
5439
|
+
}, {
|
|
5440
|
+
carryCurrentMetadataToNewDocument: true,
|
|
4070
5441
|
});
|
|
4071
5442
|
markFileBackedBaseline(sourceTextEl.value);
|
|
4072
5443
|
}
|
|
@@ -4137,7 +5508,12 @@
|
|
|
4137
5508
|
: null;
|
|
4138
5509
|
|
|
4139
5510
|
setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
|
|
4140
|
-
setSourceState({
|
|
5511
|
+
setSourceState({
|
|
5512
|
+
source: nextSource,
|
|
5513
|
+
label: nextLabel,
|
|
5514
|
+
path: nextPath,
|
|
5515
|
+
draftId: typeof nextDoc.draftId === "string" && nextDoc.draftId.trim() ? nextDoc.draftId.trim() : null,
|
|
5516
|
+
});
|
|
4141
5517
|
if (nextPath) {
|
|
4142
5518
|
markFileBackedBaseline(nextDoc.text);
|
|
4143
5519
|
}
|
|
@@ -4298,6 +5674,7 @@
|
|
|
4298
5674
|
root.style.setProperty(key, message.vars[key]);
|
|
4299
5675
|
}
|
|
4300
5676
|
});
|
|
5677
|
+
updateDocumentTitle();
|
|
4301
5678
|
}
|
|
4302
5679
|
}
|
|
4303
5680
|
|
|
@@ -4517,11 +5894,11 @@
|
|
|
4517
5894
|
if (!insertHeaderBtn) return;
|
|
4518
5895
|
const hasHeader = stripAnnotationHeader(sourceTextEl.value).hadHeader;
|
|
4519
5896
|
if (hasHeader) {
|
|
4520
|
-
insertHeaderBtn.textContent = "
|
|
5897
|
+
insertHeaderBtn.textContent = "Annotation header: On";
|
|
4521
5898
|
insertHeaderBtn.title = "Remove annotated-reply protocol header while keeping body text.";
|
|
4522
5899
|
return;
|
|
4523
5900
|
}
|
|
4524
|
-
insertHeaderBtn.textContent = "
|
|
5901
|
+
insertHeaderBtn.textContent = "Annotation header: Off";
|
|
4525
5902
|
insertHeaderBtn.title = "Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).";
|
|
4526
5903
|
}
|
|
4527
5904
|
|
|
@@ -4588,6 +5965,8 @@
|
|
|
4588
5965
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
4589
5966
|
window.addEventListener("beforeunload", () => {
|
|
4590
5967
|
stopFooterSpinner();
|
|
5968
|
+
flushScratchpadPersistence();
|
|
5969
|
+
flushReviewNotesPersistence();
|
|
4591
5970
|
});
|
|
4592
5971
|
|
|
4593
5972
|
editorViewSelect.addEventListener("change", () => {
|
|
@@ -4617,7 +5996,7 @@
|
|
|
4617
5996
|
|
|
4618
5997
|
if (highlightSelect) {
|
|
4619
5998
|
highlightSelect.addEventListener("change", () => {
|
|
4620
|
-
|
|
5999
|
+
setEditorHighlightMode(highlightSelect.value);
|
|
4621
6000
|
});
|
|
4622
6001
|
}
|
|
4623
6002
|
|
|
@@ -4627,9 +6006,9 @@
|
|
|
4627
6006
|
});
|
|
4628
6007
|
}
|
|
4629
6008
|
|
|
4630
|
-
if (
|
|
4631
|
-
|
|
4632
|
-
|
|
6009
|
+
if (lineNumbersSelect) {
|
|
6010
|
+
lineNumbersSelect.addEventListener("change", () => {
|
|
6011
|
+
setLineNumbersEnabled(lineNumbersSelect.value === "on");
|
|
4633
6012
|
});
|
|
4634
6013
|
}
|
|
4635
6014
|
|
|
@@ -4739,26 +6118,31 @@
|
|
|
4739
6118
|
sourceTextEl.addEventListener("input", () => {
|
|
4740
6119
|
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
4741
6120
|
scheduleEditorMetaUpdate();
|
|
6121
|
+
if (isReviewNotesOpen() && reviewNotes.length > 0) {
|
|
6122
|
+
renderReviewNotesList();
|
|
6123
|
+
updateReviewNotesUi();
|
|
6124
|
+
}
|
|
4742
6125
|
});
|
|
4743
6126
|
|
|
4744
6127
|
sourceTextEl.addEventListener("scroll", () => {
|
|
4745
|
-
if (
|
|
6128
|
+
if (editorView !== "markdown") return;
|
|
4746
6129
|
syncEditorHighlightScroll();
|
|
4747
6130
|
});
|
|
4748
6131
|
|
|
4749
6132
|
sourceTextEl.addEventListener("keyup", () => {
|
|
4750
|
-
if (
|
|
6133
|
+
if (editorView !== "markdown") return;
|
|
4751
6134
|
syncEditorHighlightScroll();
|
|
4752
6135
|
});
|
|
4753
6136
|
|
|
4754
6137
|
sourceTextEl.addEventListener("mouseup", () => {
|
|
4755
|
-
if (
|
|
6138
|
+
if (editorView !== "markdown") return;
|
|
4756
6139
|
syncEditorHighlightScroll();
|
|
4757
6140
|
});
|
|
4758
6141
|
|
|
4759
6142
|
window.addEventListener("resize", () => {
|
|
4760
|
-
if (
|
|
6143
|
+
if (editorView !== "markdown") return;
|
|
4761
6144
|
syncEditorHighlightScroll();
|
|
6145
|
+
scheduleEditorLineNumberRender();
|
|
4762
6146
|
});
|
|
4763
6147
|
|
|
4764
6148
|
insertHeaderBtn.addEventListener("click", () => {
|
|
@@ -5083,6 +6467,47 @@
|
|
|
5083
6467
|
}
|
|
5084
6468
|
});
|
|
5085
6469
|
|
|
6470
|
+
if (reviewNotesBtn) {
|
|
6471
|
+
reviewNotesBtn.addEventListener("click", () => {
|
|
6472
|
+
toggleReviewNotes();
|
|
6473
|
+
});
|
|
6474
|
+
}
|
|
6475
|
+
|
|
6476
|
+
if (reviewNotesCloseBtn) {
|
|
6477
|
+
reviewNotesCloseBtn.addEventListener("click", () => {
|
|
6478
|
+
closeReviewNotes();
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
|
|
6482
|
+
if (reviewNotesDoneBtn) {
|
|
6483
|
+
reviewNotesDoneBtn.addEventListener("click", () => {
|
|
6484
|
+
closeReviewNotes();
|
|
6485
|
+
});
|
|
6486
|
+
}
|
|
6487
|
+
|
|
6488
|
+
if (reviewNotesAddBtn) {
|
|
6489
|
+
reviewNotesAddBtn.addEventListener("click", () => {
|
|
6490
|
+
addReviewNoteFromEditorSelection();
|
|
6491
|
+
});
|
|
6492
|
+
}
|
|
6493
|
+
|
|
6494
|
+
if (reviewNotesInlineAllBtn) {
|
|
6495
|
+
reviewNotesInlineAllBtn.addEventListener("click", () => {
|
|
6496
|
+
toggleAllReviewNotesInlineAnnotations();
|
|
6497
|
+
});
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
if (reviewNoteGutterContentEl) {
|
|
6501
|
+
reviewNoteGutterContentEl.addEventListener("click", (event) => {
|
|
6502
|
+
const target = event.target;
|
|
6503
|
+
const markerBtn = target instanceof Element ? target.closest(".editor-review-note-marker") : null;
|
|
6504
|
+
if (!markerBtn) return;
|
|
6505
|
+
const noteId = markerBtn.getAttribute("data-review-note-id") || "";
|
|
6506
|
+
if (!noteId) return;
|
|
6507
|
+
focusReviewNoteInPanel(noteId);
|
|
6508
|
+
});
|
|
6509
|
+
}
|
|
6510
|
+
|
|
5086
6511
|
if (scratchpadBtn) {
|
|
5087
6512
|
scratchpadBtn.addEventListener("click", () => {
|
|
5088
6513
|
openScratchpad();
|
|
@@ -5217,6 +6642,11 @@
|
|
|
5217
6642
|
syncActionButtons();
|
|
5218
6643
|
renderSourcePreview();
|
|
5219
6644
|
}
|
|
6645
|
+
if (sourceBadgeEl) {
|
|
6646
|
+
sourceBadgeEl.addEventListener("click", () => {
|
|
6647
|
+
resetEditorOrigin();
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
5220
6650
|
if (resourceDirBtn) {
|
|
5221
6651
|
resourceDirBtn.addEventListener("click", () => {
|
|
5222
6652
|
showResourceDirState("input");
|
|
@@ -5286,20 +6716,31 @@
|
|
|
5286
6716
|
reader.readAsText(file);
|
|
5287
6717
|
});
|
|
5288
6718
|
|
|
6719
|
+
if (sourceEditorWrapEl && typeof ResizeObserver === "function") {
|
|
6720
|
+
const editorResizeObserver = new ResizeObserver(() => {
|
|
6721
|
+
if (editorView !== "markdown") return;
|
|
6722
|
+
scheduleEditorLineNumberRender();
|
|
6723
|
+
});
|
|
6724
|
+
editorResizeObserver.observe(sourceEditorWrapEl);
|
|
6725
|
+
}
|
|
6726
|
+
|
|
5289
6727
|
setSourceState(initialSourceState);
|
|
5290
6728
|
refreshResponseUi();
|
|
5291
6729
|
updateAnnotatedReplyHeaderButton();
|
|
5292
6730
|
setActivePane("left");
|
|
5293
|
-
setScratchpadText(readStoredScratchpadText() || "", { persist: false });
|
|
5294
6731
|
|
|
5295
6732
|
const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
|
|
5296
|
-
const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value
|
|
6733
|
+
const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value !== "off");
|
|
5297
6734
|
setEditorHighlightEnabled(initialHighlightEnabled);
|
|
5298
6735
|
|
|
5299
6736
|
const initialDetectedLang = detectLanguageFromName(initialSourceState.path || initialSourceState.label || "");
|
|
5300
6737
|
const storedLang = readStoredEditorLanguage();
|
|
5301
6738
|
setEditorLanguage(initialDetectedLang || storedLang || "markdown");
|
|
5302
6739
|
|
|
6740
|
+
const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
|
|
6741
|
+
const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
|
|
6742
|
+
setLineNumbersEnabled(initialLineNumbersEnabled);
|
|
6743
|
+
|
|
5303
6744
|
const storedResponseHighlightEnabled = readStoredResponseHighlightEnabled();
|
|
5304
6745
|
const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
|
|
5305
6746
|
setResponseHighlightEnabled(initialResponseHighlightEnabled);
|