pi-studio 0.5.44 → 0.5.46

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.
@@ -47,10 +47,13 @@
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");
53
55
  const sourcePreviewEl = document.getElementById("sourcePreview");
56
+ const editorSelectionCommentBtn = document.getElementById("editorSelectionCommentBtn");
54
57
  const leftPaneEl = document.getElementById("leftPane");
55
58
  const rightPaneEl = document.getElementById("rightPane");
56
59
  const sourceBadgeEl = document.getElementById("sourceBadge");
@@ -98,6 +101,7 @@
98
101
  const compactBtn = document.getElementById("compactBtn");
99
102
  const leftFocusBtn = document.getElementById("leftFocusBtn");
100
103
  const rightFocusBtn = document.getElementById("rightFocusBtn");
104
+ const reviewNotesBtn = document.getElementById("reviewNotesBtn");
101
105
  const scratchpadBtn = document.getElementById("scratchpadBtn");
102
106
  const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
103
107
  const scratchpadDialogEl = document.getElementById("scratchpadDialog");
@@ -108,16 +112,35 @@
108
112
  const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
109
113
  const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
110
114
  const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
115
+ const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
116
+ const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
117
+ const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
118
+ const reviewNotesListEl = document.getElementById("reviewNotesList");
119
+ const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
120
+ const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
121
+ const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
122
+ const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
123
+ const reviewNotesDoneBtn = document.getElementById("reviewNotesDoneBtn");
111
124
 
112
125
  const studioMode = (document.body && document.body.dataset && document.body.dataset.studioMode) === "editor-only"
113
126
  ? "editor-only"
114
127
  : "full";
115
128
  const isEditorOnlyMode = studioMode === "editor-only";
116
129
 
130
+ const initialQueryParams = new URLSearchParams(window.location.search || "");
131
+ const explicitDocumentIdentityFromUrl = initialQueryParams.has("docSource")
132
+ || initialQueryParams.has("docLabel")
133
+ || initialQueryParams.has("docPath")
134
+ || initialQueryParams.has("draftId");
117
135
  const initialSourceState = {
118
- source: (document.body && document.body.dataset && document.body.dataset.initialSource) || "blank",
119
- label: (document.body && document.body.dataset && document.body.dataset.initialLabel) || "blank",
120
- path: (document.body && document.body.dataset && document.body.dataset.initialPath) || null,
136
+ source: initialQueryParams.get("docSource")
137
+ || ((document.body && document.body.dataset && document.body.dataset.initialSource) || "blank"),
138
+ label: initialQueryParams.get("docLabel")
139
+ || ((document.body && document.body.dataset && document.body.dataset.initialLabel) || "blank"),
140
+ path: initialQueryParams.get("docPath")
141
+ || ((document.body && document.body.dataset && document.body.dataset.initialPath) || null),
142
+ draftId: initialQueryParams.get("draftId")
143
+ || ((document.body && document.body.dataset && document.body.dataset.initialDraftId) || null),
121
144
  };
122
145
 
123
146
  let ws = null;
@@ -206,6 +229,7 @@
206
229
  source: initialSourceState.source,
207
230
  label: initialSourceState.label,
208
231
  path: initialSourceState.path,
232
+ draftId: initialSourceState.draftId,
209
233
  };
210
234
  let fileBackedBaselineText = null;
211
235
  let activePane = "left";
@@ -255,7 +279,6 @@
255
279
  const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
256
280
  const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
257
281
  const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
258
- const SCRATCHPAD_STORAGE_KEY = "piStudio.scratchpad";
259
282
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
260
283
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
261
284
  const previewPendingTimers = new WeakMap();
@@ -274,6 +297,16 @@
274
297
  let annotationsEnabled = true;
275
298
  let scratchpadText = "";
276
299
  let scratchpadReturnFocusEl = null;
300
+ let scratchpadPersistTimer = null;
301
+ let scratchpadLoadNonce = 0;
302
+ let reviewNotes = [];
303
+ let reviewNotesReturnFocusEl = null;
304
+ let reviewNotesPersistTimer = null;
305
+ let reviewNotesLoadNonce = 0;
306
+ let pendingReviewNoteFocusId = null;
307
+ let pendingReviewNoteInlineFocusId = null;
308
+ let activePreviewCommentSelection = null;
309
+ const previewJumpHighlightState = new WeakMap();
277
310
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
278
311
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
279
312
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
@@ -740,7 +773,8 @@
740
773
  if (terminalActivityPhase === "responding") {
741
774
  if (activeKind === "critique") return "Critiquing…";
742
775
  if (activeKind === "annotation") return "Replying…";
743
- return "Responding…";
776
+ if (activeKind === "direct") return "Thinking…";
777
+ return "Working…";
744
778
  }
745
779
 
746
780
  if (activeKind) return getTitleActionMessage(activeKind);
@@ -756,29 +790,26 @@
756
790
  }
757
791
 
