pi-studio 0.5.50 → 0.5.52

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.
@@ -53,7 +53,9 @@
53
53
  const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
54
54
  const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
55
55
  const sourcePreviewEl = document.getElementById("sourcePreview");
56
+ const editorSelectionActionsEl = document.getElementById("editorSelectionActions");
56
57
  const editorSelectionCommentBtn = document.getElementById("editorSelectionCommentBtn");
58
+ const editorSelectionJumpBtn = document.getElementById("editorSelectionJumpBtn");
57
59
  const leftPaneEl = document.getElementById("leftPane");
58
60
  const rightPaneEl = document.getElementById("rightPane");
59
61
  const sourceBadgeEl = document.getElementById("sourceBadge");
@@ -102,6 +104,7 @@
102
104
  const leftFocusBtn = document.getElementById("leftFocusBtn");
103
105
  const rightFocusBtn = document.getElementById("rightFocusBtn");
104
106
  const reviewNotesBtn = document.getElementById("reviewNotesBtn");
107
+ const outlineBtn = document.getElementById("outlineBtn");
105
108
  const scratchpadBtn = document.getElementById("scratchpadBtn");
106
109
  const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
107
110
  const scratchpadDialogEl = document.getElementById("scratchpadDialog");
@@ -112,6 +115,13 @@
112
115
  const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
113
116
  const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
114
117
  const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
118
+ const outlineOverlayEl = document.getElementById("outlineOverlay");
119
+ const outlineDialogEl = document.getElementById("outlineDialog");
120
+ const outlineMetaEl = document.getElementById("outlineMeta");
121
+ const outlineListEl = document.getElementById("outlineList");
122
+ const outlineEmptyStateEl = document.getElementById("outlineEmptyState");
123
+ const outlineCloseBtn = document.getElementById("outlineCloseBtn");
124
+ const outlineDoneBtn = document.getElementById("outlineDoneBtn");
115
125
  const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
116
126
  const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
117
127
  const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
@@ -303,6 +313,8 @@
303
313
  let reviewNotesReturnFocusEl = null;
304
314
  let reviewNotesPersistTimer = null;
305
315
  let reviewNotesLoadNonce = 0;
316
+ let outlineEntries = [];
317
+ let outlineReturnFocusEl = null;
306
318
  let pendingReviewNoteFocusId = null;
307
319
  let pendingReviewNoteInlineFocusId = null;
308
320
  let activePreviewCommentSelection = null;
@@ -1128,6 +1140,12 @@
1128
1140
  && typeof reviewNotesDialogEl.contains === "function"
1129
1141
  && reviewNotesDialogEl.contains(event.target)
1130
1142
  );
1143
+ const outlineOwnsEvent = Boolean(
1144
+ outlineDialogEl
1145
+ && event.target
1146
+ && typeof outlineDialogEl.contains === "function"
1147
+ && outlineDialogEl.contains(event.target)
1148
+ );
1131
1149
 
1132
1150
  if (isScratchpadOpen() && plainEscape) {
1133
1151
  event.preventDefault();
@@ -1141,7 +1159,13 @@
1141
1159
  return;
1142
1160
  }
1143
1161
 
1144
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent) {
1162
+ if (isOutlineOpen() && plainEscape) {
1163
+ event.preventDefault();
1164
+ closeOutline();
1165
+ return;
1166
+ }
1167
+
1168
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
1145
1169
  return;
1146
1170
  }
1147
1171
 
@@ -2989,6 +3013,7 @@
2989
3013
  scheduleEditorMetaUpdate();
2990
3014
  }
2991
3015
  updateEditorSelectionCommentUi();
3016
+ updateOutlineUi();
2992
3017
  }
2993
3018
 
2994
3019
  function setEditorView(nextView) {
@@ -3023,6 +3048,7 @@
3023
3048
  }
3024
3049
  updateReviewNotesUi();
3025
3050
  updateEditorSelectionCommentUi();
3051
+ updateOutlineUi();
3026
3052
  }
3027
3053
 
3028
3054
  function setRightView(nextView) {
@@ -3859,11 +3885,6 @@
3859
3885
  + " data-review-note-line-end='" + String(lineNumber) + "'"
3860
3886
  + " data-preview-comment-kind='" + escapeHtml(kind) + "'"
3861
3887
  + ">"
3862
- + "<div class='preview-comment-controls'>"
3863
- + "<button type='button' class='preview-comment-summary' hidden></button>"
3864
- + "<button type='button' class='preview-comment-add' data-preview-comment-action='comment'>Comment</button>"
3865
- + "<button type='button' class='preview-comment-jump' data-preview-comment-action='jump'>Jump</button>"
3866
- + "</div>"
3867
3888
  + "<div class='preview-comment-block-content preview-code-line-content'>" + lineHtml + "</div>"
3868
3889
  + "</div>",
3869
3890
  );
@@ -3879,6 +3900,7 @@
3879
3900
  clearPreviewJumpHighlight(targetEl);
3880
3901
  finishPreviewRender(targetEl);
3881
3902
  targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
3903
+ ensurePreviewSelectionActions(targetEl);
3882
3904
  updatePreviewCommentBlocksForElement(targetEl);
3883
3905
  if (pane === "response") {
3884
3906
  applyPendingResponseScrollReset();
@@ -4027,6 +4049,10 @@
4027
4049
  return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
4028
4050
  }
4029
4051
 
4052
+ function isOutlineOpen() {
4053
+ return Boolean(outlineOverlayEl && !outlineOverlayEl.hidden);
4054
+ }
4055
+
4030
4056
  function isReviewNotesOpen() {
4031
4057
  return Boolean(reviewNotesOverlayEl && !reviewNotesOverlayEl.hidden);
4032
4058
  }
@@ -4200,6 +4226,400 @@
4200
4226
  };
4201
4227
  }
4202
4228
 
