pi-studio 0.5.43 → 0.5.45

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