758
792
  function buildStudioFaviconHref() {
759
- const fg = readThemeColor("--text", "#111111");
760
- const bg = readThemeColor("--bg", "#ffffff");
761
- const accent = readThemeColor("--accent", fg);
793
+ const idleColor = readThemeColor("--text", "#111111");
794
+ const accent = readThemeColor("--accent", "#2563eb");
762
795
  const ok = readThemeColor("--ok", "#16a34a");
763
- const warn = readThemeColor("--warn", accent);
796
+ const warn = readThemeColor("--warn", "#d97706");
764
797
  const error = readThemeColor("--error", "#dc2626");
765
798
 
766
- let badgeSvg = "";
767
-
799
+ let piColor = idleColor;
768
800
  if (titleAttentionMessage) {
769
- badgeSvg = `<circle cx="50" cy="14" r="9" fill="${ok}" stroke="${bg}" stroke-width="4" />`;
801
+ piColor = ok;
770
802
  } else if (wsState === "Disconnected") {
771
- badgeSvg = `<circle cx="50" cy="14" r="9" fill="${error}" stroke="${bg}" stroke-width="4" />`;
803
+ piColor = error;
772
804
  } else if (wsState === "Connecting") {
773
- badgeSvg = `<circle cx="50" cy="14" r="9" fill="${accent}" stroke="${bg}" stroke-width="4" />`;
805
+ piColor = accent;
774
806
  } else if (getTitleBusyMessage()) {
775
- badgeSvg = `<circle cx="50" cy="14" r="10" fill="none" stroke="${warn}" stroke-width="5" />`;
807
+ piColor = warn;
776
808
  }
777
809
 
778
810
  const svg = [
779
811
  '<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="${fg}">π</text>`,
781
- badgeSvg,
812
+ `<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
813
  "</svg>",
783
814
  ].join("");
784
815
  return "data:image/svg+xml," + encodeURIComponent(svg);
@@ -938,6 +969,12 @@
938
969
  function updateSourceBadge() {
939
970
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
940
971
  sourceBadgeEl.textContent = "Editor origin: " + label;
972
+ const descriptor = getCurrentStudioDocumentDescriptor();
973
+ if (sourceBadgeEl) {
974
+ sourceBadgeEl.title = descriptor.fileBacked
975
+ ? ("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.")
976
+ : ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes.");
977
+ }
941
978
  // Show "Set working dir" button when not file-backed
942
979
  var isFileBacked = hasRefreshableFilePath();
943
980
  if (isFileBacked) {
@@ -961,6 +998,26 @@
961
998
  }
962
999
  }
963
1000
 
1001
+ function resetEditorOrigin() {
1002
+ const descriptor = getCurrentStudioDocumentDescriptor();
1003
+ const message = descriptor.fileBacked
1004
+ ? ("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.")
1005
+ : ("Reset editor origin and start a new independent draft? The current editor text, scratchpad, and review notes will carry into the new draft.");
1006
+ if (!window.confirm(message)) {
1007
+ return;
1008
+ }
1009
+ const nextLabel = String(sourceTextEl.value || "").trim() ? "draft" : "blank";
1010
+ setSourceState({
1011
+ source: "blank",
1012
+ label: nextLabel,
1013
+ path: null,
1014
+ draftId: makeStudioDraftId(),
1015
+ }, {
1016
+ carryCurrentMetadataToNewDocument: true,
1017
+ });
1018
+ setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
1019
+ }
1020
+
964
1021
  function updatePaneFocusButtons() {
965
1022
  [
966
1023
  [leftFocusBtn, "left"],
@@ -1062,6 +1119,12 @@
1062
1119
  && typeof scratchpadDialogEl.contains === "function"
1063
1120
  && scratchpadDialogEl.contains(event.target)
1064
1121
  );
1122
+ const reviewNotesOwnsEvent = Boolean(
1123
+ reviewNotesDialogEl
1124
+ && event.target
1125
+ && typeof reviewNotesDialogEl.contains === "function"
1126
+ && reviewNotesDialogEl.contains(event.target)
1127
+ );
1065
1128
 
1066
1129
  if (isScratchpadOpen() && plainEscape) {
1067
1130
  event.preventDefault();
@@ -1069,7 +1132,13 @@
1069
1132
  return;
1070
1133
  }
1071
1134
 
1072
- if (scratchpadOwnsEvent) {
1135
+ if (isReviewNotesOpen() && plainEscape) {
1136
+ event.preventDefault();
1137
+ closeReviewNotes();
1138
+ return;
1139
+ }
1140
+
1141
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent) {
1073
1142
  return;
1074
1143
  }
1075
1144
 
@@ -2462,6 +2531,7 @@
2462
2531
  if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
2463
2532
  }
2464
2533
 
2534
+ clearPreviewJumpHighlight(targetEl);
2465
2535
  finishPreviewRender(targetEl);
2466
2536
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2467
2537
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
@@ -2475,6 +2545,15 @@
2475
2545
  await renderMermaidInElement(targetEl);
2476
2546
  await renderMathFallbackInElement(targetEl);
2477
2547
 
2548
+ const shouldDecoratePreviewComments = supportsPreviewCommentsForCurrentEditor()
2549
+ && (
2550
+ (pane === "source" && editorView === "preview")
2551
+ || (pane === "response" && rightView === "editor-preview")
2552
+ );
2553
+ if (shouldDecoratePreviewComments) {
2554
+ decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
2555
+ }
2556
+
2478
2557
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
2479
2558
  if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
2480
2559
  var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
@@ -2496,6 +2575,7 @@
2496
2575
  }
2497
2576
 
2498
2577
  const detail = error && error.message ? error.message : String(error || "unknown error");
2578
+ clearPreviewJumpHighlight(targetEl);
2499
2579
  finishPreviewRender(targetEl);
2500
2580
  targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2501
2581
  if (pane === "response") {
@@ -2545,7 +2625,7 @@
2545
2625
  if (editorHighlightEnabled && editorView === "markdown") {
2546
2626
  scheduleEditorHighlightRender();
2547
2627
  }
2548
- if (lineNumbersEnabled && editorView === "markdown") {
2628
+ if (editorView === "markdown") {
2549
2629
  scheduleEditorLineNumberRender();
2550
2630
  }
2551
2631
  if (rightView === "editor-preview") {
@@ -2772,6 +2852,7 @@
2772
2852
  const canRefreshFromDisk = hasRefreshableFilePath();
2773
2853
 
2774
2854
  fileInput.disabled = uiBusy;
2855
+ if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
2775
2856
  saveAsBtn.disabled = uiBusy;
2776
2857
  saveOverBtn.disabled = uiBusy || !canSaveOver;
2777
2858
  if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
@@ -2805,23 +2886,42 @@
2805
2886
  syncActionButtons();
2806
2887
  }
2807
2888
 
2808
- function setSourceState(next) {
2889
+ function setSourceState(next, options) {
2890
+ const previousDescriptor = getCurrentStudioDocumentDescriptor();
2891
+ const nextPath = next && next.path ? next.path : null;
2809
2892
  sourceState = {
2810
2893
  source: next && next.source ? next.source : "blank",
2811
2894
  label: next && next.label ? next.label : "blank",
2812
- path: next && next.path ? next.path : null,
2895
+ path: nextPath,
2896
+ draftId: nextPath
2897
+ ? null
2898
+ : (next && next.draftId ? next.draftId : makeStudioDraftId()),
2813
2899
  };
2814
2900
  if (!sourceState.path) {
2815
2901
  clearFileBackedBaseline();
2816
2902
  }
2903
+ updateStudioDocumentUrlState(sourceState);
2817
2904
  updateSourceBadge();
2818
2905
  syncActionButtons();
2906
+ updateScratchpadUi();
2907
+ updateReviewNotesUi();
2908
+ loadScratchpadForCurrentDocument({
2909
+ previousDescriptor: previousDescriptor,
2910
+ carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
2911
+ });
2912
+ void loadReviewNotesForCurrentDocument({
2913
+ previousDescriptor: previousDescriptor,
2914
+ carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
2915
+ });
2819
2916
  }
2820
2917
 
2821
2918
  function setEditorText(nextText, options) {
2822
2919
  const value = String(nextText || "");
2823
2920
  const preserveScroll = Boolean(options && options.preserveScroll);
2824
2921
  const preserveSelection = Boolean(options && options.preserveSelection);
2922
+ if (activePreviewCommentSelection) {
2923
+ clearPreviewCommentSelection();
2924
+ }
2825
2925
  const previousScrollTop = sourceTextEl.scrollTop;
2826
2926
  const previousScrollLeft = sourceTextEl.scrollLeft;
2827
2927
  const previousSelectionStart = sourceTextEl.selectionStart;
@@ -2848,7 +2948,7 @@
2848
2948
  schedule(() => {
2849
2949
  syncEditorHighlightScroll();
2850
2950
  });
2851
- if (lineNumbersEnabled && editorView === "markdown") {
2951
+ if (editorView === "markdown") {
2852
2952
  scheduleEditorLineNumberRender();
2853
2953
  }
2854
2954
 
@@ -2860,6 +2960,7 @@
2860
2960
  if (!options || options.updateMeta !== false) {
2861
2961
  scheduleEditorMetaUpdate();
2862
2962
  }
2963
+ updateEditorSelectionCommentUi();
2863
2964
  }
2864
2965
 
2865
2966
  function setEditorView(nextView) {
@@ -2878,6 +2979,7 @@
2878
2979
  }
2879
2980
 
2880
2981
  if (!showPreview) {
2982
+ clearPreviewJumpHighlight(sourcePreviewEl);
2881
2983
  finishPreviewRender(sourcePreviewEl);
2882
2984
  }
2883
2985
 
@@ -2888,9 +2990,11 @@
2888
2990
  updateEditorHighlightState();
2889
2991
  syncHighlightSelectUi();
2890
2992
  updateLineNumberGutterVisibility();
2891
- if (!showPreview && lineNumbersEnabled) {
2993
+ if (!showPreview) {
2892
2994
  scheduleEditorLineNumberRender();
2893
2995
  }
2996
+ updateReviewNotesUi();
2997
+ updateEditorSelectionCommentUi();
2894
2998
  }
2895
2999
 
2896
3000
  function setRightView(nextView) {
@@ -2905,6 +3009,9 @@
2905
3009
  window.clearTimeout(responseEditorPreviewTimer);
2906
3010
  responseEditorPreviewTimer = null;
2907
3011
  }
3012
+ if (rightView !== "editor-preview") {
3013
+ clearPreviewJumpHighlight(critiqueViewEl);
3014
+ }
2908
3015
 
2909
3016
  refreshResponseUi();
2910
3017
  syncActionButtons();
@@ -2921,27 +3028,53 @@
2921
3028
  );
2922
3029
  }
2923
3030
 
3031
+ function reviewNoteGutterShouldBeVisible() {
3032
+ return Boolean(
3033
+ editorView === "markdown"
3034
+ && sourceEditorWrapEl
3035
+ && reviewNoteGutterEl
3036
+ && reviewNoteGutterContentEl
3037
+ && lineNumberMeasureEl
3038
+ && Array.isArray(reviewNotes)
3039
+ && reviewNotes.length > 0,
3040
+ );
3041
+ }
3042
+
2924
3043
  function getEditorLineNumberGutterWidthCss(lineCount) {
2925
3044
  const digits = Math.max(2, String(Math.max(1, lineCount || 0)).length);
2926
3045
  return "calc(" + digits + "ch + 18px)";
2927
3046
  }
2928
3047
 
2929
3048
  function updateLineNumberGutterVisibility() {
2930
- const visible = lineNumbersShouldBeVisible();
3049
+ const lineNumbersVisible = lineNumbersShouldBeVisible();
3050
+ const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
3051
+ const anyVisible = lineNumbersVisible || reviewMarkersVisible;
2931
3052
  if (sourceEditorWrapEl) {
2932
- sourceEditorWrapEl.classList.toggle("line-numbers-enabled", visible);
2933
- if (!visible) {
2934
- sourceEditorWrapEl.style.setProperty("--editor-line-number-gutter-width", "0px");
2935
- }
3053
+ sourceEditorWrapEl.classList.toggle("line-numbers-enabled", lineNumbersVisible);
3054
+ sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
3055
+ sourceEditorWrapEl.style.setProperty(
3056
+ "--editor-line-number-gutter-width",
3057
+ lineNumbersVisible
3058
+ ? getEditorLineNumberGutterWidthCss(Math.max(1, String(sourceTextEl.value || "").replace(/\r\n/g, "\n").split("\n").length))
3059
+ : "0px",
3060
+ );
3061
+ }
3062
+ if (reviewNoteGutterEl) {
3063
+ reviewNoteGutterEl.hidden = !reviewMarkersVisible;
2936
3064
  }
2937
3065
  if (lineNumberGutterEl) {
2938
- lineNumberGutterEl.hidden = !visible;
3066
+ lineNumberGutterEl.hidden = !lineNumbersVisible;
3067
+ }
3068
+ if (!reviewMarkersVisible && reviewNoteGutterContentEl) {
3069
+ reviewNoteGutterContentEl.innerHTML = "";
2939
3070
  }
2940
- if (!visible) {
2941
- if (lineNumberGutterContentEl) lineNumberGutterContentEl.innerHTML = "";
2942
- if (lineNumberMeasureEl) lineNumberMeasureEl.innerHTML = "";
3071
+ if (!lineNumbersVisible && lineNumberGutterContentEl) {
3072
+ lineNumberGutterContentEl.innerHTML = "";
2943
3073
  }
2944
- return visible;
3074
+ if (!anyVisible && lineNumberMeasureEl) {
3075
+ lineNumberMeasureEl.innerHTML = "";
3076
+ }
3077
+ return anyVisible;
2945
3078
  }
2946
3079
 
2947
3080
  function renderEditorLineNumbersNow() {
@@ -2950,7 +3083,16 @@
2950
3083
  const text = String(sourceTextEl.value || "").replace(/\r\n/g, "\n");
2951
3084
  const lines = text.split("\n");
2952
3085
  const lineCount = Math.max(1, lines.length);
2953
- sourceEditorWrapEl.style.setProperty("--editor-line-number-gutter-width", getEditorLineNumberGutterWidthCss(lineCount));
3086
+ const lineNumbersVisible = lineNumbersShouldBeVisible();
3087
+ const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
3088
+
3089
+ if (sourceEditorWrapEl) {
3090
+ sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
3091
+ sourceEditorWrapEl.style.setProperty(
3092
+ "--editor-line-number-gutter-width",
3093
+ lineNumbersVisible ? getEditorLineNumberGutterWidthCss(lineCount) : "0px",
3094
+ );
3095
+ }
2954
3096
 
2955
3097
  const styles = window.getComputedStyle(sourceTextEl);
2956
3098
  const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
@@ -2960,24 +3102,107 @@
2960
3102
  const paddingLeft = parseFloat(styles.paddingLeft) || 0;
2961
3103
  const contentWidth = Math.max(1, sourceTextEl.clientWidth - paddingLeft - paddingRight);
2962
3104
 
2963
- lineNumberGutterContentEl.style.paddingTop = paddingTop + "px";
2964
- lineNumberGutterContentEl.style.paddingBottom = paddingBottom + "px";
3105
+ if (lineNumberGutterContentEl) {
3106
+ lineNumberGutterContentEl.style.paddingTop = paddingTop + "px";
3107
+ lineNumberGutterContentEl.style.paddingBottom = paddingBottom + "px";
3108
+ }
3109
+ if (reviewNoteGutterContentEl) {
3110
+ reviewNoteGutterContentEl.style.paddingTop = paddingTop + "px";
3111
+ reviewNoteGutterContentEl.style.paddingBottom = paddingBottom + "px";
3112
+ }
2965
3113
  lineNumberMeasureEl.style.width = contentWidth + "px";
2966
3114
  lineNumberMeasureEl.innerHTML = lines
2967
3115
  .map((line) => "<div class='editor-line-number-measure-line'>" + (line.length ? escapeHtml(line) : "&#8203;") + "</div>")
2968
3116
  .join("");
2969
3117
 
2970
3118
  const measureLines = Array.from(lineNumberMeasureEl.children);
2971
- lineNumberGutterContentEl.innerHTML = measureLines
2972
- .map((lineEl, index) => {
2973
- const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
2974
- return "<div class='editor-line-number-row' style='height:" + height.toFixed(2) + "px'>" + (index + 1) + "</div>";
2975
- })
2976
- .join("");
3119
+ const reviewNoteLineMap = reviewMarkersVisible ? buildReviewNoteLineMap(text) : null;
3120
+
3121
+ if (lineNumbersVisible && lineNumberGutterContentEl) {
3122
+ lineNumberGutterContentEl.innerHTML = measureLines
3123
+ .map((lineEl, index) => {
3124
+ const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
3125
+ return "<div class='editor-line-number-row' style='height:" + height.toFixed(2) + "px'>" + (index + 1) + "</div>";
3126
+ })
3127
+ .join("");
3128
+ } else if (lineNumberGutterContentEl) {
3129
+ lineNumberGutterContentEl.innerHTML = "";
3130
+ }
3131
+
3132
+ if (reviewMarkersVisible && reviewNoteGutterContentEl && reviewNoteLineMap) {
3133
+ reviewNoteGutterContentEl.innerHTML = measureLines
3134
+ .map((lineEl, index) => {
3135
+ const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
3136
+ const lineNumber = index + 1;
3137
+ const notesForLine = reviewNoteLineMap.get(lineNumber) || [];
3138
+ const count = notesForLine.length;
3139
+ if (count <= 0) {
3140
+ return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'></div>";
3141
+ }
3142
+ const title = count === 1
3143
+ ? ("1 local comment on line " + lineNumber + ". Open comments.")
3144
+ : (count + " local comments on line " + lineNumber + ". Open comments.");
3145
+ const markerLabel = count > 9 ? "9+" : (count > 1 ? String(count) : "•");
3146
+ return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'><button type='button' class='editor-review-note-marker"
3147
+ + (count > 1 ? " has-multiple" : "")
3148
+ + "' data-review-note-id='" + escapeHtml(notesForLine[0].id) + "' title='" + escapeHtml(title) + "' aria-label='" + escapeHtml(title) + "'>"
3149
+ + escapeHtml(markerLabel)
3150
+ + "</button></div>";
3151
+ })
3152
+ .join("");
3153
+ } else if (reviewNoteGutterContentEl) {
3154
+ reviewNoteGutterContentEl.innerHTML = "";
3155
+ }
2977
3156
 
2978
3157
  syncEditorHighlightScroll();
2979
3158
  }
2980
3159
 
3160
+ function scrollEditorRangeIntoView(range) {
3161
+ if (!range || editorView !== "markdown") return;
3162
+ renderEditorLineNumbersNow();
3163
+
3164
+ const text = String(sourceTextEl.value || "");
3165
+ const startLine = getLineNumberAtOffset(text, range.start);
3166
+ const endLine = getLineNumberAtOffset(text, Math.max(range.start, range.end > range.start ? range.end - 1 : range.end));
3167
+ const styles = window.getComputedStyle(sourceTextEl);
3168
+ const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
3169
+ const paddingTop = parseFloat(styles.paddingTop) || 0;
3170
+ const paddingBottom = parseFloat(styles.paddingBottom) || 0;
3171
+ const measureLines = lineNumberMeasureEl ? Array.from(lineNumberMeasureEl.children) : [];
3172
+
3173
+ function getLineTop(lineNumber) {
3174
+ let top = paddingTop;
3175
+ for (let i = 0; i < lineNumber - 1; i += 1) {
3176
+ const lineEl = measureLines[i];
3177
+ top += Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
3178
+ }
3179
+ return top;
3180
+ }
3181
+
3182
+ function getLineBottom(lineNumber) {
3183
+ const lineEl = measureLines[Math.max(0, lineNumber - 1)];
3184
+ return getLineTop(lineNumber) + Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
3185
+ }
3186
+
3187
+ const rangeTop = getLineTop(startLine);
3188
+ const rangeBottom = getLineBottom(endLine);
3189
+ const viewportTop = sourceTextEl.scrollTop;
3190
+ const viewportBottom = viewportTop + sourceTextEl.clientHeight;
3191
+ const margin = Math.max(18, Math.round(sourceTextEl.clientHeight * 0.12));
3192
+
3193
+ let nextScrollTop = viewportTop;
3194
+ if (rangeTop - margin < viewportTop) {
3195
+ nextScrollTop = Math.max(0, rangeTop - margin);
3196
+ } else if (rangeBottom + margin > viewportBottom) {
3197
+ nextScrollTop = Math.max(0, rangeBottom - sourceTextEl.clientHeight + margin + paddingBottom);
3198
+ }
3199
+
3200
+ if (Math.abs(nextScrollTop - viewportTop) > 1) {
3201
+ sourceTextEl.scrollTop = nextScrollTop;
3202
+ syncEditorHighlightScroll();
3203
+ }
3204
+ }
3205
+
2981
3206
  function scheduleEditorLineNumberRender() {
2982
3207
  if (lineNumbersRenderRaf !== null) {
2983
3208
  if (typeof window.cancelAnimationFrame === "function") {
@@ -3025,6 +3250,78 @@
3025
3250
  return query.get("token") || hash.get("token") || "";
3026
3251
  }
3027
3252
 
3253
+ function buildAuthedStudioUrl(pathname, extraParams) {
3254
+ const token = getToken();
3255
+ if (!token) {
3256
+ throw new Error("Missing Studio token in URL.");
3257
+ }
3258
+ const params = new URLSearchParams(extraParams || {});
3259
+ params.set("token", token);
3260
+ return pathname + "?" + params.toString();
3261
+ }
3262
+
3263
+ function updateStudioDocumentUrlState(state) {
3264
+ try {
3265
+ const currentUrl = new URL(window.location.href);
3266
+ const params = currentUrl.searchParams;
3267
+ const nextState = state && typeof state === "object" ? state : sourceState;
3268
+ const nextSource = nextState && nextState.source ? String(nextState.source) : "blank";
3269
+ const nextLabel = nextState && nextState.label ? String(nextState.label) : "blank";
3270
+ const nextPath = nextState && nextState.path ? String(nextState.path) : "";
3271
+ const nextDraftId = nextState && nextState.draftId ? String(nextState.draftId) : "";
3272
+ if (nextSource) params.set("docSource", nextSource);
3273
+ else params.delete("docSource");
3274
+ if (nextLabel) params.set("docLabel", nextLabel);
3275
+ else params.delete("docLabel");
3276
+ if (nextPath) params.set("docPath", nextPath);
3277
+ else params.delete("docPath");
3278
+ if (nextDraftId) params.set("draftId", nextDraftId);
3279
+ else params.delete("draftId");
3280
+ window.history.replaceState(null, "", currentUrl.toString());
3281
+ } catch {
3282
+ // Ignore URL-state update failures.
3283
+ }
3284
+ }
3285
+
3286
+ async function fetchStudioJson(pathname, options) {
3287
+ const init = options || {};
3288
+ const headers = new Headers(init.headers || undefined);
3289
+ const method = String(init.method || "GET").toUpperCase();
3290
+ if (init.body != null && !headers.has("Content-Type")) {
3291
+ headers.set("Content-Type", "application/json");
3292
+ }
3293
+ const response = await fetch(buildAuthedStudioUrl(pathname, init.query), {
3294
+ method,
3295
+ headers,
3296
+ body: init.body,
3297
+ cache: "no-store",
3298
+ });
3299
+ let payload = null;
3300
+ try {
3301
+ payload = await response.json();
3302
+ } catch {
3303
+ payload = null;
3304
+ }
3305
+ if (!response.ok || !payload || payload.ok === false) {
3306
+ const message = payload && typeof payload.error === "string"
3307
+ ? payload.error
3308
+ : (response.status + " " + response.statusText).trim();
3309
+ throw new Error(message || (method + " " + pathname + " failed."));
3310
+ }
3311
+ return payload;
3312
+ }
3313
+
3314
+ function trySendStudioJsonBeacon(pathname, payload, extraParams) {
3315
+ try {
3316
+ if (!navigator.sendBeacon || typeof navigator.sendBeacon !== "function") return false;
3317
+ const body = JSON.stringify(payload || {});
3318
+ const blob = new Blob([body], { type: "application/json" });
3319
+ return navigator.sendBeacon(buildAuthedStudioUrl(pathname, extraParams), blob);
3320
+ } catch {
3321
+ return false;
3322
+ }
3323
+ }
3324
+
3028
3325
  function makeRequestId() {
3029
3326
  if (window.crypto && typeof window.crypto.randomUUID === "function") {
3030
3327
  return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -3032,6 +3329,10 @@
3032
3329
  return "req_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
3033
3330
  }
3034
3331
 
3332
+ function makeStudioDraftId() {
3333
+ return "draft_" + makeRequestId();
3334
+ }
3335
+
3035
3336
  function escapeHtml(text) {
3036
3337
  return text
3037
3338
  .replace(/&/g, "&amp;")
@@ -3549,6 +3850,9 @@
3549
3850
  sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
3550
3851
  sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
3551
3852
  }
3853
+ if (reviewNoteGutterEl) {
3854
+ reviewNoteGutterEl.scrollTop = sourceTextEl.scrollTop;
3855
+ }
3552
3856
  if (lineNumberGutterEl) {
3553
3857
  lineNumberGutterEl.scrollTop = sourceTextEl.scrollTop;
3554
3858
  }
@@ -3628,187 +3932,2081 @@
3628
3932
  persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
3629
3933
  }
3630
3934
 
3631
- function readStoredText(storageKey) {
3632
- if (!window.localStorage) return null;
3633
- try {
3634
- const value = window.localStorage.getItem(storageKey);
3635
- return typeof value === "string" ? value : null;
3636
- } catch {
3637
- return null;
3638
- }
3639
- }
3640
-
3641
- function persistStoredText(storageKey, value) {
3642
- if (!window.localStorage) return;
3643
- try {
3644
- window.localStorage.setItem(storageKey, String(value ?? ""));
3645
- } catch {
3646
- // ignore storage failures
3647
- }
3648
- }
3649
-
3650
3935
  function isScratchpadOpen() {
3651
3936
  return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
3652
3937
  }
3653
3938
 
3654
- function readStoredScratchpadText() {
3655
- return readStoredText(SCRATCHPAD_STORAGE_KEY);
3939
+ function isReviewNotesOpen() {
3940
+ return Boolean(reviewNotesOverlayEl && !reviewNotesOverlayEl.hidden);
3656
3941
  }
3657
3942
 
3658
- function persistScratchpadText(value) {
3659
- persistStoredText(SCRATCHPAD_STORAGE_KEY, value);
3943
+ function syncModalOpenState() {
3944
+ document.body.classList.toggle("scratchpad-open", isScratchpadOpen());
3660
3945
  }
3661
3946
 
3662
- function updateScratchpadUi() {
3663
- const normalized = String(scratchpadText || "");
3664
- const hasContent = Boolean(normalized.trim());
3665
- if (scratchpadBtn) {
3666
- scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
3667
- scratchpadBtn.classList.toggle("has-content", hasContent);
3668
- scratchpadBtn.title = hasContent
3669
- ? "Open your local persistent scratchpad. Current notes persist after closing until you edit or clear them."
3670
- : "Open a local persistent scratchpad for quick notes. Anything you type will persist after closing until you edit or clear it.";
3947
+ function describeStudioDocument(state) {
3948
+ const currentState = state && typeof state === "object" ? state : sourceState;
3949
+ const source = currentState && currentState.source ? String(currentState.source) : "blank";
3950
+ const label = currentState && currentState.label ? String(currentState.label) : "blank";
3951
+ const path = currentState && currentState.path ? String(currentState.path) : "";
3952
+ const draftId = currentState && currentState.draftId ? String(currentState.draftId) : "";
3953
+ if (path) {
3954
+ return {
3955
+ key: "file:" + path,
3956
+ label: path,
3957
+ fileBacked: true,
3958
+ draftBacked: false,
3959
+ };
3671
3960
  }
3672
- if (scratchpadMetaEl) {
3673
- scratchpadMetaEl.textContent = hasContent
3674
- ? "Saved locally · persists after close · " + normalized.length + " chars"
3675
- : "Empty · local only";
3961
+ const normalizedLabel = label.trim().replace(/\s+/g, " ") || source;
3962
+ if (draftId) {
3963
+ return {
3964
+ key: "draft:" + draftId,
3965
+ label: normalizedLabel,
3966
+ fileBacked: false,
3967
+ draftBacked: true,
3968
+ };
3676
3969
  }
3677
- if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
3678
- if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
3679
- if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
3970
+ return {
3971
+ key: "doc:" + source + ":" + normalizedLabel,
3972
+ label: normalizedLabel,
3973
+ fileBacked: false,
3974
+ draftBacked: false,
3975
+ };
3680
3976
  }
3681
3977
 
3682
- function setScratchpadText(nextText, options) {
3683
- scratchpadText = String(nextText || "");
3684
- if (scratchpadTextEl && scratchpadTextEl.value !== scratchpadText) {
3685
- scratchpadTextEl.value = scratchpadText;
3686
- }
3687
- if (!options || options.persist !== false) {
3688
- persistScratchpadText(scratchpadText);
3689
- }
3690
- updateScratchpadUi();
3978
+ function getCurrentStudioDocumentDescriptor() {
3979
+ return describeStudioDocument(sourceState);
3691
3980
  }
3692
3981
 
3693
- function closeScratchpad(options) {
3694
- if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
3695
- scratchpadOverlayEl.hidden = true;
3696
- document.body.classList.remove("scratchpad-open");
3697
- const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
3698
- ? options.focusTarget
3699
- : (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
3700
- scratchpadReturnFocusEl = null;
3701
- if (focusTarget && typeof focusTarget.focus === "function") {
3702
- const schedule = typeof window.requestAnimationFrame === "function"
3703
- ? window.requestAnimationFrame.bind(window)
3704
- : (cb) => window.setTimeout(cb, 16);
3705
- schedule(() => focusTarget.focus());
3982
+ async function fetchScratchpadTextForDocumentKey(documentKey) {
3983
+ const payload = await fetchStudioJson("/scratchpad-state", {
3984
+ query: { documentKey: documentKey },
3985
+ });
3986
+ return payload && typeof payload.text === "string" ? payload.text : "";
3987
+ }
3988
+
3989
+ function flushScratchpadPersistence(documentKeyOverride, textOverride) {
3990
+ const descriptor = documentKeyOverride
3991
+ ? { key: String(documentKeyOverride || "").trim() }
3992
+ : getCurrentStudioDocumentDescriptor();
3993
+ const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
3994
+ if (!key) return;
3995
+ if (scratchpadPersistTimer !== null) {
3996
+ window.clearTimeout(scratchpadPersistTimer);
3997
+ scratchpadPersistTimer = null;
3998
+ }
3999
+ const snapshot = String(arguments.length >= 2 ? textOverride : scratchpadText || "");
4000
+ if (trySendStudioJsonBeacon("/scratchpad-state", { documentKey: key, text: snapshot })) {
4001
+ return;
3706
4002
  }
4003
+ void fetchStudioJson("/scratchpad-state", {
4004
+ method: "POST",
4005
+ body: JSON.stringify({ documentKey: key, text: snapshot }),
4006
+ }).catch(() => {
4007
+ // Ignore scratchpad persistence failures for now.
4008
+ });
3707
4009
  }
3708
4010
 
3709
- function openScratchpad() {
3710
- if (!scratchpadOverlayEl) return;
3711
- scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
3712
- ? document.activeElement
3713
- : sourceTextEl;
3714
- scratchpadOverlayEl.hidden = false;
3715
- document.body.classList.add("scratchpad-open");
3716
- if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
3717
- const schedule = typeof window.requestAnimationFrame === "function"
3718
- ? window.requestAnimationFrame.bind(window)
3719
- : (cb) => window.setTimeout(cb, 16);
3720
- schedule(() => {
3721
- scratchpadTextEl.focus();
3722
- if (typeof scratchpadTextEl.selectionStart === "number") {
3723
- const end = scratchpadTextEl.value.length;
3724
- scratchpadTextEl.setSelectionRange(end, end);
3725
- }
3726
- });
4011
+ function scheduleScratchpadPersistence(text, documentKey) {
4012
+ if (scratchpadPersistTimer !== null) {
4013
+ window.clearTimeout(scratchpadPersistTimer);
3727
4014
  }
4015
+ const snapshot = String(text || "");
4016
+ const key = String(documentKey || "").trim();
4017
+ if (!key) return;
4018
+ scratchpadPersistTimer = window.setTimeout(() => {
4019
+ scratchpadPersistTimer = null;
4020
+ flushScratchpadPersistence(key, snapshot);
4021
+ }, 180);
3728
4022
  }
3729
4023
 
3730
- function insertScratchpadIntoEditor() {
3731
- const content = String(scratchpadText || "");
3732
- if (!content.trim()) {
3733
- setStatus("Scratchpad is empty.", "warning");
4024
+ async function loadScratchpadForDocumentKey(documentKey) {
4025
+ const key = String(documentKey || "").trim();
4026
+ const loadNonce = ++scratchpadLoadNonce;
4027
+ if (!key) {
4028
+ setScratchpadText("", { persist: false });
3734
4029
  return;
3735
4030
  }
3736
-
3737
- const current = sourceTextEl.value || "";
3738
- const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : current.length;
3739
- const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
3740
- const safeStart = Math.max(0, Math.min(start, current.length));
3741
- const safeEnd = Math.max(safeStart, Math.min(end, current.length));
3742
- const next = current.slice(0, safeStart) + content + current.slice(safeEnd);
3743
- setEditorText(next, { preserveScroll: false, preserveSelection: false });
3744
- const caret = safeStart + content.length;
3745
- sourceTextEl.setSelectionRange(caret, caret);
3746
- setActivePane("left");
3747
- closeScratchpad({ focusTarget: sourceTextEl });
3748
- setStatus("Inserted scratchpad into editor.", "success");
4031
+ try {
4032
+ const serverText = await fetchScratchpadTextForDocumentKey(key);
4033
+ if (loadNonce !== scratchpadLoadNonce) return;
4034
+ if (key !== getCurrentStudioDocumentDescriptor().key) return;
4035
+ setScratchpadText(serverText, { persist: false });
4036
+ } catch {
4037
+ if (loadNonce !== scratchpadLoadNonce) return;
4038
+ if (key !== getCurrentStudioDocumentDescriptor().key) return;
4039
+ setScratchpadText("", { persist: false });
4040
+ }
3749
4041
  }
3750
4042
 
3751
- function updateEditorHighlightState() {
3752
- const enabled = editorHighlightEnabled && editorView === "markdown";
3753
-
3754
- sourceTextEl.classList.toggle("highlight-active", enabled);
3755
-
3756
- if (sourceHighlightEl) {
3757
- sourceHighlightEl.hidden = !enabled;
4043
+ async function maybeCarryScratchpadToNewDocument(previousDescriptor, nextDescriptor) {
4044
+ if (!previousDescriptor || !nextDescriptor || previousDescriptor.key === nextDescriptor.key) return;
4045
+ const snapshot = String(scratchpadText || "");
4046
+ if (!snapshot.trim()) return;
4047
+ try {
4048
+ const existing = await fetchScratchpadTextForDocumentKey(nextDescriptor.key);
4049
+ if (String(existing || "").trim()) return;
4050
+ await fetchStudioJson("/scratchpad-state", {
4051
+ method: "POST",
4052
+ body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot }),
4053
+ });
4054
+ } catch {
4055
+ // Ignore carry-over failures and just fall back to normal scope loading.
3758
4056
  }
4057
+ }
3759
4058
 
3760
- if (!enabled) {
3761
- if (editorHighlightRenderRaf !== null) {
3762
- if (typeof window.cancelAnimationFrame === "function") {
3763
- window.cancelAnimationFrame(editorHighlightRenderRaf);
3764
- } else {
3765
- window.clearTimeout(editorHighlightRenderRaf);
3766
- }
3767
- editorHighlightRenderRaf = null;
4059
+ function loadScratchpadForCurrentDocument(options) {
4060
+ const previousDescriptor = options && options.previousDescriptor ? options.previousDescriptor : null;
4061
+ const shouldCarryToNewDocument = Boolean(options && options.carryCurrentMetadataToNewDocument);
4062
+ const currentDescriptor = getCurrentStudioDocumentDescriptor();
4063
+ void (async () => {
4064
+ if (shouldCarryToNewDocument && previousDescriptor) {
4065
+ await maybeCarryScratchpadToNewDocument(previousDescriptor, currentDescriptor);
3768
4066
  }
4067
+ await loadScratchpadForDocumentKey(currentDescriptor.key);
4068
+ })();
4069
+ }
3769
4070
 
3770
- if (sourceHighlightEl) {
3771
- sourceHighlightEl.innerHTML = "";
3772
- sourceHighlightEl.scrollTop = 0;
3773
- sourceHighlightEl.scrollLeft = 0;
3774
- }
3775
- return;
3776
- }
4071
+ function persistScratchpadText(value) {
4072
+ const descriptor = getCurrentStudioDocumentDescriptor();
4073
+ scheduleScratchpadPersistence(value, descriptor.key);
4074
+ }
3777
4075
 
3778
- scheduleEditorHighlightRender();
3779
- syncEditorHighlightScroll();
4076
+ function normalizeReviewNote(note) {
4077
+ if (!note || typeof note !== "object") return null;
4078
+ const id = typeof note.id === "string" && note.id.trim() ? note.id : makeRequestId();
4079
+ const text = typeof note.text === "string" ? note.text : "";
4080
+ const createdAt = typeof note.createdAt === "number" && Number.isFinite(note.createdAt)
4081
+ ? note.createdAt
4082
+ : Date.now();
4083
+ const updatedAt = typeof note.updatedAt === "number" && Number.isFinite(note.updatedAt)
4084
+ ? note.updatedAt
4085
+ : createdAt;
4086
+ const selectionStart = typeof note.selectionStart === "number" && Number.isFinite(note.selectionStart)
4087
+ ? Math.max(0, Math.floor(note.selectionStart))
4088
+ : 0;
4089
+ const selectionEnd = typeof note.selectionEnd === "number" && Number.isFinite(note.selectionEnd)
4090
+ ? Math.max(selectionStart, Math.floor(note.selectionEnd))
4091
+ : selectionStart;
4092
+ const lineStart = typeof note.lineStart === "number" && Number.isFinite(note.lineStart)
4093
+ ? Math.max(1, Math.floor(note.lineStart))
4094
+ : 1;
4095
+ const lineEnd = typeof note.lineEnd === "number" && Number.isFinite(note.lineEnd)
4096
+ ? Math.max(lineStart, Math.floor(note.lineEnd))
4097
+ : lineStart;
4098
+ return {
4099
+ id,
4100
+ text,
4101
+ createdAt,
4102
+ updatedAt,
4103
+ selectionStart,
4104
+ selectionEnd,
4105
+ lineStart,
4106
+ lineEnd,
4107
+ selectedText: typeof note.selectedText === "string" ? note.selectedText : "",
4108
+ selectedDisplayText: typeof note.selectedDisplayText === "string" ? note.selectedDisplayText : "",
4109
+ };
3780
4110
  }
3781
4111
 
3782
- function syncHighlightSelectUi() {
3783
- if (!highlightSelect) return;
3784
- if (!editorHighlightEnabled) {
3785
- highlightSelect.value = "off";
4112
+ function cloneReviewNotes(notes) {
4113
+ return Array.isArray(notes)
4114
+ ? notes
4115
+ .map((note) => normalizeReviewNote(note))
4116
+ .filter(Boolean)
4117
+ .map((note) => ({ ...note }))
4118
+ : [];
4119
+ }
4120
+
4121
+ async function fetchReviewNotesForDocumentKey(documentKey) {
4122
+ const payload = await fetchStudioJson("/review-notes", {
4123
+ query: { documentKey: documentKey },
4124
+ });
4125
+ return cloneReviewNotes(payload && Array.isArray(payload.notes) ? payload.notes : []);
4126
+ }
4127
+
4128
+ function flushReviewNotesPersistence(documentKeyOverride, notesOverride) {
4129
+ const descriptor = documentKeyOverride
4130
+ ? { key: String(documentKeyOverride || "").trim() }
4131
+ : getCurrentStudioDocumentDescriptor();
4132
+ const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
4133
+ if (!key) return;
4134
+ if (reviewNotesPersistTimer !== null) {
4135
+ window.clearTimeout(reviewNotesPersistTimer);
4136
+ reviewNotesPersistTimer = null;
4137
+ }
4138
+ const snapshot = cloneReviewNotes(arguments.length >= 2 ? notesOverride : reviewNotes);
4139
+ if (trySendStudioJsonBeacon("/review-notes", { documentKey: key, notes: snapshot })) {
3786
4140
  return;
3787
4141
  }
3788
- highlightSelect.value = (editorLanguage && SUPPORTED_LANGUAGES.indexOf(editorLanguage) !== -1)
3789
- ? editorLanguage
3790
- : "markdown";
4142
+ void fetchStudioJson("/review-notes", {
4143
+ method: "POST",
4144
+ body: JSON.stringify({ documentKey: key, notes: snapshot }),
4145
+ }).catch(() => {
4146
+ // Ignore persistence failures; the in-memory notes list remains available for this session.
4147
+ });
3791
4148
  }
3792
4149
 
3793
- function setEditorHighlightEnabled(enabled) {
3794
- editorHighlightEnabled = Boolean(enabled);
3795
- persistEditorHighlightEnabled(editorHighlightEnabled);
3796
- syncHighlightSelectUi();
3797
- updateEditorHighlightState();
4150
+ function scheduleReviewNotesPersistence() {
4151
+ if (reviewNotesPersistTimer !== null) {
4152
+ window.clearTimeout(reviewNotesPersistTimer);
4153
+ }
4154
+ const descriptor = getCurrentStudioDocumentDescriptor();
4155
+ const snapshot = cloneReviewNotes(reviewNotes);
4156
+ reviewNotesPersistTimer = window.setTimeout(() => {
4157
+ reviewNotesPersistTimer = null;
4158
+ flushReviewNotesPersistence(descriptor.key, snapshot);
4159
+ }, 180);
3798
4160
  }
3799
4161
 
3800
- function readStoredEditorLanguage() {
3801
- if (!window.localStorage) return null;
4162
+ async function maybeCarryReviewNotesToNewDocument(previousDescriptor, nextDescriptor) {
4163
+ if (!previousDescriptor || !nextDescriptor || previousDescriptor.key === nextDescriptor.key) return;
4164
+ const snapshot = cloneReviewNotes(reviewNotes);
4165
+ if (!snapshot.length) return;
3802
4166
  try {
3803
- const value = window.localStorage.getItem(EDITOR_LANGUAGE_STORAGE_KEY);
3804
- if (value && SUPPORTED_LANGUAGES.indexOf(value) !== -1) return value;
3805
- return null;
4167
+ const existing = await fetchReviewNotesForDocumentKey(nextDescriptor.key);
4168
+ if (existing.length > 0) return;
4169
+ await fetchStudioJson("/review-notes", {
4170
+ method: "POST",
4171
+ body: JSON.stringify({ documentKey: nextDescriptor.key, notes: snapshot }),
4172
+ });
3806
4173
  } catch {
3807
- return null;
4174
+ // Ignore carry-over failures and just fall back to normal scope loading.
3808
4175
  }
3809
4176
  }
3810
4177
 
3811
- function persistEditorLanguage(lang) {
4178
+ async function loadReviewNotesForCurrentDocument(options) {
4179
+ const descriptor = getCurrentStudioDocumentDescriptor();
4180
+ const previousDescriptor = options && options.previousDescriptor ? options.previousDescriptor : null;
4181
+ const shouldCarryToNewDocument = Boolean(options && options.carryCurrentMetadataToNewDocument);
4182
+ const loadNonce = ++reviewNotesLoadNonce;
4183
+ try {
4184
+ if (shouldCarryToNewDocument && previousDescriptor) {
4185
+ await maybeCarryReviewNotesToNewDocument(previousDescriptor, descriptor);
4186
+ }
4187
+ const notes = await fetchReviewNotesForDocumentKey(descriptor.key);
4188
+ if (loadNonce !== reviewNotesLoadNonce) return;
4189
+ if (descriptor.key !== getCurrentStudioDocumentDescriptor().key) return;
4190
+ reviewNotes = notes;
4191
+ } catch {
4192
+ if (loadNonce !== reviewNotesLoadNonce) return;
4193
+ if (descriptor.key !== getCurrentStudioDocumentDescriptor().key) return;
4194
+ reviewNotes = [];
4195
+ }
4196
+ updateReviewNotesUi();
4197
+ renderReviewNotesList();
4198
+ refreshRenderedEditorPreviewComments();
4199
+ if (editorView === "markdown") {
4200
+ scheduleEditorLineNumberRender();
4201
+ }
4202
+ }
4203
+
4204
+ function formatReviewNoteTimestamp(timestamp) {
4205
+ if (!Number.isFinite(timestamp)) return "Saved locally";
4206
+ try {
4207
+ return "Updated " + new Date(timestamp).toLocaleString();
4208
+ } catch {
4209
+ return "Saved locally";
4210
+ }
4211
+ }
4212
+
4213
+ function summarizeReviewNoteAnchor(note) {
4214
+ const start = Math.max(1, Number(note && note.lineStart) || 1);
4215
+ const end = Math.max(start, Number(note && note.lineEnd) || start);
4216
+ return start === end ? "Line " + start : ("Lines " + start + "–" + end);
4217
+ }
4218
+
4219
+ function summarizeReviewNoteQuote(note) {
4220
+ const normalized = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
4221
+ .replace(/\s+/g, " ")
4222
+ .trim();
4223
+ if (!normalized) return "Anchor: current line / empty selection";
4224
+ return normalized.length > 140 ? normalized.slice(0, 137) + "…" : normalized;
4225
+ }
4226
+
4227
+ function getLineNumberAtOffset(text, offset) {
4228
+ const source = String(text || "");
4229
+ const safeOffset = Math.max(0, Math.min(Number(offset) || 0, source.length));
4230
+ let line = 1;
4231
+ for (let i = 0; i < safeOffset; i += 1) {
4232
+ if (source[i] === "\n") line += 1;
4233
+ }
4234
+ return line;
4235
+ }
4236
+
4237
+ function getLineRangeAtOffset(text, offset) {
4238
+ const source = String(text || "");
4239
+ const safeOffset = Math.max(0, Math.min(Number(offset) || 0, source.length));
4240
+ let start = safeOffset;
4241
+ while (start > 0 && source[start - 1] !== "\n") start -= 1;
4242
+ let end = safeOffset;
4243
+ while (end < source.length && source[end] !== "\n") end += 1;
4244
+ return {
4245
+ start,
4246
+ end,
4247
+ lineNumber: getLineNumberAtOffset(source, safeOffset),
4248
+ };
4249
+ }
4250
+
4251
+ function getLineRangeForNumbers(text, lineStart, lineEnd) {
4252
+ const lines = String(text || "").split("\n");
4253
+ const safeLineStart = Math.max(1, Math.min(Math.floor(lineStart || 1), Math.max(1, lines.length)));
4254
+ const safeLineEnd = Math.max(safeLineStart, Math.min(Math.floor(lineEnd || safeLineStart), Math.max(1, lines.length)));
4255
+ let start = 0;
4256
+ for (let i = 0; i < safeLineStart - 1; i += 1) {
4257
+ start += lines[i].length + 1;
4258
+ }
4259
+ let end = start;
4260
+ for (let i = safeLineStart - 1; i < safeLineEnd; i += 1) {
4261
+ end += lines[i].length;
4262
+ if (i < safeLineEnd - 1) end += 1;
4263
+ }
4264
+ return { start, end };
4265
+ }
4266
+
4267
+ function getEditorAnchorForReviewNote() {
4268
+ const current = String(sourceTextEl.value || "");
4269
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
4270
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
4271
+ const safeStart = Math.max(0, Math.min(start, current.length));
4272
+ const safeEnd = Math.max(safeStart, Math.min(end, current.length));
4273
+ if (safeStart !== safeEnd) {
4274
+ return {
4275
+ selectionStart: safeStart,
4276
+ selectionEnd: safeEnd,
4277
+ lineStart: getLineNumberAtOffset(current, safeStart),
4278
+ lineEnd: getLineNumberAtOffset(current, Math.max(safeStart, safeEnd - 1)),
4279
+ selectedText: current.slice(safeStart, safeEnd),
4280
+ selectedDisplayText: current.slice(safeStart, safeEnd),
4281
+ };
4282
+ }
4283
+ const lineRange = getLineRangeAtOffset(current, safeStart);
4284
+ return {
4285
+ selectionStart: lineRange.start,
4286
+ selectionEnd: lineRange.end,
4287
+ lineStart: lineRange.lineNumber,
4288
+ lineEnd: lineRange.lineNumber,
4289
+ selectedText: current.slice(lineRange.start, lineRange.end),
4290
+ selectedDisplayText: current.slice(lineRange.start, lineRange.end),
4291
+ };
4292
+ }
4293
+
4294
+ function getEditorLineAnchorForReviewNote() {
4295
+ const current = String(sourceTextEl.value || "");
4296
+ const caret = typeof sourceTextEl.selectionStart === "number"
4297
+ ? sourceTextEl.selectionStart
4298
+ : 0;
4299
+ const lineRange = getLineRangeAtOffset(current, Math.max(0, Math.min(caret, current.length)));
4300
+ return {
4301
+ selectionStart: lineRange.start,
4302
+ selectionEnd: lineRange.end,
4303
+ lineStart: lineRange.lineNumber,
4304
+ lineEnd: lineRange.lineNumber,
4305
+ selectedText: current.slice(lineRange.start, lineRange.end),
4306
+ selectedDisplayText: current.slice(lineRange.start, lineRange.end),
4307
+ };
4308
+ }
4309
+
4310
+ function resolveReviewNoteRange(note, text) {
4311
+ const source = String(text || "");
4312
+ const safeStart = Math.max(0, Math.min(Number(note && note.selectionStart) || 0, source.length));
4313
+ const safeEnd = Math.max(safeStart, Math.min(Number(note && note.selectionEnd) || safeStart, source.length));
4314
+ const selectedText = String(note && note.selectedText ? note.selectedText : "");
4315
+ if (selectedText && source.slice(safeStart, safeEnd) === selectedText) {
4316
+ return { start: safeStart, end: safeEnd };
4317
+ }
4318
+ if (!selectedText && safeEnd >= safeStart) {
4319
+ return { start: safeStart, end: safeEnd };
4320
+ }
4321
+ if (selectedText) {
4322
+ const foundIndex = source.indexOf(selectedText);
4323
+ if (foundIndex >= 0) {
4324
+ return { start: foundIndex, end: foundIndex + selectedText.length };
4325
+ }
4326
+ }
4327
+ return getLineRangeForNumbers(source, note && note.lineStart, note && note.lineEnd);
4328
+ }
4329
+
4330
+ function getResolvedReviewNoteLineBounds(note, text) {
4331
+ const source = String(text || "");
4332
+ const range = resolveReviewNoteRange(note, source);
4333
+ if (!range) return null;
4334
+ const startLine = getLineNumberAtOffset(source, range.start);
4335
+ const endLookupOffset = range.end > range.start ? range.end - 1 : range.start;
4336
+ const endLine = getLineNumberAtOffset(source, endLookupOffset);
4337
+ return {
4338
+ start: range.start,
4339
+ end: range.end,
4340
+ lineStart: startLine,
4341
+ lineEnd: Math.max(startLine, endLine),
4342
+ };
4343
+ }
4344
+
4345
+ function buildReviewNoteLineMap(text) {
4346
+ const source = String(text || "");
4347
+ const lineMap = new Map();
4348
+ for (const note of reviewNotes) {
4349
+ const bounds = getResolvedReviewNoteLineBounds(note, source);
4350
+ if (!bounds) continue;
4351
+ for (let line = bounds.lineStart; line <= bounds.lineEnd; line += 1) {
4352
+ const notesForLine = lineMap.get(line) || [];
4353
+ notesForLine.push(note);
4354
+ lineMap.set(line, notesForLine);
4355
+ }
4356
+ }
4357
+ return lineMap;
4358
+ }
4359
+
4360
+ function supportsPreviewCommentsForCurrentEditor() {
4361
+ return editorLanguage === "markdown";
4362
+ }
4363
+
4364
+ function getPreviewCommentBlockKindLabel(kind) {
4365
+ if (kind === "heading") return "heading";
4366
+ if (kind === "blockquote") return "quote block";
4367
+ if (kind === "list") return "list";
4368
+ if (kind === "code") return "code block";
4369
+ if (kind === "table") return "table";
4370
+ return "paragraph";
4371
+ }
4372
+
4373
+ function supportsPreviewSelectionCommentsForBlockKind(kind) {
4374
+ return kind === "paragraph" || kind === "heading" || kind === "blockquote" || kind === "list";
4375
+ }
4376
+
4377
+ function normalizeVisiblePreviewText(text) {
4378
+ return String(text || "").replace(/\s+/g, " ").trim();
4379
+ }
4380
+
4381
+ function appendMappedPreviewSlice(chars, rawOffsets, lineText, lineBaseOffset, start, end) {
4382
+ const safeStart = Math.max(0, Math.min(start, lineText.length));
4383
+ const safeEnd = Math.max(safeStart, Math.min(end, lineText.length));
4384
+ for (let i = safeStart; i < safeEnd; i += 1) {
4385
+ chars.push(lineText[i]);
4386
+ rawOffsets.push(lineBaseOffset + i);
4387
+ }
4388
+ }
4389
+
4390
+ function buildPreviewSelectionSourceBody(blockText, kind) {
4391
+ const source = String(blockText || "");
4392
+ const lines = source.split("\n");
4393
+ const lineOffsets = [];
4394
+ let runningOffset = 0;
4395
+ for (const line of lines) {
4396
+ lineOffsets.push(runningOffset);
4397
+ runningOffset += line.length + 1;
4398
+ }
4399
+
4400
+ const chars = [];
4401
+ const rawOffsets = [];
4402
+
4403
+ function appendLineWithStart(lineIndex, start, end) {
4404
+ const line = lineIndex >= 0 && lineIndex < lines.length ? lines[lineIndex] : "";
4405
+ appendMappedPreviewSlice(chars, rawOffsets, line, lineOffsets[lineIndex] || 0, start, end);
4406
+ if (lineIndex < lines.length - 1) {
4407
+ chars.push("\n");
4408
+ rawOffsets.push((lineOffsets[lineIndex] || 0) + line.length);
4409
+ }
4410
+ }
4411
+
4412
+ if (kind === "heading") {
4413
+ const firstLine = lines[0] || "";
4414
+ const atxMatch = firstLine.match(/^ {0,3}#{1,6}(?:[ \t]+|$)/);
4415
+ if (atxMatch) {
4416
+ const start = atxMatch[0].length;
4417
+ let end = firstLine.length;
4418
+ const closingMatch = firstLine.slice(start).match(/[ \t]+#+[ \t]*$/);
4419
+ if (closingMatch) {
4420
+ end -= closingMatch[0].length;
4421
+ }
4422
+ appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, start, end);
4423
+ return { text: chars.join(""), rawOffsets };
4424
+ }
4425
+ if (lines.length >= 2 && /^ {0,3}(?:={3,}|-{3,})\s*$/.test(lines[1] || "")) {
4426
+ appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, 0, firstLine.length);
4427
+ return { text: chars.join(""), rawOffsets };
4428
+ }
4429
+ }
4430
+
4431
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4432
+ const line = lines[lineIndex] || "";
4433
+ if (kind === "blockquote") {
4434
+ const prefixMatch = line.match(/^ {0,3}> ?/);
4435
+ appendLineWithStart(lineIndex, prefixMatch ? prefixMatch[0].length : 0, line.length);
4436
+ continue;
4437
+ }
4438
+ if (kind === "list") {
4439
+ if (!line.trim()) {
4440
+ appendLineWithStart(lineIndex, 0, 0);
4441
+ continue;
4442
+ }
4443
+ const itemMatch = line.match(/^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/);
4444
+ if (itemMatch) {
4445
+ appendLineWithStart(lineIndex, itemMatch[0].length, line.length);
4446
+ continue;
4447
+ }
4448
+ const continuationMatch = line.match(/^(?: {1,4}|\t)/);
4449
+ appendLineWithStart(lineIndex, continuationMatch ? continuationMatch[0].length : 0, line.length);
4450
+ continue;
4451
+ }
4452
+ appendLineWithStart(lineIndex, 0, line.length);
4453
+ }
4454
+
4455
+ return { text: chars.join(""), rawOffsets };
4456
+ }
4457
+
4458
+ function buildPreviewInlineDisplayMap(text, rawOffsets) {
4459
+ const source = String(text || "");
4460
+ const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
4461
+ const displayChars = [];
4462
+ const charStarts = [];
4463
+ const charEnds = [];
4464
+
4465
+ function appendChar(character, rawStart, rawEnd) {
4466
+ displayChars.push(character);
4467
+ charStarts.push(rawStart);
4468
+ charEnds.push(rawEnd);
4469
+ }
4470
+
4471
+ function appendNestedRange(startIndex, endIndex) {
4472
+ const nested = buildPreviewInlineDisplayMap(
4473
+ source.slice(startIndex, endIndex),
4474
+ rawMap.slice(startIndex, endIndex),
4475
+ );
4476
+ for (let i = 0; i < nested.text.length; i += 1) {
4477
+ appendChar(nested.text[i], nested.charStarts[i], nested.charEnds[i]);
4478
+ }
4479
+ }
4480
+
4481
+ let index = 0;
4482
+ while (index < source.length) {
4483
+ const remaining = source.slice(index);
4484
+ const linkMatch = remaining.match(/^!?\[([^\]]*)\]\(([^)]*)\)/);
4485
+ if (linkMatch) {
4486
+ const labelStart = index + (remaining[0] === "!" ? 2 : 1);
4487
+ const labelEnd = labelStart + String(linkMatch[1] || "").length;
4488
+ appendNestedRange(labelStart, labelEnd);
4489
+ index += linkMatch[0].length;
4490
+ continue;
4491
+ }
4492
+
4493
+ if (source[index] === "`") {
4494
+ let tickCount = 1;
4495
+ while (source[index + tickCount] === "`") tickCount += 1;
4496
+ const fence = "`".repeat(tickCount);
4497
+ const closeIndex = source.indexOf(fence, index + tickCount);
4498
+ if (closeIndex >= 0) {
4499
+ for (let i = index + tickCount; i < closeIndex; i += 1) {
4500
+ appendChar(source[i], rawMap[i], rawMap[i] + 1);
4501
+ }
4502
+ index = closeIndex + tickCount;
4503
+ continue;
4504
+ }
4505
+ }
4506
+
4507
+ if (source[index] === "\\" && index + 1 < source.length) {
4508
+ appendChar(source[index + 1], rawMap[index], rawMap[index + 1] + 1);
4509
+ index += 2;
4510
+ continue;
4511
+ }
4512
+
4513
+ const htmlTagMatch = remaining.match(/^<\/?[A-Za-z][^>]*>/);
4514
+ if (htmlTagMatch) {
4515
+ index += htmlTagMatch[0].length;
4516
+ continue;
4517
+ }
4518
+
4519
+ const emphasisMatch = remaining.match(/^(?:\*\*\*|\*\*|\*|___|__|_|~~)/);
4520
+ if (emphasisMatch) {
4521
+ index += emphasisMatch[0].length;
4522
+ continue;
4523
+ }
4524
+
4525
+ appendChar(source[index], rawMap[index], rawMap[index] + 1);
4526
+ index += 1;
4527
+ }
4528
+
4529
+ return {
4530
+ text: displayChars.join(""),
4531
+ charStarts,
4532
+ charEnds,
4533
+ };
4534
+ }
4535
+
4536
+ function buildNormalizedPreviewDisplayMap(displayText, charStarts, charEnds) {
4537
+ const source = String(displayText || "");
4538
+ const outChars = [];
4539
+ const outStarts = [];
4540
+ const outEnds = [];
4541
+ let pendingWhitespaceStart = null;
4542
+ let pendingWhitespaceEnd = null;
4543
+
4544
+ for (let i = 0; i < source.length; i += 1) {
4545
+ const character = source[i];
4546
+ if (/\s/.test(character)) {
4547
+ if (outChars.length === 0) continue;
4548
+ if (pendingWhitespaceStart == null) {
4549
+ pendingWhitespaceStart = charStarts[i];
4550
+ }
4551
+ pendingWhitespaceEnd = charEnds[i];
4552
+ continue;
4553
+ }
4554
+
4555
+ if (pendingWhitespaceStart != null && pendingWhitespaceEnd != null) {
4556
+ outChars.push(" ");
4557
+ outStarts.push(pendingWhitespaceStart);
4558
+ outEnds.push(pendingWhitespaceEnd);
4559
+ pendingWhitespaceStart = null;
4560
+ pendingWhitespaceEnd = null;
4561
+ }
4562
+
4563
+ outChars.push(character);
4564
+ outStarts.push(charStarts[i]);
4565
+ outEnds.push(charEnds[i]);
4566
+ }
4567
+
4568
+ return {
4569
+ text: outChars.join(""),
4570
+ charStarts: outStarts,
4571
+ charEnds: outEnds,
4572
+ };
4573
+ }
4574
+
4575
+ function buildNormalizedDomTextMap(rootEl) {
4576
+ if (!rootEl || typeof document.createTreeWalker !== "function") {
4577
+ return { text: "", charStarts: [], charEnds: [] };
4578
+ }
4579
+ const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT);
4580
+ const chars = [];
4581
+ const starts = [];
4582
+ const ends = [];
4583
+ let node = walker.nextNode();
4584
+ while (node) {
4585
+ const textNode = node;
4586
+ const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
4587
+ for (let i = 0; i < value.length; i += 1) {
4588
+ chars.push(value[i]);
4589
+ starts.push({ node: textNode, offset: i });
4590
+ ends.push({ node: textNode, offset: i + 1 });
4591
+ }
4592
+ node = walker.nextNode();
4593
+ }
4594
+ return buildNormalizedPreviewDisplayMap(chars.join(""), starts, ends);
4595
+ }
4596
+
4597
+ function findPreferredNormalizedTextMatch(haystack, needle, preferredIndex) {
4598
+ const source = String(haystack || "");
4599
+ const query = String(needle || "");
4600
+ if (!source || !query) return -1;
4601
+ let bestIndex = -1;
4602
+ let bestScore = Number.POSITIVE_INFINITY;
4603
+ const desiredIndex = Number.isFinite(preferredIndex) ? Math.max(0, preferredIndex) : 0;
4604
+ for (let matchIndex = source.indexOf(query); matchIndex >= 0; matchIndex = source.indexOf(query, matchIndex + 1)) {
4605
+ const score = Math.abs(matchIndex - desiredIndex);
4606
+ if (score < bestScore) {
4607
+ bestScore = score;
4608
+ bestIndex = matchIndex;
4609
+ }
4610
+ }
4611
+ return bestIndex;
4612
+ }
4613
+
4614
+ function buildPreviewSelectionDisplayMap(blockText, kind) {
4615
+ const body = buildPreviewSelectionSourceBody(blockText, kind);
4616
+ const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
4617
+ return buildNormalizedPreviewDisplayMap(inlineMap.text, inlineMap.charStarts, inlineMap.charEnds);
4618
+ }
4619
+
4620
+ function getPreviewCommentBlockKey(blockEl) {
4621
+ if (!blockEl || !blockEl.dataset) return "";
4622
+ return [
4623
+ String(blockEl.dataset.reviewNoteStart || ""),
4624
+ String(blockEl.dataset.reviewNoteEnd || ""),
4625
+ String(blockEl.dataset.previewCommentKind || ""),
4626
+ ].join(":");
4627
+ }
4628
+
4629
+ function getPreviewCommentSelectionKey(selection) {
4630
+ if (!selection) return "";
4631
+ return [
4632
+ String(selection.blockKey || ""),
4633
+ String(selection.selectionStart || 0),
4634
+ String(selection.selectionEnd || 0),
4635
+ String(selection.selectedDisplayText || ""),
4636
+ ].join(":");
4637
+ }
4638
+
4639
+ function setActivePreviewCommentSelection(nextSelection) {
4640
+ const currentKey = getPreviewCommentSelectionKey(activePreviewCommentSelection);
4641
+ const nextKey = getPreviewCommentSelectionKey(nextSelection);
4642
+ if (currentKey === nextKey) return;
4643
+ activePreviewCommentSelection = nextSelection || null;
4644
+ refreshRenderedEditorPreviewComments();
4645
+ }
4646
+
4647
+ function clearPreviewCommentSelection() {
4648
+ setActivePreviewCommentSelection(null);
4649
+ }
4650
+
4651
+ function findPreviewCommentBlockFromNode(node) {
4652
+ if (!node) return null;
4653
+ const element = node instanceof Element ? node : node.parentElement;
4654
+ return element && typeof element.closest === "function"
4655
+ ? element.closest(".preview-comment-block")
4656
+ : null;
4657
+ }
4658
+
4659
+ function unwrapPreviewJumpHighlightElement(element) {
4660
+ if (!element || !element.parentNode) return;
4661
+ const parent = element.parentNode;
4662
+ while (element.firstChild) {
4663
+ parent.insertBefore(element.firstChild, element);
4664
+ }
4665
+ parent.removeChild(element);
4666
+ if (typeof parent.normalize === "function") {
4667
+ parent.normalize();
4668
+ }
4669
+ }
4670
+
4671
+ function clearPreviewJumpHighlight(targetEl) {
4672
+ if (!targetEl) return;
4673
+ const state = previewJumpHighlightState.get(targetEl);
4674
+ if (!state) return;
4675
+ if (state.timer != null) {
4676
+ window.clearTimeout(state.timer);
4677
+ }
4678
+ if (state.inlineHighlightEl) {
4679
+ unwrapPreviewJumpHighlightElement(state.inlineHighlightEl);
4680
+ }
4681
+ if (state.contentEl && state.contentEl.classList) {
4682
+ state.contentEl.classList.remove("preview-jump-highlight");
4683
+ }
4684
+ previewJumpHighlightState.delete(targetEl);
4685
+ }
4686
+
4687
+ function setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl) {
4688
+ if (!targetEl || !contentEl) return;
4689
+ clearPreviewJumpHighlight(targetEl);
4690
+ if (contentEl.classList) {
4691
+ contentEl.classList.add("preview-jump-highlight");
4692
+ }
4693
+ const timer = window.setTimeout(() => {
4694
+ clearPreviewJumpHighlight(targetEl);
4695
+ }, 1800);
4696
+ previewJumpHighlightState.set(targetEl, {
4697
+ contentEl,
4698
+ inlineHighlightEl: inlineHighlightEl || null,
4699
+ timer,
4700
+ });
4701
+ }
4702
+
4703
+ function rangesOverlap(startA, endA, startB, endB) {
4704
+ const safeStartA = Math.max(0, Number(startA) || 0);
4705
+ const safeStartB = Math.max(0, Number(startB) || 0);
4706
+ const safeEndA = Math.max(safeStartA + 1, Number(endA) || safeStartA);
4707
+ const safeEndB = Math.max(safeStartB + 1, Number(endB) || safeStartB);
4708
+ return safeStartA < safeEndB && safeStartB < safeEndA;
4709
+ }
4710
+
4711
+ function scanMarkdownPreviewCommentBlocks(markdown) {
4712
+ const source = String(markdown || "").replace(/\r\n/g, "\n");
4713
+ const lines = source.split("\n");
4714
+ const lineOffsets = [];
4715
+ let runningOffset = 0;
4716
+ for (const line of lines) {
4717
+ lineOffsets.push(runningOffset);
4718
+ runningOffset += line.length + 1;
4719
+ }
4720
+
4721
+ function getLine(index) {
4722
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
4723
+ }
4724
+
4725
+ function isBlankLine(index) {
4726
+ return /^\s*$/.test(getLine(index));
4727
+ }
4728
+
4729
+ function lineStartsFence(index) {
4730
+ return getLine(index).match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
4731
+ }
4732
+
4733
+ function isAtxHeadingLine(index) {
4734
+ return /^ {0,3}#{1,6}(?:[ \t]+|$)/.test(getLine(index));
4735
+ }
4736
+
4737
+ function isSetextUnderlineLine(index) {
4738
+ return /^ {0,3}(?:={3,}|-{3,})\s*$/.test(getLine(index));
4739
+ }
4740
+
4741
+ function isThematicBreakLine(index) {
4742
+ return /^ {0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/.test(getLine(index));
4743
+ }
4744
+
4745
+ function isBlockquoteLine(index) {
4746
+ return /^ {0,3}> ?/.test(getLine(index));
4747
+ }
4748
+
4749
+ function isListLine(index) {
4750
+ return /^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/.test(getLine(index));
4751
+ }
4752
+
4753
+ function isContinuationIndentedLine(index) {
4754
+ return /^(?: {2,}|\t+)/.test(getLine(index));
4755
+ }
4756
+
4757
+ function isPotentialTableRow(index) {
4758
+ const line = getLine(index);
4759
+ return /\|/.test(line) && !/^\s*</.test(line);
4760
+ }
4761
+
4762
+ function isTableDividerLine(index) {
4763
+ return /^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/.test(getLine(index));
4764
+ }
4765
+
4766
+ function isHtmlCommentStart(index) {
4767
+ return /^\s*<!--/.test(getLine(index));
4768
+ }
4769
+
4770
+ function makeBlock(kind, startLineIndex, endLineIndex) {
4771
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
4772
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
4773
+ const start = lineOffsets[safeStartLine] || 0;
4774
+ const end = (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length;
4775
+ return {
4776
+ kind,
4777
+ start,
4778
+ end,
4779
+ lineStart: safeStartLine + 1,
4780
+ lineEnd: safeEndLine + 1,
4781
+ };
4782
+ }
4783
+
4784
+ const blocks = [];
4785
+ let index = 0;
4786
+
4787
+ if (/^\s*---\s*$/.test(getLine(0))) {
4788
+ for (let i = 1; i < Math.min(lines.length, 80); i += 1) {
4789
+ if (/^\s*(?:---|\.\.\.)\s*$/.test(getLine(i))) {
4790
+ index = i + 1;
4791
+ break;
4792
+ }
4793
+ }
4794
+ }
4795
+
4796
+ while (index < lines.length) {
4797
+ if (isBlankLine(index)) {
4798
+ index += 1;
4799
+ continue;
4800
+ }
4801
+
4802
+ if (isHtmlCommentStart(index)) {
4803
+ let endComment = index;
4804
+ while (endComment < lines.length && getLine(endComment).indexOf("-->") === -1) {
4805
+ endComment += 1;
4806
+ }
4807
+ index = Math.min(lines.length, endComment + 1);
4808
+ continue;
4809
+ }
4810
+
4811
+ if (isThematicBreakLine(index)) {
4812
+ index += 1;
4813
+ continue;
4814
+ }
4815
+
4816
+ const fenceMatch = lineStartsFence(index);
4817
+ if (fenceMatch) {
4818
+ const marker = fenceMatch[1] || "";
4819
+ const markerChar = marker[0] || "`";
4820
+ const markerLength = marker.length;
4821
+ let endFence = index;
4822
+ for (let i = index + 1; i < lines.length; i += 1) {
4823
+ const closingMatch = getLine(i).match(/^ {0,3}(`{3,}|~{3,})\s*$/);
4824
+ if (closingMatch && closingMatch[1] && closingMatch[1][0] === markerChar && closingMatch[1].length >= markerLength) {
4825
+ endFence = i;
4826
+ break;
4827
+ }
4828
+ endFence = i;
4829
+ }
4830
+ blocks.push(makeBlock("code", index, endFence));
4831
+ index = endFence + 1;
4832
+ continue;
4833
+ }
4834
+
4835
+ if (isAtxHeadingLine(index)) {
4836
+ blocks.push(makeBlock("heading", index, index));
4837
+ index += 1;
4838
+ continue;
4839
+ }
4840
+
4841
+ if (!isBlankLine(index) && index + 1 < lines.length && isSetextUnderlineLine(index + 1)) {
4842
+ blocks.push(makeBlock("heading", index, index + 1));
4843
+ index += 2;
4844
+ continue;
4845
+ }
4846
+
4847
+ if (isPotentialTableRow(index) && index + 1 < lines.length && isTableDividerLine(index + 1)) {
4848
+ let endTable = index + 1;
4849
+ for (let i = index + 2; i < lines.length; i += 1) {
4850
+ if (isBlankLine(i) || !isPotentialTableRow(i)) break;
4851
+ endTable = i;
4852
+ }
4853
+ blocks.push(makeBlock("table", index, endTable));
4854
+ index = endTable + 1;
4855
+ continue;
4856
+ }
4857
+
4858
+ if (isBlockquoteLine(index)) {
4859
+ let endQuote = index;
4860
+ for (let i = index + 1; i < lines.length; i += 1) {
4861
+ if (isBlockquoteLine(i)) {
4862
+ endQuote = i;
4863
+ continue;
4864
+ }
4865
+ if (isBlankLine(i) && i + 1 < lines.length && isBlockquoteLine(i + 1)) {
4866
+ endQuote = i;
4867
+ continue;
4868
+ }
4869
+ break;
4870
+ }
4871
+ blocks.push(makeBlock("blockquote", index, endQuote));
4872
+ index = endQuote + 1;
4873
+ continue;
4874
+ }
4875
+
4876
+ if (isListLine(index)) {
4877
+ let endList = index;
4878
+ for (let i = index + 1; i < lines.length; i += 1) {
4879
+ if (isBlankLine(i)) {
4880
+ if (i + 1 < lines.length && (isListLine(i + 1) || isContinuationIndentedLine(i + 1))) {
4881
+ endList = i;
4882
+ continue;
4883
+ }
4884
+ break;
4885
+ }
4886
+ if (isListLine(i) || isContinuationIndentedLine(i)) {
4887
+ endList = i;
4888
+ continue;
4889
+ }
4890
+ if (isAtxHeadingLine(i) || isBlockquoteLine(i) || lineStartsFence(i) || (isPotentialTableRow(i) && i + 1 < lines.length && isTableDividerLine(i + 1))) {
4891
+ break;
4892
+ }
4893
+ endList = i;
4894
+ }
4895
+ blocks.push(makeBlock("list", index, endList));
4896
+ index = endList + 1;
4897
+ continue;
4898
+ }
4899
+
4900
+ let endParagraph = index;
4901
+ for (let i = index + 1; i < lines.length; i += 1) {
4902
+ if (isBlankLine(i) || isHtmlCommentStart(i) || lineStartsFence(i) || isAtxHeadingLine(i) || isBlockquoteLine(i) || isListLine(i)) {
4903
+ break;
4904
+ }
4905
+ if (i + 1 < lines.length && (isSetextUnderlineLine(i + 1) || (isPotentialTableRow(i) && isTableDividerLine(i + 1)))) {
4906
+ break;
4907
+ }
4908
+ endParagraph = i;
4909
+ }
4910
+ blocks.push(makeBlock("paragraph", index, endParagraph));
4911
+ index = endParagraph + 1;
4912
+ }
4913
+
4914
+ return blocks;
4915
+ }
4916
+
4917
+ function getPreviewCommentTargetKind(element) {
4918
+ if (!element || !(element instanceof Element)) return "";
4919
+ const tag = element.tagName ? element.tagName.toUpperCase() : "";
4920
+ if (/^H[1-6]$/.test(tag)) return "heading";
4921
+ if (tag === "P") return "paragraph";
4922
+ if (tag === "BLOCKQUOTE") return "blockquote";
4923
+ if (tag === "UL" || tag === "OL") return "list";
4924
+ if (tag === "TABLE") return "table";
4925
+ if (tag === "PRE") return "code";
4926
+ if (element.classList) {
4927
+ if (
4928
+ element.classList.contains("sourceCode")
4929
+ || element.classList.contains("mermaid-container")
4930
+ ) {
4931
+ return "code";
4932
+ }
4933
+ if (
4934
+ element.classList.contains("callout-note")
4935
+ || element.classList.contains("callout-tip")
4936
+ || element.classList.contains("callout-warning")
4937
+ || element.classList.contains("callout-important")
4938
+ || element.classList.contains("callout-caution")
4939
+ ) {
4940
+ return "blockquote";
4941
+ }
4942
+ }
4943
+ return "";
4944
+ }
4945
+
4946
+ function isPreviewCommentTargetElement(element) {
4947
+ return Boolean(getPreviewCommentTargetKind(element));
4948
+ }
4949
+
4950
+ function collectPreviewCommentTargetElements(targetEl) {
4951
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
4952
+ const selector = "h1, h2, h3, h4, h5, h6, p, blockquote, ul, ol, table, div.sourceCode, pre, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
4953
+ return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
4954
+ if (!isPreviewCommentTargetElement(element)) return false;
4955
+ let ancestor = element.parentElement;
4956
+ while (ancestor && ancestor !== targetEl) {
4957
+ if (ancestor.classList && ancestor.classList.contains("preview-comment-block")) return false;
4958
+ if (isPreviewCommentTargetElement(ancestor)) return false;
4959
+ ancestor = ancestor.parentElement;
4960
+ }
4961
+ return true;
4962
+ }).map((element) => ({
4963
+ element,
4964
+ kind: getPreviewCommentTargetKind(element),
4965
+ }));
4966
+ }
4967
+
4968
+ function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
4969
+ if (!sourceBlock) return "";
4970
+ const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
4971
+ if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
4972
+ return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
4973
+ }
4974
+ if (sourceBlock.kind === "code") {
4975
+ return normalizeVisiblePreviewText(
4976
+ blockText
4977
+ .replace(/^ {0,3}(`{3,}|~{3,}).*$/gm, "")
4978
+ .replace(/^ {0,3}$/gm, ""),
4979
+ );
4980
+ }
4981
+ if (sourceBlock.kind === "table") {
4982
+ return normalizeVisiblePreviewText(
4983
+ blockText
4984
+ .replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/gm, "")
4985
+ .replace(/\|/g, " "),
4986
+ );
4987
+ }
4988
+ return normalizeVisiblePreviewText(blockText);
4989
+ }
4990
+
4991
+ function getNormalizedPreviewCommentTargetText(targetEntry) {
4992
+ if (!targetEntry) return "";
4993
+ if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
4994
+ targetEntry.normalizedText = normalizeVisiblePreviewText(
4995
+ targetEntry.element && typeof targetEntry.element.textContent === "string"
4996
+ ? targetEntry.element.textContent
4997
+ : "",
4998
+ );
4999
+ return targetEntry.normalizedText;
5000
+ }
5001
+
5002
+ function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5003
+ const desiredKind = sourceBlock ? sourceBlock.kind : "";
5004
+ const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
5005
+ let fallbackIndex = -1;
5006
+ let containsIndex = -1;
5007
+
5008
+ for (let i = Math.max(0, startIndex || 0); i < targetBlocks.length; i += 1) {
5009
+ const targetEntry = targetBlocks[i];
5010
+ if (!targetEntry || targetEntry.kind !== desiredKind) continue;
5011
+ if (fallbackIndex < 0) fallbackIndex = i;
5012
+ const targetText = getNormalizedPreviewCommentTargetText(targetEntry);
5013
+ if (desiredText && targetText) {
5014
+ if (targetText === desiredText) {
5015
+ return i;
5016
+ }
5017
+ if (containsIndex < 0 && (targetText.includes(desiredText) || desiredText.includes(targetText))) {
5018
+ containsIndex = i;
5019
+ }
5020
+ }
5021
+ }
5022
+
5023
+ if (containsIndex >= 0) return containsIndex;
5024
+ return fallbackIndex;
5025
+ }
5026
+
5027
+ function getPreviewCommentNotesForRange(start, end, sourceText, displayNotes) {
5028
+ const source = String(sourceText || "");
5029
+ const notes = Array.isArray(displayNotes) ? displayNotes : getDisplayReviewNotes();
5030
+ return notes.filter((note) => {
5031
+ const range = resolveReviewNoteRange(note, source);
5032
+ return range && rangesOverlap(range.start, range.end, start, end);
5033
+ });
5034
+ }
5035
+
5036
+ function updatePreviewCommentBlockState(blockEl, sourceText, displayNotes) {
5037
+ if (!blockEl || !blockEl.dataset) return;
5038
+ const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
5039
+ const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5040
+ const summaryBtn = blockEl.querySelector(".preview-comment-summary");
5041
+ const addBtn = blockEl.querySelector(".preview-comment-add");
5042
+ const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
5043
+ const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
5044
+ const blockKey = getPreviewCommentBlockKey(blockEl);
5045
+ const hasSelection = Boolean(activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey);
5046
+
5047
+ blockEl.classList.remove("has-comments");
5048
+ blockEl.classList.toggle("has-selection", hasSelection);
5049
+
5050
+ if (summaryBtn) {
5051
+ summaryBtn.hidden = true;
5052
+ summaryBtn.textContent = "";
5053
+ summaryBtn.dataset.reviewNoteId = "";
5054
+ }
5055
+
5056
+ if (addBtn) {
5057
+ addBtn.hidden = !hasSelection;
5058
+ addBtn.textContent = "Comment";
5059
+ addBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
5060
+ addBtn.title = hasSelection
5061
+ ? ("Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").")
5062
+ : "";
5063
+ addBtn.setAttribute("aria-label", addBtn.title || "Comment");
5064
+ }
5065
+ }
5066
+
5067
+ function updatePreviewCommentBlocksForElement(targetEl) {
5068
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5069
+ const sourceText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5070
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5071
+ updatePreviewCommentBlockState(blockEl, sourceText);
5072
+ });
5073
+ }
5074
+
5075
+ function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
5076
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5077
+ const sourceBlocks = scanMarkdownPreviewCommentBlocks(sourceText);
5078
+ const targetBlocks = collectPreviewCommentTargetElements(targetEl);
5079
+ if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
5080
+
5081
+ let targetIndex = 0;
5082
+ for (const sourceBlock of sourceBlocks) {
5083
+ const matchedTargetIndex = findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, targetIndex);
5084
+ if (matchedTargetIndex < 0) continue;
5085
+
5086
+ const targetEntry = targetBlocks[matchedTargetIndex];
5087
+ targetIndex = matchedTargetIndex + 1;
5088
+ const originalElement = targetEntry && targetEntry.element ? targetEntry.element : null;
5089
+ if (!originalElement || !originalElement.parentNode) continue;
5090
+
5091
+ const wrapper = document.createElement("div");
5092
+ wrapper.className = "preview-comment-block";
5093
+ wrapper.dataset.reviewNoteStart = String(sourceBlock.start);
5094
+ wrapper.dataset.reviewNoteEnd = String(sourceBlock.end);
5095
+ wrapper.dataset.reviewNoteLineStart = String(sourceBlock.lineStart);
5096
+ wrapper.dataset.reviewNoteLineEnd = String(sourceBlock.lineEnd);
5097
+ wrapper.dataset.previewCommentKind = sourceBlock.kind;
5098
+
5099
+ const controls = document.createElement("div");
5100
+ controls.className = "preview-comment-controls";
5101
+
5102
+ const summaryBtn = document.createElement("button");
5103
+ summaryBtn.type = "button";
5104
+ summaryBtn.className = "preview-comment-summary";
5105
+ summaryBtn.hidden = true;
5106
+ controls.appendChild(summaryBtn);
5107
+
5108
+ const addBtn = document.createElement("button");
5109
+ addBtn.type = "button";
5110
+ addBtn.className = "preview-comment-add";
5111
+ addBtn.textContent = "Comment";
5112
+ controls.appendChild(addBtn);
5113
+
5114
+ originalElement.replaceWith(wrapper);
5115
+ wrapper.appendChild(controls);
5116
+ originalElement.classList.add("preview-comment-block-content");
5117
+ wrapper.appendChild(originalElement);
5118
+ }
5119
+
5120
+ updatePreviewCommentBlocksForElement(targetEl);
5121
+ }
5122
+
5123
+ function refreshRenderedEditorPreviewComments() {
5124
+ if (sourcePreviewEl && !sourcePreviewEl.hidden) {
5125
+ updatePreviewCommentBlocksForElement(sourcePreviewEl);
5126
+ }
5127
+ if (critiqueViewEl && rightView === "editor-preview") {
5128
+ updatePreviewCommentBlocksForElement(critiqueViewEl);
5129
+ }
5130
+ }
5131
+
5132
+ function buildReviewNoteAnchorFromPreviewBlock(blockEl) {
5133
+ if (!blockEl || !blockEl.dataset) return null;
5134
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5135
+ const selectionStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5136
+ const selectionEnd = Math.max(selectionStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || selectionStart, source.length));
5137
+ const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
5138
+ const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5139
+ return {
5140
+ selectionStart,
5141
+ selectionEnd,
5142
+ lineStart,
5143
+ lineEnd,
5144
+ selectedText: source.slice(selectionStart, selectionEnd),
5145
+ selectedDisplayText: source.slice(selectionStart, selectionEnd),
5146
+ };
5147
+ }
5148
+
5149
+ function buildReviewNoteAnchorFromPreviewSelection(blockEl, contentEl, range) {
5150
+ if (!blockEl || !blockEl.dataset || !contentEl || !range) return null;
5151
+ const kind = String(blockEl.dataset.previewCommentKind || "");
5152
+ if (!supportsPreviewSelectionCommentsForBlockKind(kind)) return null;
5153
+ if (!contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) return null;
5154
+
5155
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5156
+ const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5157
+ const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5158
+ if (blockEnd <= blockStart) return null;
5159
+
5160
+ const sourceBlockText = source.slice(blockStart, blockEnd);
5161
+ const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5162
+ if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
5163
+
5164
+ const prefixRange = document.createRange();
5165
+ prefixRange.selectNodeContents(contentEl);
5166
+ prefixRange.setEnd(range.startContainer, range.startOffset);
5167
+ const prefixText = normalizeVisiblePreviewText(prefixRange.toString());
5168
+ const selectedDisplayText = normalizeVisiblePreviewText(range.toString());
5169
+ if (!selectedDisplayText) return null;
5170
+
5171
+ const desiredStart = Math.max(0, Math.min(prefixText.length, displayMap.text.length));
5172
+ const bestIndex = findPreferredNormalizedTextMatch(displayMap.text, selectedDisplayText, desiredStart);
5173
+ if (bestIndex < 0) return null;
5174
+
5175
+ const endIndex = bestIndex + selectedDisplayText.length - 1;
5176
+ const rawStartRel = displayMap.charStarts[bestIndex];
5177
+ const rawEndRel = displayMap.charEnds[endIndex];
5178
+ if (!Number.isFinite(rawStartRel) || !Number.isFinite(rawEndRel) || rawEndRel <= rawStartRel) {
5179
+ return null;
5180
+ }
5181
+
5182
+ const selectionStart = blockStart + rawStartRel;
5183
+ const selectionEnd = blockStart + rawEndRel;
5184
+ return {
5185
+ selectionStart,
5186
+ selectionEnd,
5187
+ lineStart: getLineNumberAtOffset(source, selectionStart),
5188
+ lineEnd: getLineNumberAtOffset(source, Math.max(selectionStart, selectionEnd - 1)),
5189
+ selectedText: source.slice(selectionStart, selectionEnd),
5190
+ selectedDisplayText,
5191
+ };
5192
+ }
5193
+
5194
+ function getPreviewJumpNormalizedSelectionStart(note, blockEl, range) {
5195
+ if (!note || !blockEl || !blockEl.dataset || !range) return 0;
5196
+ const kind = String(blockEl.dataset.previewCommentKind || "");
5197
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5198
+ const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5199
+ const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5200
+ const displayMap = buildPreviewSelectionDisplayMap(source.slice(blockStart, blockEnd), kind);
5201
+ if (!displayMap || !displayMap.charStarts || displayMap.charStarts.length === 0) return 0;
5202
+ const relativeStart = Math.max(0, range.start - blockStart);
5203
+ for (let i = 0; i < displayMap.charStarts.length; i += 1) {
5204
+ const charStart = Number(displayMap.charStarts[i]);
5205
+ const charEnd = Number(displayMap.charEnds[i]);
5206
+ if (charEnd > relativeStart && charStart <= relativeStart) {
5207
+ return i;
5208
+ }
5209
+ if (charStart >= relativeStart) {
5210
+ return i;
5211
+ }
5212
+ }
5213
+ return Math.max(0, displayMap.text.length - 1);
5214
+ }
5215
+
5216
+ function createPreviewJumpInlineHighlight(contentEl, blockEl, note, range) {
5217
+ if (!contentEl || !note || !range) return null;
5218
+ const selectedDisplayText = normalizeVisiblePreviewText(note.selectedDisplayText || note.selectedText || "");
5219
+ if (!selectedDisplayText) return null;
5220
+ const domMap = buildNormalizedDomTextMap(contentEl);
5221
+ if (!domMap.text || !domMap.charStarts.length || !domMap.charEnds.length) return null;
5222
+ const preferredStart = getPreviewJumpNormalizedSelectionStart(note, blockEl, range);
5223
+ const matchIndex = findPreferredNormalizedTextMatch(domMap.text, selectedDisplayText, preferredStart);
5224
+ if (matchIndex < 0) return null;
5225
+ const endIndex = matchIndex + selectedDisplayText.length - 1;
5226
+ const startRef = domMap.charStarts[matchIndex];
5227
+ const endRef = domMap.charEnds[endIndex];
5228
+ if (!startRef || !endRef || !startRef.node || !endRef.node) return null;
5229
+
5230
+ const domRange = document.createRange();
5231
+ domRange.setStart(startRef.node, startRef.offset);
5232
+ domRange.setEnd(endRef.node, endRef.offset);
5233
+
5234
+ const highlightEl = document.createElement("span");
5235
+ highlightEl.className = "preview-comment-inline-highlight";
5236
+ try {
5237
+ domRange.surroundContents(highlightEl);
5238
+ } catch {
5239
+ const fragment = domRange.extractContents();
5240
+ highlightEl.appendChild(fragment);
5241
+ domRange.insertNode(highlightEl);
5242
+ }
5243
+ return highlightEl;
5244
+ }
5245
+
5246
+ function findPreviewCommentBlockForRange(targetEl, range) {
5247
+ if (!targetEl || !range || typeof targetEl.querySelectorAll !== "function") return null;
5248
+ let bestBlock = null;
5249
+ let bestScore = Number.NEGATIVE_INFINITY;
5250
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5251
+ const blockStart = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
5252
+ const blockEnd = Math.max(blockStart, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || blockStart);
5253
+ const overlapStart = Math.max(blockStart, range.start);
5254
+ const overlapEnd = Math.min(blockEnd, range.end);
5255
+ const overlap = Math.max(0, overlapEnd - overlapStart);
5256
+ const contains = range.start >= blockStart && range.end <= blockEnd;
5257
+ const distance = contains
5258
+ ? 0
5259
+ : Math.min(Math.abs(range.start - blockEnd), Math.abs(range.end - blockStart));
5260
+ const score = contains
5261
+ ? (1000000 - (blockEnd - blockStart))
5262
+ : (overlap > 0 ? overlap : -distance);
5263
+ if (score > bestScore) {
5264
+ bestScore = score;
5265
+ bestBlock = blockEl;
5266
+ }
5267
+ });
5268
+ return bestBlock;
5269
+ }
5270
+
5271
+ function revealReviewNoteInPreviewElement(targetEl, note) {
5272
+ if (!targetEl || !note) return false;
5273
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5274
+ const range = resolveReviewNoteRange(note, source);
5275
+ if (!range) return false;
5276
+ const blockEl = findPreviewCommentBlockForRange(targetEl, range);
5277
+ if (!blockEl) return false;
5278
+ const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5279
+ const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
5280
+ if (typeof blockEl.scrollIntoView === "function") {
5281
+ blockEl.scrollIntoView({ block: "center", inline: "nearest" });
5282
+ }
5283
+ setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl);
5284
+ return true;
5285
+ }
5286
+
5287
+ function revealReviewNoteInPreview(note) {
5288
+ if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
5289
+ revealReviewNoteInPreviewElement(critiqueViewEl, note);
5290
+ }
5291
+ }
5292
+
5293
+ function updateActivePreviewCommentSelectionFromDom() {
5294
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
5295
+ if (!selection || selection.rangeCount <= 0 || selection.isCollapsed) {
5296
+ clearPreviewCommentSelection();
5297
+ return;
5298
+ }
5299
+
5300
+ const range = selection.getRangeAt(0);
5301
+ const startBlock = findPreviewCommentBlockFromNode(range.startContainer);
5302
+ const endBlock = findPreviewCommentBlockFromNode(range.endContainer);
5303
+ if (!startBlock || !endBlock || startBlock !== endBlock) {
5304
+ clearPreviewCommentSelection();
5305
+ return;
5306
+ }
5307
+
5308
+ const contentEl = startBlock.querySelector(".preview-comment-block-content");
5309
+ if (!contentEl || !contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) {
5310
+ clearPreviewCommentSelection();
5311
+ return;
5312
+ }
5313
+
5314
+ const anchor = buildReviewNoteAnchorFromPreviewSelection(startBlock, contentEl, range);
5315
+ if (!anchor) {
5316
+ clearPreviewCommentSelection();
5317
+ return;
5318
+ }
5319
+
5320
+ setActivePreviewCommentSelection({
5321
+ ...anchor,
5322
+ blockKey: getPreviewCommentBlockKey(startBlock),
5323
+ });
5324
+ }
5325
+
5326
+ function getDisplayReviewNotes() {
5327
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5328
+ return reviewNotes.slice().sort((left, right) => {
5329
+ const leftBounds = getResolvedReviewNoteLineBounds(left, source);
5330
+ const rightBounds = getResolvedReviewNoteLineBounds(right, source);
5331
+ const leftLine = leftBounds ? leftBounds.lineStart : Math.max(1, Number(left && left.lineStart) || 1);
5332
+ const rightLine = rightBounds ? rightBounds.lineStart : Math.max(1, Number(right && right.lineStart) || 1);
5333
+ if (leftLine !== rightLine) return leftLine - rightLine;
5334
+
5335
+ const leftStart = leftBounds ? leftBounds.start : Math.max(0, Number(left && left.selectionStart) || 0);
5336
+ const rightStart = rightBounds ? rightBounds.start : Math.max(0, Number(right && right.selectionStart) || 0);
5337
+ if (leftStart !== rightStart) return leftStart - rightStart;
5338
+
5339
+ const leftCreated = Number(left && left.createdAt) || 0;
5340
+ const rightCreated = Number(right && right.createdAt) || 0;
5341
+ if (leftCreated !== rightCreated) return leftCreated - rightCreated;
5342
+
5343
+ return String(left && left.id ? left.id : "").localeCompare(String(right && right.id ? right.id : ""));
5344
+ });
5345
+ }
5346
+
5347
+ function focusReviewNoteInPanel(noteId) {
5348
+ const note = reviewNotes.find((entry) => entry && entry.id === noteId);
5349
+ if (!note) return;
5350
+ pendingReviewNoteFocusId = note.id;
5351
+ openReviewNotes();
5352
+ }
5353
+
5354
+ function escapeReviewNoteAnnotationText(text) {
5355
+ return String(text || "")
5356
+ .replace(/\\/g, "\\\\")
5357
+ .replace(/\]/g, "\\]")
5358
+ .trim();
5359
+ }
5360
+
5361
+ function getReviewNoteInlineState(note, text) {
5362
+ const source = String(text || "");
5363
+ const annotationBody = escapeReviewNoteAnnotationText(note && note.text);
5364
+ if (!annotationBody) {
5365
+ return {
5366
+ annotationBody: "",
5367
+ range: null,
5368
+ markerText: "",
5369
+ exists: false,
5370
+ canToggle: false,
5371
+ };
5372
+ }
5373
+ const range = resolveReviewNoteRange(note, source);
5374
+ if (!range) {
5375
+ return {
5376
+ annotationBody,
5377
+ range: null,
5378
+ markerText: "",
5379
+ exists: false,
5380
+ canToggle: false,
5381
+ };
5382
+ }
5383
+ const markerText = (range.start === range.end ? "" : " ") + "[an: " + annotationBody + "]";
5384
+ const exists = source.slice(range.end, range.end + markerText.length) === markerText;
5385
+ return {
5386
+ annotationBody,
5387
+ range,
5388
+ markerText,
5389
+ exists,
5390
+ canToggle: true,
5391
+ };
5392
+ }
5393
+
5394
+ function setReviewNotes(nextNotes, options) {
5395
+ reviewNotes = cloneReviewNotes(nextNotes);
5396
+ updateReviewNotesUi();
5397
+ renderReviewNotesList();
5398
+ refreshRenderedEditorPreviewComments();
5399
+ if (editorView === "markdown") {
5400
+ scheduleEditorLineNumberRender();
5401
+ }
5402
+ if (!options || options.persist !== false) {
5403
+ scheduleReviewNotesPersistence();
5404
+ }
5405
+ }
5406
+
5407
+ function updateEditorSelectionCommentUi() {
5408
+ if (!editorSelectionCommentBtn) return;
5409
+ const hasSelection = Boolean(
5410
+ editorView === "markdown"
5411
+ && document.activeElement === sourceTextEl
5412
+ && typeof sourceTextEl.selectionStart === "number"
5413
+ && typeof sourceTextEl.selectionEnd === "number"
5414
+ && sourceTextEl.selectionEnd > sourceTextEl.selectionStart
5415
+ );
5416
+ editorSelectionCommentBtn.hidden = !hasSelection;
5417
+ if (hasSelection) {
5418
+ editorSelectionCommentBtn.title = "Create a new local comment from the current editor selection.";
5419
+ editorSelectionCommentBtn.setAttribute("aria-label", editorSelectionCommentBtn.title);
5420
+ }
5421
+ }
5422
+
5423
+ function updateReviewNotesUi() {
5424
+ const descriptor = getCurrentStudioDocumentDescriptor();
5425
+ const count = reviewNotes.length;
5426
+ const hasNotes = count > 0;
5427
+ const isOpen = isReviewNotesOpen();
5428
+ if (reviewNotesBtn) {
5429
+ reviewNotesBtn.textContent = hasNotes ? "Comments •" : "Comments";
5430
+ reviewNotesBtn.classList.toggle("has-content", hasNotes);
5431
+ reviewNotesBtn.classList.toggle("is-active", isOpen);
5432
+ reviewNotesBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
5433
+ reviewNotesBtn.title = isOpen
5434
+ ? "Hide local comments."
5435
+ : (hasNotes
5436
+ ? (count + " local comment" + (count === 1 ? "" : "s") + " for " + descriptor.label + ". Open the side-by-side comments rail.")
5437
+ : "Open local comments beside the current editor document or draft. Comments stay outside the document text and can later be converted into [an: ...] annotations.");
5438
+ }
5439
+ if (reviewNotesMetaEl) {
5440
+ const scopeLabel = descriptor.fileBacked
5441
+ ? "file-backed"
5442
+ : (descriptor.draftBacked ? "draft-backed" : "local buffer");
5443
+ reviewNotesMetaEl.textContent = hasNotes
5444
+ ? (count + " comment" + (count === 1 ? "" : "s") + " · " + scopeLabel + " · " + descriptor.label)
5445
+ : ("No comments yet · " + scopeLabel);
5446
+ }
5447
+ if (reviewNotesAddBtn) {
5448
+ reviewNotesAddBtn.disabled = editorView !== "markdown";
5449
+ reviewNotesAddBtn.title = editorView === "markdown"
5450
+ ? "Create a new local comment on the current editor line."
5451
+ : (supportsPreviewCommentsForCurrentEditor()
5452
+ ? "Select preview text and use Comment for a local preview-anchored comment."
5453
+ : "Switch to Editor (Raw) to comment on the current line.");
5454
+ }
5455
+ if (reviewNotesInlineAllBtn) {
5456
+ const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5457
+ const toggleCandidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, currentText).canToggle);
5458
+ const allInline = toggleCandidates.length > 0 && toggleCandidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
5459
+ reviewNotesInlineAllBtn.disabled = uiBusy || toggleCandidates.length === 0;
5460
+ reviewNotesInlineAllBtn.textContent = allInline ? "All inline: On" : "All inline: Off";
5461
+ reviewNotesInlineAllBtn.setAttribute("aria-pressed", allInline ? "true" : "false");
5462
+ reviewNotesInlineAllBtn.title = allInline
5463
+ ? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
5464
+ : "Inline annotations derived from all non-empty comments are currently off. Click to add them.";
5465
+ }
5466
+ if (reviewNotesDoneBtn) {
5467
+ reviewNotesDoneBtn.disabled = !isOpen;
5468
+ }
5469
+ if (reviewNotesEmptyStateEl) {
5470
+ reviewNotesEmptyStateEl.hidden = hasNotes;
5471
+ }
5472
+ }
5473
+
5474
+ function renderReviewNotesList() {
5475
+ if (!reviewNotesListEl) return;
5476
+ reviewNotesListEl.innerHTML = "";
5477
+ for (const note of getDisplayReviewNotes()) {
5478
+ const card = document.createElement("article");
5479
+ card.className = "review-note-card";
5480
+
5481
+ const header = document.createElement("div");
5482
+ header.className = "review-note-card-header";
5483
+
5484
+ const titleWrap = document.createElement("div");
5485
+ titleWrap.className = "review-note-card-title";
5486
+
5487
+ const anchor = document.createElement("span");
5488
+ anchor.className = "review-note-anchor";
5489
+ anchor.textContent = summarizeReviewNoteAnchor(note);
5490
+ titleWrap.appendChild(anchor);
5491
+
5492
+ const quote = document.createElement("div");
5493
+ quote.className = "review-note-quote";
5494
+ quote.textContent = summarizeReviewNoteQuote(note);
5495
+ titleWrap.appendChild(quote);
5496
+ header.appendChild(titleWrap);
5497
+
5498
+ card.appendChild(header);
5499
+
5500
+ const textarea = document.createElement("textarea");
5501
+ textarea.value = String(note.text || "");
5502
+ textarea.placeholder = "Write a local comment here…";
5503
+ textarea.title = "Write a local comment. Press Enter to finish editing, or Shift+Enter for a new line.";
5504
+ card.appendChild(textarea);
5505
+
5506
+ const footer = document.createElement("div");
5507
+ footer.className = "review-note-card-footer";
5508
+
5509
+ const timestamp = document.createElement("span");
5510
+ timestamp.className = "review-note-timestamp";
5511
+ timestamp.textContent = formatReviewNoteTimestamp(note.updatedAt);
5512
+
5513
+ const actions = document.createElement("div");
5514
+ actions.className = "review-note-card-actions";
5515
+
5516
+ const jumpBtn = document.createElement("button");
5517
+ jumpBtn.type = "button";
5518
+ jumpBtn.textContent = "Jump";
5519
+ jumpBtn.title = "Jump to this comment's anchored location in the editor.";
5520
+ jumpBtn.addEventListener("click", () => {
5521
+ jumpToReviewNote(note.id);
5522
+ });
5523
+ actions.appendChild(jumpBtn);
5524
+
5525
+ const inlineState = getReviewNoteInlineState(note, sourceTextEl.value || "");
5526
+ const convertBtn = document.createElement("button");
5527
+ convertBtn.type = "button";
5528
+ convertBtn.className = "review-note-inline-btn";
5529
+ convertBtn.textContent = inlineState.exists ? "Inline: On" : "Inline: Off";
5530
+ convertBtn.setAttribute("aria-pressed", inlineState.exists ? "true" : "false");
5531
+ convertBtn.disabled = !inlineState.canToggle || uiBusy;
5532
+ convertBtn.title = inlineState.exists
5533
+ ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
5534
+ : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
5535
+ convertBtn.addEventListener("click", () => {
5536
+ convertReviewNoteToAnnotation(note.id);
5537
+ });
5538
+ actions.appendChild(convertBtn);
5539
+
5540
+ const deleteBtn = document.createElement("button");
5541
+ deleteBtn.type = "button";
5542
+ deleteBtn.className = "review-note-delete-btn";
5543
+ deleteBtn.textContent = "Delete";
5544
+ deleteBtn.title = "Delete this local comment.";
5545
+ deleteBtn.addEventListener("click", () => {
5546
+ deleteReviewNote(note.id);
5547
+ });
5548
+ actions.appendChild(deleteBtn);
5549
+
5550
+ footer.appendChild(timestamp);
5551
+ footer.appendChild(actions);
5552
+ card.appendChild(footer);
5553
+
5554
+ textarea.addEventListener("input", () => {
5555
+ note.text = textarea.value;
5556
+ note.updatedAt = Date.now();
5557
+ timestamp.textContent = formatReviewNoteTimestamp(note.updatedAt);
5558
+ const nextInlineState = getReviewNoteInlineState(note, sourceTextEl.value || "");
5559
+ convertBtn.disabled = !nextInlineState.canToggle || uiBusy;
5560
+ convertBtn.textContent = nextInlineState.exists ? "Inline: On" : "Inline: Off";
5561
+ convertBtn.setAttribute("aria-pressed", nextInlineState.exists ? "true" : "false");
5562
+ convertBtn.title = nextInlineState.exists
5563
+ ? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
5564
+ : "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
5565
+ scheduleReviewNotesPersistence();
5566
+ updateReviewNotesUi();
5567
+ });
5568
+
5569
+ textarea.addEventListener("keydown", (event) => {
5570
+ if (
5571
+ event.key === "Enter"
5572
+ && !event.shiftKey
5573
+ && !event.altKey
5574
+ && !event.ctrlKey
5575
+ && !event.metaKey
5576
+ ) {
5577
+ event.preventDefault();
5578
+ textarea.blur();
5579
+ if (!convertBtn.disabled) {
5580
+ convertBtn.focus();
5581
+ }
5582
+ }
5583
+ });
5584
+
5585
+ reviewNotesListEl.appendChild(card);
5586
+
5587
+ if (pendingReviewNoteInlineFocusId && pendingReviewNoteInlineFocusId === note.id && isReviewNotesOpen()) {
5588
+ const schedule = typeof window.requestAnimationFrame === "function"
5589
+ ? window.requestAnimationFrame.bind(window)
5590
+ : (cb) => window.setTimeout(cb, 16);
5591
+ schedule(() => {
5592
+ card.scrollIntoView({ block: "nearest" });
5593
+ if (!convertBtn.disabled) convertBtn.focus();
5594
+ });
5595
+ } else if (pendingReviewNoteFocusId && pendingReviewNoteFocusId === note.id && isReviewNotesOpen()) {
5596
+ const schedule = typeof window.requestAnimationFrame === "function"
5597
+ ? window.requestAnimationFrame.bind(window)
5598
+ : (cb) => window.setTimeout(cb, 16);
5599
+ schedule(() => {
5600
+ card.scrollIntoView({ block: "nearest" });
5601
+ textarea.focus();
5602
+ const end = textarea.value.length;
5603
+ textarea.setSelectionRange(end, end);
5604
+ });
5605
+ }
5606
+ }
5607
+ pendingReviewNoteFocusId = null;
5608
+ pendingReviewNoteInlineFocusId = null;
5609
+ }
5610
+
5611
+ function focusReviewNotesForPreviewBlock(blockEl) {
5612
+ if (!blockEl) return;
5613
+ const start = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
5614
+ const end = Math.max(start, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || start);
5615
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5616
+ const notes = getPreviewCommentNotesForRange(start, end, source);
5617
+ if (!notes.length) return;
5618
+ focusReviewNoteInPanel(notes[0].id);
5619
+ }
5620
+
5621
+ function addReviewNoteFromPreviewBlock(blockEl) {
5622
+ const anchor = buildReviewNoteAnchorFromPreviewBlock(blockEl);
5623
+ if (!anchor) return null;
5624
+ return addReviewNoteFromAnchor(anchor, {
5625
+ statusMessage: "Added local comment from editor preview.",
5626
+ });
5627
+ }
5628
+
5629
+ function addReviewNoteFromPreviewSelection(blockEl) {
5630
+ if (!blockEl) return null;
5631
+ const blockKey = getPreviewCommentBlockKey(blockEl);
5632
+ const anchor = activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
5633
+ ? activePreviewCommentSelection
5634
+ : null;
5635
+ if (!anchor) {
5636
+ setStatus("Select some preview text within a single block first.", "warning");
5637
+ return null;
5638
+ }
5639
+ const note = addReviewNoteFromAnchor(anchor, {
5640
+ statusMessage: "Added local comment from preview selection.",
5641
+ });
5642
+ if (note) {
5643
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
5644
+ if (selection && typeof selection.removeAllRanges === "function") {
5645
+ selection.removeAllRanges();
5646
+ }
5647
+ clearPreviewCommentSelection();
5648
+ }
5649
+ return note;
5650
+ }
5651
+
5652
+ function addReviewNoteFromAnchor(anchor, options) {
5653
+ if (!anchor || typeof anchor !== "object") return null;
5654
+ const note = normalizeReviewNote({
5655
+ id: makeRequestId(),
5656
+ text: "",
5657
+ createdAt: Date.now(),
5658
+ updatedAt: Date.now(),
5659
+ selectionStart: anchor.selectionStart,
5660
+ selectionEnd: anchor.selectionEnd,
5661
+ lineStart: anchor.lineStart,
5662
+ lineEnd: anchor.lineEnd,
5663
+ selectedText: anchor.selectedText,
5664
+ selectedDisplayText: typeof anchor.selectedDisplayText === "string" ? anchor.selectedDisplayText : (typeof anchor.selectedText === "string" ? anchor.selectedText : ""),
5665
+ });
5666
+ if (!note) return null;
5667
+ if (editorSelectionCommentBtn) {
5668
+ editorSelectionCommentBtn.hidden = true;
5669
+ }
5670
+ pendingReviewNoteFocusId = note.id;
5671
+ setReviewNotes(reviewNotes.concat([note]));
5672
+ if (!isReviewNotesOpen()) {
5673
+ openReviewNotes();
5674
+ }
5675
+ const schedule = typeof window.requestAnimationFrame === "function"
5676
+ ? window.requestAnimationFrame.bind(window)
5677
+ : (cb) => window.setTimeout(cb, 16);
5678
+ schedule(() => {
5679
+ updateEditorSelectionCommentUi();
5680
+ });
5681
+ if (!options || options.status !== false) {
5682
+ setStatus((options && options.statusMessage) || "Added local comment.", "success");
5683
+ }
5684
+ return note;
5685
+ }
5686
+
5687
+ function addReviewNoteFromEditorSelection() {
5688
+ if (editorView !== "markdown") {
5689
+ setStatus("Switch to Editor (Raw) before adding an anchored comment.", "warning");
5690
+ return;
5691
+ }
5692
+ addReviewNoteFromAnchor(getEditorAnchorForReviewNote(), {
5693
+ statusMessage: "Added local comment.",
5694
+ });
5695
+ }
5696
+
5697
+ function addReviewNoteFromEditorLine() {
5698
+ if (editorView !== "markdown") {
5699
+ setStatus("Switch to Editor (Raw) before adding a line comment.", "warning");
5700
+ return;
5701
+ }
5702
+ addReviewNoteFromAnchor(getEditorLineAnchorForReviewNote(), {
5703
+ statusMessage: "Added local line comment.",
5704
+ });
5705
+ }
5706
+
5707
+ function jumpToReviewNote(noteId) {
5708
+ const note = reviewNotes.find((entry) => entry && entry.id === noteId);
5709
+ if (!note) return;
5710
+ const current = String(sourceTextEl.value || "");
5711
+ const range = resolveReviewNoteRange(note, current);
5712
+ if (!range) {
5713
+ setStatus("Could not find the anchored location for this comment.", "warning");
5714
+ return;
5715
+ }
5716
+ setEditorView("markdown");
5717
+ setActivePane("left");
5718
+ sourceTextEl.focus();
5719
+ sourceTextEl.setSelectionRange(range.start, range.end);
5720
+ const schedule = typeof window.requestAnimationFrame === "function"
5721
+ ? window.requestAnimationFrame.bind(window)
5722
+ : (cb) => window.setTimeout(cb, 16);
5723
+ schedule(() => {
5724
+ scrollEditorRangeIntoView(range);
5725
+ revealReviewNoteInPreview(note);
5726
+ });
5727
+ }
5728
+
5729
+ function deleteReviewNote(noteId) {
5730
+ const note = reviewNotes.find((entry) => entry && entry.id === noteId);
5731
+ if (!note) return;
5732
+ const confirmed = window.confirm("Delete this local comment?");
5733
+ if (!confirmed) return;
5734
+ setReviewNotes(reviewNotes.filter((entry) => entry && entry.id !== noteId));
5735
+ setStatus("Deleted local comment.", "success");
5736
+ }
5737
+
5738
+ function convertReviewNoteToAnnotation(noteId) {
5739
+ if (uiBusy) {
5740
+ setStatus("Wait until the current Studio action finishes before toggling inline annotation state.", "warning");
5741
+ return;
5742
+ }
5743
+ const note = reviewNotes.find((entry) => entry && entry.id === noteId);
5744
+ if (!note) return;
5745
+ const current = String(sourceTextEl.value || "");
5746
+ const inlineState = getReviewNoteInlineState(note, current);
5747
+ if (!inlineState.annotationBody) {
5748
+ setStatus("Comment is empty. Add some text before toggling inline annotation state.", "warning");
5749
+ return;
5750
+ }
5751
+ if (!inlineState.range || !inlineState.canToggle) {
5752
+ setStatus("Could not find the anchored location for this comment.", "warning");
5753
+ return;
5754
+ }
5755
+ const next = inlineState.exists
5756
+ ? current.slice(0, inlineState.range.end) + current.slice(inlineState.range.end + inlineState.markerText.length)
5757
+ : current.slice(0, inlineState.range.end) + inlineState.markerText + current.slice(inlineState.range.end);
5758
+ setEditorView("markdown");
5759
+ setEditorText(next, { preserveScroll: true, preserveSelection: true });
5760
+ pendingReviewNoteInlineFocusId = note.id;
5761
+ renderReviewNotesList();
5762
+ updateReviewNotesUi();
5763
+ setStatus(inlineState.exists ? "Removed inline annotation from local comment." : "Added inline annotation from local comment.", "success");
5764
+ }
5765
+
5766
+ function toggleAllReviewNotesInlineAnnotations() {
5767
+ if (uiBusy) {
5768
+ setStatus("Wait until the current Studio action finishes before toggling inline annotations.", "warning");
5769
+ return;
5770
+ }
5771
+ const candidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, sourceTextEl.value || "").canToggle);
5772
+ if (candidates.length === 0) {
5773
+ setStatus("No non-empty comments are ready to toggle inline.", "warning");
5774
+ return;
5775
+ }
5776
+ let currentText = String(sourceTextEl.value || "");
5777
+ const shouldRemoveAll = candidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
5778
+ const ordered = candidates
5779
+ .map((note) => ({ note, state: getReviewNoteInlineState(note, currentText) }))
5780
+ .filter((entry) => entry.state.range)
5781
+ .sort((left, right) => (right.state.range ? right.state.range.end : 0) - (left.state.range ? left.state.range.end : 0));
5782
+
5783
+ let changed = false;
5784
+ for (const entry of ordered) {
5785
+ const liveState = getReviewNoteInlineState(entry.note, currentText);
5786
+ if (!liveState.range || !liveState.canToggle) continue;
5787
+ if (shouldRemoveAll) {
5788
+ if (!liveState.exists) continue;
5789
+ currentText = currentText.slice(0, liveState.range.end) + currentText.slice(liveState.range.end + liveState.markerText.length);
5790
+ changed = true;
5791
+ } else {
5792
+ if (liveState.exists) continue;
5793
+ currentText = currentText.slice(0, liveState.range.end) + liveState.markerText + currentText.slice(liveState.range.end);
5794
+ changed = true;
5795
+ }
5796
+ }
5797
+
5798
+ if (!changed) {
5799
+ setStatus(shouldRemoveAll ? "No inline annotations were removed." : "No inline annotations were added.", "warning");
5800
+ return;
5801
+ }
5802
+
5803
+ setEditorView("markdown");
5804
+ setEditorText(currentText, { preserveScroll: true, preserveSelection: true });
5805
+ renderReviewNotesList();
5806
+ updateReviewNotesUi();
5807
+ if (reviewNotesInlineAllBtn && typeof reviewNotesInlineAllBtn.focus === "function") {
5808
+ reviewNotesInlineAllBtn.focus();
5809
+ }
5810
+ setStatus(shouldRemoveAll ? "Removed inline annotations from all comments." : "Added inline annotations from all comments.", "success");
5811
+ }
5812
+
5813
+ function updateScratchpadUi() {
5814
+ const normalized = String(scratchpadText || "");
5815
+ const hasContent = Boolean(normalized.trim());
5816
+ const descriptor = getCurrentStudioDocumentDescriptor();
5817
+ if (scratchpadBtn) {
5818
+ scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
5819
+ scratchpadBtn.classList.toggle("has-content", hasContent);
5820
+ scratchpadBtn.title = hasContent
5821
+ ? ("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.")
5822
+ : ("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.");
5823
+ }
5824
+ if (scratchpadMetaEl) {
5825
+ scratchpadMetaEl.textContent = hasContent
5826
+ ? ("Saved locally for this document/draft · " + normalized.length + " chars")
5827
+ : "Empty · local to this document/draft";
5828
+ }
5829
+ if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
5830
+ if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
5831
+ if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
5832
+ }
5833
+
5834
+ function setScratchpadText(nextText, options) {
5835
+ scratchpadText = String(nextText || "");
5836
+ if (scratchpadTextEl && scratchpadTextEl.value !== scratchpadText) {
5837
+ scratchpadTextEl.value = scratchpadText;
5838
+ }
5839
+ if (!options || options.persist !== false) {
5840
+ persistScratchpadText(scratchpadText);
5841
+ }
5842
+ updateScratchpadUi();
5843
+ }
5844
+
5845
+ function closeScratchpad(options) {
5846
+ if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
5847
+ scratchpadOverlayEl.hidden = true;
5848
+ syncModalOpenState();
5849
+ const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
5850
+ ? options.focusTarget
5851
+ : (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
5852
+ scratchpadReturnFocusEl = null;
5853
+ if (focusTarget && typeof focusTarget.focus === "function") {
5854
+ const schedule = typeof window.requestAnimationFrame === "function"
5855
+ ? window.requestAnimationFrame.bind(window)
5856
+ : (cb) => window.setTimeout(cb, 16);
5857
+ schedule(() => focusTarget.focus());
5858
+ }
5859
+ }
5860
+
5861
+ function openScratchpad() {
5862
+ if (!scratchpadOverlayEl) return;
5863
+ if (isReviewNotesOpen()) {
5864
+ closeReviewNotes({ focusTarget: null });
5865
+ }
5866
+ scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
5867
+ ? document.activeElement
5868
+ : sourceTextEl;
5869
+ scratchpadOverlayEl.hidden = false;
5870
+ syncModalOpenState();
5871
+ if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
5872
+ const schedule = typeof window.requestAnimationFrame === "function"
5873
+ ? window.requestAnimationFrame.bind(window)
5874
+ : (cb) => window.setTimeout(cb, 16);
5875
+ schedule(() => {
5876
+ scratchpadTextEl.focus();
5877
+ if (typeof scratchpadTextEl.selectionStart === "number") {
5878
+ const end = scratchpadTextEl.value.length;
5879
+ scratchpadTextEl.setSelectionRange(end, end);
5880
+ }
5881
+ });
5882
+ }
5883
+ }
5884
+
5885
+ function closeReviewNotes(options) {
5886
+ if (!reviewNotesOverlayEl || reviewNotesOverlayEl.hidden) return;
5887
+ reviewNotesOverlayEl.hidden = true;
5888
+ updateReviewNotesUi();
5889
+ if (editorView === "markdown") {
5890
+ scheduleEditorLineNumberRender();
5891
+ }
5892
+ const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
5893
+ ? options.focusTarget
5894
+ : (reviewNotesReturnFocusEl || reviewNotesBtn || sourceTextEl);
5895
+ reviewNotesReturnFocusEl = null;
5896
+ if (focusTarget && typeof focusTarget.focus === "function") {
5897
+ const schedule = typeof window.requestAnimationFrame === "function"
5898
+ ? window.requestAnimationFrame.bind(window)
5899
+ : (cb) => window.setTimeout(cb, 16);
5900
+ schedule(() => focusTarget.focus());
5901
+ }
5902
+ }
5903
+
5904
+ function openReviewNotes() {
5905
+ if (!reviewNotesOverlayEl) return;
5906
+ if (isScratchpadOpen()) {
5907
+ closeScratchpad({ focusTarget: null });
5908
+ }
5909
+ reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
5910
+ ? document.activeElement
5911
+ : sourceTextEl;
5912
+ reviewNotesOverlayEl.hidden = false;
5913
+ renderReviewNotesList();
5914
+ updateReviewNotesUi();
5915
+ if (editorView === "markdown") {
5916
+ scheduleEditorLineNumberRender();
5917
+ }
5918
+ }
5919
+
5920
+ function toggleReviewNotes() {
5921
+ if (isReviewNotesOpen()) {
5922
+ closeReviewNotes({ focusTarget: reviewNotesBtn || sourceTextEl });
5923
+ } else {
5924
+ openReviewNotes();
5925
+ }
5926
+ }
5927
+
5928
+ function insertScratchpadIntoEditor() {
5929
+ const content = String(scratchpadText || "");
5930
+ if (!content.trim()) {
5931
+ setStatus("Scratchpad is empty.", "warning");
5932
+ return;
5933
+ }
5934
+
5935
+ const current = sourceTextEl.value || "";
5936
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : current.length;
5937
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
5938
+ const safeStart = Math.max(0, Math.min(start, current.length));
5939
+ const safeEnd = Math.max(safeStart, Math.min(end, current.length));
5940
+ const next = current.slice(0, safeStart) + content + current.slice(safeEnd);
5941
+ setEditorText(next, { preserveScroll: false, preserveSelection: false });
5942
+ const caret = safeStart + content.length;
5943
+ sourceTextEl.setSelectionRange(caret, caret);
5944
+ setActivePane("left");
5945
+ closeScratchpad({ focusTarget: sourceTextEl });
5946
+ setStatus("Inserted scratchpad into editor.", "success");
5947
+ }
5948
+
5949
+ function updateEditorHighlightState() {
5950
+ const enabled = editorHighlightEnabled && editorView === "markdown";
5951
+
5952
+ sourceTextEl.classList.toggle("highlight-active", enabled);
5953
+
5954
+ if (sourceHighlightEl) {
5955
+ sourceHighlightEl.hidden = !enabled;
5956
+ }
5957
+
5958
+ if (!enabled) {
5959
+ if (editorHighlightRenderRaf !== null) {
5960
+ if (typeof window.cancelAnimationFrame === "function") {
5961
+ window.cancelAnimationFrame(editorHighlightRenderRaf);
5962
+ } else {
5963
+ window.clearTimeout(editorHighlightRenderRaf);
5964
+ }
5965
+ editorHighlightRenderRaf = null;
5966
+ }
5967
+
5968
+ if (sourceHighlightEl) {
5969
+ sourceHighlightEl.innerHTML = "";
5970
+ sourceHighlightEl.scrollTop = 0;
5971
+ sourceHighlightEl.scrollLeft = 0;
5972
+ }
5973
+ return;
5974
+ }
5975
+
5976
+ scheduleEditorHighlightRender();
5977
+ syncEditorHighlightScroll();
5978
+ }
5979
+
5980
+ function syncHighlightSelectUi() {
5981
+ if (!highlightSelect) return;
5982
+ if (!editorHighlightEnabled) {
5983
+ highlightSelect.value = "off";
5984
+ return;
5985
+ }
5986
+ highlightSelect.value = (editorLanguage && SUPPORTED_LANGUAGES.indexOf(editorLanguage) !== -1)
5987
+ ? editorLanguage
5988
+ : "markdown";
5989
+ }
5990
+
5991
+ function setEditorHighlightEnabled(enabled) {
5992
+ editorHighlightEnabled = Boolean(enabled);
5993
+ persistEditorHighlightEnabled(editorHighlightEnabled);
5994
+ syncHighlightSelectUi();
5995
+ updateEditorHighlightState();
5996
+ }
5997
+
5998
+ function readStoredEditorLanguage() {
5999
+ if (!window.localStorage) return null;
6000
+ try {
6001
+ const value = window.localStorage.getItem(EDITOR_LANGUAGE_STORAGE_KEY);
6002
+ if (value && SUPPORTED_LANGUAGES.indexOf(value) !== -1) return value;
6003
+ return null;
6004
+ } catch {
6005
+ return null;
6006
+ }
6007
+ }
6008
+
6009
+ function persistEditorLanguage(lang) {
3812
6010
  if (!window.localStorage) return;
3813
6011
  try {
3814
6012
  window.localStorage.setItem(EDITOR_LANGUAGE_STORAGE_KEY, lang || "markdown");
@@ -3932,8 +6130,8 @@
3932
6130
  if (annotationModeSelect) {
3933
6131
  annotationModeSelect.value = annotationsEnabled ? "on" : "off";
3934
6132
  annotationModeSelect.title = annotationsEnabled
3935
- ? "Annotations On: keep and send [an: ...] markers."
3936
- : "Annotations Hidden: keep markers in editor, hide in preview, and strip before Run/Critique.";
6133
+ ? "Inline annotations On: keep and send [an: ...] markers."
6134
+ : "Inline annotations Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.";
3937
6135
  }
3938
6136
 
3939
6137
  syncRunAndCritiqueButtons();
@@ -4110,6 +6308,7 @@
4110
6308
 
4111
6309
  let loadedInitialDocument = false;
4112
6310
  if (
6311
+ !explicitDocumentIdentityFromUrl &&
4113
6312
  !initialDocumentApplied &&
4114
6313
  message.initialDocument &&
4115
6314
  typeof message.initialDocument.text === "string"
@@ -4121,6 +6320,9 @@
4121
6320
  source: message.initialDocument.source || "blank",
4122
6321
  label: message.initialDocument.label || "blank",
4123
6322
  path: message.initialDocument.path || null,
6323
+ draftId: typeof message.initialDocument.draftId === "string" && message.initialDocument.draftId.trim()
6324
+ ? message.initialDocument.draftId.trim()
6325
+ : (initialSourceState.draftId || null),
4124
6326
  });
4125
6327
  if (message.initialDocument.path) {
4126
6328
  markFileBackedBaseline(message.initialDocument.text);
@@ -4333,6 +6535,8 @@
4333
6535
  source: "file",
4334
6536
  label: message.label || message.path,
4335
6537
  path: message.path,
6538
+ }, {
6539
+ carryCurrentMetadataToNewDocument: true,
4336
6540
  });