4229
+ function buildOutlineLineIndex(text) {
4230
+ const source = String(text || "").replace(/\r\n/g, "\n");
4231
+ const lines = source.split("\n");
4232
+ const lineOffsets = [];
4233
+ let runningOffset = 0;
4234
+ for (const line of lines) {
4235
+ lineOffsets.push(runningOffset);
4236
+ runningOffset += line.length + 1;
4237
+ }
4238
+ return { source, lines, lineOffsets };
4239
+ }
4240
+
4241
+ function makeOutlineEntry(options) {
4242
+ const entry = options && typeof options === "object" ? options : {};
4243
+ const label = typeof entry.label === "string" ? entry.label.trim() : "";
4244
+ if (!label) return null;
4245
+ const selectionStart = Math.max(0, Math.floor(Number(entry.selectionStart) || 0));
4246
+ const selectionEnd = Math.max(selectionStart, Math.floor(Number(entry.selectionEnd) || selectionStart));
4247
+ return {
4248
+ id: typeof entry.id === "string" && entry.id ? entry.id : makeRequestId(),
4249
+ kind: typeof entry.kind === "string" && entry.kind ? entry.kind : "section",
4250
+ depth: Math.max(1, Math.floor(Number(entry.depth) || 1)),
4251
+ label,
4252
+ lineStart: Math.max(1, Math.floor(Number(entry.lineStart) || 1)),
4253
+ lineEnd: Math.max(Math.max(1, Math.floor(Number(entry.lineStart) || 1)), Math.floor(Number(entry.lineEnd) || Math.max(1, Math.floor(Number(entry.lineStart) || 1)))),
4254
+ selectionStart,
4255
+ selectionEnd,
4256
+ selectedText: typeof entry.selectedText === "string" ? entry.selectedText : "",
4257
+ selectedDisplayText: typeof entry.selectedDisplayText === "string" && entry.selectedDisplayText ? entry.selectedDisplayText : label,
4258
+ };
4259
+ }
4260
+
4261
+ function getOutlineKindLabel(kind) {
4262
+ switch (String(kind || "")) {
4263
+ case "heading": return "Heading";
4264
+ case "section": return "Section";
4265
+ case "subsection": return "Subsection";
4266
+ case "subsubsection": return "Subsubsection";
4267
+ case "paragraph": return "Paragraph";
4268
+ case "subparagraph": return "Subparagraph";
4269
+ case "class": return "Class";
4270
+ case "function": return "Function";
4271
+ case "interface": return "Interface";
4272
+ case "enum": return "Enum";
4273
+ case "type": return "Type";
4274
+ case "struct": return "Struct";
4275
+ case "module": return "Module";
4276
+ case "macro": return "Macro";
4277
+ case "file": return "File";
4278
+ case "hunk": return "Hunk";
4279
+ default: return "Item";
4280
+ }
4281
+ }
4282
+
4283
+ function getOutlineKindBadge(kind) {
4284
+ switch (String(kind || "")) {
4285
+ case "section": return "§";
4286
+ case "subsection": return "§§";
4287
+ case "subsubsection": return "§3";
4288
+ case "paragraph": return "¶";
4289
+ case "subparagraph": return "¶2";
4290
+ case "class": return "class";
4291
+ case "function": return "def";
4292
+ case "interface": return "iface";
4293
+ case "enum": return "enum";
4294
+ case "type": return "type";
4295
+ case "struct": return "struct";
4296
+ case "module": return "mod";
4297
+ case "macro": return "macro";
4298
+ case "file": return "file";
4299
+ case "hunk": return "@@";
4300
+ default: return "#";
4301
+ }
4302
+ }
4303
+
4304
+ function scanMarkdownOutlineEntries(text) {
4305
+ const { source, lines, lineOffsets } = buildOutlineLineIndex(text);
4306
+ const entries = [];
4307
+ let activeFence = null;
4308
+
4309
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4310
+ const line = String(lines[lineIndex] || "");
4311
+ const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
4312
+ if (fenceMatch) {
4313
+ if (!activeFence) {
4314
+ activeFence = fenceMatch[1];
4315
+ } else if (fenceMatch[1][0] === activeFence[0] && fenceMatch[1].length >= activeFence.length) {
4316
+ activeFence = null;
4317
+ }
4318
+ continue;
4319
+ }
4320
+ if (activeFence) continue;
4321
+
4322
+ const atxMatch = line.match(/^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/);
4323
+ if (atxMatch) {
4324
+ const label = normalizeVisiblePreviewText(atxMatch[2] || "");
4325
+ const entry = makeOutlineEntry({
4326
+ kind: atxMatch[1].length === 1 ? "section" : atxMatch[1].length === 2 ? "subsection" : atxMatch[1].length === 3 ? "subsubsection" : "heading",
4327
+ depth: atxMatch[1].length,
4328
+ label,
4329
+ lineStart: lineIndex + 1,
4330
+ lineEnd: lineIndex + 1,
4331
+ selectionStart: lineOffsets[lineIndex] || 0,
4332
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4333
+ selectedText: line,
4334
+ selectedDisplayText: label,
4335
+ });
4336
+ if (entry) entries.push(entry);
4337
+ continue;
4338
+ }
4339
+
4340
+ const nextLine = lineIndex + 1 < lines.length ? String(lines[lineIndex + 1] || "") : "";
4341
+ const setextMatch = nextLine.match(/^ {0,3}(=+|-+)\s*$/);
4342
+ if (setextMatch && normalizeVisiblePreviewText(line)) {
4343
+ const depth = setextMatch[1][0] === "=" ? 1 : 2;
4344
+ const label = normalizeVisiblePreviewText(line);
4345
+ const entry = makeOutlineEntry({
4346
+ kind: depth === 1 ? "section" : "subsection",
4347
+ depth,
4348
+ label,
4349
+ lineStart: lineIndex + 1,
4350
+ lineEnd: lineIndex + 1,
4351
+ selectionStart: lineOffsets[lineIndex] || 0,
4352
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4353
+ selectedText: line,
4354
+ selectedDisplayText: label,
4355
+ });
4356
+ if (entry) entries.push(entry);
4357
+ lineIndex += 1;
4358
+ }
4359
+ }
4360
+
4361
+ return entries;
4362
+ }
4363
+
4364
+ const LATEX_OUTLINE_LEVEL_BY_COMMAND = {
4365
+ part: 1,
4366
+ chapter: 1,
4367
+ section: 1,
4368
+ subsection: 2,
4369
+ subsubsection: 3,
4370
+ paragraph: 4,
4371
+ subparagraph: 5,
4372
+ };
4373
+
4374
+ function scanLatexOutlineEntries(text) {
4375
+ const source = String(text || "").replace(/\r\n/g, "\n");
4376
+ const bodyRange = findLatexDocumentBodyRange(source);
4377
+ const bodyStart = Math.max(0, Math.min(bodyRange.start, source.length));
4378
+ const bodyEnd = Math.max(bodyStart, Math.min(bodyRange.end, source.length));
4379
+ const bodyText = source.slice(bodyStart, bodyEnd);
4380
+ const { lines, lineOffsets } = buildOutlineLineIndex(bodyText);
4381
+ const entries = [];
4382
+
4383
+ function getLine(index) {
4384
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
4385
+ }
4386
+
4387
+ function getStrippedLine(index) {
4388
+ return stripLatexPreviewComments(getLine(index)).trim();
4389
+ }
4390
+
4391
+ function isBibliographyCommandLine(index) {
4392
+ return /^\\(?:bibliographystyle|bibliography|printbibliography)\b/i.test(getStrippedLine(index));
4393
+ }
4394
+
4395
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4396
+ let chunk = getLine(lineIndex);
4397
+ let endLineIndex = lineIndex;
4398
+ let heading = readLatexHeadingChunk(chunk);
4399
+ if (/^\s*\\(?:part|chapter|section|subsection|subsubsection|paragraph|subparagraph)\b/.test(chunk)) {
4400
+ while (!heading && endLineIndex + 1 < lines.length && endLineIndex < lineIndex + 5) {
4401
+ endLineIndex += 1;
4402
+ chunk += "\n" + getLine(endLineIndex);
4403
+ heading = readLatexHeadingChunk(chunk);
4404
+ }
4405
+ }
4406
+ if (heading) {
4407
+ const label = extractLatexPreviewVisibleText(heading.titleText || "");
4408
+ const kind = String(heading.commandName || "section").replace(/\*$/, "").toLowerCase();
4409
+ const entry = makeOutlineEntry({
4410
+ kind,
4411
+ depth: LATEX_OUTLINE_LEVEL_BY_COMMAND[kind] || 1,
4412
+ label,
4413
+ lineStart: lineIndex + 1,
4414
+ lineEnd: endLineIndex + 1,
4415
+ selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
4416
+ selectionEnd: bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length,
4417
+ selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length),
4418
+ selectedDisplayText: label,
4419
+ });
4420
+ if (entry) entries.push(entry);
4421
+ lineIndex = endLineIndex;
4422
+ continue;
4423
+ }
4424
+
4425
+ if (isBibliographyCommandLine(lineIndex)) {
4426
+ let endLine = lineIndex;
4427
+ while (endLine + 1 < lines.length && isBibliographyCommandLine(endLine + 1)) {
4428
+ endLine += 1;
4429
+ }
4430
+ const entry = makeOutlineEntry({
4431
+ kind: "section",
4432
+ depth: 1,
4433
+ label: "References",
4434
+ lineStart: lineIndex + 1,
4435
+ lineEnd: endLine + 1,
4436
+ selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
4437
+ selectionEnd: bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length,
4438
+ selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length),
4439
+ selectedDisplayText: "References",
4440
+ });
4441
+ if (entry) entries.push(entry);
4442
+ lineIndex = endLine;
4443
+ }
4444
+ }
4445
+
4446
+ return entries;
4447
+ }
4448
+
4449
+ function scanPythonOutlineEntries(text) {
4450
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4451
+ const entries = [];
4452
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4453
+ const line = String(lines[lineIndex] || "");
4454
+ const classMatch = line.match(/^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
4455
+ const defMatch = line.match(/^(\s*)(?:async\s+def|def)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
4456
+ const match = classMatch || defMatch;
4457
+ if (!match) continue;
4458
+ const indent = String(match[1] || "").replace(/\t/g, " ").length;
4459
+ const label = String(match[2] || "");
4460
+ const kind = classMatch ? "class" : "function";
4461
+ const entry = makeOutlineEntry({
4462
+ kind,
4463
+ depth: Math.max(1, Math.floor(indent / 4) + 1),
4464
+ label,
4465
+ lineStart: lineIndex + 1,
4466
+ lineEnd: lineIndex + 1,
4467
+ selectionStart: lineOffsets[lineIndex] || 0,
4468
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4469
+ selectedText: line,
4470
+ selectedDisplayText: label,
4471
+ });
4472
+ if (entry) entries.push(entry);
4473
+ }
4474
+ return entries;
4475
+ }
4476
+
4477
+ function scanJsLikeOutlineEntries(text) {
4478
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4479
+ const entries = [];
4480
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4481
+ const line = String(lines[lineIndex] || "");
4482
+ const patterns = [
4483
+ { kind: "class", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4484
+ { kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/) },
4485
+ { kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/) },
4486
+ { kind: "interface", match: line.match(/^(\s*)(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4487
+ { kind: "enum", match: line.match(/^(\s*)(?:export\s+)?enum\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4488
+ { kind: "type", match: line.match(/^(\s*)(?:export\s+)?type\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
4489
+ ];
4490
+ const found = patterns.find((entry) => entry.match);
4491
+ if (!found || !found.match) continue;
4492
+ const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
4493
+ const label = String(found.match[2] || "");
4494
+ const entry = makeOutlineEntry({
4495
+ kind: found.kind,
4496
+ depth: Math.max(1, Math.floor(indent / 2) + 1),
4497
+ label,
4498
+ lineStart: lineIndex + 1,
4499
+ lineEnd: lineIndex + 1,
4500
+ selectionStart: lineOffsets[lineIndex] || 0,
4501
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4502
+ selectedText: line,
4503
+ selectedDisplayText: label,
4504
+ });
4505
+ if (entry) entries.push(entry);
4506
+ }
4507
+ return entries;
4508
+ }
4509
+
4510
+ function scanJuliaOutlineEntries(text) {
4511
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4512
+ const entries = [];
4513
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4514
+ const line = String(lines[lineIndex] || "");
4515
+ const patterns = [
4516
+ { kind: "module", match: line.match(/^(\s*)module\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
4517
+ { kind: "struct", match: line.match(/^(\s*)(?:mutable\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
4518
+ { kind: "function", match: line.match(/^(\s*)function\s+([A-Za-z_][A-Za-z0-9_!]*)\s*\(/) },
4519
+ { kind: "macro", match: line.match(/^(\s*)macro\s+([A-Za-z_][A-Za-z0-9_!]*)\b/) },
4520
+ ];
4521
+ const found = patterns.find((entry) => entry.match);
4522
+ if (!found || !found.match) continue;
4523
+ const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
4524
+ const label = String(found.match[2] || "");
4525
+ const entry = makeOutlineEntry({
4526
+ kind: found.kind,
4527
+ depth: Math.max(1, Math.floor(indent / 2) + 1),
4528
+ label,
4529
+ lineStart: lineIndex + 1,
4530
+ lineEnd: lineIndex + 1,
4531
+ selectionStart: lineOffsets[lineIndex] || 0,
4532
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4533
+ selectedText: line,
4534
+ selectedDisplayText: label,
4535
+ });
4536
+ if (entry) entries.push(entry);
4537
+ }
4538
+ return entries;
4539
+ }
4540
+
4541
+ function scanBashOutlineEntries(text) {
4542
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4543
+ const entries = [];
4544
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4545
+ const line = String(lines[lineIndex] || "");
4546
+ const match = line.match(/^(\s*)(?:function\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*\{/);
4547
+ if (!match) continue;
4548
+ const indent = String(match[1] || "").replace(/\t/g, " ").length;
4549
+ const label = String(match[2] || "");
4550
+ const entry = makeOutlineEntry({
4551
+ kind: "function",
4552
+ depth: Math.max(1, Math.floor(indent / 2) + 1),
4553
+ label,
4554
+ lineStart: lineIndex + 1,
4555
+ lineEnd: lineIndex + 1,
4556
+ selectionStart: lineOffsets[lineIndex] || 0,
4557
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4558
+ selectedText: line,
4559
+ selectedDisplayText: label,
4560
+ });
4561
+ if (entry) entries.push(entry);
4562
+ }
4563
+ return entries;
4564
+ }
4565
+
4566
+ function scanDiffOutlineEntries(text) {
4567
+ const { lines, lineOffsets } = buildOutlineLineIndex(text);
4568
+ const entries = [];
4569
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4570
+ const line = String(lines[lineIndex] || "");
4571
+ let kind = "";
4572
+ let label = "";
4573
+ let depth = 1;
4574
+ const fileMatch = line.match(/^diff\s+--git\s+a\/([^\s]+)\s+b\/([^\s]+)/);
4575
+ if (fileMatch) {
4576
+ kind = "file";
4577
+ label = String(fileMatch[2] || fileMatch[1] || "");
4578
+ depth = 1;
4579
+ } else if (/^@@/.test(line)) {
4580
+ kind = "hunk";
4581
+ label = line.replace(/^@@\s*|\s*@@.*$/g, "").trim() || line.trim();
4582
+ depth = 2;
4583
+ }
4584
+ if (!kind || !label) continue;
4585
+ const entry = makeOutlineEntry({
4586
+ kind,
4587
+ depth,
4588
+ label,
4589
+ lineStart: lineIndex + 1,
4590
+ lineEnd: lineIndex + 1,
4591
+ selectionStart: lineOffsets[lineIndex] || 0,
4592
+ selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
4593
+ selectedText: line,
4594
+ selectedDisplayText: label,
4595
+ });
4596
+ if (entry) entries.push(entry);
4597
+ }
4598
+ return entries;
4599
+ }
4600
+
4601
+ function scanOutlineEntries(text, language) {
4602
+ switch (String(language || "").toLowerCase()) {
4603
+ case "markdown":
4604
+ return scanMarkdownOutlineEntries(text);
4605
+ case "latex":
4606
+ return scanLatexOutlineEntries(text);
4607
+ case "python":
4608
+ return scanPythonOutlineEntries(text);
4609
+ case "javascript":
4610
+ case "typescript":
4611
+ return scanJsLikeOutlineEntries(text);
4612
+ case "julia":
4613
+ return scanJuliaOutlineEntries(text);
4614
+ case "bash":
4615
+ return scanBashOutlineEntries(text);
4616
+ case "diff":
4617
+ return scanDiffOutlineEntries(text);
4618
+ default:
4619
+ return [];
4620
+ }
4621
+ }
4622
+
4203
4623
  function cloneReviewNotes(notes) {
4204
4624
  return Array.isArray(notes)
4205
4625
  ? notes
@@ -5575,8 +5995,23 @@
5575
5995
  return bestIndex;
5576
5996
  }
5577
5997
 
5998
+ function buildLiteralPreviewDisplayMap(text, rawOffsets) {
5999
+ const source = String(text || "");
6000
+ const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
6001
+ const charStarts = [];
6002
+ const charEnds = [];
6003
+ for (let i = 0; i < source.length; i += 1) {
6004
+ charStarts.push(rawMap[i]);
6005
+ charEnds.push(rawMap[i] + 1);
6006
+ }
6007
+ return buildNormalizedPreviewDisplayMap(source, charStarts, charEnds);
6008
+ }
6009
+
5578
6010
  function buildPreviewSelectionDisplayMap(blockText, kind) {
5579
6011
  const body = buildPreviewSelectionSourceBody(blockText, kind);
6012
+ if (kind === "code-line" || kind === "diff-line" || kind === "text-line") {
6013
+ return buildLiteralPreviewDisplayMap(body.text, body.rawOffsets);
6014
+ }
5580
6015
  const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
5581
6016
  return buildNormalizedPreviewDisplayMap(inlineMap.text, inlineMap.charStarts, inlineMap.charEnds);
5582
6017
  }
@@ -5593,6 +6028,7 @@
5593
6028
  function getPreviewCommentSelectionKey(selection) {
5594
6029
  if (!selection) return "";
5595
6030
  return [
6031
+ String(selection.paneId || ""),
5596
6032
  String(selection.blockKey || ""),
5597
6033
  String(selection.selectionStart || 0),
5598
6034
  String(selection.selectionEnd || 0),
@@ -5620,6 +6056,96 @@
5620
6056
  : null;
5621
6057
  }
5622
6058
 
6059
+ function getPreviewSelectionPaneIdForNode(node) {
6060
+ if (!node) return "";
6061
+ const element = node instanceof Element ? node : node.parentElement;
6062
+ const paneEl = element && typeof element.closest === "function"
6063
+ ? element.closest("#sourcePreview, #critiqueView")
6064
+ : null;
6065
+ return paneEl && paneEl.id ? String(paneEl.id) : "";
6066
+ }
6067
+
6068
+ function getPreviewSelectionPaneElement(paneId) {
6069
+ if (paneId === "sourcePreview") return sourcePreviewEl;
6070
+ if (paneId === "critiqueView") return critiqueViewEl;
6071
+ return null;
6072
+ }
6073
+
6074
+ function getActivePreviewSelectionForPane(paneId) {
6075
+ if (!paneId) return null;
6076
+ return activePreviewCommentSelection && activePreviewCommentSelection.paneId === paneId
6077
+ ? activePreviewCommentSelection
6078
+ : null;
6079
+ }
6080
+
6081
+ function ensurePreviewSelectionActions(targetEl) {
6082
+ if (!targetEl || typeof document.createElement !== "function") return null;
6083
+ const paneId = targetEl.id ? String(targetEl.id) : "";
6084
+ if (!paneId) return null;
6085
+ const existing = Array.from(targetEl.children || []).find((child) => child.classList && child.classList.contains("preview-selection-actions"));
6086
+ if (existing) {
6087
+ existing.dataset.previewPane = paneId;
6088
+ return existing;
6089
+ }
6090
+
6091
+ const actionsEl = document.createElement("div");
6092
+ actionsEl.className = "preview-selection-actions";
6093
+ actionsEl.dataset.previewPane = paneId;
6094
+ actionsEl.hidden = true;
6095
+
6096
+ const commentBtn = document.createElement("button");
6097
+ commentBtn.type = "button";
6098
+ commentBtn.className = "preview-comment-add";
6099
+ commentBtn.dataset.previewCommentAction = "comment";
6100
+ commentBtn.textContent = "Comment";
6101
+ commentBtn.hidden = true;
6102
+ actionsEl.appendChild(commentBtn);
6103
+
6104
+ const jumpBtn = document.createElement("button");
6105
+ jumpBtn.type = "button";
6106
+ jumpBtn.className = "preview-comment-jump";
6107
+ jumpBtn.dataset.previewCommentAction = "jump";
6108
+ jumpBtn.textContent = "Jump";
6109
+ jumpBtn.hidden = true;
6110
+ actionsEl.appendChild(jumpBtn);
6111
+
6112
+ targetEl.insertBefore(actionsEl, targetEl.firstChild || null);
6113
+ return actionsEl;
6114
+ }
6115
+
6116
+ function updatePreviewSelectionActions(targetEl) {
6117
+ if (!targetEl) return;
6118
+ const actionsEl = ensurePreviewSelectionActions(targetEl);
6119
+ if (!actionsEl) return;
6120
+ const paneId = targetEl.id ? String(targetEl.id) : "";
6121
+ const selection = getActivePreviewSelectionForPane(paneId);
6122
+ const commentBtn = actionsEl.querySelector(".preview-comment-add");
6123
+ const jumpBtn = actionsEl.querySelector(".preview-comment-jump");
6124
+ if (!selection) {
6125
+ actionsEl.hidden = true;
6126
+ if (commentBtn) commentBtn.hidden = true;
6127
+ if (jumpBtn) jumpBtn.hidden = true;
6128
+ return;
6129
+ }
6130
+ const lineLabel = summarizeReviewNoteAnchor(selection).toLowerCase();
6131
+ const blockKindLabel = getPreviewCommentBlockKindLabel(selection.previewCommentKind || "paragraph");
6132
+ actionsEl.hidden = false;
6133
+ if (commentBtn) {
6134
+ commentBtn.hidden = false;
6135
+ commentBtn.dataset.previewCommentMode = "selection";
6136
+ commentBtn.dataset.previewPane = paneId;
6137
+ commentBtn.title = "Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").";
6138
+ commentBtn.setAttribute("aria-label", commentBtn.title || "Comment");
6139
+ }
6140
+ if (jumpBtn) {
6141
+ jumpBtn.hidden = false;
6142
+ jumpBtn.dataset.previewCommentMode = "selection";
6143
+ jumpBtn.dataset.previewPane = paneId;
6144
+ jumpBtn.title = "Jump to the current preview selection on this " + blockKindLabel + " in the raw editor (" + lineLabel + ").";
6145
+ jumpBtn.setAttribute("aria-label", jumpBtn.title || "Jump");
6146
+ }
6147
+ }
6148
+
5623
6149
  function unwrapPreviewJumpHighlightElement(element) {
5624
6150
  if (!element || !element.parentNode) return;
5625
6151
  const parent = element.parentNode;
@@ -6449,52 +6975,26 @@
6449
6975
 
6450
6976
  function updatePreviewCommentBlockState(blockEl, sourceText, displayNotes) {
6451
6977
  if (!blockEl || !blockEl.dataset) return;
6452
- const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
6453
- const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
6454
- const summaryBtn = blockEl.querySelector(".preview-comment-summary");
6455
- const addBtn = blockEl.querySelector(".preview-comment-add");
6456
- const jumpBtn = blockEl.querySelector(".preview-comment-jump");
6457
- const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
6458
- const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
6459
6978
  const blockKey = getPreviewCommentBlockKey(blockEl);
6460
- const hasSelection = Boolean(activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey);
6979
+ const paneId = getPreviewSelectionPaneIdForNode(blockEl);
6980
+ const hasSelection = Boolean(
6981
+ activePreviewCommentSelection
6982
+ && activePreviewCommentSelection.paneId === paneId
6983
+ && activePreviewCommentSelection.blockKey === blockKey
6984
+ );
6461
6985
 
6462
6986
  blockEl.classList.remove("has-comments");
6463
6987
  blockEl.classList.toggle("has-selection", hasSelection);
6464
-
6465
- if (summaryBtn) {
6466
- summaryBtn.hidden = true;
6467
- summaryBtn.textContent = "";
6468
- summaryBtn.dataset.reviewNoteId = "";
6469
- }
6470
-
6471
- if (addBtn) {
6472
- addBtn.hidden = !hasSelection;
6473
- addBtn.textContent = "Comment";
6474
- addBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
6475
- addBtn.title = hasSelection
6476
- ? ("Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").")
6477
- : "";
6478
- addBtn.setAttribute("aria-label", addBtn.title || "Comment");
6479
- }
6480
-
6481
- if (jumpBtn) {
6482
- jumpBtn.hidden = !hasSelection;
6483
- jumpBtn.textContent = "Jump";
6484
- jumpBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
6485
- jumpBtn.title = hasSelection
6486
- ? ("Jump to the current preview selection on this " + blockKindLabel + " in the raw editor (" + lineLabel + ").")
6487
- : "";
6488
- jumpBtn.setAttribute("aria-label", jumpBtn.title || "Jump");
6489
- }
6490
6988
  }
6491
6989
 
6492
6990
  function updatePreviewCommentBlocksForElement(targetEl) {
6493
6991
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6992
+ ensurePreviewSelectionActions(targetEl);
6494
6993
  const sourceText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
6495
6994
  Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
6496
6995
  updatePreviewCommentBlockState(blockEl, sourceText);
6497
6996
  });
6997
+ updatePreviewSelectionActions(targetEl);
6498
6998
  }
6499
6999
 
6500
7000
  function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
@@ -6522,43 +7022,20 @@
6522
7022
  wrapper.dataset.reviewNoteLineEnd = String(sourceBlock.lineEnd);
6523
7023
  wrapper.dataset.previewCommentKind = sourceBlock.kind;
6524
7024
 
6525
- const controls = document.createElement("div");
6526
- controls.className = "preview-comment-controls";
6527
-
6528
- const summaryBtn = document.createElement("button");
6529
- summaryBtn.type = "button";
6530
- summaryBtn.className = "preview-comment-summary";
6531
- summaryBtn.hidden = true;
6532
- controls.appendChild(summaryBtn);
6533
-
6534
- const addBtn = document.createElement("button");
6535
- addBtn.type = "button";
6536
- addBtn.className = "preview-comment-add";
6537
- addBtn.dataset.previewCommentAction = "comment";
6538
- addBtn.textContent = "Comment";
6539
- controls.appendChild(addBtn);
6540
-
6541
- const jumpBtn = document.createElement("button");
6542
- jumpBtn.type = "button";
6543
- jumpBtn.className = "preview-comment-jump";
6544
- jumpBtn.dataset.previewCommentAction = "jump";
6545
- jumpBtn.textContent = "Jump";
6546
- controls.appendChild(jumpBtn);
6547
-
6548
7025
  originalElement.replaceWith(wrapper);
6549
- wrapper.appendChild(controls);
6550
7026
  originalElement.classList.add("preview-comment-block-content");
6551
7027
  wrapper.appendChild(originalElement);
6552
7028
  }
6553
7029
 
7030
+ ensurePreviewSelectionActions(targetEl);
6554
7031
  updatePreviewCommentBlocksForElement(targetEl);
6555
7032
  }
6556
7033
 
6557
7034
  function refreshRenderedEditorPreviewComments() {
6558
- if (sourcePreviewEl && !sourcePreviewEl.hidden) {
7035
+ if (sourcePreviewEl) {
6559
7036
  updatePreviewCommentBlocksForElement(sourcePreviewEl);
6560
7037
  }
6561
- if (critiqueViewEl && rightView === "editor-preview") {
7038
+ if (critiqueViewEl) {
6562
7039
  updatePreviewCommentBlocksForElement(critiqueViewEl);
6563
7040
  }
6564
7041
  }
@@ -6792,10 +7269,11 @@
6792
7269
  }
6793
7270
 
6794
7271
  function revealReviewNoteInPreview(note) {
6795
- if (!supportsPreviewCommentsForCurrentEditor()) return;
7272
+ if (!supportsPreviewCommentsForCurrentEditor()) return false;
6796
7273
  if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
6797
- revealReviewNoteInPreviewElement(critiqueViewEl, note);
7274
+ return revealReviewNoteInPreviewElement(critiqueViewEl, note);
6798
7275
  }
7276
+ return false;
6799
7277
  }
6800
7278
 
6801
7279
  function updateActivePreviewCommentSelectionFromDom() {
@@ -6827,7 +7305,9 @@
6827
7305
 
6828
7306
  setActivePreviewCommentSelection({
6829
7307
  ...anchor,
7308
+ paneId: getPreviewSelectionPaneIdForNode(startBlock),
6830
7309
  blockKey: getPreviewCommentBlockKey(startBlock),
7310
+ previewCommentKind: String(startBlock.dataset && startBlock.dataset.previewCommentKind || "paragraph"),
6831
7311
  });
6832
7312
  }
6833
7313
 
@@ -6922,11 +7402,27 @@
6922
7402
  && typeof sourceTextEl.selectionEnd === "number"
6923
7403
  && sourceTextEl.selectionEnd > sourceTextEl.selectionStart
6924
7404
  );
7405
+ const canJumpToPreview = Boolean(
7406
+ hasSelection
7407
+ && rightView === "editor-preview"
7408
+ && critiqueViewEl
7409
+ && supportsPreviewCommentsForCurrentEditor()
7410
+ );
6925
7411
  editorSelectionCommentBtn.hidden = !hasSelection;
7412
+ if (editorSelectionJumpBtn) {
7413
+ editorSelectionJumpBtn.hidden = !canJumpToPreview;
7414
+ }
7415
+ if (editorSelectionActionsEl) {
7416
+ editorSelectionActionsEl.hidden = !hasSelection;
7417
+ }
6926
7418
  if (hasSelection) {
6927
7419
  editorSelectionCommentBtn.title = "Create a new local comment from the current editor selection.";
6928
7420
  editorSelectionCommentBtn.setAttribute("aria-label", editorSelectionCommentBtn.title);
6929
7421
  }
7422
+ if (editorSelectionJumpBtn && canJumpToPreview) {
7423
+ editorSelectionJumpBtn.title = "Jump to the current editor selection in the preview.";
7424
+ editorSelectionJumpBtn.setAttribute("aria-label", editorSelectionJumpBtn.title);
7425
+ }
6930
7426
  }
6931
7427
 
6932
7428
  function clearSuppressedEditorSelectionComment() {
@@ -6937,6 +7433,138 @@
6937
7433
  updateEditorSelectionCommentUi();
6938
7434
  }
6939
7435
 
7436
+ function getOutlineEntriesForCurrentEditor() {
7437
+ return scanOutlineEntries(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "", editorLanguage || "markdown");
7438
+ }
7439
+
7440
+ function updateOutlineUi() {
7441
+ outlineEntries = getOutlineEntriesForCurrentEditor();
7442
+ const descriptor = getCurrentStudioDocumentDescriptor();
7443
+ const count = outlineEntries.length;
7444
+ const hasEntries = count > 0;
7445
+ const isOpen = isOutlineOpen();
7446
+ if (outlineBtn) {
7447
+ outlineBtn.textContent = "Outline";
7448
+ outlineBtn.classList.remove("has-content");
7449
+ outlineBtn.classList.toggle("is-active", isOpen);
7450
+ outlineBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
7451
+ outlineBtn.title = isOpen
7452
+ ? "Hide document outline."
7453
+ : (hasEntries
7454
+ ? (count + " outline entr" + (count === 1 ? "y" : "ies") + " for " + descriptor.label + ". Open the outline rail.")
7455
+ : "Open document outline for the current editor text.");
7456
+ }
7457
+ if (outlineMetaEl) {
7458
+ outlineMetaEl.textContent = hasEntries
7459
+ ? (count + " entr" + (count === 1 ? "y" : "ies") + " · " + (editorLanguage || "text") + " · " + descriptor.label)
7460
+ : ("No outline entries · " + (editorLanguage || "text"));
7461
+ }
7462
+ if (outlineDoneBtn) {
7463
+ outlineDoneBtn.disabled = !isOpen;
7464
+ }
7465
+ if (outlineEmptyStateEl) {
7466
+ outlineEmptyStateEl.hidden = hasEntries;
7467
+ }
7468
+ renderOutlineList();
7469
+ }
7470
+
7471
+ function renderOutlineList() {
7472
+ if (!outlineListEl) return;
7473
+ outlineListEl.innerHTML = "";
7474
+ for (const entry of outlineEntries) {
7475
+ const itemBtn = document.createElement("button");
7476
+ itemBtn.type = "button";
7477
+ itemBtn.className = "outline-entry";
7478
+ itemBtn.dataset.outlineId = String(entry.id || "");
7479
+ itemBtn.style.paddingLeft = (10 + Math.max(0, (entry.depth || 1) - 1) * 14) + "px";
7480
+ itemBtn.title = getOutlineKindLabel(entry.kind) + " · line " + String(entry.lineStart || 1) + "\n" + String(entry.label || "");
7481
+
7482
+ const kindEl = document.createElement("span");
7483
+ kindEl.className = "outline-entry-kind";
7484
+ kindEl.textContent = getOutlineKindBadge(entry.kind);
7485
+ itemBtn.appendChild(kindEl);
7486
+
7487
+ const titleEl = document.createElement("span");
7488
+ titleEl.className = "outline-entry-title";
7489
+ titleEl.textContent = String(entry.label || "");
7490
+ itemBtn.appendChild(titleEl);
7491
+
7492
+ const metaEl = document.createElement("span");
7493
+ metaEl.className = "outline-entry-meta";
7494
+ metaEl.textContent = "L" + String(entry.lineStart || 1);
7495
+ itemBtn.appendChild(metaEl);
7496
+
7497
+ outlineListEl.appendChild(itemBtn);
7498
+ }
7499
+ }
7500
+
7501
+ function buildOutlineEntryAnchor(entry) {
7502
+ if (!entry) return null;
7503
+ return normalizeReviewNote({
7504
+ selectionStart: entry.selectionStart,
7505
+ selectionEnd: entry.selectionEnd,
7506
+ lineStart: entry.lineStart,
7507
+ lineEnd: entry.lineEnd,
7508
+ selectedText: entry.selectedText,
7509
+ selectedDisplayText: entry.selectedDisplayText || entry.label,
7510
+ });
7511
+ }
7512
+
7513
+ function jumpToOutlineEntry(entryId) {
7514
+ const entry = outlineEntries.find((candidate) => candidate && String(candidate.id || "") === String(entryId || ""));
7515
+ if (!entry) return false;
7516
+ const anchor = buildOutlineEntryAnchor(entry);
7517
+ if (!anchor) return false;
7518
+ return jumpToReviewAnchor(anchor, {
7519
+ statusMessage: "Jumped to outline entry.",
7520
+ afterJump: () => {
7521
+ revealReviewNoteInPreview(anchor);
7522
+ },
7523
+ });
7524
+ }
7525
+
7526
+ function closeOutline(options) {
7527
+ if (!outlineOverlayEl || outlineOverlayEl.hidden) return;
7528
+ outlineOverlayEl.hidden = true;
7529
+ updateOutlineUi();
7530
+ if (editorView === "markdown") {
7531
+ scheduleEditorLineNumberRender();
7532
+ }
7533
+ const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
7534
+ ? options.focusTarget
7535
+ : (outlineReturnFocusEl || outlineBtn || sourceTextEl);
7536
+ outlineReturnFocusEl = null;
7537
+ if (focusTarget && typeof focusTarget.focus === "function") {
7538
+ const schedule = typeof window.requestAnimationFrame === "function"
7539
+ ? window.requestAnimationFrame.bind(window)
7540
+ : (cb) => window.setTimeout(cb, 16);
7541
+ schedule(() => focusTarget.focus());
7542
+ }
7543
+ }
7544
+
7545
+ function openOutline() {
7546
+ if (!outlineOverlayEl) return;
7547
+ if (isReviewNotesOpen()) {
7548
+ closeReviewNotes({ focusTarget: null });
7549
+ }
7550
+ outlineReturnFocusEl = document.activeElement && document.activeElement !== document.body
7551
+ ? document.activeElement
7552
+ : sourceTextEl;
7553
+ outlineOverlayEl.hidden = false;
7554
+ updateOutlineUi();
7555
+ if (editorView === "markdown") {
7556
+ scheduleEditorLineNumberRender();
7557
+ }
7558
+ }
7559
+
7560
+ function toggleOutline() {
7561
+ if (isOutlineOpen()) {
7562
+ closeOutline({ focusTarget: outlineBtn || sourceTextEl });
7563
+ } else {
7564
+ openOutline();
7565
+ }
7566
+ }
7567
+
6940
7568
  function updateReviewNotesUi() {
6941
7569
  const descriptor = getCurrentStudioDocumentDescriptor();
6942
7570
  const count = reviewNotes.length;
@@ -7143,17 +7771,12 @@
7143
7771
  });
7144
7772
  }
7145
7773
 
7146
- function getActivePreviewSelectionAnchorForBlock(blockEl) {
7147
- if (!blockEl) return null;
7148
- const blockKey = getPreviewCommentBlockKey(blockEl);
7149
- return activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
7150
- ? activePreviewCommentSelection
7151
- : null;
7774
+ function getActivePreviewSelectionAnchorForPane(paneId) {
7775
+ return getActivePreviewSelectionForPane(paneId);
7152
7776
  }
7153
7777
 
7154
- function addReviewNoteFromPreviewSelection(blockEl) {
7155
- if (!blockEl) return null;
7156
- const anchor = getActivePreviewSelectionAnchorForBlock(blockEl);
7778
+ function addReviewNoteFromPreviewSelection(paneId) {
7779
+ const anchor = getActivePreviewSelectionAnchorForPane(paneId);
7157
7780
  if (!anchor) {
7158
7781
  setStatus("Select some preview text within a single block first.", "warning");
7159
7782
  return null;
@@ -7189,6 +7812,12 @@
7189
7812
  if (editorSelectionCommentBtn) {
7190
7813
  editorSelectionCommentBtn.hidden = true;
7191
7814
  }
7815
+ if (editorSelectionJumpBtn) {
7816
+ editorSelectionJumpBtn.hidden = true;
7817
+ }
7818
+ if (editorSelectionActionsEl) {
7819
+ editorSelectionActionsEl.hidden = true;
7820
+ }
7192
7821
  const shouldOpenReviewNotes = !isReviewNotesOpen();
7193
7822
  pendingReviewNoteFocusId = note.id;
7194
7823
  setReviewNotes(reviewNotes.concat([note]));
@@ -7218,6 +7847,30 @@
7218
7847
  });
7219
7848
  }
7220
7849
 
7850
+ function jumpToEditorSelectionInPreview() {
7851
+ if (editorView !== "markdown") {
7852
+ setStatus("Switch to Editor (Raw) before jumping from an editor selection.", "warning");
7853
+ return false;
7854
+ }
7855
+ if (rightView !== "editor-preview" || !critiqueViewEl || !supportsPreviewCommentsForCurrentEditor()) {
7856
+ setStatus("Open Editor (Preview) on the right to jump the current editor selection there.", "warning");
7857
+ return false;
7858
+ }
7859
+ const anchor = getEditorAnchorForReviewNote();
7860
+ const jumped = revealReviewNoteInPreview(anchor);
7861
+ if (!jumped) {
7862
+ setStatus("Could not find the current editor selection in the preview.", "warning");
7863
+ return false;
7864
+ }
7865
+ const current = String(sourceTextEl.value || "");
7866
+ const range = resolveReviewNoteRange(anchor, current);
7867
+ if (range) {
7868
+ scrollEditorRangeIntoView(range);
7869
+ }
7870
+ setStatus("Jumped to the current editor selection in the preview.", "success");
7871
+ return true;
7872
+ }
7873
+
7221
7874
  function addReviewNoteFromEditorLine() {
7222
7875
  if (editorView !== "markdown") {
7223
7876
  setStatus("Switch to Editor (Raw) before adding a line comment.", "warning");
@@ -7260,23 +7913,44 @@
7260
7913
  return true;
7261
7914
  }
7262
7915
 
7263
- function jumpToPreviewSelection(blockEl) {
7264
- if (!blockEl) return false;
7265
- const anchor = getActivePreviewSelectionAnchorForBlock(blockEl);
7916
+ function jumpToPreviewSelection(paneId) {
7917
+ const anchor = getActivePreviewSelectionAnchorForPane(paneId);
7266
7918
  if (!anchor) {
7267
7919
  setStatus("Select some preview text within a single block first.", "warning");
7268
7920
  return false;
7269
7921
  }
7270
- const jumped = jumpToReviewAnchor(anchor, {
7922
+ const previewNote = normalizeReviewNote(anchor);
7923
+ const jumped = jumpToReviewAnchor(previewNote, {
7271
7924
  statusMessage: "Jumped to preview selection in the raw editor.",
7925
+ afterJump: () => {
7926
+ const paneEl = getPreviewSelectionPaneElement(paneId);
7927
+ if (paneEl && previewNote) {
7928
+ revealReviewNoteInPreviewElement(paneEl, previewNote);
7929
+ }
7930
+ const schedule = typeof window.requestAnimationFrame === "function"
7931
+ ? window.requestAnimationFrame.bind(window)
7932
+ : (cb) => window.setTimeout(cb, 16);
7933
+ schedule(() => {
7934
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
7935
+ if (selection && typeof selection.removeAllRanges === "function") {
7936
+ selection.removeAllRanges();
7937
+ }
7938
+ clearPreviewCommentSelection();
7939
+ const current = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
7940
+ const range = resolveReviewNoteRange(previewNote, current);
7941
+ if (range && sourceTextEl) {
7942
+ try {
7943
+ sourceTextEl.focus({ preventScroll: true });
7944
+ } catch {
7945
+ sourceTextEl.focus();
7946
+ }
7947
+ if (typeof sourceTextEl.setSelectionRange === "function") {
7948
+ sourceTextEl.setSelectionRange(range.start, range.end);
7949
+ }
7950
+ }
7951
+ });
7952
+ },
7272
7953
  });
7273
- if (jumped) {
7274
- const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
7275
- if (selection && typeof selection.removeAllRanges === "function") {
7276
- selection.removeAllRanges();
7277
- }
7278
- clearPreviewCommentSelection();
7279
- }
7280
7954
  return jumped;
7281
7955
  }
7282
7956
 
@@ -7429,6 +8103,9 @@
7429
8103
  if (isReviewNotesOpen()) {
7430
8104
  closeReviewNotes({ focusTarget: null });
7431
8105
  }
8106
+ if (isOutlineOpen()) {
8107
+ closeOutline({ focusTarget: null });
8108
+ }
7432
8109
  scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
7433
8110
  ? document.activeElement
7434
8111
  : sourceTextEl;
@@ -7472,6 +8149,9 @@
7472
8149
  if (isScratchpadOpen()) {
7473
8150
  closeScratchpad({ focusTarget: null });
7474
8151
  }
8152
+ if (isOutlineOpen()) {
8153
+ closeOutline({ focusTarget: null });
8154
+ }
7475
8155
  reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
7476
8156
  ? document.activeElement
7477
8157
  : sourceTextEl;
@@ -7589,6 +8269,7 @@
7589
8269
  if (editorView === "preview") {
7590
8270
  scheduleSourcePreviewRender(0);
7591
8271
  }
8272
+ updateOutlineUi();
7592
8273
  }
7593
8274
 
7594
8275
  function setEditorHighlightMode(mode) {
@@ -8788,6 +9469,7 @@
8788
9469
  renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
8789
9470
  scheduleEditorMetaUpdate();
8790
9471
  updateEditorSelectionCommentUi();
9472
+ updateOutlineUi();
8791
9473
  if (isReviewNotesOpen() && reviewNotes.length > 0) {
8792
9474
  renderReviewNotesList();
8793
9475
  updateReviewNotesUi();
@@ -9176,6 +9858,35 @@
9176
9858
  });
9177
9859
  }
9178
9860
 
9861
+ if (outlineBtn) {
9862
+ outlineBtn.addEventListener("click", () => {
9863
+ toggleOutline();
9864
+ });
9865
+ }
9866
+
9867
+ if (outlineCloseBtn) {
9868
+ outlineCloseBtn.addEventListener("click", () => {
9869
+ closeOutline();
9870
+ });
9871
+ }
9872
+
9873
+ if (outlineDoneBtn) {
9874
+ outlineDoneBtn.addEventListener("click", () => {
9875
+ closeOutline();
9876
+ });
9877
+ }
9878
+
9879
+ if (outlineListEl) {
9880
+ outlineListEl.addEventListener("click", (event) => {
9881
+ const target = event.target;
9882
+ const entryBtn = target instanceof Element ? target.closest(".outline-entry") : null;
9883
+ if (!entryBtn) return;
9884
+ const outlineId = entryBtn.getAttribute("data-outline-id") || "";
9885
+ if (!outlineId) return;
9886
+ jumpToOutlineEntry(outlineId);
9887
+ });
9888
+ }
9889
+
9179
9890
  if (reviewNotesCloseBtn) {
9180
9891
  reviewNotesCloseBtn.addEventListener("click", () => {
9181
9892
  closeReviewNotes();
@@ -9203,6 +9914,15 @@
9203
9914
  });
9204
9915
  }
9205
9916
 
9917
+ if (editorSelectionJumpBtn) {
9918
+ editorSelectionJumpBtn.addEventListener("mousedown", (event) => {
9919
+ event.preventDefault();
9920
+ });
9921
+ editorSelectionJumpBtn.addEventListener("click", () => {
9922
+ jumpToEditorSelectionInPreview();
9923
+ });
9924
+ }
9925
+
9206
9926
  if (reviewNotesInlineAllBtn) {
9207
9927
  reviewNotesInlineAllBtn.addEventListener("click", () => {
9208
9928
  toggleAllReviewNotesInlineAnnotations();
@@ -9231,18 +9951,17 @@
9231
9951
  const target = event.target;
9232
9952
  const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
9233
9953
  if (!actionBtn) return;
9234
- const blockEl = actionBtn.closest(".preview-comment-block");
9235
- if (!blockEl) return;
9236
9954
  event.preventDefault();
9237
9955
  event.stopPropagation();
9238
9956
  const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
9239
9957
  if (!mode || !mode.startsWith("selection")) return;
9958
+ const paneId = String(actionBtn.dataset && actionBtn.dataset.previewPane ? actionBtn.dataset.previewPane : "");
9240
9959
  const action = String(actionBtn.dataset && actionBtn.dataset.previewCommentAction ? actionBtn.dataset.previewCommentAction : "comment");
9241
9960
  if (action === "jump") {
9242
- jumpToPreviewSelection(blockEl);
9961
+ jumpToPreviewSelection(paneId);
9243
9962
  return;
9244
9963
  }
9245
- addReviewNoteFromPreviewSelection(blockEl);
9964
+ addReviewNoteFromPreviewSelection(paneId);
9246
9965
  }
9247
9966
 
9248
9967
  if (leftPaneEl) {