pi-studio 0.5.44 → 0.5.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,6 +47,8 @@
47
47
  const sourceEditorWrapEl = document.getElementById("sourceEditorWrap");
48
48
  const sourceTextEl = document.getElementById("sourceText");
49
49
  const sourceHighlightEl = document.getElementById("sourceHighlight");
50
+ const reviewNoteGutterEl = document.getElementById("reviewNoteGutter");
51
+ const reviewNoteGutterContentEl = document.getElementById("reviewNoteGutterContent");
50
52
  const lineNumberGutterEl = document.getElementById("lineNumberGutter");
51
53
  const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
52
54
  const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
@@ -98,6 +100,7 @@
98
100
  const compactBtn = document.getElementById("compactBtn");
99
101
  const leftFocusBtn = document.getElementById("leftFocusBtn");
100
102
  const rightFocusBtn = document.getElementById("rightFocusBtn");
103
+ const reviewNotesBtn = document.getElementById("reviewNotesBtn");
101
104
  const scratchpadBtn = document.getElementById("scratchpadBtn");
102
105
  const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
103
106
  const scratchpadDialogEl = document.getElementById("scratchpadDialog");
@@ -108,16 +111,35 @@
108
111
  const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
109
112
  const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
110
113
  const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
114
+ const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
115
+ const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
116
+ const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
117
+ const reviewNotesListEl = document.getElementById("reviewNotesList");
118
+ const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
119
+ const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
120
+ const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
121
+ const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
122
+ const reviewNotesDoneBtn = document.getElementById("reviewNotesDoneBtn");
111
123
 
112
124
  const studioMode = (document.body && document.body.dataset && document.body.dataset.studioMode) === "editor-only"
113
125
  ? "editor-only"
114
126
  : "full";
115
127
  const isEditorOnlyMode = studioMode === "editor-only";
116
128
 
129
+ const initialQueryParams = new URLSearchParams(window.location.search || "");
130
+ const explicitDocumentIdentityFromUrl = initialQueryParams.has("docSource")
131
+ || initialQueryParams.has("docLabel")
132
+ || initialQueryParams.has("docPath")
133
+ || initialQueryParams.has("draftId");
117
134
  const initialSourceState = {
118
- source: (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,
135
+ source: initialQueryParams.get("docSource")
136
+ || ((document.body && document.body.dataset && document.body.dataset.initialSource) || "blank"),
137
+ label: initialQueryParams.get("docLabel")
138
+ || ((document.body && document.body.dataset && document.body.dataset.initialLabel) || "blank"),
139
+ path: initialQueryParams.get("docPath")
140
+ || ((document.body && document.body.dataset && document.body.dataset.initialPath) || null),
141
+ draftId: initialQueryParams.get("draftId")
142
+ || ((document.body && document.body.dataset && document.body.dataset.initialDraftId) || null),
121
143
  };
122
144
 
123
145
  let ws = null;
@@ -206,6 +228,7 @@
206
228
  source: initialSourceState.source,
207
229
  label: initialSourceState.label,
208
230
  path: initialSourceState.path,
231
+ draftId: initialSourceState.draftId,
209
232
  };
210
233
  let fileBackedBaselineText = null;
211
234
  let activePane = "left";
@@ -255,7 +278,6 @@
255
278
  const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
256
279
  const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
257
280
  const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
258
- const SCRATCHPAD_STORAGE_KEY = "piStudio.scratchpad";
259
281
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
260
282
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
261
283
  const previewPendingTimers = new WeakMap();
@@ -274,6 +296,14 @@
274
296
  let annotationsEnabled = true;
275
297
  let scratchpadText = "";
276
298
  let scratchpadReturnFocusEl = null;
299
+ let scratchpadPersistTimer = null;
300
+ let scratchpadLoadNonce = 0;
301
+ let reviewNotes = [];
302
+ let reviewNotesReturnFocusEl = null;
303
+ let reviewNotesPersistTimer = null;
304
+ let reviewNotesLoadNonce = 0;
305
+ let pendingReviewNoteFocusId = null;
306
+ let pendingReviewNoteInlineFocusId = null;
277
307
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
278
308
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
279
309
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
@@ -740,7 +770,8 @@
740
770
  if (terminalActivityPhase === "responding") {
741
771
  if (activeKind === "critique") return "Critiquing…";
742
772
  if (activeKind === "annotation") return "Replying…";
743
- return "Responding…";
773
+ if (activeKind === "direct") return "Thinking…";
774
+ return "Working…";
744
775
  }
745
776
 
746
777
  if (activeKind) return getTitleActionMessage(activeKind);
@@ -756,29 +787,26 @@
756
787
  }
757
788
 