4337
6541
  markFileBackedBaseline(sourceTextEl.value);
4338
6542
  }
@@ -4403,7 +6607,12 @@
4403
6607
  : null;
4404
6608
 
4405
6609
  setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
4406
- setSourceState({ source: nextSource, label: nextLabel, path: nextPath });
6610
+ setSourceState({
6611
+ source: nextSource,
6612
+ label: nextLabel,
6613
+ path: nextPath,
6614
+ draftId: typeof nextDoc.draftId === "string" && nextDoc.draftId.trim() ? nextDoc.draftId.trim() : null,
6615
+ });
4407
6616
  if (nextPath) {
4408
6617
  markFileBackedBaseline(nextDoc.text);
4409
6618
  }
@@ -4855,6 +7064,8 @@
4855
7064
  window.addEventListener("keydown", handlePaneShortcut);
4856
7065
  window.addEventListener("beforeunload", () => {
4857
7066
  stopFooterSpinner();
7067
+ flushScratchpadPersistence();
7068
+ flushReviewNotesPersistence();
4858
7069
  });
4859
7070
 
4860
7071
  editorViewSelect.addEventListener("change", () => {
@@ -5004,8 +7215,41 @@
5004
7215
  });
5005
7216
 
5006
7217
  sourceTextEl.addEventListener("input", () => {
7218
+ if (activePreviewCommentSelection) {
7219
+ clearPreviewCommentSelection();
7220
+ }
5007
7221
  renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
5008
7222
  scheduleEditorMetaUpdate();
7223
+ updateEditorSelectionCommentUi();
7224
+ if (isReviewNotesOpen() && reviewNotes.length > 0) {
7225
+ renderReviewNotesList();
7226
+ updateReviewNotesUi();
7227
+ }
7228
+ });
7229
+
7230
+ sourceTextEl.addEventListener("select", () => {
7231
+ updateEditorSelectionCommentUi();
7232
+ });
7233
+
7234
+ sourceTextEl.addEventListener("keyup", () => {
7235
+ updateEditorSelectionCommentUi();
7236
+ });
7237
+
7238
+ sourceTextEl.addEventListener("mouseup", () => {
7239
+ updateEditorSelectionCommentUi();
7240
+ });
7241
+
7242
+ sourceTextEl.addEventListener("focus", () => {
7243
+ updateEditorSelectionCommentUi();
7244
+ });
7245
+
7246
+ sourceTextEl.addEventListener("blur", () => {
7247
+ const schedule = typeof window.requestAnimationFrame === "function"
7248
+ ? window.requestAnimationFrame.bind(window)
7249
+ : (cb) => window.setTimeout(cb, 16);
7250
+ schedule(() => {
7251
+ updateEditorSelectionCommentUi();
7252
+ });
5009
7253
  });
