pi-studio 0.5.44 → 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 +13 -0
- package/README.md +2 -1
- package/client/studio-client.js +1230 -71
- package/client/studio.css +317 -6
- package/index.ts +444 -19
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
const sourceEditorWrapEl = document.getElementById("sourceEditorWrap");
|
|
48
48
|
const sourceTextEl = document.getElementById("sourceText");
|
|
49
49
|
const sourceHighlightEl = document.getElementById("sourceHighlight");
|
|
50
|
+
const reviewNoteGutterEl = document.getElementById("reviewNoteGutter");
|
|
51
|
+
const reviewNoteGutterContentEl = document.getElementById("reviewNoteGutterContent");
|
|
50
52
|
const lineNumberGutterEl = document.getElementById("lineNumberGutter");
|
|
51
53
|
const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
|
|
52
54
|
const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
|
|
@@ -98,6 +100,7 @@
|
|
|
98
100
|
const compactBtn = document.getElementById("compactBtn");
|
|
99
101
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
100
102
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
103
|
+
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
101
104
|
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
102
105
|
const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
|
|
103
106
|
const scratchpadDialogEl = document.getElementById("scratchpadDialog");
|
|
@@ -108,16 +111,35 @@
|
|
|
108
111
|
const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
|
|
109
112
|
const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
|
|
110
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");
|
|
111
123
|
|
|
112
124
|
const studioMode = (document.body && document.body.dataset && document.body.dataset.studioMode) === "editor-only"
|
|
113
125
|
? "editor-only"
|
|
114
126
|
: "full";
|
|
115
127
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
116
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");
|
|
117
134
|
const initialSourceState = {
|
|
118
|
-
source: (
|
|
119
|
-
|
|
120
|
-
|
|
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),
|
|
121
143
|
};
|
|
122
144
|
|
|
123
145
|
let ws = null;
|
|
@@ -206,6 +228,7 @@
|
|
|
206
228
|
source: initialSourceState.source,
|
|
207
229
|
label: initialSourceState.label,
|
|
208
230
|
path: initialSourceState.path,
|
|
231
|
+
draftId: initialSourceState.draftId,
|
|
209
232
|
};
|
|
210
233
|
let fileBackedBaselineText = null;
|
|
211
234
|
let activePane = "left";
|
|
@@ -255,7 +278,6 @@
|
|
|
255
278
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
256
279
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
257
280
|
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
258
|
-
const SCRATCHPAD_STORAGE_KEY = "piStudio.scratchpad";
|
|
259
281
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
260
282
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
261
283
|
const previewPendingTimers = new WeakMap();
|
|
@@ -274,6 +296,14 @@
|
|
|
274
296
|
let annotationsEnabled = true;
|
|
275
297
|
let scratchpadText = "";
|
|
276
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;
|
|
277
307
|
const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
|
|
278
308
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
279
309
|
if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
|
|
@@ -740,7 +770,8 @@
|
|
|
740
770
|
if (terminalActivityPhase === "responding") {
|
|
741
771
|
if (activeKind === "critique") return "Critiquing…";
|
|
742
772
|
if (activeKind === "annotation") return "Replying…";
|
|
743
|
-
return "
|
|
773
|
+
if (activeKind === "direct") return "Thinking…";
|
|
774
|
+
return "Working…";
|
|
744
775
|
}
|
|
745
776
|
|
|
746
777
|
if (activeKind) return getTitleActionMessage(activeKind);
|
|
@@ -756,29 +787,26 @@
|
|
|
756
787
|
}
|
|
757
788
|
|
|
758
789
|
function buildStudioFaviconHref() {
|
|
759
|
-
const
|
|
760
|
-
const
|
|
761
|
-
const accent = readThemeColor("--accent", fg);
|
|
790
|
+
const idleColor = readThemeColor("--text", "#111111");
|
|
791
|
+
const accent = readThemeColor("--accent", "#2563eb");
|
|
762
792
|
const ok = readThemeColor("--ok", "#16a34a");
|
|
763
|
-
const warn = readThemeColor("--warn",
|
|
793
|
+
const warn = readThemeColor("--warn", "#d97706");
|
|
764
794
|
const error = readThemeColor("--error", "#dc2626");
|
|
765
795
|
|
|
766
|
-
let
|
|
767
|
-
|
|
796
|
+
let piColor = idleColor;
|
|
768
797
|
if (titleAttentionMessage) {
|
|
769
|
-
|
|
798
|
+
piColor = ok;
|
|
770
799
|
} else if (wsState === "Disconnected") {
|
|
771
|
-
|
|
800
|
+
piColor = error;
|
|
772
801
|
} else if (wsState === "Connecting") {
|
|
773
|
-
|
|
802
|
+
piColor = accent;
|
|
774
803
|
} else if (getTitleBusyMessage()) {
|
|
775
|
-
|
|
804
|
+
piColor = warn;
|
|
776
805
|
}
|
|
777
806
|
|
|
778
807
|
const svg = [
|
|
779
808
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">',
|
|
780
|
-
`<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="${
|
|
781
|
-
badgeSvg,
|
|
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>`,
|
|
782
810
|
"</svg>",
|
|
783
811
|
].join("");
|
|
784
812
|
return "data:image/svg+xml," + encodeURIComponent(svg);
|
|
@@ -938,6 +966,12 @@
|
|
|
938
966
|
function updateSourceBadge() {
|
|
939
967
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
940
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
|
+
}
|
|
941
975
|
// Show "Set working dir" button when not file-backed
|
|
942
976
|
var isFileBacked = hasRefreshableFilePath();
|
|
943
977
|
if (isFileBacked) {
|
|
@@ -961,6 +995,26 @@
|
|
|
961
995
|
}
|
|
962
996
|
}
|
|
963
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
|
+
|
|
964
1018
|
function updatePaneFocusButtons() {
|
|
965
1019
|
[
|
|
966
1020
|
[leftFocusBtn, "left"],
|
|
@@ -1062,6 +1116,12 @@
|
|
|
1062
1116
|
&& typeof scratchpadDialogEl.contains === "function"
|
|
1063
1117
|
&& scratchpadDialogEl.contains(event.target)
|
|
1064
1118
|
);
|
|
1119
|
+
const reviewNotesOwnsEvent = Boolean(
|
|
1120
|
+
reviewNotesDialogEl
|
|
1121
|
+
&& event.target
|
|
1122
|
+
&& typeof reviewNotesDialogEl.contains === "function"
|
|
1123
|
+
&& reviewNotesDialogEl.contains(event.target)
|
|
1124
|
+
);
|
|
1065
1125
|
|
|
1066
1126
|
if (isScratchpadOpen() && plainEscape) {
|
|
1067
1127
|
event.preventDefault();
|
|
@@ -1069,7 +1129,13 @@
|
|
|
1069
1129
|
return;
|
|
1070
1130
|
}
|
|
1071
1131
|
|
|
1072
|
-
if (
|
|
1132
|
+
if (isReviewNotesOpen() && plainEscape) {
|
|
1133
|
+
event.preventDefault();
|
|
1134
|
+
closeReviewNotes();
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent) {
|
|
1073
1139
|
return;
|
|
1074
1140
|
}
|
|
1075
1141
|
|
|
@@ -2545,7 +2611,7 @@
|
|
|
2545
2611
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
2546
2612
|
scheduleEditorHighlightRender();
|
|
2547
2613
|
}
|
|
2548
|
-
if (
|
|
2614
|
+
if (editorView === "markdown") {
|
|
2549
2615
|
scheduleEditorLineNumberRender();
|
|
2550
2616
|
}
|
|
2551
2617
|
if (rightView === "editor-preview") {
|
|
@@ -2772,6 +2838,7 @@
|
|
|
2772
2838
|
const canRefreshFromDisk = hasRefreshableFilePath();
|
|
2773
2839
|
|
|
2774
2840
|
fileInput.disabled = uiBusy;
|
|
2841
|
+
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
2775
2842
|
saveAsBtn.disabled = uiBusy;
|
|
2776
2843
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
2777
2844
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
@@ -2805,17 +2872,33 @@
|
|
|
2805
2872
|
syncActionButtons();
|
|
2806
2873
|
}
|
|
2807
2874
|
|
|
2808
|
-
function setSourceState(next) {
|
|
2875
|
+
function setSourceState(next, options) {
|
|
2876
|
+
const previousDescriptor = getCurrentStudioDocumentDescriptor();
|
|
2877
|
+
const nextPath = next && next.path ? next.path : null;
|
|
2809
2878
|
sourceState = {
|
|
2810
2879
|
source: next && next.source ? next.source : "blank",
|
|
2811
2880
|
label: next && next.label ? next.label : "blank",
|
|
2812
|
-
path:
|
|
2881
|
+
path: nextPath,
|
|
2882
|
+
draftId: nextPath
|
|
2883
|
+
? null
|
|
2884
|
+
: (next && next.draftId ? next.draftId : makeStudioDraftId()),
|
|
2813
2885
|
};
|
|
2814
2886
|
if (!sourceState.path) {
|
|
2815
2887
|
clearFileBackedBaseline();
|
|
2816
2888
|
}
|
|
2889
|
+
updateStudioDocumentUrlState(sourceState);
|
|
2817
2890
|
updateSourceBadge();
|
|
2818
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
|
+
});
|
|
2819
2902
|
}
|
|
2820
2903
|
|
|
2821
2904
|
function setEditorText(nextText, options) {
|
|
@@ -2848,7 +2931,7 @@
|
|
|
2848
2931
|
schedule(() => {
|
|
2849
2932
|
syncEditorHighlightScroll();
|
|
2850
2933
|
});
|
|
2851
|
-
if (
|
|
2934
|
+
if (editorView === "markdown") {
|
|
2852
2935
|
scheduleEditorLineNumberRender();
|
|
2853
2936
|
}
|
|
2854
2937
|
|
|
@@ -2888,9 +2971,10 @@
|
|
|
2888
2971
|
updateEditorHighlightState();
|
|
2889
2972
|
syncHighlightSelectUi();
|
|
2890
2973
|
updateLineNumberGutterVisibility();
|
|
2891
|
-
if (!showPreview
|
|
2974
|
+
if (!showPreview) {
|
|
2892
2975
|
scheduleEditorLineNumberRender();
|
|
2893
2976
|
}
|
|
2977
|
+
updateReviewNotesUi();
|
|
2894
2978
|
}
|
|
2895
2979
|
|
|
2896
2980
|
function setRightView(nextView) {
|
|
@@ -2921,27 +3005,53 @@
|
|
|
2921
3005
|
);
|
|
2922
3006
|
}
|
|
2923
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
|
+
|
|
2924
3020
|
function getEditorLineNumberGutterWidthCss(lineCount) {
|
|
2925
3021
|
const digits = Math.max(2, String(Math.max(1, lineCount || 0)).length);
|
|
2926
3022
|
return "calc(" + digits + "ch + 18px)";
|
|
2927
3023
|
}
|
|
2928
3024
|
|
|
2929
3025
|
function updateLineNumberGutterVisibility() {
|
|
2930
|
-
const
|
|
3026
|
+
const lineNumbersVisible = lineNumbersShouldBeVisible();
|
|
3027
|
+
const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
|
|
3028
|
+
const anyVisible = lineNumbersVisible || reviewMarkersVisible;
|
|
2931
3029
|
if (sourceEditorWrapEl) {
|
|
2932
|
-
sourceEditorWrapEl.classList.toggle("line-numbers-enabled",
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
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;
|
|
2936
3041
|
}
|
|
2937
3042
|
if (lineNumberGutterEl) {
|
|
2938
|
-
lineNumberGutterEl.hidden = !
|
|
3043
|
+
lineNumberGutterEl.hidden = !lineNumbersVisible;
|
|
3044
|
+
}
|
|
3045
|
+
if (!reviewMarkersVisible && reviewNoteGutterContentEl) {
|
|
3046
|
+
reviewNoteGutterContentEl.innerHTML = "";
|
|
3047
|
+
}
|
|
3048
|
+
if (!lineNumbersVisible && lineNumberGutterContentEl) {
|
|
3049
|
+
lineNumberGutterContentEl.innerHTML = "";
|
|
2939
3050
|
}
|
|
2940
|
-
if (!
|
|
2941
|
-
|
|
2942
|
-
if (lineNumberMeasureEl) lineNumberMeasureEl.innerHTML = "";
|
|
3051
|
+
if (!anyVisible && lineNumberMeasureEl) {
|
|
3052
|
+
lineNumberMeasureEl.innerHTML = "";
|
|
2943
3053
|
}
|
|
2944
|
-
return
|
|
3054
|
+
return anyVisible;
|
|
2945
3055
|
}
|
|
2946
3056
|
|
|
2947
3057
|
function renderEditorLineNumbersNow() {
|
|
@@ -2950,7 +3060,16 @@
|
|
|
2950
3060
|
const text = String(sourceTextEl.value || "").replace(/\r\n/g, "\n");
|
|
2951
3061
|
const lines = text.split("\n");
|
|
2952
3062
|
const lineCount = Math.max(1, lines.length);
|
|
2953
|
-
|
|
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
|
+
}
|
|
2954
3073
|
|
|
2955
3074
|
const styles = window.getComputedStyle(sourceTextEl);
|
|
2956
3075
|
const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
|
|
@@ -2960,24 +3079,107 @@
|
|
|
2960
3079
|
const paddingLeft = parseFloat(styles.paddingLeft) || 0;
|
|
2961
3080
|
const contentWidth = Math.max(1, sourceTextEl.clientWidth - paddingLeft - paddingRight);
|
|
2962
3081
|
|
|
2963
|
-
lineNumberGutterContentEl
|
|
2964
|
-
|
|
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
|
+
}
|
|
2965
3090
|
lineNumberMeasureEl.style.width = contentWidth + "px";
|
|
2966
3091
|
lineNumberMeasureEl.innerHTML = lines
|
|
2967
3092
|
.map((line) => "<div class='editor-line-number-measure-line'>" + (line.length ? escapeHtml(line) : "​") + "</div>")
|
|
2968
3093
|
.join("");
|
|
2969
3094
|
|
|
2970
3095
|
const measureLines = Array.from(lineNumberMeasureEl.children);
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
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
|
+
}
|
|
2977
3133
|
|
|
2978
3134
|
syncEditorHighlightScroll();
|
|
2979
3135
|
}
|
|
2980
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
|
+
|
|
2981
3183
|
function scheduleEditorLineNumberRender() {
|
|
2982
3184
|
if (lineNumbersRenderRaf !== null) {
|
|
2983
3185
|
if (typeof window.cancelAnimationFrame === "function") {
|
|
@@ -3025,6 +3227,78 @@
|
|
|
3025
3227
|
return query.get("token") || hash.get("token") || "";
|
|
3026
3228
|
}
|
|
3027
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
|
+
|
|
3028
3302
|
function makeRequestId() {
|
|
3029
3303
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
3030
3304
|
return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -3032,6 +3306,10 @@
|
|
|
3032
3306
|
return "req_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
|
|
3033
3307
|
}
|
|
3034
3308
|
|
|
3309
|
+
function makeStudioDraftId() {
|
|
3310
|
+
return "draft_" + makeRequestId();
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3035
3313
|
function escapeHtml(text) {
|
|
3036
3314
|
return text
|
|
3037
3315
|
.replace(/&/g, "&")
|
|
@@ -3549,6 +3827,9 @@
|
|
|
3549
3827
|
sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
|
|
3550
3828
|
sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
|
|
3551
3829
|
}
|
|
3830
|
+
if (reviewNoteGutterEl) {
|
|
3831
|
+
reviewNoteGutterEl.scrollTop = sourceTextEl.scrollTop;
|
|
3832
|
+
}
|
|
3552
3833
|
if (lineNumberGutterEl) {
|
|
3553
3834
|
lineNumberGutterEl.scrollTop = sourceTextEl.scrollTop;
|
|
3554
3835
|
}
|
|
@@ -3628,51 +3909,823 @@
|
|
|
3628
3909
|
persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
|
|
3629
3910
|
}
|
|
3630
3911
|
|
|
3631
|
-
function
|
|
3632
|
-
|
|
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
|
+
}
|
|
3633
4008
|
try {
|
|
3634
|
-
const
|
|
3635
|
-
|
|
4009
|
+
const serverText = await fetchScratchpadTextForDocumentKey(key);
|
|
4010
|
+
if (loadNonce !== scratchpadLoadNonce) return;
|
|
4011
|
+
if (key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4012
|
+
setScratchpadText(serverText, { persist: false });
|
|
3636
4013
|
} catch {
|
|
3637
|
-
return
|
|
4014
|
+
if (loadNonce !== scratchpadLoadNonce) return;
|
|
4015
|
+
if (key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4016
|
+
setScratchpadText("", { persist: false });
|
|
3638
4017
|
}
|
|
3639
4018
|
}
|
|
3640
4019
|
|
|
3641
|
-
function
|
|
3642
|
-
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;
|
|
3643
4024
|
try {
|
|
3644
|
-
|
|
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
|
+
});
|
|
3645
4031
|
} catch {
|
|
3646
|
-
//
|
|
4032
|
+
// Ignore carry-over failures and just fall back to normal scope loading.
|
|
3647
4033
|
}
|
|
3648
4034
|
}
|
|
3649
4035
|
|
|
3650
|
-
function
|
|
3651
|
-
|
|
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
|
+
})();
|
|
3652
4046
|
}
|
|
3653
4047
|
|
|
3654
|
-
function
|
|
3655
|
-
|
|
4048
|
+
function persistScratchpadText(value) {
|
|
4049
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4050
|
+
scheduleScratchpadPersistence(value, descriptor.key);
|
|
3656
4051
|
}
|
|
3657
4052
|
|
|
3658
|
-
function
|
|
3659
|
-
|
|
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");
|
|
3660
4712
|
}
|
|
3661
4713
|
|
|
3662
4714
|
function updateScratchpadUi() {
|
|
3663
4715
|
const normalized = String(scratchpadText || "");
|
|
3664
4716
|
const hasContent = Boolean(normalized.trim());
|
|
4717
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
3665
4718
|
if (scratchpadBtn) {
|
|
3666
4719
|
scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
|
|
3667
4720
|
scratchpadBtn.classList.toggle("has-content", hasContent);
|
|
3668
4721
|
scratchpadBtn.title = hasContent
|
|
3669
|
-
? "Open
|
|
3670
|
-
: "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.");
|
|
3671
4724
|
}
|
|
3672
4725
|
if (scratchpadMetaEl) {
|
|
3673
4726
|
scratchpadMetaEl.textContent = hasContent
|
|
3674
|
-
? "Saved locally
|
|
3675
|
-
: "Empty · local
|
|
4727
|
+
? ("Saved locally for this document/draft · " + normalized.length + " chars")
|
|
4728
|
+
: "Empty · local to this document/draft";
|
|
3676
4729
|
}
|
|
3677
4730
|
if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
|
|
3678
4731
|
if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
|
|
@@ -3693,7 +4746,7 @@
|
|
|
3693
4746
|
function closeScratchpad(options) {
|
|
3694
4747
|
if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
|
|
3695
4748
|
scratchpadOverlayEl.hidden = true;
|
|
3696
|
-
|
|
4749
|
+
syncModalOpenState();
|
|
3697
4750
|
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
3698
4751
|
? options.focusTarget
|
|
3699
4752
|
: (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
|
|
@@ -3708,11 +4761,14 @@
|
|
|
3708
4761
|
|
|
3709
4762
|
function openScratchpad() {
|
|
3710
4763
|
if (!scratchpadOverlayEl) return;
|
|
4764
|
+
if (isReviewNotesOpen()) {
|
|
4765
|
+
closeReviewNotes({ focusTarget: null });
|
|
4766
|
+
}
|
|
3711
4767
|
scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
3712
4768
|
? document.activeElement
|
|
3713
4769
|
: sourceTextEl;
|
|
3714
4770
|
scratchpadOverlayEl.hidden = false;
|
|
3715
|
-
|
|
4771
|
+
syncModalOpenState();
|
|
3716
4772
|
if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
|
|
3717
4773
|
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3718
4774
|
? window.requestAnimationFrame.bind(window)
|
|
@@ -3727,6 +4783,49 @@
|
|
|
3727
4783
|
}
|
|
3728
4784
|
}
|
|
3729
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
|
+
|
|
3730
4829
|
function insertScratchpadIntoEditor() {
|
|
3731
4830
|
const content = String(scratchpadText || "");
|
|
3732
4831
|
if (!content.trim()) {
|
|
@@ -3932,8 +5031,8 @@
|
|
|
3932
5031
|
if (annotationModeSelect) {
|
|
3933
5032
|
annotationModeSelect.value = annotationsEnabled ? "on" : "off";
|
|
3934
5033
|
annotationModeSelect.title = annotationsEnabled
|
|
3935
|
-
? "
|
|
3936
|
-
: "
|
|
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.";
|
|
3937
5036
|
}
|
|
3938
5037
|
|
|
3939
5038
|
syncRunAndCritiqueButtons();
|
|
@@ -4110,6 +5209,7 @@
|
|
|
4110
5209
|
|
|
4111
5210
|
let loadedInitialDocument = false;
|
|
4112
5211
|
if (
|
|
5212
|
+
!explicitDocumentIdentityFromUrl &&
|
|
4113
5213
|
!initialDocumentApplied &&
|
|
4114
5214
|
message.initialDocument &&
|
|
4115
5215
|
typeof message.initialDocument.text === "string"
|
|
@@ -4121,6 +5221,9 @@
|
|
|
4121
5221
|
source: message.initialDocument.source || "blank",
|
|
4122
5222
|
label: message.initialDocument.label || "blank",
|
|
4123
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),
|
|
4124
5227
|
});
|
|
4125
5228
|
if (message.initialDocument.path) {
|
|
4126
5229
|
markFileBackedBaseline(message.initialDocument.text);
|
|
@@ -4333,6 +5436,8 @@
|
|
|
4333
5436
|
source: "file",
|
|
4334
5437
|
label: message.label || message.path,
|
|
4335
5438
|
path: message.path,
|
|
5439
|
+
}, {
|
|
5440
|
+
carryCurrentMetadataToNewDocument: true,
|
|
4336
5441
|
});
|
|
4337
5442
|
markFileBackedBaseline(sourceTextEl.value);
|
|
4338
5443
|
}
|
|
@@ -4403,7 +5508,12 @@
|
|
|
4403
5508
|
: null;
|
|
4404
5509
|
|
|
4405
5510
|
setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
|
|
4406
|
-
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
|
+
});
|
|
4407
5517
|
if (nextPath) {
|
|
4408
5518
|
markFileBackedBaseline(nextDoc.text);
|
|
4409
5519
|
}
|
|
@@ -4855,6 +5965,8 @@
|
|
|
4855
5965
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
4856
5966
|
window.addEventListener("beforeunload", () => {
|
|
4857
5967
|
stopFooterSpinner();
|
|
5968
|
+
flushScratchpadPersistence();
|
|
5969
|
+
flushReviewNotesPersistence();
|
|
4858
5970
|
});
|
|
4859
5971
|
|
|
4860
5972
|
editorViewSelect.addEventListener("change", () => {
|
|
@@ -5006,6 +6118,10 @@
|
|
|
5006
6118
|
sourceTextEl.addEventListener("input", () => {
|
|
5007
6119
|
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
5008
6120
|
scheduleEditorMetaUpdate();
|
|
6121
|
+
if (isReviewNotesOpen() && reviewNotes.length > 0) {
|
|
6122
|
+
renderReviewNotesList();
|
|
6123
|
+
updateReviewNotesUi();
|
|
6124
|
+
}
|
|
5009
6125
|
});
|
|
5010
6126
|
|
|
5011
6127
|
sourceTextEl.addEventListener("scroll", () => {
|
|
@@ -5026,9 +6142,7 @@
|
|
|
5026
6142
|
window.addEventListener("resize", () => {
|
|
5027
6143
|
if (editorView !== "markdown") return;
|
|
5028
6144
|
syncEditorHighlightScroll();
|
|
5029
|
-
|
|
5030
|
-
scheduleEditorLineNumberRender();
|
|
5031
|
-
}
|
|
6145
|
+
scheduleEditorLineNumberRender();
|
|
5032
6146
|
});
|
|
5033
6147
|
|
|
5034
6148
|
insertHeaderBtn.addEventListener("click", () => {
|
|
@@ -5353,6 +6467,47 @@
|
|
|
5353
6467
|
}
|
|
5354
6468
|
});
|
|
5355
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
|
+
|
|
5356
6511
|
if (scratchpadBtn) {
|
|
5357
6512
|
scratchpadBtn.addEventListener("click", () => {
|
|
5358
6513
|
openScratchpad();
|
|
@@ -5487,6 +6642,11 @@
|
|
|
5487
6642
|
syncActionButtons();
|
|
5488
6643
|
renderSourcePreview();
|
|
5489
6644
|
}
|
|
6645
|
+
if (sourceBadgeEl) {
|
|
6646
|
+
sourceBadgeEl.addEventListener("click", () => {
|
|
6647
|
+
resetEditorOrigin();
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
5490
6650
|
if (resourceDirBtn) {
|
|
5491
6651
|
resourceDirBtn.addEventListener("click", () => {
|
|
5492
6652
|
showResourceDirState("input");
|
|
@@ -5558,7 +6718,7 @@
|
|
|
5558
6718
|
|
|
5559
6719
|
if (sourceEditorWrapEl && typeof ResizeObserver === "function") {
|
|
5560
6720
|
const editorResizeObserver = new ResizeObserver(() => {
|
|
5561
|
-
if (editorView !== "markdown"
|
|
6721
|
+
if (editorView !== "markdown") return;
|
|
5562
6722
|
scheduleEditorLineNumberRender();
|
|
5563
6723
|
});
|
|
5564
6724
|
editorResizeObserver.observe(sourceEditorWrapEl);
|
|
@@ -5568,7 +6728,6 @@
|
|
|
5568
6728
|
refreshResponseUi();
|
|
5569
6729
|
updateAnnotatedReplyHeaderButton();
|
|
5570
6730
|
setActivePane("left");
|
|
5571
|
-
setScratchpadText(readStoredScratchpadText() || "", { persist: false });
|
|
5572
6731
|
|
|
5573
6732
|
const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
|
|
5574
6733
|
const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value !== "off");
|