758
789
  function buildStudioFaviconHref() {
759
- const fg = readThemeColor("--text", "#111111");
760
- const bg = readThemeColor("--bg", "#ffffff");
761
- const accent = readThemeColor("--accent", fg);
790
+ const idleColor = readThemeColor("--text", "#111111");
791
+ const accent = readThemeColor("--accent", "#2563eb");
762
792
  const ok = readThemeColor("--ok", "#16a34a");
763
- const warn = readThemeColor("--warn", accent);
793
+ const warn = readThemeColor("--warn", "#d97706");
764
794
  const error = readThemeColor("--error", "#dc2626");
765
795
 
766
- let badgeSvg = "";
767
-
796
+ let piColor = idleColor;
768
797
  if (titleAttentionMessage) {
769
- badgeSvg = `<circle cx="50" cy="14" r="9" fill="${ok}" stroke="${bg}" stroke-width="4" />`;
798
+ piColor = ok;
770
799
  } else if (wsState === "Disconnected") {
771
- badgeSvg = `<circle cx="50" cy="14" r="9" fill="${error}" stroke="${bg}" stroke-width="4" />`;
800
+ piColor = error;
772
801
  } else if (wsState === "Connecting") {
773
- badgeSvg = `<circle cx="50" cy="14" r="9" fill="${accent}" stroke="${bg}" stroke-width="4" />`;
802
+ piColor = accent;
774
803
  } else if (getTitleBusyMessage()) {
775
- badgeSvg = `<circle cx="50" cy="14" r="10" fill="none" stroke="${warn}" stroke-width="5" />`;
804
+ piColor = warn;
776
805
  }
777
806
 
778
807
  const svg = [
779
808
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">',
780
- `<text x="32" y="35" text-anchor="middle" dominant-baseline="middle" font-size="50" font-weight="700" font-family="ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" fill="${fg}">π</text>`,
781
- badgeSvg,
809
+ `<text x="32" y="35" text-anchor="middle" dominant-baseline="middle" font-size="50" font-weight="700" font-family="ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" fill="${piColor}">π</text>`,
782
810
  "</svg>",
783
811
  ].join("");
784
812
  return "data:image/svg+xml," + encodeURIComponent(svg);
@@ -938,6 +966,12 @@
938
966
  function updateSourceBadge() {
939
967
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
940
968
  sourceBadgeEl.textContent = "Editor origin: " + label;
969
+ const descriptor = getCurrentStudioDocumentDescriptor();
970
+ if (sourceBadgeEl) {
971
+ sourceBadgeEl.title = descriptor.fileBacked
972
+ ? ("Editor origin: " + label + "\nClick to reset origin and detach the current editor text into a new draft. The file on disk will not be changed.")
973
+ : ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes.");
974
+ }
941
975
  // Show "Set working dir" button when not file-backed
942
976
  var isFileBacked = hasRefreshableFilePath();
943
977
  if (isFileBacked) {
@@ -961,6 +995,26 @@
961
995
  }
962
996
  }
963
997
 
998
+ function resetEditorOrigin() {
999
+ const descriptor = getCurrentStudioDocumentDescriptor();
1000
+ const message = descriptor.fileBacked
1001
+ ? ("Reset editor origin and detach the current text from\n\n" + descriptor.label + "\n\ninto a new draft? The file on disk will not be changed, and the current scratchpad/review notes will carry into the new draft.")
1002
+ : ("Reset editor origin and start a new independent draft? The current editor text, scratchpad, and review notes will carry into the new draft.");
1003
+ if (!window.confirm(message)) {
1004
+ return;
1005
+ }
1006
+ const nextLabel = String(sourceTextEl.value || "").trim() ? "draft" : "blank";
1007
+ setSourceState({
1008
+ source: "blank",
1009
+ label: nextLabel,
1010
+ path: null,
1011
+ draftId: makeStudioDraftId(),
1012
+ }, {
1013
+ carryCurrentMetadataToNewDocument: true,
1014
+ });
1015
+ setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
1016
+ }
1017
+
964
1018
  function updatePaneFocusButtons() {
965
1019
  [
966
1020
  [leftFocusBtn, "left"],
@@ -1062,6 +1116,12 @@
1062
1116
  && typeof scratchpadDialogEl.contains === "function"
1063
1117
  && scratchpadDialogEl.contains(event.target)
1064
1118
  );
1119
+ const reviewNotesOwnsEvent = Boolean(
1120
+ reviewNotesDialogEl
1121
+ && event.target
1122
+ && typeof reviewNotesDialogEl.contains === "function"
1123
+ && reviewNotesDialogEl.contains(event.target)
1124
+ );
1065
1125
 
1066
1126
  if (isScratchpadOpen() && plainEscape) {
1067
1127
  event.preventDefault();
@@ -1069,7 +1129,13 @@
1069
1129
  return;
1070
1130
  }
1071
1131
 
1072
- if (scratchpadOwnsEvent) {
1132
+ if (isReviewNotesOpen() && plainEscape) {
1133
+ event.preventDefault();
1134
+ closeReviewNotes();
1135
+ return;
1136
+ }
1137
+
1138
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent) {
1073
1139
  return;
1074
1140
  }
1075
1141
 
@@ -2545,7 +2611,7 @@
2545
2611
  if (editorHighlightEnabled && editorView === "markdown") {
2546
2612
  scheduleEditorHighlightRender();
2547
2613
  }
2548
- if (lineNumbersEnabled && editorView === "markdown") {
2614
+ if (editorView === "markdown") {
2549
2615
  scheduleEditorLineNumberRender();
2550
2616
  }
2551
2617
  if (rightView === "editor-preview") {
@@ -2772,6 +2838,7 @@
2772
2838
  const canRefreshFromDisk = hasRefreshableFilePath();
2773
2839
 
2774
2840
  fileInput.disabled = uiBusy;
2841
+ if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
2775
2842
  saveAsBtn.disabled = uiBusy;
2776
2843
  saveOverBtn.disabled = uiBusy || !canSaveOver;
2777
2844
  if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
@@ -2805,17 +2872,33 @@
2805
2872
  syncActionButtons();
2806
2873
  }
2807
2874
 
2808
- function setSourceState(next) {
2875
+ function setSourceState(next, options) {
2876
+ const previousDescriptor = getCurrentStudioDocumentDescriptor();
2877
+ const nextPath = next && next.path ? next.path : null;
2809
2878
  sourceState = {
2810
2879
  source: next && next.source ? next.source : "blank",
2811
2880
  label: next && next.label ? next.label : "blank",
2812
- path: next && next.path ? next.path : null,
2881
+ path: nextPath,
2882
+ draftId: nextPath
2883
+ ? null
2884
+ : (next && next.draftId ? next.draftId : makeStudioDraftId()),
2813
2885
  };
2814
2886
  if (!sourceState.path) {
2815
2887
  clearFileBackedBaseline();
2816
2888
  }
2889
+ updateStudioDocumentUrlState(sourceState);
2817
2890
  updateSourceBadge();
2818
2891
  syncActionButtons();
2892
+ updateScratchpadUi();
2893
+ updateReviewNotesUi();
2894
+ loadScratchpadForCurrentDocument({
2895
+ previousDescriptor: previousDescriptor,
2896
+ carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
2897
+ });
2898
+ void loadReviewNotesForCurrentDocument({
2899
+ previousDescriptor: previousDescriptor,
2900
+ carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
2901
+ });
2819
2902
  }
2820
2903
 
2821
2904
  function setEditorText(nextText, options) {
@@ -2848,7 +2931,7 @@
2848
2931
  schedule(() => {
2849
2932
  syncEditorHighlightScroll();
2850
2933
  });
2851
- if (lineNumbersEnabled && editorView === "markdown") {
2934
+ if (editorView === "markdown") {
2852
2935
  scheduleEditorLineNumberRender();
2853
2936
  }
2854
2937
 
@@ -2888,9 +2971,10 @@
2888
2971
  updateEditorHighlightState();
2889
2972
  syncHighlightSelectUi();
2890
2973
  updateLineNumberGutterVisibility();
2891
- if (!showPreview && lineNumbersEnabled) {
2974
+ if (!showPreview) {
2892
2975
  scheduleEditorLineNumberRender();
2893
2976
  }
2977
+ updateReviewNotesUi();
2894
2978
  }
2895
2979
 
2896
2980
  function setRightView(nextView) {
@@ -2921,27 +3005,53 @@
2921
3005
  );
2922
3006
  }
2923
3007
 
3008
+ function reviewNoteGutterShouldBeVisible() {
3009
+ return Boolean(
3010
+ editorView === "markdown"
3011
+ && sourceEditorWrapEl
3012
+ && reviewNoteGutterEl
3013
+ && reviewNoteGutterContentEl
3014
+ && lineNumberMeasureEl
3015
+ && Array.isArray(reviewNotes)
3016
+ && reviewNotes.length > 0,
3017
+ );
3018
+ }
3019
+
2924
3020
  function getEditorLineNumberGutterWidthCss(lineCount) {
2925
3021
  const digits = Math.max(2, String(Math.max(1, lineCount || 0)).length);
2926
3022
  return "calc(" + digits + "ch + 18px)";
2927
3023
  }
2928
3024
 
2929
3025
  function updateLineNumberGutterVisibility() {
2930
- const visible = lineNumbersShouldBeVisible();
3026
+ const lineNumbersVisible = lineNumbersShouldBeVisible();
3027
+ const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
3028
+ const anyVisible = lineNumbersVisible || reviewMarkersVisible;
2931
3029
  if (sourceEditorWrapEl) {
2932
- sourceEditorWrapEl.classList.toggle("line-numbers-enabled", visible);
2933
- if (!visible) {
2934
- sourceEditorWrapEl.style.setProperty("--editor-line-number-gutter-width", "0px");
2935
- }
3030
+ sourceEditorWrapEl.classList.toggle("line-numbers-enabled", lineNumbersVisible);
3031
+ sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
3032
+ sourceEditorWrapEl.style.setProperty(
3033
+ "--editor-line-number-gutter-width",
3034
+ lineNumbersVisible
3035
+ ? getEditorLineNumberGutterWidthCss(Math.max(1, String(sourceTextEl.value || "").replace(/\r\n/g, "\n").split("\n").length))
3036
+ : "0px",
3037
+ );
3038
+ }
3039
+ if (reviewNoteGutterEl) {
3040
+ reviewNoteGutterEl.hidden = !reviewMarkersVisible;
2936
3041
  }
2937
3042
  if (lineNumberGutterEl) {
2938
- lineNumberGutterEl.hidden = !visible;
3043
+ lineNumberGutterEl.hidden = !lineNumbersVisible;
3044
+ }
3045
+ if (!reviewMarkersVisible && reviewNoteGutterContentEl) {
3046
+ reviewNoteGutterContentEl.innerHTML = "";
3047
+ }
3048
+ if (!lineNumbersVisible && lineNumberGutterContentEl) {
3049
+ lineNumberGutterContentEl.innerHTML = "";
2939
3050
  }
2940
- if (!visible) {
2941
- if (lineNumberGutterContentEl) lineNumberGutterContentEl.innerHTML = "";
2942
- if (lineNumberMeasureEl) lineNumberMeasureEl.innerHTML = "";
3051
+ if (!anyVisible && lineNumberMeasureEl) {
3052
+ lineNumberMeasureEl.innerHTML = "";
2943
3053
  }
2944
- return visible;
3054
+ return anyVisible;
2945
3055
  }
2946
3056
 
2947
3057
  function renderEditorLineNumbersNow() {
@@ -2950,7 +3060,16 @@
2950
3060
  const text = String(sourceTextEl.value || "").replace(/\r\n/g, "\n");
2951
3061
  const lines = text.split("\n");
2952
3062
  const lineCount = Math.max(1, lines.length);
2953
- sourceEditorWrapEl.style.setProperty("--editor-line-number-gutter-width", getEditorLineNumberGutterWidthCss(lineCount));
3063
+ const lineNumbersVisible = lineNumbersShouldBeVisible();
3064
+ const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
3065
+
3066
+ if (sourceEditorWrapEl) {
3067
+ sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
3068
+ sourceEditorWrapEl.style.setProperty(
3069
+ "--editor-line-number-gutter-width",
3070
+ lineNumbersVisible ? getEditorLineNumberGutterWidthCss(lineCount) : "0px",
3071
+ );
3072
+ }
2954
3073
 
2955
3074
  const styles = window.getComputedStyle(sourceTextEl);
2956
3075
  const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
@@ -2960,24 +3079,107 @@
2960
3079
  const paddingLeft = parseFloat(styles.paddingLeft) || 0;
2961
3080
  const contentWidth = Math.max(1, sourceTextEl.clientWidth - paddingLeft - paddingRight);
2962
3081
 
2963
- lineNumberGutterContentEl.style.paddingTop = paddingTop + "px";
2964
- lineNumberGutterContentEl.style.paddingBottom = paddingBottom + "px";
3082
+ if (lineNumberGutterContentEl) {
3083
+ lineNumberGutterContentEl.style.paddingTop = paddingTop + "px";
3084
+ lineNumberGutterContentEl.style.paddingBottom = paddingBottom + "px";
3085
+ }
3086
+ if (reviewNoteGutterContentEl) {
3087
+ reviewNoteGutterContentEl.style.paddingTop = paddingTop + "px";
3088
+ reviewNoteGutterContentEl.style.paddingBottom = paddingBottom + "px";
3089
+ }
2965
3090
  lineNumberMeasureEl.style.width = contentWidth + "px";
2966
3091
  lineNumberMeasureEl.innerHTML = lines
2967
3092
  .map((line) => "<div class='editor-line-number-measure-line'>" + (line.length ? escapeHtml(line) : "&#8203;") + "</div>")
2968
3093
  .join("");
2969
3094
 
2970
3095
  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("");
3096
+ const reviewNoteLineMap = reviewMarkersVisible ? buildReviewNoteLineMap(text) : null;
3097
+
3098
+ if (lineNumbersVisible && lineNumberGutterContentEl) {
3099
+ lineNumberGutterContentEl.innerHTML = measureLines
3100
+ .map((lineEl, index) => {
3101
+ const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
3102
+ return "<div class='editor-line-number-row' style='height:" + height.toFixed(2) + "px'>" + (index + 1) + "</div>";
3103
+ })
3104
+ .join("");
3105
+ } else if (lineNumberGutterContentEl) {
3106
+ lineNumberGutterContentEl.innerHTML = "";
3107
+ }
3108
+
3109
+ if (reviewMarkersVisible && reviewNoteGutterContentEl && reviewNoteLineMap) {
3110
+ reviewNoteGutterContentEl.innerHTML = measureLines
3111
+ .map((lineEl, index) => {
3112
+ const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
3113
+ const lineNumber = index + 1;
3114
+ const notesForLine = reviewNoteLineMap.get(lineNumber) || [];
3115
+ const count = notesForLine.length;
3116
+ if (count <= 0) {
3117
+ return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'></div>";
3118
+ }
3119
+ const title = count === 1
3120
+ ? ("1 local comment on line " + lineNumber + ". Open comments.")
3121
+ : (count + " local comments on line " + lineNumber + ". Open comments.");
3122
+ const markerLabel = count > 9 ? "9+" : (count > 1 ? String(count) : "•");
3123
+ return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'><button type='button' class='editor-review-note-marker"
3124
+ + (count > 1 ? " has-multiple" : "")
3125
+ + "' data-review-note-id='" + escapeHtml(notesForLine[0].id) + "' title='" + escapeHtml(title) + "' aria-label='" + escapeHtml(title) + "'>"
3126
+ + escapeHtml(markerLabel)
3127
+ + "</button></div>";
3128
+ })
3129
+ .join("");
3130
+ } else if (reviewNoteGutterContentEl) {
3131
+ reviewNoteGutterContentEl.innerHTML = "";
3132
+ }
2977
3133
 
2978
3134
  syncEditorHighlightScroll();
2979
3135
  }
2980
3136
 
3137
+ function scrollEditorRangeIntoView(range) {
3138
+ if (!range || editorView !== "markdown") return;
3139
+ renderEditorLineNumbersNow();
3140
+
3141
+ const text = String(sourceTextEl.value || "");
3142
+ const startLine = getLineNumberAtOffset(text, range.start);
3143
+ const endLine = getLineNumberAtOffset(text, Math.max(range.start, range.end > range.start ? range.end - 1 : range.end));
3144
+ const styles = window.getComputedStyle(sourceTextEl);
3145
+ const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
3146
+ const paddingTop = parseFloat(styles.paddingTop) || 0;
3147
+ const paddingBottom = parseFloat(styles.paddingBottom) || 0;
3148
+ const measureLines = lineNumberMeasureEl ? Array.from(lineNumberMeasureEl.children) : [];
3149
+
3150
+ function getLineTop(lineNumber) {
3151
+ let top = paddingTop;
3152
+ for (let i = 0; i < lineNumber - 1; i += 1) {
3153
+ const lineEl = measureLines[i];
3154
+ top += Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
3155
+ }
3156
+ return top;
3157
+ }
3158
+
3159
+ function getLineBottom(lineNumber) {
3160
+ const lineEl = measureLines[Math.max(0, lineNumber - 1)];
3161
+ return getLineTop(lineNumber) + Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
3162
+ }
3163
+
3164
+ const rangeTop = getLineTop(startLine);
3165
+ const rangeBottom = getLineBottom(endLine);
3166
+ const viewportTop = sourceTextEl.scrollTop;
3167
+ const viewportBottom = viewportTop + sourceTextEl.clientHeight;
3168
+ const margin = Math.max(18, Math.round(sourceTextEl.clientHeight * 0.12));
3169
+
3170
+ let nextScrollTop = viewportTop;
3171
+ if (rangeTop - margin < viewportTop) {
3172
+ nextScrollTop = Math.max(0, rangeTop - margin);
3173
+ } else if (rangeBottom + margin > viewportBottom) {
3174
+ nextScrollTop = Math.max(0, rangeBottom - sourceTextEl.clientHeight + margin + paddingBottom);
3175
+ }
3176
+
3177
+ if (Math.abs(nextScrollTop - viewportTop) > 1) {
3178
+ sourceTextEl.scrollTop = nextScrollTop;
3179
+ syncEditorHighlightScroll();
3180
+ }
3181
+ }
3182
+
2981
3183
  function scheduleEditorLineNumberRender() {
2982
3184
  if (lineNumbersRenderRaf !== null) {
2983
3185
  if (typeof window.cancelAnimationFrame === "function") {
@@ -3025,6 +3227,78 @@
3025
3227
  return query.get("token") || hash.get("token") || "";
3026
3228
  }
3027
3229
 
3230
+ function buildAuthedStudioUrl(pathname, extraParams) {
3231
+ const token = getToken();
3232
+ if (!token) {
3233
+ throw new Error("Missing Studio token in URL.");
3234
+ }
3235
+ const params = new URLSearchParams(extraParams || {});
3236
+ params.set("token", token);
3237
+ return pathname + "?" + params.toString();
3238
+ }
3239
+
3240
+ function updateStudioDocumentUrlState(state) {
3241
+ try {
3242
+ const currentUrl = new URL(window.location.href);
3243
+ const params = currentUrl.searchParams;
3244
+ const nextState = state && typeof state === "object" ? state : sourceState;
3245
+ const nextSource = nextState && nextState.source ? String(nextState.source) : "blank";
3246
+ const nextLabel = nextState && nextState.label ? String(nextState.label) : "blank";
3247
+ const nextPath = nextState && nextState.path ? String(nextState.path) : "";
3248
+ const nextDraftId = nextState && nextState.draftId ? String(nextState.draftId) : "";
3249
+ if (nextSource) params.set("docSource", nextSource);
3250
+ else params.delete("docSource");
3251
+ if (nextLabel) params.set("docLabel", nextLabel);
3252
+ else params.delete("docLabel");
3253
+ if (nextPath) params.set("docPath", nextPath);
3254
+ else params.delete("docPath");
3255
+ if (nextDraftId) params.set("draftId", nextDraftId);
3256
+ else params.delete("draftId");
3257
+ window.history.replaceState(null, "", currentUrl.toString());
3258
+ } catch {
3259
+ // Ignore URL-state update failures.
3260
+ }
3261
+ }
3262
+
3263
+ async function fetchStudioJson(pathname, options) {
3264
+ const init = options || {};
3265
+ const headers = new Headers(init.headers || undefined);
3266
+ const method = String(init.method || "GET").toUpperCase();
3267
+ if (init.body != null && !headers.has("Content-Type")) {
3268
+ headers.set("Content-Type", "application/json");
3269
+ }
3270
+ const response = await fetch(buildAuthedStudioUrl(pathname, init.query), {
3271
+ method,
3272
+ headers,
3273
+ body: init.body,
3274
+ cache: "no-store",
3275
+ });
3276
+ let payload = null;
3277
+ try {
3278
+ payload = await response.json();
3279
+ } catch {
3280
+ payload = null;
3281
+ }
3282
+ if (!response.ok || !payload || payload.ok === false) {
3283
+ const message = payload && typeof payload.error === "string"
3284
+ ? payload.error
3285
+ : (response.status + " " + response.statusText).trim();
3286
+ throw new Error(message || (method + " " + pathname + " failed."));
3287
+ }
3288
+ return payload;
3289
+ }
3290
+
3291
+ function trySendStudioJsonBeacon(pathname, payload, extraParams) {
3292
+ try {
3293
+ if (!navigator.sendBeacon || typeof navigator.sendBeacon !== "function") return false;
3294
+ const body = JSON.stringify(payload || {});
3295
+ const blob = new Blob([body], { type: "application/json" });
3296
+ return navigator.sendBeacon(buildAuthedStudioUrl(pathname, extraParams), blob);
3297
+ } catch {
3298
+ return false;
3299
+ }
3300
+ }
3301
+
3028
3302
  function makeRequestId() {
3029
3303
  if (window.crypto && typeof window.crypto.randomUUID === "function") {
3030
3304
  return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -3032,6 +3306,10 @@
3032
3306
  return "req_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
3033
3307
  }
3034
3308
 
3309
+ function makeStudioDraftId() {
3310
+ return "draft_" + makeRequestId();
3311
+ }
3312
+
3035
3313
  function escapeHtml(text) {
3036
3314
  return text
3037
3315
  .replace(/&/g, "&amp;")
@@ -3549,6 +3827,9 @@
3549
3827
  sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
3550
3828
  sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
3551
3829
  }
3830
+ if (reviewNoteGutterEl) {
3831
+ reviewNoteGutterEl.scrollTop = sourceTextEl.scrollTop;
3832
+ }
3552
3833
  if (lineNumberGutterEl) {
3553
3834
  lineNumberGutterEl.scrollTop = sourceTextEl.scrollTop;
3554
3835
  }
@@ -3628,51 +3909,823 @@
3628
3909
  persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
3629
3910
  }
3630
3911
 
3631
- function readStoredText(storageKey) {
3632
- 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
+ }
3633
4008
  try {
3634
- const value = window.localStorage.getItem(storageKey);
3635
- 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 });
3636
4013
  } catch {
3637
- return null;
4014
+ if (loadNonce !== scratchpadLoadNonce) return;
4015
+ if (key !== getCurrentStudioDocumentDescriptor().key) return;
4016
+ setScratchpadText("", { persist: false });
3638
4017
  }
3639
4018
  }
3640
4019
 
3641
- function persistStoredText(storageKey, value) {
3642
- 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;
3643
4024
  try {
3644
- 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
+ });
3645
4031
  } catch {
3646
- // ignore storage failures
4032
+ // Ignore carry-over failures and just fall back to normal scope loading.
3647
4033
  }
3648
4034
  }
3649
4035
 
3650
- function isScratchpadOpen() {
3651
- 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
+ })();
3652
4046
  }
3653
4047
 
3654
- function readStoredScratchpadText() {
3655
- return readStoredText(SCRATCHPAD_STORAGE_KEY);
4048
+ function persistScratchpadText(value) {
4049
+ const descriptor = getCurrentStudioDocumentDescriptor();
4050
+ scheduleScratchpadPersistence(value, descriptor.key);
3656
4051
  }
3657
4052
 
3658
- function persistScratchpadText(value) {
3659
- 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");
3660
4712
  }
3661
4713
 
3662
4714
  function updateScratchpadUi() {
3663
4715
  const normalized = String(scratchpadText || "");
3664
4716
  const hasContent = Boolean(normalized.trim());
4717
+ const descriptor = getCurrentStudioDocumentDescriptor();
3665
4718
  if (scratchpadBtn) {
3666
4719
  scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
3667
4720
  scratchpadBtn.classList.toggle("has-content", hasContent);
3668
4721
  scratchpadBtn.title = hasContent
3669
- ? "Open 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.";
4722
+ ? ("Open the local persistent scratchpad for this document/draft. Scope: " + descriptor.label + ". File-backed docs come back across Pi restarts; unsaved drafts stay with this draft instance until saved or cleared.")
4723
+ : ("Open a local persistent scratchpad for this document/draft. Scope: " + descriptor.label + ". File-backed docs come back across Pi restarts; unsaved drafts stay with this draft instance until saved or cleared.");
3671
4724
  }
3672
4725
  if (scratchpadMetaEl) {
3673
4726
  scratchpadMetaEl.textContent = hasContent
3674
- ? "Saved locally · persists after close · " + normalized.length + " chars"
3675
- : "Empty · local only";
4727
+ ? ("Saved locally for this document/draft · " + normalized.length + " chars")
4728
+ : "Empty · local to this document/draft";
3676
4729
  }
3677
4730
  if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
3678
4731
  if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
@@ -3693,7 +4746,7 @@
3693
4746
  function closeScratchpad(options) {
3694
4747
  if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
3695
4748
  scratchpadOverlayEl.hidden = true;
3696
- document.body.classList.remove("scratchpad-open");
4749
+ syncModalOpenState();
3697
4750
  const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
3698
4751
  ? options.focusTarget
3699
4752
  : (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
@@ -3708,11 +4761,14 @@
3708
4761
 
3709
4762
  function openScratchpad() {
3710
4763
  if (!scratchpadOverlayEl) return;
4764
+ if (isReviewNotesOpen()) {
4765
+ closeReviewNotes({ focusTarget: null });
4766
+ }
3711
4767
  scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
3712
4768
  ? document.activeElement
3713
4769
  : sourceTextEl;
3714
4770
  scratchpadOverlayEl.hidden = false;
3715
- document.body.classList.add("scratchpad-open");
4771
+ syncModalOpenState();
3716
4772
  if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
3717
4773
  const schedule = typeof window.requestAnimationFrame === "function"
3718
4774
  ? window.requestAnimationFrame.bind(window)
@@ -3727,6 +4783,49 @@
3727
4783
  }
3728
4784
  }
3729
4785
 
4786
+ function closeReviewNotes(options) {
4787
+ if (!reviewNotesOverlayEl || reviewNotesOverlayEl.hidden) return;
4788
+ reviewNotesOverlayEl.hidden = true;
4789
+ updateReviewNotesUi();
4790
+ if (editorView === "markdown") {
4791
+ scheduleEditorLineNumberRender();
4792
+ }
4793
+ const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
4794
+ ? options.focusTarget
4795
+ : (reviewNotesReturnFocusEl || reviewNotesBtn || sourceTextEl);
4796
+ reviewNotesReturnFocusEl = null;
4797
+ if (focusTarget && typeof focusTarget.focus === "function") {
4798
+ const schedule = typeof window.requestAnimationFrame === "function"
4799
+ ? window.requestAnimationFrame.bind(window)
4800
+ : (cb) => window.setTimeout(cb, 16);
4801
+ schedule(() => focusTarget.focus());
4802
+ }
4803
+ }
4804
+
4805
+ function openReviewNotes() {
4806
+ if (!reviewNotesOverlayEl) return;
4807
+ if (isScratchpadOpen()) {
4808
+ closeScratchpad({ focusTarget: null });
4809
+ }
4810
+ reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
4811
+ ? document.activeElement
4812
+ : sourceTextEl;
4813
+ reviewNotesOverlayEl.hidden = false;
4814
+ renderReviewNotesList();
4815
+ updateReviewNotesUi();
4816
+ if (editorView === "markdown") {
4817
+ scheduleEditorLineNumberRender();
4818
+ }
4819
+ }
4820
+
4821
+ function toggleReviewNotes() {
4822
+ if (isReviewNotesOpen()) {
4823
+ closeReviewNotes({ focusTarget: reviewNotesBtn || sourceTextEl });
4824
+ } else {
4825
+ openReviewNotes();
4826
+ }
4827
+ }
4828
+
3730
4829
  function insertScratchpadIntoEditor() {
3731
4830
  const content = String(scratchpadText || "");
3732
4831
  if (!content.trim()) {
@@ -3932,8 +5031,8 @@
3932
5031
  if (annotationModeSelect) {
3933
5032
  annotationModeSelect.value = annotationsEnabled ? "on" : "off";
3934
5033
  annotationModeSelect.title = annotationsEnabled
3935
- ? "Annotations On: keep and send [an: ...] markers."
3936
- : "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.";
3937
5036
  }
3938
5037
 
3939
5038
  syncRunAndCritiqueButtons();
@@ -4110,6 +5209,7 @@
4110
5209
 
4111
5210
  let loadedInitialDocument = false;
4112
5211
  if (
5212
+ !explicitDocumentIdentityFromUrl &&
4113
5213
  !initialDocumentApplied &&
4114
5214
  message.initialDocument &&
4115
5215
  typeof message.initialDocument.text === "string"
@@ -4121,6 +5221,9 @@
4121
5221
  source: message.initialDocument.source || "blank",
4122
5222
  label: message.initialDocument.label || "blank",
4123
5223
  path: message.initialDocument.path || null,
5224
+ draftId: typeof message.initialDocument.draftId === "string" && message.initialDocument.draftId.trim()
5225
+ ? message.initialDocument.draftId.trim()
5226
+ : (initialSourceState.draftId || null),
4124
5227
  });
4125
5228
  if (message.initialDocument.path) {
4126
5229
  markFileBackedBaseline(message.initialDocument.text);
@@ -4333,6 +5436,8 @@
4333
5436
  source: "file",
4334
5437
  label: message.label || message.path,
4335
5438
  path: message.path,
5439
+ }, {
5440
+ carryCurrentMetadataToNewDocument: true,
4336
5441
  });
4337
5442
  markFileBackedBaseline(sourceTextEl.value);
4338
5443
  }
@@ -4403,7 +5508,12 @@
4403
5508
  : null;
4404
5509
 
4405
5510
  setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
4406
- setSourceState({ 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
+ });
4407
5517
  if (nextPath) {
4408
5518
  markFileBackedBaseline(nextDoc.text);
4409
5519
  }
@@ -4855,6 +5965,8 @@
4855
5965
  window.addEventListener("keydown", handlePaneShortcut);
4856
5966
  window.addEventListener("beforeunload", () => {
4857
5967
  stopFooterSpinner();
5968
+ flushScratchpadPersistence();
5969
+ flushReviewNotesPersistence();
4858
5970
  });
4859
5971
 
4860
5972
  editorViewSelect.addEventListener("change", () => {
@@ -5006,6 +6118,10 @@
5006
6118
  sourceTextEl.addEventListener("input", () => {
5007
6119
  renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
5008
6120
  scheduleEditorMetaUpdate();
6121
+ if (isReviewNotesOpen() && reviewNotes.length > 0) {
6122
+ renderReviewNotesList();
6123
+ updateReviewNotesUi();
6124
+ }
5009
6125
  });
5010
6126
 
5011
6127
  sourceTextEl.addEventListener("scroll", () => {
@@ -5026,9 +6142,7 @@
5026
6142
  window.addEventListener("resize", () => {
5027
6143
  if (editorView !== "markdown") return;
5028
6144
  syncEditorHighlightScroll();
5029
- if (lineNumbersEnabled) {
5030
- scheduleEditorLineNumberRender();
5031
- }
6145
+ scheduleEditorLineNumberRender();
5032
6146
  });
5033
6147
 
5034
6148
  insertHeaderBtn.addEventListener("click", () => {
@@ -5353,6 +6467,47 @@
5353
6467
  }
5354
6468
  });
5355
6469
 
6470
+ if (reviewNotesBtn) {
6471
+ reviewNotesBtn.addEventListener("click", () => {
6472
+ toggleReviewNotes();
6473
+ });
6474
+ }
6475
+
6476
+ if (reviewNotesCloseBtn) {
6477
+ reviewNotesCloseBtn.addEventListener("click", () => {
6478
+ closeReviewNotes();
6479
+ });
6480
+ }
6481
+
6482
+ if (reviewNotesDoneBtn) {
6483
+ reviewNotesDoneBtn.addEventListener("click", () => {
6484
+ closeReviewNotes();
6485
+ });
6486
+ }
6487
+
6488
+ if (reviewNotesAddBtn) {
6489
+ reviewNotesAddBtn.addEventListener("click", () => {
6490
+ addReviewNoteFromEditorSelection();
6491
+ });
6492
+ }
6493
+
6494
+ if (reviewNotesInlineAllBtn) {
6495
+ reviewNotesInlineAllBtn.addEventListener("click", () => {
6496
+ toggleAllReviewNotesInlineAnnotations();
6497
+ });
6498
+ }
6499
+
6500
+ if (reviewNoteGutterContentEl) {
6501
+ reviewNoteGutterContentEl.addEventListener("click", (event) => {
6502
+ const target = event.target;
6503
+ const markerBtn = target instanceof Element ? target.closest(".editor-review-note-marker") : null;
6504
+ if (!markerBtn) return;
6505
+ const noteId = markerBtn.getAttribute("data-review-note-id") || "";
6506
+ if (!noteId) return;
6507
+ focusReviewNoteInPanel(noteId);
6508
+ });
6509
+ }
6510
+
5356
6511
  if (scratchpadBtn) {
5357
6512
  scratchpadBtn.addEventListener("click", () => {
5358
6513
  openScratchpad();
@@ -5487,6 +6642,11 @@
5487
6642
  syncActionButtons();
5488
6643
  renderSourcePreview();
5489
6644
  }
6645
+ if (sourceBadgeEl) {
6646
+ sourceBadgeEl.addEventListener("click", () => {
6647
+ resetEditorOrigin();
6648
+ });
6649
+ }
5490
6650
  if (resourceDirBtn) {
5491
6651
  resourceDirBtn.addEventListener("click", () => {
5492
6652
  showResourceDirState("input");
@@ -5558,7 +6718,7 @@
5558
6718
 
5559
6719
  if (sourceEditorWrapEl && typeof ResizeObserver === "function") {
5560
6720
  const editorResizeObserver = new ResizeObserver(() => {
5561
- if (editorView !== "markdown" || !lineNumbersEnabled) return;
6721
+ if (editorView !== "markdown") return;
5562
6722
  scheduleEditorLineNumberRender();
5563
6723
  });
5564
6724
  editorResizeObserver.observe(sourceEditorWrapEl);
@@ -5568,7 +6728,6 @@
5568
6728
  refreshResponseUi();
5569
6729
  updateAnnotatedReplyHeaderButton();
5570
6730
  setActivePane("left");
5571
- setScratchpadText(readStoredScratchpadText() || "", { persist: false });
5572
6731
 
5573
6732
  const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
5574
6733
  const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value !== "off");