5010
7254
 
5011
7255
  sourceTextEl.addEventListener("scroll", () => {
@@ -5026,9 +7270,7 @@
5026
7270
  window.addEventListener("resize", () => {
5027
7271
  if (editorView !== "markdown") return;
5028
7272
  syncEditorHighlightScroll();
5029
- if (lineNumbersEnabled) {
5030
- scheduleEditorLineNumberRender();
5031
- }
7273
+ scheduleEditorLineNumberRender();
5032
7274
  });
5033
7275
 
5034
7276
  insertHeaderBtn.addEventListener("click", () => {
@@ -5353,6 +7595,92 @@
5353
7595
  }
5354
7596
  });
5355
7597
 
7598
+ if (reviewNotesBtn) {
7599
+ reviewNotesBtn.addEventListener("click", () => {
7600
+ toggleReviewNotes();
7601
+ });
7602
+ }
7603
+
7604
+ if (reviewNotesCloseBtn) {
7605
+ reviewNotesCloseBtn.addEventListener("click", () => {
7606
+ closeReviewNotes();
7607
+ });
7608
+ }
7609
+
7610
+ if (reviewNotesDoneBtn) {
7611
+ reviewNotesDoneBtn.addEventListener("click", () => {
7612
+ closeReviewNotes();
7613
+ });
7614
+ }
7615
+
7616
+ if (reviewNotesAddBtn) {
7617
+ reviewNotesAddBtn.addEventListener("click", () => {
7618
+ addReviewNoteFromEditorLine();
7619
+ });
7620
+ }
7621
+
7622
+ if (editorSelectionCommentBtn) {
7623
+ editorSelectionCommentBtn.addEventListener("mousedown", (event) => {
7624
+ event.preventDefault();
7625
+ });
7626
+ editorSelectionCommentBtn.addEventListener("click", () => {
7627
+ addReviewNoteFromEditorSelection();
7628
+ });
7629
+ }
7630
+
7631
+ if (reviewNotesInlineAllBtn) {
7632
+ reviewNotesInlineAllBtn.addEventListener("click", () => {
7633
+ toggleAllReviewNotesInlineAnnotations();
7634
+ });
7635
+ }
7636
+
7637
+ if (reviewNoteGutterContentEl) {
7638
+ reviewNoteGutterContentEl.addEventListener("click", (event) => {
7639
+ const target = event.target;
7640
+ const markerBtn = target instanceof Element ? target.closest(".editor-review-note-marker") : null;
7641
+ if (!markerBtn) return;
7642
+ const noteId = markerBtn.getAttribute("data-review-note-id") || "";
7643
+ if (!noteId) return;
7644
+ focusReviewNoteInPanel(noteId);
7645
+ });
7646
+ }
7647
+
7648
+ function handlePreviewCommentActionMouseDown(event) {
7649
+ const target = event.target;
7650
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
7651
+ if (!actionBtn) return;
7652
+ event.preventDefault();
7653
+ }
7654
+
7655
+ function handlePreviewCommentActionClick(event) {
7656
+ const target = event.target;
7657
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
7658
+ if (!actionBtn) return;
7659
+ const blockEl = actionBtn.closest(".preview-comment-block");
7660
+ if (!blockEl) return;
7661
+ event.preventDefault();
7662
+ event.stopPropagation();
7663
+ const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
7664
+ if (mode !== "selection") return;
7665
+ addReviewNoteFromPreviewSelection(blockEl);
7666
+ }
7667
+
7668
+ if (leftPaneEl) {
7669
+ leftPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
7670
+ leftPaneEl.addEventListener("click", handlePreviewCommentActionClick);
7671
+ }
7672
+
7673
+ if (rightPaneEl) {
7674
+ rightPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
7675
+ rightPaneEl.addEventListener("click", handlePreviewCommentActionClick);
7676
+ }
7677
+
7678
+ if (typeof document.addEventListener === "function") {
7679
+ document.addEventListener("selectionchange", () => {
7680
+ updateActivePreviewCommentSelectionFromDom();
7681
+ });
7682
+ }
7683
+
5356
7684
  if (scratchpadBtn) {
5357
7685
  scratchpadBtn.addEventListener("click", () => {
5358
7686
  openScratchpad();
@@ -5487,6 +7815,11 @@
5487
7815
  syncActionButtons();
5488
7816
  renderSourcePreview();
5489
7817
  }
7818
+ if (sourceBadgeEl) {
7819
+ sourceBadgeEl.addEventListener("click", () => {
7820
+ resetEditorOrigin();
7821
+ });
7822
+ }
5490
7823
  if (resourceDirBtn) {
5491
7824
  resourceDirBtn.addEventListener("click", () => {
5492
7825
  showResourceDirState("input");
@@ -5558,7 +7891,7 @@
5558
7891
 
5559
7892
  if (sourceEditorWrapEl && typeof ResizeObserver === "function") {
5560
7893
  const editorResizeObserver = new ResizeObserver(() => {
5561
- if (editorView !== "markdown" || !lineNumbersEnabled) return;
7894
+ if (editorView !== "markdown") return;
5562
7895
  scheduleEditorLineNumberRender();
5563
7896
  });
5564
7897
  editorResizeObserver.observe(sourceEditorWrapEl);
@@ -5568,7 +7901,6 @@
5568
7901
  refreshResponseUi();
5569
7902
  updateAnnotatedReplyHeaderButton();
5570
7903
  setActivePane("left");
5571
- setScratchpadText(readStoredScratchpadText() || "", { persist: false });
5572
7904
 
5573
7905
  const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
5574
7906
  const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value !== "off");