@nkzw/codiff-core 1.9.1 → 1.11.0

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.
Files changed (31) hide show
  1. package/dist/SharedWalkthroughApp.d.ts +2 -1
  2. package/dist/app/components/DefinitionPopover.d.ts +12 -0
  3. package/dist/app/components/OpenReviewSourceDialog.d.ts +6 -0
  4. package/dist/app/components/OpenReviewSourceMenu.d.ts +5 -0
  5. package/dist/app/components/Panels.d.ts +8 -2
  6. package/dist/app/components/ReadOnlyMarkdownView.d.ts +1 -0
  7. package/dist/app/components/ResolvedThreadDisclosure.d.ts +9 -0
  8. package/dist/app/components/ReviewCodeView.d.ts +4 -2
  9. package/dist/app/components/ReviewModeControl.d.ts +14 -0
  10. package/dist/app/components/ReviewTopBar.d.ts +14 -13
  11. package/dist/app/components/merge-request/GeneralComments.d.ts +2 -1
  12. package/dist/app/components/walkthrough/NarrativeWalkthroughView.d.ts +6 -4
  13. package/dist/app/components/walkthrough/useNarrativeNavigation.d.ts +1 -1
  14. package/dist/app/hooks/useAppCommands.d.ts +3 -2
  15. package/dist/app/hooks/useAppReviewComments.d.ts +1 -0
  16. package/dist/app/hooks/useAppWalkthrough.d.ts +1 -1
  17. package/dist/app/hooks/useResizableSidebar.d.ts +2 -1
  18. package/dist/app/hooks/useReviewCommentDrafts.d.ts +1 -0
  19. package/dist/config/keyboard-layout.d.ts +13 -0
  20. package/dist/config/keymap.d.ts +3 -1
  21. package/dist/config/types.d.ts +3 -0
  22. package/dist/lib/app-types.d.ts +1 -0
  23. package/dist/lib/code-view-options.d.ts +1 -1
  24. package/dist/lib/files.d.ts +10 -1
  25. package/dist/lib/identifier-navigation.d.ts +10 -0
  26. package/dist/lib/keyboard.d.ts +1 -0
  27. package/dist/react.mjs +1163 -278
  28. package/dist/share.mjs +8 -7
  29. package/dist/style.css +392 -52
  30. package/dist/types.d.ts +37 -1
  31. package/package.json +6 -5
package/dist/react.mjs CHANGED
@@ -2,12 +2,13 @@ import { filterPatchToHunkIds, getSectionWalkthroughHunks, isGeneratedWalkthroug
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { Fragment, Suspense, forwardRef, lazy, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useOptimistic, useRef, useState, useTransition } from "react";
5
- import { useFormStatus } from "react-dom";
5
+ import { createPortal, useFormStatus } from "react-dom";
6
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { MarkdownEditor } from "@nkzw/mdx-editor";
8
8
  import { frontmatterPlugin, imagePlugin } from "@nkzw/mdx-editor/core";
9
9
  import { DownloadSimpleIcon } from "@phosphor-icons/react/DownloadSimple";
10
10
  import { Copy, Trash2, X } from "lucide-react";
11
+ import { CheckIcon } from "@phosphor-icons/react/Check";
11
12
  import { ArrowSquareOutIcon } from "@phosphor-icons/react/ArrowSquareOut";
12
13
  import { ChatCircleIcon } from "@phosphor-icons/react/ChatCircle";
13
14
  import { PathIcon } from "@phosphor-icons/react/Path";
@@ -17,8 +18,8 @@ import { hydratePartialDiff, parseDiffFromFile, parsePatchFiles, registerCustomT
17
18
  import useRelativeTime from "@nkzw/use-relative-time";
18
19
  import { CaretDownIcon } from "@phosphor-icons/react/CaretDown";
19
20
  import "@phosphor-icons/react/CaretUp";
20
- import { CheckIcon } from "@phosphor-icons/react/Check";
21
21
  import { CheckCircleIcon } from "@phosphor-icons/react/CheckCircle";
22
+ import "@phosphor-icons/react/CircleNotch";
22
23
  import { PowerIcon } from "@phosphor-icons/react/Power";
23
24
  import { SealQuestionIcon } from "@phosphor-icons/react/SealQuestion";
24
25
  import { WarningOctagonIcon } from "@phosphor-icons/react/WarningOctagon";
@@ -27,6 +28,8 @@ import { ColumnsIcon } from "@phosphor-icons/react/Columns";
27
28
  import { ImageBrokenIcon } from "@phosphor-icons/react/ImageBroken";
28
29
  import { SquareSplitVerticalIcon } from "@phosphor-icons/react/SquareSplitVertical";
29
30
  import { CodeView, WorkerPoolContextProvider } from "@pierre/diffs/react";
31
+ import { CrosshairSimpleIcon } from "@phosphor-icons/react/CrosshairSimple";
32
+ import { ProhibitIcon } from "@phosphor-icons/react/Prohibit";
30
33
  import { PersistentMarkdownEditor } from "@nkzw/mdx-editor/persistence";
31
34
  import { GitBranchIcon } from "@phosphor-icons/react/GitBranch";
32
35
  import { ShareNetworkIcon } from "@phosphor-icons/react/ShareNetwork";
@@ -293,13 +296,40 @@ const renderInlineMarkdown = (text) => {
293
296
  return nodes.length > 0 ? nodes : text;
294
297
  };
295
298
  //#endregion
299
+ //#region app/components/useCopiedState.ts
300
+ function useCopiedState(timeoutMs) {
301
+ const [copied, setCopied] = useState(false);
302
+ const copiedTimerRef = useRef(null);
303
+ useEffect(() => () => {
304
+ if (copiedTimerRef.current != null) window.clearTimeout(copiedTimerRef.current);
305
+ }, []);
306
+ return [copied, useCallback(() => {
307
+ setCopied(true);
308
+ if (copiedTimerRef.current != null) window.clearTimeout(copiedTimerRef.current);
309
+ copiedTimerRef.current = window.setTimeout(() => {
310
+ setCopied(false);
311
+ copiedTimerRef.current = null;
312
+ }, timeoutMs);
313
+ }, [timeoutMs])];
314
+ }
315
+ //#endregion
296
316
  //#region app/components/ReadOnlyMarkdownView.tsx
297
- const detailsBlockPattern = /<details\b([^>]*)>\s*<summary\b[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi;
298
317
  const htmlCommentPattern$1 = /<!--[\s\S]*?-->/g;
318
+ const detailsOpenPattern = /<details\b([^>]*)>/gi;
319
+ const detailsTagPattern = /<\/?details\b[^>]*>/gi;
320
+ const summaryOpenPattern = /^\s*<summary\b[^>]*>/i;
321
+ const summaryClosePattern = /<\/summary>/gi;
299
322
  const hasOpenAttribute = (attributes) => /(?:^|\s)open(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?(?:\s|$)/i.test(attributes);
300
323
  const stripHtmlComments$1 = (value) => value.replaceAll(htmlCommentPattern$1, "");
301
- const getFenceMarker = (line) => {
302
- return /^(?: {0,3})(`{3,}|~{3,})/.exec(line)?.[1] ?? null;
324
+ const getOpeningFenceMarker = (line) => {
325
+ const match = /^(?: {0,3})(`{3,}|~{3,})/.exec(line);
326
+ const marker = match?.[1] ?? null;
327
+ if (!marker || marker[0] === "`" && line.slice(match[0].length).includes("`")) return null;
328
+ return marker;
329
+ };
330
+ const isClosingFence = (marker, line) => {
331
+ const candidate = /^(?: {0,3})(`{3,}|~{3,})[ \t]*$/.exec(line)?.[1] ?? null;
332
+ return candidate != null && candidate[0] === marker[0] && candidate.length >= marker.length;
303
333
  };
304
334
  const normalizeReadOnlyMarkdownValue = (value) => {
305
335
  const normalizedLineEndings = value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
@@ -309,10 +339,9 @@ const normalizeReadOnlyMarkdownValue = (value) => {
309
339
  let pendingBlankLine = false;
310
340
  let fenceMarker = null;
311
341
  for (const line of lines) {
312
- const currentFenceMarker = getFenceMarker(line);
313
342
  if (fenceMarker) {
314
343
  normalizedLines.push(line);
315
- if (currentFenceMarker?.startsWith(fenceMarker[0]) && currentFenceMarker.length >= fenceMarker.length) fenceMarker = null;
344
+ if (isClosingFence(fenceMarker, line)) fenceMarker = null;
316
345
  continue;
317
346
  }
318
347
  if (!line.trim()) {
@@ -322,27 +351,83 @@ const normalizeReadOnlyMarkdownValue = (value) => {
322
351
  if (pendingBlankLine && normalizedLines.length > 0) normalizedLines.push("");
323
352
  pendingBlankLine = false;
324
353
  normalizedLines.push(line);
325
- if (currentFenceMarker) fenceMarker = currentFenceMarker;
354
+ const openingFenceMarker = getOpeningFenceMarker(line);
355
+ if (openingFenceMarker) fenceMarker = openingFenceMarker;
326
356
  }
327
357
  const normalizedValue = normalizedLines.join("\n");
328
358
  return preserveSingleTrailingNewline && normalizedValue ? `${normalizedValue}\n` : normalizedValue;
329
359
  };
360
+ const extractFencedCodeBlocks = (value) => {
361
+ const blocks = [];
362
+ let fenceMarker = null;
363
+ let currentBlock = [];
364
+ for (const line of value.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n")) {
365
+ if (fenceMarker) {
366
+ if (isClosingFence(fenceMarker, line)) {
367
+ blocks.push(currentBlock.join("\n"));
368
+ currentBlock = [];
369
+ fenceMarker = null;
370
+ continue;
371
+ }
372
+ currentBlock.push(line);
373
+ continue;
374
+ }
375
+ const openingFenceMarker = getOpeningFenceMarker(line);
376
+ if (openingFenceMarker) fenceMarker = openingFenceMarker;
377
+ }
378
+ while (fenceMarker && currentBlock.at(-1) === "") currentBlock.pop();
379
+ if (fenceMarker && currentBlock.length > 0) blocks.push(currentBlock.join("\n"));
380
+ return blocks.filter((block) => block.trim().length > 0);
381
+ };
330
382
  const parseMarkdownDetails = (value) => {
331
383
  const parts = [];
332
384
  let lastIndex = 0;
333
- let match;
334
- while (match = detailsBlockPattern.exec(value)) {
335
- if (match.index > lastIndex) parts.push({
385
+ let searchIndex = 0;
386
+ while (searchIndex < value.length) {
387
+ detailsOpenPattern.lastIndex = searchIndex;
388
+ const detailsOpen = detailsOpenPattern.exec(value);
389
+ if (!detailsOpen) break;
390
+ const detailsBodyStart = detailsOpenPattern.lastIndex;
391
+ const summaryOpen = summaryOpenPattern.exec(value.slice(detailsBodyStart));
392
+ if (!summaryOpen) {
393
+ searchIndex = detailsBodyStart;
394
+ continue;
395
+ }
396
+ const summaryStart = detailsBodyStart + summaryOpen[0].length;
397
+ summaryClosePattern.lastIndex = summaryStart;
398
+ const summaryClose = summaryClosePattern.exec(value);
399
+ if (!summaryClose) {
400
+ searchIndex = detailsBodyStart;
401
+ continue;
402
+ }
403
+ const bodyStart = summaryClosePattern.lastIndex;
404
+ detailsTagPattern.lastIndex = bodyStart;
405
+ let depth = 1;
406
+ let detailsClose = null;
407
+ while (depth > 0) {
408
+ const tag = detailsTagPattern.exec(value);
409
+ if (!tag) break;
410
+ if (/^<\/details\b/i.test(tag[0])) {
411
+ depth -= 1;
412
+ if (depth === 0) detailsClose = tag;
413
+ } else depth += 1;
414
+ }
415
+ if (!detailsClose) {
416
+ searchIndex = detailsBodyStart;
417
+ continue;
418
+ }
419
+ if (detailsOpen.index > lastIndex) parts.push({
336
420
  type: "markdown",
337
- value: value.slice(lastIndex, match.index)
421
+ value: value.slice(lastIndex, detailsOpen.index)
338
422
  });
339
423
  parts.push({
340
- body: match[3] ?? "",
341
- open: hasOpenAttribute(match[1] ?? ""),
342
- summary: stripHtmlComments$1(match[2] ?? "").trim(),
424
+ body: value.slice(bodyStart, detailsClose.index),
425
+ open: hasOpenAttribute(detailsOpen[1] ?? ""),
426
+ summary: stripHtmlComments$1(value.slice(summaryStart, summaryClose.index)).trim(),
343
427
  type: "details"
344
428
  });
345
- lastIndex = detailsBlockPattern.lastIndex;
429
+ lastIndex = detailsTagPattern.lastIndex;
430
+ searchIndex = lastIndex;
346
431
  }
347
432
  if (lastIndex < value.length) parts.push({
348
433
  type: "markdown",
@@ -351,6 +436,42 @@ const parseMarkdownDetails = (value) => {
351
436
  return parts;
352
437
  };
353
438
  const hasDetailsBlock = (parts) => parts.some((part) => part.type === "details");
439
+ const getOwnFencedCode = (parts) => parts.filter((part) => part.type === "markdown").flatMap((part) => extractFencedCodeBlocks(part.value)).join("\n\n");
440
+ function CopyCodeButton({ code }) {
441
+ const [copied, markCopied] = useCopiedState(1600);
442
+ const handleClick = useCallback(async (event) => {
443
+ event.preventDefault();
444
+ event.stopPropagation();
445
+ try {
446
+ await navigator.clipboard.writeText(code);
447
+ } catch {
448
+ return;
449
+ }
450
+ markCopied();
451
+ }, [code, markCopied]);
452
+ const label = copied ? "Code copied" : "Copy code";
453
+ return /* @__PURE__ */ jsx("button", {
454
+ "aria-label": label,
455
+ className: `codiff-copy-path-button codiff-copy-code-button${copied ? " copied" : ""}`,
456
+ onClick: (event) => void handleClick(event),
457
+ onKeyDown: (event) => {
458
+ if (event.key === "Enter" || event.key === " ") event.stopPropagation();
459
+ },
460
+ title: label,
461
+ type: "button",
462
+ children: copied ? /* @__PURE__ */ jsx(CheckIcon, {
463
+ "aria-hidden": true,
464
+ className: "codiff-copy-path-icon check",
465
+ size: 16,
466
+ weight: "bold"
467
+ }) : /* @__PURE__ */ jsx(Copy, {
468
+ "aria-hidden": true,
469
+ className: "codiff-copy-path-icon",
470
+ size: 16,
471
+ strokeWidth: 2.25
472
+ })
473
+ });
474
+ }
354
475
  function MarkdownSegment({ additionalPlugins, ariaLabel, contentClassName, editorClassName, onHeightChange, value }) {
355
476
  const normalizedValue = normalizeReadOnlyMarkdownValue(value);
356
477
  if (!normalizedValue.trim()) return null;
@@ -382,17 +503,22 @@ function MarkdownParts({ additionalPlugins, ariaLabel, contentClassName, editorC
382
503
  onHeightChange,
383
504
  value: part.value
384
505
  }, `markdown:${index}`);
506
+ const bodyParts = parseMarkdownDetails(part.body);
507
+ const code = getOwnFencedCode(bodyParts);
385
508
  return /* @__PURE__ */ jsxs("details", {
386
509
  className: "codiff-markdown-details",
387
510
  onToggle: (event) => onHeightChange?.(event.currentTarget.getBoundingClientRect().height),
388
511
  open: part.open,
389
- children: [/* @__PURE__ */ jsx("summary", { children: renderInlineMarkdown(part.summary || "Details") }), /* @__PURE__ */ jsx(MarkdownParts, {
512
+ children: [/* @__PURE__ */ jsxs("summary", { children: [/* @__PURE__ */ jsx("span", {
513
+ className: "codiff-markdown-details-summary-label",
514
+ children: renderInlineMarkdown(part.summary || "Details")
515
+ }), code ? /* @__PURE__ */ jsx(CopyCodeButton, { code }) : null] }), /* @__PURE__ */ jsx(MarkdownParts, {
390
516
  additionalPlugins,
391
517
  ariaLabel: `${ariaLabel} details`,
392
518
  contentClassName,
393
519
  editorClassName,
394
520
  onHeightChange,
395
- parts: parseMarkdownDetails(part.body)
521
+ parts: bodyParts
396
522
  })]
397
523
  }, `details:${index}`);
398
524
  });
@@ -496,8 +622,7 @@ const getPlanCommentTargetLabel = (anchor) => {
496
622
  const entity = anchor.kind === "text" ? "Selection" : planCommentEntityLabels[anchor.block.type] ?? "Block";
497
623
  const targetText = (anchor.kind === "text" ? anchor.quote?.exact : anchor.block.text)?.replaceAll(/\s+/g, " ").trim();
498
624
  if (!targetText) return entity;
499
- const maximumLength = 48;
500
- return `${entity} · ${targetText.length > maximumLength ? `${targetText.slice(0, maximumLength - 1).trimEnd()}…` : targetText}`;
625
+ return `${entity} · ${targetText.length > 48 ? `${targetText.slice(0, 47).trimEnd()}…` : targetText}`;
501
626
  };
502
627
  const escapeInlineMarkdown = (value) => value.replaceAll("\\", String.raw`\\`).replaceAll(/([`*_[\]<>])/g, String.raw`\$1`);
503
628
  const getPlanCommentAuthorLabel = (author) => {
@@ -1419,22 +1544,14 @@ const getFirstVisibleSection = (file, showWhitespace) => getVisibleDiffSections(
1419
1544
  //#region lib/files.ts
1420
1545
  const statusForTree = {
1421
1546
  added: "added",
1547
+ conflicted: "modified",
1422
1548
  deleted: "deleted",
1423
1549
  modified: "modified",
1424
1550
  renamed: "renamed",
1425
1551
  untracked: "untracked"
1426
1552
  };
1427
1553
  const fileTreeSort = (left, right) => compareTreePaths(left.path, right.path);
1428
- const compactPath = (path) => {
1429
- const homePath = path.replace(/^\/Users\/[^/]+(?=\/|$)/, "~").replace(/^\/home\/[^/]+(?=\/|$)/, "~");
1430
- const parts = homePath.split("/").filter(Boolean);
1431
- if (parts.length <= 2) return homePath;
1432
- const prefix = homePath.startsWith("/") ? "/" : "";
1433
- const [first, ...rest] = parts;
1434
- const last = rest.pop();
1435
- const middle = rest.map((part) => part[0]).join("/");
1436
- return `${prefix}${first}/${middle ? `${middle}/` : ""}${last}`;
1437
- };
1554
+ const abbreviateHomePath = (path) => path.replace(/^\/Users\/[^/]+(?=\/|$)/, "~").replace(/^\/home\/[^/]+(?=\/|$)/, "~").replace(/^[A-Za-z]:[/\\]Users[/\\][^/\\]+(?=[/\\]|$)/, "~");
1438
1555
  function compareTreePaths(leftPath, rightPath) {
1439
1556
  const leftParts = leftPath.split("/");
1440
1557
  const rightParts = rightPath.split("/");
@@ -1566,22 +1683,23 @@ function ReviewFileTree({ files, onActivatePath, reloadDeltaPaths = emptyPaths$2
1566
1683
  scrollSelectedPathIntoView,
1567
1684
  selectedPath
1568
1685
  ]);
1686
+ const handleTreeClick = useCallback((event) => {
1687
+ for (const target of event.nativeEvent.composedPath()) {
1688
+ if (!("getAttribute" in target) || typeof target.getAttribute !== "function") continue;
1689
+ const path = target.getAttribute("data-item-path");
1690
+ if (path && filePathSet.has(path)) {
1691
+ onActivatePath(path);
1692
+ return;
1693
+ }
1694
+ }
1695
+ }, [filePathSet, onActivatePath]);
1569
1696
  return /* @__PURE__ */ jsx("div", {
1570
1697
  className: "file-tree-shell",
1571
1698
  ref: treeHostRef,
1572
1699
  children: /* @__PURE__ */ jsx(FileTree, {
1573
1700
  className: "file-tree",
1574
1701
  model,
1575
- onClick: useCallback((event) => {
1576
- for (const target of event.nativeEvent.composedPath()) {
1577
- if (!("getAttribute" in target) || typeof target.getAttribute !== "function") continue;
1578
- const path = target.getAttribute("data-item-path");
1579
- if (path && filePathSet.has(path)) {
1580
- onActivatePath(path);
1581
- return;
1582
- }
1583
- }
1584
- }, [filePathSet, onActivatePath])
1702
+ onClick: handleTreeClick
1585
1703
  })
1586
1704
  });
1587
1705
  }
@@ -1633,26 +1751,168 @@ const syncTreeShadowStyle = (treeHost, styleAttribute, css) => {
1633
1751
  if (!existingStyle) shadowRoot.append(style);
1634
1752
  return true;
1635
1753
  };
1754
+ [...[..."abcdefghijklmnopqrstuvwxyz"].map((letter) => `Key${letter.toUpperCase()}`), ..."1234567890".split("").map((digit) => `Digit${digit}`)];
1755
+ const codeForCharacter = (character, shiftKey) => null;
1756
+ const shiftedCharacterForCode = (code) => null;
1757
+ const hasKeyboardLayout = () => false;
1636
1758
  //#endregion
1637
1759
  //#region config/keymap.ts
1638
1760
  const isMac = () => navigator.platform.toLowerCase().includes("mac");
1761
+ const unshiftedPunctuationCodes = {
1762
+ "-": "Minus",
1763
+ ",": "Comma",
1764
+ ";": "Semicolon",
1765
+ ".": "Period",
1766
+ "'": "Quote",
1767
+ "[": "BracketLeft",
1768
+ "]": "BracketRight",
1769
+ "/": "Slash",
1770
+ "\\": "Backslash",
1771
+ "`": "Backquote",
1772
+ "=": "Equal"
1773
+ };
1774
+ const shiftedPunctuationCodes = {
1775
+ _: "Minus",
1776
+ ":": "Semicolon",
1777
+ "!": "Digit1",
1778
+ "?": "Slash",
1779
+ "\"": "Quote",
1780
+ "(": "Digit9",
1781
+ ")": "Digit0",
1782
+ "{": "BracketLeft",
1783
+ "}": "BracketRight",
1784
+ "@": "Digit2",
1785
+ "*": "Digit8",
1786
+ "&": "Digit7",
1787
+ "#": "Digit3",
1788
+ "%": "Digit5",
1789
+ "^": "Digit6",
1790
+ "+": "Equal",
1791
+ "<": "Comma",
1792
+ ">": "Period",
1793
+ "|": "Backslash",
1794
+ "~": "Backquote",
1795
+ $: "Digit4"
1796
+ };
1797
+ const physicalCode = (key, shiftKey) => {
1798
+ const layoutCode = codeForCharacter(key, shiftKey);
1799
+ if (layoutCode !== null) return layoutCode;
1800
+ if (hasKeyboardLayout()) return null;
1801
+ if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
1802
+ if (/^[0-9]$/.test(key)) return shiftKey ? null : `Digit${key}`;
1803
+ return (shiftKey ? shiftedPunctuationCodes : unshiftedPunctuationCodes)[key] ?? null;
1804
+ };
1805
+ const resolveComboKey = (spelled, shiftKey) => {
1806
+ if (!shiftKey || spelled.length !== 1 || !hasKeyboardLayout()) return {
1807
+ code: physicalCode(spelled, shiftKey),
1808
+ key: spelled,
1809
+ requiresCode: false
1810
+ };
1811
+ const shiftedCode = codeForCharacter(spelled, true);
1812
+ if (shiftedCode !== null) return {
1813
+ code: shiftedCode,
1814
+ key: spelled,
1815
+ requiresCode: false
1816
+ };
1817
+ const unshiftedCode = codeForCharacter(spelled, false);
1818
+ const shiftedCharacter = unshiftedCode === null ? null : shiftedCharacterForCode(unshiftedCode);
1819
+ if (unshiftedCode !== null && shiftedCharacter !== null) return {
1820
+ code: unshiftedCode,
1821
+ key: shiftedCharacter,
1822
+ requiresCode: true
1823
+ };
1824
+ return {
1825
+ code: null,
1826
+ key: spelled,
1827
+ requiresCode: false
1828
+ };
1829
+ };
1639
1830
  const parseKeyCombo = (combo) => {
1640
1831
  const parts = combo.split("+").map((part) => part.trim().toLowerCase());
1641
1832
  const mac = isMac();
1833
+ const shiftKey = parts.includes("shift");
1834
+ const { code, key, requiresCode } = resolveComboKey(parts.find((part) => part !== "mod" && part !== "ctrl" && part !== "alt" && part !== "shift" && part !== "meta") ?? "", shiftKey);
1642
1835
  return {
1643
1836
  altKey: parts.includes("alt"),
1837
+ code,
1644
1838
  ctrlKey: mac ? parts.includes("ctrl") : parts.includes("mod") || parts.includes("ctrl"),
1645
- key: parts.find((part) => part !== "mod" && part !== "ctrl" && part !== "alt" && part !== "shift" && part !== "meta") ?? "",
1839
+ key,
1646
1840
  metaKey: mac ? parts.includes("mod") || parts.includes("meta") : parts.includes("meta"),
1647
- shiftKey: parts.includes("shift")
1841
+ requiresCode,
1842
+ shiftKey
1648
1843
  };
1649
1844
  };
1650
1845
  const getBindingCombos = (binding) => Array.isArray(binding) ? binding : [binding];
1651
- const matchesKeyCombo = (event, combo) => {
1846
+ const isDeadOrCharacterKey = (key) => key === "Dead" || key.length === 1;
1847
+ const matchesTypedKey = (event, parsed) => event.key.toLowerCase() === parsed.key && (!parsed.requiresCode || parsed.code !== null && event.code === parsed.code);
1848
+ const matchesPhysicalKey = (event, parsed) => parsed.altKey && parsed.code !== null && event.code === parsed.code;
1849
+ const matchesCombo = (event, combo, matchesKey) => {
1652
1850
  const parsed = parseKeyCombo(combo);
1653
- return event.key.toLowerCase() === parsed.key && event.altKey === parsed.altKey && event.ctrlKey === parsed.ctrlKey && event.metaKey === parsed.metaKey && event.shiftKey === parsed.shiftKey;
1851
+ return matchesKey(event, parsed) && event.altKey === parsed.altKey && event.ctrlKey === parsed.ctrlKey && event.metaKey === parsed.metaKey && event.shiftKey === parsed.shiftKey;
1654
1852
  };
1655
- const matchesShortcut = (event, keymap, action) => getBindingCombos(keymap[action]).some((combo) => matchesKeyCombo(event, combo));
1853
+ const matchesBinding = (event, binding, matchesKey) => getBindingCombos(binding).some((combo) => matchesCombo(event, combo, matchesKey));
1854
+ const canFallBackToPhysicalKey = (event, keymap) => event.altKey && isMac() && isDeadOrCharacterKey(event.key) && !Object.values(keymap).some((binding) => matchesBinding(event, binding, matchesTypedKey));
1855
+ const matchesShortcut = (event, keymap, action) => matchesBinding(event, keymap[action], matchesTypedKey) || canFallBackToPhysicalKey(event, keymap) && matchesBinding(event, keymap[action], matchesPhysicalKey);
1856
+ const getShortcutLabel = (keymap, action) => {
1857
+ const combo = getBindingCombos(keymap[action])[0] ?? "";
1858
+ const mac = isMac();
1859
+ return combo.split("+").map((part) => {
1860
+ const lower = part.trim().toLowerCase();
1861
+ if (lower === "mod") return mac ? "⌘" : "Ctrl";
1862
+ if (lower === "shift") return mac ? "⇧" : "Shift";
1863
+ if (lower === "alt") return mac ? "⌥" : "Alt";
1864
+ if (lower === "ctrl") return mac ? "⌃" : "Ctrl";
1865
+ if (lower === "meta") return mac ? "⌘" : "Win";
1866
+ if (lower === "enter") return mac ? "↩" : "Enter";
1867
+ if (lower === "escape") return "Esc";
1868
+ if (lower === "arrowup") return "↑";
1869
+ if (lower === "arrowdown") return "↓";
1870
+ if (lower === "arrowleft") return "←";
1871
+ if (lower === "arrowright") return "→";
1872
+ return part.trim().toUpperCase();
1873
+ }).join("+");
1874
+ };
1875
+ //#endregion
1876
+ //#region app/components/ResolvedThreadDisclosure.tsx
1877
+ function ResolvedThreadDisclosure({ children, commentCount, focused = false, focusRequest = 0, initialExpanded = focused, saving = false }) {
1878
+ const [expansion, setExpansion] = useState({
1879
+ expanded: initialExpanded,
1880
+ focusRequest
1881
+ });
1882
+ const expanded = focused && expansion.focusRequest !== focusRequest ? true : expansion.expanded;
1883
+ const countLabel = `${commentCount} ${commentCount === 1 ? "comment" : "comments"}`;
1884
+ return /* @__PURE__ */ jsxs("div", {
1885
+ className: `resolved-thread-disclosure${expanded ? " expanded" : ""}`,
1886
+ children: [/* @__PURE__ */ jsxs("button", {
1887
+ "aria-expanded": expanded,
1888
+ className: "resolved-thread-toggle",
1889
+ onClick: () => setExpansion({
1890
+ expanded: !expanded,
1891
+ focusRequest
1892
+ }),
1893
+ type: "button",
1894
+ children: [
1895
+ /* @__PURE__ */ jsx("span", {
1896
+ className: "resolved-thread-label",
1897
+ children: saving ? "Saving resolved conversation" : "Resolved conversation"
1898
+ }),
1899
+ /* @__PURE__ */ jsx("span", {
1900
+ className: "resolved-thread-count",
1901
+ children: countLabel
1902
+ }),
1903
+ /* @__PURE__ */ jsx(CaretDownIcon, {
1904
+ "aria-hidden": true,
1905
+ className: "resolved-thread-caret",
1906
+ size: 14,
1907
+ weight: "bold"
1908
+ })
1909
+ ]
1910
+ }), expanded ? /* @__PURE__ */ jsx("div", {
1911
+ className: "resolved-thread-content",
1912
+ children
1913
+ }) : null]
1914
+ });
1915
+ }
1656
1916
  //#endregion
1657
1917
  //#region app/components/merge-request/GeneralComments.tsx
1658
1918
  const getAuthorDisplayName = (author) => author.name || author.login;
@@ -1696,13 +1956,14 @@ function SubmittedAtTime({ submittedAt }) {
1696
1956
  }
1697
1957
  function ReadOnlyGeneralCommentCard({ className = "", comment, focused = false }) {
1698
1958
  const displayName = getAuthorDisplayName(comment.author);
1959
+ const classes = [
1960
+ "review-comment",
1961
+ "general-comment-card",
1962
+ focused ? "focused" : "",
1963
+ className
1964
+ ].filter(Boolean).join(" ");
1699
1965
  return /* @__PURE__ */ jsxs("article", {
1700
- className: [
1701
- "review-comment",
1702
- "general-comment-card",
1703
- focused ? "focused" : "",
1704
- className
1705
- ].filter(Boolean).join(" "),
1966
+ className: classes,
1706
1967
  id: getGeneralCommentElementId(comment.id),
1707
1968
  children: [/* @__PURE__ */ jsx(Avatar, {
1708
1969
  name: displayName,
@@ -1839,13 +2100,16 @@ function GeneralCommentCard({ comment, editDraft, editError, editing, editSubmit
1839
2100
  })]
1840
2101
  });
1841
2102
  }
1842
- function GeneralCommentThreadCard({ canComment, editDraft, editError, editingCommentId, editSubmitting, focusedCommentId, keymap, onCancelEdit, onChangeEditDraft, onDelete, onReply, onResolve, onSaveEdit, onStartEdit, thread }) {
2103
+ function GeneralCommentThreadCard({ canComment, editDraft, editError, editingCommentId, editSubmitting, focusedCommentId, focusedCommentRequest, keymap, onCancelEdit, onChangeEditDraft, onDelete, onReply, onResolve, onSaveEdit, onStartEdit, thread }) {
1843
2104
  const [replyDraft, setReplyDraft] = useState("");
1844
2105
  const [replyError, setReplyError] = useState(null);
1845
2106
  const [replying, setReplying] = useState(false);
1846
2107
  const [showReply, setShowReply] = useState(false);
1847
2108
  const [resolving, setResolving] = useState(false);
1848
2109
  const resolved = thread.isResolved === true;
2110
+ const hasFocusedComment = focusedCommentId != null && thread.comments.some((comment) => comment.id === focusedCommentId);
2111
+ const [resolutionOverride, setResolutionOverride] = useState(null);
2112
+ const effectiveResolved = resolutionOverride?.base === resolved ? resolutionOverride.value : resolved;
1849
2113
  const submitReply = useCallback(() => {
1850
2114
  const body = replyDraft.trim();
1851
2115
  if (!body || replying) return;
@@ -1865,62 +2129,79 @@ function GeneralCommentThreadCard({ canComment, editDraft, editError, editingCom
1865
2129
  ]);
1866
2130
  const toggleResolved = useCallback(() => {
1867
2131
  if (resolving) return;
2132
+ const nextResolved = !effectiveResolved;
2133
+ setResolutionOverride({
2134
+ base: resolved,
2135
+ value: nextResolved
2136
+ });
1868
2137
  setResolving(true);
1869
- onResolve(thread.id, !resolved).catch((error) => {
2138
+ onResolve(thread.id, nextResolved).catch((error) => {
2139
+ setResolutionOverride(null);
1870
2140
  window.alert(error instanceof Error ? error.message : String(error));
1871
2141
  }).finally(() => setResolving(false));
1872
2142
  }, [
2143
+ effectiveResolved,
1873
2144
  onResolve,
1874
2145
  resolved,
1875
2146
  resolving,
1876
2147
  thread.id
1877
2148
  ]);
1878
- return /* @__PURE__ */ jsxs("section", {
2149
+ const threadContent = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2150
+ thread.comments.map((comment) => /* @__PURE__ */ jsx(GeneralCommentCard, {
2151
+ comment,
2152
+ editDraft,
2153
+ editError: editingCommentId === comment.id ? editError : null,
2154
+ editing: editingCommentId === comment.id,
2155
+ editSubmitting: editSubmitting && editingCommentId === comment.id,
2156
+ focused: comment.id === focusedCommentId,
2157
+ keymap,
2158
+ onCancelEdit,
2159
+ onChangeEditDraft,
2160
+ onDelete,
2161
+ onSaveEdit,
2162
+ onStartEdit
2163
+ }, comment.id)),
2164
+ thread.canReply && canComment && !effectiveResolved && !resolving ? showReply ? /* @__PURE__ */ jsx(GeneralCommentComposer, {
2165
+ disabled: false,
2166
+ draft: replyDraft,
2167
+ error: replyError,
2168
+ gitIdentity: null,
2169
+ keymap,
2170
+ onChangeDraft: setReplyDraft,
2171
+ onSubmit: submitReply,
2172
+ submitting: replying
2173
+ }) : /* @__PURE__ */ jsx("div", {
2174
+ className: "review-comment-thread-footer",
2175
+ children: /* @__PURE__ */ jsx("button", {
2176
+ className: "review-comment-action",
2177
+ onClick: () => setShowReply(true),
2178
+ type: "button",
2179
+ children: "Reply"
2180
+ })
2181
+ }) : null,
2182
+ thread.canResolve ? /* @__PURE__ */ jsx("div", {
2183
+ className: "review-comment-thread-footer",
2184
+ children: /* @__PURE__ */ jsx("button", {
2185
+ className: "review-comment-action",
2186
+ disabled: resolving,
2187
+ onClick: toggleResolved,
2188
+ type: "button",
2189
+ children: resolving ? "Saving" : effectiveResolved ? "Reopen" : "Resolve"
2190
+ })
2191
+ }) : null
2192
+ ] });
2193
+ return effectiveResolved ? /* @__PURE__ */ jsx("section", {
1879
2194
  className: "general-comment-thread",
1880
- children: [
1881
- thread.comments.map((comment) => /* @__PURE__ */ jsx(GeneralCommentCard, {
1882
- comment,
1883
- editDraft,
1884
- editError: editingCommentId === comment.id ? editError : null,
1885
- editing: editingCommentId === comment.id,
1886
- editSubmitting: editSubmitting && editingCommentId === comment.id,
1887
- focused: comment.id === focusedCommentId,
1888
- keymap,
1889
- onCancelEdit,
1890
- onChangeEditDraft,
1891
- onDelete,
1892
- onSaveEdit,
1893
- onStartEdit
1894
- }, comment.id)),
1895
- thread.canReply && canComment && !resolved ? showReply ? /* @__PURE__ */ jsx(GeneralCommentComposer, {
1896
- disabled: false,
1897
- draft: replyDraft,
1898
- error: replyError,
1899
- gitIdentity: null,
1900
- keymap,
1901
- onChangeDraft: setReplyDraft,
1902
- onSubmit: submitReply,
1903
- submitting: replying
1904
- }) : /* @__PURE__ */ jsx("div", {
1905
- className: "review-comment-thread-footer",
1906
- children: /* @__PURE__ */ jsx("button", {
1907
- className: "review-comment-action",
1908
- onClick: () => setShowReply(true),
1909
- type: "button",
1910
- children: "Reply"
1911
- })
1912
- }) : null,
1913
- thread.canResolve ? /* @__PURE__ */ jsx("div", {
1914
- className: "review-comment-thread-footer",
1915
- children: /* @__PURE__ */ jsx("button", {
1916
- className: "review-comment-action",
1917
- disabled: resolving,
1918
- onClick: toggleResolved,
1919
- type: "button",
1920
- children: resolving ? "Saving" : resolved ? "Reopen" : "Resolve"
1921
- })
1922
- }) : null
1923
- ]
2195
+ children: /* @__PURE__ */ jsx(ResolvedThreadDisclosure, {
2196
+ commentCount: thread.comments.length,
2197
+ focused: hasFocusedComment,
2198
+ focusRequest: focusedCommentRequest,
2199
+ saving: resolving,
2200
+ children: threadContent
2201
+ })
2202
+ }) : /* @__PURE__ */ jsx("section", {
2203
+ className: "general-comment-thread",
2204
+ children: threadContent
1924
2205
  });
1925
2206
  }
1926
2207
  function SidebarGeneralCommentList({ comments, focusedCommentId, onActivateComment }) {
@@ -2028,7 +2309,7 @@ function GeneralCommentComposer({ disabled, draft, error, gitIdentity, keymap, o
2028
2309
  })
2029
2310
  });
2030
2311
  }
2031
- function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, editError, editingCommentId, editSubmitting, error, focusedCommentId, focusedCommentRequest, gitIdentity, keymap, onCancelEdit, onChangeDraft, onChangeEditDraft, onSaveEdit, onStartEdit, onSubmit, signInLabel, sourceDescription, submitting, threads }) {
2312
+ function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, editError, editingCommentId, editSubmitting, error, focusedCommentId, focusedCommentRequest, gitIdentity, keymap, onCancelEdit, onChangeDraft, onChangeEditDraft, onResolveDiscussion, onSaveEdit, onStartEdit, onSubmit, signInLabel, sourceDescription, submitting, threads }) {
2032
2313
  const commentsRef = useRef(null);
2033
2314
  useEffect(() => {
2034
2315
  if (focusedCommentId == null) return;
@@ -2054,6 +2335,7 @@ function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, ed
2054
2335
  editingCommentId,
2055
2336
  editSubmitting,
2056
2337
  focusedCommentId,
2338
+ focusedCommentRequest,
2057
2339
  keymap,
2058
2340
  onCancelEdit,
2059
2341
  onChangeEditDraft,
@@ -2063,7 +2345,7 @@ function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, ed
2063
2345
  });
2064
2346
  },
2065
2347
  onReply: (threadId, body) => commenting?.onReplyGeneralComment(threadId, body) ?? Promise.reject(/* @__PURE__ */ new Error("Replying is unavailable.")),
2066
- onResolve: (threadId, resolved) => commenting?.onResolveDiscussion(threadId, resolved) ?? Promise.reject(/* @__PURE__ */ new Error("Resolving is unavailable.")),
2348
+ onResolve: (threadId, resolved) => onResolveDiscussion?.(threadId, resolved) ?? commenting?.onResolveDiscussion(threadId, resolved) ?? Promise.reject(/* @__PURE__ */ new Error("Resolving is unavailable.")),
2067
2349
  onSaveEdit,
2068
2350
  onStartEdit,
2069
2351
  thread
@@ -2209,23 +2491,6 @@ const getReviewCommentsFromState = (state) => (state.reviewComments ?? []).flatM
2209
2491
  });
2210
2492
  const shouldDiscardReviewCommentOnEscape = (body, confirmDiscard = window.confirm) => body.trim().length === 0 || confirmDiscard("Discard this review comment?");
2211
2493
  //#endregion
2212
- //#region app/components/useCopiedState.ts
2213
- function useCopiedState(timeoutMs) {
2214
- const [copied, setCopied] = useState(false);
2215
- const copiedTimerRef = useRef(null);
2216
- useEffect(() => () => {
2217
- if (copiedTimerRef.current != null) window.clearTimeout(copiedTimerRef.current);
2218
- }, []);
2219
- return [copied, useCallback(() => {
2220
- setCopied(true);
2221
- if (copiedTimerRef.current != null) window.clearTimeout(copiedTimerRef.current);
2222
- copiedTimerRef.current = window.setTimeout(() => {
2223
- setCopied(false);
2224
- copiedTimerRef.current = null;
2225
- }, timeoutMs);
2226
- }, [timeoutMs])];
2227
- }
2228
- //#endregion
2229
2494
  //#region app/components/Panels.tsx
2230
2495
  const getPullRequestReviewActionStatus = (reviewStatus, event) => event === "APPROVE" ? reviewStatus?.approve : event === "COMMENT" ? reviewStatus?.comment : reviewStatus?.requestChanges;
2231
2496
  const isPullRequestReviewActionDisabled = (reviewStatus, event) => getPullRequestReviewActionStatus(reviewStatus, event)?.disabled === true;
@@ -3273,6 +3538,7 @@ setCustomExtension("cts", "typescript");
3273
3538
  setCustomExtension("mts", "typescript");
3274
3539
  const statusLabel = {
3275
3540
  added: "Added",
3541
+ conflicted: "Conflicted",
3276
3542
  deleted: "Deleted",
3277
3543
  modified: "Modified",
3278
3544
  renamed: "Renamed",
@@ -3405,6 +3671,20 @@ const codeViewUnsafeCSS = `
3405
3671
  cursor: progress;
3406
3672
  }
3407
3673
 
3674
+ :host([data-codiff-definition-mode]) [data-codiff-identifier] {
3675
+ cursor: pointer;
3676
+ text-decoration-color: color-mix(in srgb, currentColor 38%, transparent);
3677
+ text-decoration-line: underline;
3678
+ text-decoration-style: dotted;
3679
+ text-decoration-thickness: 1px;
3680
+ text-underline-offset: 3px;
3681
+ }
3682
+
3683
+ :host([data-codiff-definition-mode]) [data-codiff-identifier]:hover {
3684
+ text-decoration-color: currentColor;
3685
+ text-decoration-style: solid;
3686
+ }
3687
+
3408
3688
  .codiff-search-mark {
3409
3689
  background: var(--diffs-find-highlight-bg, rgb(255 216 92 / 0.65));
3410
3690
  border-radius: 3px;
@@ -3431,8 +3711,179 @@ const codeViewUnsafeCSS = `
3431
3711
  [data-selected-line] [data-gutter-utility-slot] {
3432
3712
  display: none;
3433
3713
  }
3714
+
3715
+ /* The imperative API wraps annotations in a node that resets white-space, but
3716
+ the React API slots them in bare, so they inherit "pre" from the shadow
3717
+ <pre> and prose never wraps. */
3718
+ [data-annotation-content] slot::slotted(*) {
3719
+ white-space: normal;
3720
+ }
3434
3721
  `;
3435
3722
  //#endregion
3723
+ //#region lib/identifier-navigation.ts
3724
+ const identifierPattern = /[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*/gu;
3725
+ const exactIdentifierPattern = /^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u;
3726
+ const identifierContainedPattern = /[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*/u;
3727
+ const commonKeywords = /* @__PURE__ */ new Set([
3728
+ "abstract",
3729
+ "as",
3730
+ "async",
3731
+ "await",
3732
+ "break",
3733
+ "case",
3734
+ "catch",
3735
+ "class",
3736
+ "const",
3737
+ "continue",
3738
+ "def",
3739
+ "default",
3740
+ "defer",
3741
+ "do",
3742
+ "else",
3743
+ "enum",
3744
+ "export",
3745
+ "extends",
3746
+ "false",
3747
+ "finally",
3748
+ "fn",
3749
+ "for",
3750
+ "from",
3751
+ "func",
3752
+ "function",
3753
+ "if",
3754
+ "implements",
3755
+ "import",
3756
+ "in",
3757
+ "interface",
3758
+ "let",
3759
+ "match",
3760
+ "mod",
3761
+ "namespace",
3762
+ "new",
3763
+ "nil",
3764
+ "none",
3765
+ "null",
3766
+ "package",
3767
+ "pass",
3768
+ "private",
3769
+ "protected",
3770
+ "pub",
3771
+ "public",
3772
+ "return",
3773
+ "self",
3774
+ "static",
3775
+ "struct",
3776
+ "super",
3777
+ "switch",
3778
+ "this",
3779
+ "throw",
3780
+ "throws",
3781
+ "trait",
3782
+ "true",
3783
+ "try",
3784
+ "type",
3785
+ "typeof",
3786
+ "undefined",
3787
+ "use",
3788
+ "var",
3789
+ "void",
3790
+ "while",
3791
+ "with",
3792
+ "yield"
3793
+ ]);
3794
+ const identifierNavigationAttribute = "data-codiff-identifier";
3795
+ const definitionModifierAttribute = "data-codiff-definition-mode";
3796
+ const nonCodeScopePattern = /(^|\s)(comment|string)([.\s]|$)/;
3797
+ const nonCodeTokenColors = new Set([dunkel_default, licht_default].flatMap((theme) => theme.tokenColors.flatMap((token) => {
3798
+ const scopes = Array.isArray(token.scope) ? token.scope : [token.scope];
3799
+ const foreground = token.settings.foreground?.toLowerCase();
3800
+ return foreground && scopes.some((scope) => typeof scope === "string" && nonCodeScopePattern.test(scope)) ? [foreground] : [];
3801
+ })));
3802
+ const isNavigableIdentifierToken = (text) => {
3803
+ return exactIdentifierPattern.test(text) && !commonKeywords.has(text.toLowerCase());
3804
+ };
3805
+ const isNonCodeSyntaxToken = (element) => {
3806
+ const style = element?.getAttribute("style")?.toLowerCase() ?? "";
3807
+ return [...nonCodeTokenColors].some((color) => style.includes(color));
3808
+ };
3809
+ const clearIdentifierNavigationWrappers = (root) => {
3810
+ for (const wrapper of Array.from(root.querySelectorAll(`[${identifierNavigationAttribute}]`))) {
3811
+ const parent = wrapper.parentElement;
3812
+ wrapper.replaceWith(document.createTextNode(wrapper.textContent ?? ""));
3813
+ parent?.normalize();
3814
+ }
3815
+ };
3816
+ const wrapIdentifierNavigationText = (root) => {
3817
+ const textNodes = [];
3818
+ for (const line of Array.from(root.querySelectorAll("[data-line]"))) {
3819
+ const walker = document.createTreeWalker(line, NodeFilter.SHOW_TEXT, { acceptNode: (node) => {
3820
+ const parent = node.parentElement;
3821
+ return node.textContent && identifierContainedPattern.test(node.textContent) && !parent?.closest(`[${identifierNavigationAttribute}], mark.codiff-search-mark`) && !isNonCodeSyntaxToken(parent) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
3822
+ } });
3823
+ let node = walker.nextNode();
3824
+ while (node) {
3825
+ textNodes.push(node);
3826
+ node = walker.nextNode();
3827
+ }
3828
+ }
3829
+ for (const textNode of textNodes) {
3830
+ const text = textNode.textContent ?? "";
3831
+ const fragment = document.createDocumentFragment();
3832
+ let offset = 0;
3833
+ let wrapped = false;
3834
+ for (const match of text.matchAll(identifierPattern)) {
3835
+ const identifier = match[0];
3836
+ if (!isNavigableIdentifierToken(identifier)) continue;
3837
+ if (match.index > offset) fragment.append(document.createTextNode(text.slice(offset, match.index)));
3838
+ const wrapper = document.createElement("span");
3839
+ wrapper.setAttribute(identifierNavigationAttribute, "");
3840
+ wrapper.textContent = identifier;
3841
+ fragment.append(wrapper);
3842
+ offset = match.index + identifier.length;
3843
+ wrapped = true;
3844
+ }
3845
+ if (!wrapped) continue;
3846
+ if (offset < text.length) fragment.append(document.createTextNode(text.slice(offset)));
3847
+ textNode.replaceWith(fragment);
3848
+ }
3849
+ };
3850
+ const applyIdentifierNavigationState = (renderedItems, active) => {
3851
+ for (const { element } of renderedItems) {
3852
+ element.toggleAttribute(definitionModifierAttribute, active);
3853
+ const root = element.shadowRoot;
3854
+ if (!root) continue;
3855
+ clearIdentifierNavigationWrappers(root);
3856
+ if (active) wrapIdentifierNavigationText(root);
3857
+ }
3858
+ };
3859
+ const getIdentifierAtOffset = (text, offset) => {
3860
+ for (const match of text.matchAll(identifierPattern)) {
3861
+ const start = match.index;
3862
+ const end = start + match[0].length;
3863
+ if (offset >= start && offset < end) return {
3864
+ identifier: match[0],
3865
+ start
3866
+ };
3867
+ }
3868
+ return null;
3869
+ };
3870
+ const getIdentifierFromPointerEvent = (event, lineElement) => {
3871
+ const caretRange = document.caretRangeFromPoint?.(event.clientX, event.clientY);
3872
+ if (caretRange && lineElement.contains(caretRange.startContainer)) {
3873
+ const prefix = document.createRange();
3874
+ prefix.selectNodeContents(lineElement);
3875
+ prefix.setEnd(caretRange.startContainer, caretRange.startOffset);
3876
+ const match = getIdentifierAtOffset(lineElement.textContent || "", prefix.toString().length);
3877
+ if (match) return match.identifier;
3878
+ }
3879
+ const target = event.target;
3880
+ if (target && lineElement.contains(target)) {
3881
+ const matches = [...(target.textContent || "").matchAll(identifierPattern)];
3882
+ if (matches.length === 1) return matches[0][0];
3883
+ }
3884
+ return null;
3885
+ };
3886
+ //#endregion
3436
3887
  //#region lib/item-version.ts
3437
3888
  const getItemVersion = (value) => {
3438
3889
  let hash = 0;
@@ -3448,6 +3899,8 @@ const isNativeInputTarget = (target) => {
3448
3899
  const candidate = target;
3449
3900
  return candidate?.closest?.("input, select, textarea") != null || candidate?.isContentEditable === true;
3450
3901
  };
3902
+ const isMacPlatform = (platform = navigator.platform) => platform.toLowerCase().includes("mac");
3903
+ const isPrimaryModifier = (event, platform = navigator.platform) => isMacPlatform(platform) ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
3451
3904
  //#endregion
3452
3905
  //#region lib/review-identity.ts
3453
3906
  const getFileReviewIdentity = (file) => ({
@@ -3527,6 +3980,101 @@ const applySearchHighlights = (renderedItems, query, activeMatch) => {
3527
3980
  }
3528
3981
  };
3529
3982
  //#endregion
3983
+ //#region lib/source.ts
3984
+ const rangeLabel = (source) => `${source.base}${source.symmetric ? "..." : ".."}${source.head}`;
3985
+ const getSourceKey = (source) => source.type === "commit" ? `commit:${source.ref}` : source.type === "branch-diff" ? `branch-diff:${source.ref}:${source.baseRef}:${source.headRef}` : source.type === "branch-working-tree" ? `branch-working-tree:${source.ref}:${source.baseRef}:${source.headRef}` : source.type === "branch" ? `branch:${source.ref}` : source.type === "range" ? `range:${rangeLabel(source)}` : source.type === "pull-request" ? `pull-request:${source.provider ?? ""}:${source.host ?? ""}:${source.projectPath ?? `${source.owner ?? ""}/${source.repo ?? ""}`}#${source.number ?? source.url}` : "working-tree";
3986
+ const getShortRef = (ref) => ref.slice(0, 7);
3987
+ const getSourceLabel = (source) => source.type === "commit" ? getShortRef(source.ref) : source.type === "branch" || source.type === "branch-diff" ? `Branch vs ${source.ref}` : source.type === "branch-working-tree" ? `Local + branch vs ${source.ref}` : source.type === "range" ? rangeLabel(source) : source.type === "pull-request" ? source.number ? `${source.provider === "gitlab" ? "MR" : "PR"} #${source.number}` : source.provider === "gitlab" ? "Merge request" : "Pull request" : "Uncommitted";
3988
+ //#endregion
3989
+ //#region app/components/DefinitionPopover.tsx
3990
+ function DefinitionPopover({ anchor, getDestination, identifier, onClose, onOpen, result }) {
3991
+ const popoverRef = useRef(null);
3992
+ useEffect(() => {
3993
+ const closeOnEscape = (event) => {
3994
+ if (event.key === "Escape") onClose();
3995
+ };
3996
+ const closeOutside = (event) => {
3997
+ const target = event.target;
3998
+ if (target && !popoverRef.current?.contains(target)) onClose();
3999
+ };
4000
+ document.addEventListener("keydown", closeOnEscape);
4001
+ document.addEventListener("pointerdown", closeOutside);
4002
+ return () => {
4003
+ document.removeEventListener("keydown", closeOnEscape);
4004
+ document.removeEventListener("pointerdown", closeOutside);
4005
+ };
4006
+ }, [onClose]);
4007
+ const left = Math.min(anchor.x, Math.max(12, window.innerWidth - 432));
4008
+ const top = Math.min(anchor.y + 10, Math.max(12, window.innerHeight - 320));
4009
+ return createPortal(/* @__PURE__ */ jsxs("div", {
4010
+ "aria-label": `Definitions for ${identifier}`,
4011
+ className: "definition-popover",
4012
+ ref: popoverRef,
4013
+ role: "dialog",
4014
+ style: {
4015
+ left,
4016
+ top
4017
+ },
4018
+ children: [/* @__PURE__ */ jsxs("div", {
4019
+ className: "definition-popover-header",
4020
+ children: [/* @__PURE__ */ jsxs("span", { children: ["Definition of ", /* @__PURE__ */ jsx("code", { children: identifier })] }), /* @__PURE__ */ jsx("button", {
4021
+ "aria-label": "Close definition results",
4022
+ onClick: onClose,
4023
+ type: "button",
4024
+ children: /* @__PURE__ */ jsx(XIcon, { size: 14 })
4025
+ })]
4026
+ }), !result ? /* @__PURE__ */ jsx("div", {
4027
+ className: "definition-popover-message",
4028
+ children: "Searching…"
4029
+ }) : result.status === "unavailable" ? /* @__PURE__ */ jsx("div", {
4030
+ className: "definition-popover-message",
4031
+ children: result.reason
4032
+ }) : result.candidates.length === 0 ? /* @__PURE__ */ jsx("div", {
4033
+ className: "definition-popover-message",
4034
+ children: "No likely definitions found."
4035
+ }) : /* @__PURE__ */ jsx("div", {
4036
+ className: "definition-popover-results",
4037
+ children: result.candidates.map((candidate) => {
4038
+ const destination = getDestination(candidate);
4039
+ const destinationLabel = destination === "diff" ? "Jump within diff" : destination === "editor" ? "Open in editor" : "Unavailable outside this historical diff";
4040
+ return /* @__PURE__ */ jsxs("button", {
4041
+ className: "definition-popover-result",
4042
+ disabled: destination === "unavailable",
4043
+ onClick: () => onOpen(candidate),
4044
+ type: "button",
4045
+ children: [/* @__PURE__ */ jsxs("span", {
4046
+ className: "definition-popover-location",
4047
+ children: [
4048
+ candidate.path,
4049
+ ":",
4050
+ candidate.lineNumber,
4051
+ /* @__PURE__ */ jsxs("span", {
4052
+ className: "definition-popover-meta",
4053
+ children: [/* @__PURE__ */ jsx("span", { children: candidate.kind }), /* @__PURE__ */ jsx("span", {
4054
+ "aria-label": destinationLabel,
4055
+ className: "definition-popover-destination",
4056
+ role: "img",
4057
+ title: destinationLabel,
4058
+ children: destination === "diff" ? /* @__PURE__ */ jsx(CrosshairSimpleIcon, {
4059
+ "aria-hidden": "true",
4060
+ size: 13
4061
+ }) : destination === "editor" ? /* @__PURE__ */ jsx(ArrowSquareOutIcon, {
4062
+ "aria-hidden": "true",
4063
+ size: 13
4064
+ }) : /* @__PURE__ */ jsx(ProhibitIcon, {
4065
+ "aria-hidden": "true",
4066
+ size: 13
4067
+ })
4068
+ })]
4069
+ })
4070
+ ]
4071
+ }), /* @__PURE__ */ jsx("code", { children: candidate.line })]
4072
+ }, `${candidate.path}:${candidate.lineNumber}`);
4073
+ })
4074
+ })]
4075
+ }), document.body);
4076
+ }
4077
+ //#endregion
3530
4078
  //#region app/components/MarkdownDocumentEditor.tsx
3531
4079
  const persistenceAdapter = { save: ({ content, document }) => window.codiff.saveMarkdownDocument({
3532
4080
  baseVersion: document.version,
@@ -3643,12 +4191,6 @@ const RepositoryMarkdownEditor = forwardRef(function RepositoryMarkdownEditor({
3643
4191
  });
3644
4192
  });
3645
4193
  //#endregion
3646
- //#region lib/source.ts
3647
- const rangeLabel = (source) => `${source.base}${source.symmetric ? "..." : ".."}${source.head}`;
3648
- const getSourceKey = (source) => source.type === "commit" ? `commit:${source.ref}` : source.type === "branch-diff" ? `branch-diff:${source.ref}:${source.baseRef}:${source.headRef}` : source.type === "branch-working-tree" ? `branch-working-tree:${source.ref}:${source.baseRef}:${source.headRef}` : source.type === "branch" ? `branch:${source.ref}` : source.type === "range" ? `range:${rangeLabel(source)}` : source.type === "pull-request" ? `pull-request:${source.provider ?? ""}:${source.host ?? ""}:${source.projectPath ?? `${source.owner ?? ""}/${source.repo ?? ""}`}#${source.number ?? source.url}` : "working-tree";
3649
- const getShortRef = (ref) => ref.slice(0, 7);
3650
- const getSourceLabel = (source) => source.type === "commit" ? getShortRef(source.ref) : source.type === "branch" || source.type === "branch-diff" ? `Branch vs ${source.ref}` : source.type === "branch-working-tree" ? `Local + branch vs ${source.ref}` : source.type === "range" ? rangeLabel(source) : source.type === "pull-request" ? source.number ? `${source.provider === "gitlab" ? "MR" : "PR"} #${source.number}` : source.provider === "gitlab" ? "Merge request" : "Pull request" : "Uncommitted";
3651
- //#endregion
3652
4194
  //#region lib/narrative-walkthrough.ts
3653
4195
  const walkthroughFileName = (path) => path.split("/").pop() ?? path;
3654
4196
  const uniquePaths = (paths) => {
@@ -4071,7 +4613,8 @@ const chapterIcons = {
4071
4613
  wrench: WrenchIcon
4072
4614
  };
4073
4615
  function ChapterIcon({ icon, size = 13 }) {
4074
- return /* @__PURE__ */ jsx(chapterIcons[icon] ?? chapterIcons.path, { size });
4616
+ const Icon = chapterIcons[icon] ?? chapterIcons.path;
4617
+ return /* @__PURE__ */ jsx(Icon, { size });
4075
4618
  }
4076
4619
  function ImportancePill({ importance }) {
4077
4620
  return /* @__PURE__ */ jsx("span", {
@@ -4356,13 +4899,14 @@ function WalkthroughProgress({ phase, responseLabelIndex, stageRevision }) {
4356
4899
  }, [stageRevision]);
4357
4900
  const elapsedSeconds = timerState.stageRevision === stageRevision ? timerState.elapsedSeconds : 0;
4358
4901
  const showTimer = elapsedSeconds >= TIMER_THRESHOLD_SECONDS;
4902
+ const label = phase === "agent-generation" ? "Analyzing changes…" : phase === "response-received" ? walkthroughResponseLabels[Math.abs(responseLabelIndex) % walkthroughResponseLabels.length] : "Generating walkthrough…";
4359
4903
  return /* @__PURE__ */ jsxs("span", {
4360
4904
  "aria-live": "polite",
4361
4905
  className: "walkthrough-progress",
4362
4906
  role: "status",
4363
4907
  children: [/* @__PURE__ */ jsx("span", {
4364
4908
  className: "walkthrough-progress-label",
4365
- children: phase === "agent-generation" ? "Analyzing changes…" : phase === "response-received" ? walkthroughResponseLabels[Math.abs(responseLabelIndex) % walkthroughResponseLabels.length] : "Generating walkthrough…"
4909
+ children: label
4366
4910
  }), /* @__PURE__ */ jsx("span", {
4367
4911
  "aria-hidden": !showTimer,
4368
4912
  className: `walkthrough-progress-timer${showTimer ? " visible" : ""}`,
@@ -5402,6 +5946,9 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5402
5946
  flushDraft,
5403
5947
  onCommentBlur
5404
5948
  ]);
5949
+ const handleAddComment = useCallback(() => {
5950
+ document.activeElement?.blur();
5951
+ }, []);
5405
5952
  const handleFocus = useCallback(() => {
5406
5953
  onCommentFocus(draftComment);
5407
5954
  }, [draftComment, onCommentFocus]);
@@ -5417,13 +5964,21 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5417
5964
  ]);
5418
5965
  const handleKeyDown = useCallback((event) => {
5419
5966
  if (matchesShortcut(event, keymap, "submitComment")) {
5420
- if (supportsReviewCommentActions && commentCanSubmit) {
5421
- event.preventDefault();
5422
- event.stopPropagation();
5423
- handleSubmitComment();
5967
+ if (supportsReviewCommentActions) {
5968
+ if (commentCanSubmit) {
5969
+ event.preventDefault();
5970
+ event.stopPropagation();
5971
+ handleSubmitComment();
5972
+ }
5424
5973
  return;
5425
5974
  }
5426
- if (!supportsReviewCommentActions && canAskCodex) {
5975
+ event.preventDefault();
5976
+ event.stopPropagation();
5977
+ handleAddComment();
5978
+ return;
5979
+ }
5980
+ if (matchesShortcut(event, keymap, "askAgent")) {
5981
+ if (canAskCodex) {
5427
5982
  event.preventDefault();
5428
5983
  event.stopPropagation();
5429
5984
  handleAskCodex();
@@ -5441,6 +5996,7 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5441
5996
  comment.id,
5442
5997
  comment.isReadOnly,
5443
5998
  draft,
5999
+ handleAddComment,
5444
6000
  handleAskCodex,
5445
6001
  handleSubmitComment,
5446
6002
  keymap,
@@ -5496,7 +6052,7 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5496
6052
  className: "review-comment-action",
5497
6053
  disabled: !canAskCodex,
5498
6054
  onClick: handleAskCodex,
5499
- title: canAskCodex ? `Ask ${agentLabel}` : `Write a note before asking ${agentLabel}`,
6055
+ title: canAskCodex ? `Ask ${agentLabel} (${getShortcutLabel(keymap, "askAgent")})` : `Write a note before asking ${agentLabel}`,
5500
6056
  type: "button",
5501
6057
  children: [/* @__PURE__ */ jsx("img", {
5502
6058
  alt: "",
@@ -5658,7 +6214,7 @@ const groupReviewCommentsByThread = (comments) => {
5658
6214
  };
5659
6215
  const noopResolveThread = () => {};
5660
6216
  const noopLayoutReady = () => {};
5661
- function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentId, focusEditorRef, identity, keymap, onAskCodex, onCommentBlur, onCommentDraftChange, onCommentFocus, onDeleteComment, onReplyToThread, onResolveThread = noopResolveThread, onSaveCommentEdit, onSubmitComment, onUpdateComment, supportsReviewCommentActions }) {
6217
+ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentId, focusCommentRequest, focusEditorRef, identity, keymap, onAskCodex, onCommentBlur, onCommentDraftChange, onCommentFocus, onDeleteComment, onReplyToThread, onResolveThread = noopResolveThread, onSaveCommentEdit, onSubmitComment, onUpdateComment, supportsReviewCommentActions }) {
5662
6218
  const [resolveState, setResolveState] = useState({
5663
6219
  error: null,
5664
6220
  submitting: false,
@@ -5667,10 +6223,13 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5667
6223
  const lastComment = comments.at(-1);
5668
6224
  const threadId = lastComment?.threadId;
5669
6225
  const threadResolved = comments.some((comment) => comment.isThreadResolved === true);
6226
+ const hasFocusedComment = focusCommentId != null && comments.some((comment) => comment.id === focusCommentId);
6227
+ const [resolutionOverride, setResolutionOverride] = useState(null);
6228
+ const effectiveThreadResolved = resolutionOverride?.base === threadResolved ? resolutionOverride.value : threadResolved;
6229
+ const resolving = resolveState.threadId === threadId && resolveState.submitting;
5670
6230
  const canResolveThread = supportsReviewCommentActions && threadId != null && comments.some((comment) => comment.canResolveThread === true);
5671
- const canReplyToThread = supportsReviewCommentActions && threadId != null && comments.some((comment) => comment.isReadOnly) && !comments.some((comment) => !comment.isReadOnly) && !comments.some((comment) => comment.canReplyThread === false) && !threadResolved;
6231
+ const canReplyToThread = supportsReviewCommentActions && threadId != null && comments.some((comment) => comment.isReadOnly) && !comments.some((comment) => !comment.isReadOnly) && !comments.some((comment) => comment.canReplyThread === false) && !effectiveThreadResolved && !resolving;
5672
6232
  const hasThreadActions = canReplyToThread || canResolveThread;
5673
- const resolving = resolveState.threadId === threadId && resolveState.submitting;
5674
6233
  const resolveError = resolveState.threadId === threadId ? resolveState.error : null;
5675
6234
  const handleReply = useCallback(() => {
5676
6235
  if (!threadId || !lastComment) return;
@@ -5682,16 +6241,22 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5682
6241
  ]);
5683
6242
  const handleResolve = useCallback(() => {
5684
6243
  if (!threadId || resolving) return;
6244
+ const nextResolved = !effectiveThreadResolved;
6245
+ setResolutionOverride({
6246
+ base: threadResolved,
6247
+ value: nextResolved
6248
+ });
5685
6249
  setResolveState({
5686
6250
  error: null,
5687
6251
  submitting: true,
5688
6252
  threadId
5689
6253
  });
5690
- Promise.resolve(onResolveThread(threadId, !threadResolved)).then(() => setResolveState({
6254
+ Promise.resolve(onResolveThread(threadId, nextResolved)).then(() => setResolveState({
5691
6255
  error: null,
5692
6256
  submitting: false,
5693
6257
  threadId
5694
6258
  })).catch((error) => {
6259
+ setResolutionOverride(null);
5695
6260
  setResolveState({
5696
6261
  error: error instanceof Error ? error.message : String(error),
5697
6262
  submitting: false,
@@ -5699,54 +6264,64 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5699
6264
  });
5700
6265
  });
5701
6266
  }, [
6267
+ effectiveThreadResolved,
5702
6268
  onResolveThread,
5703
6269
  resolving,
5704
6270
  threadId,
5705
6271
  threadResolved
5706
6272
  ]);
5707
- return /* @__PURE__ */ jsxs("div", {
6273
+ const threadContent = /* @__PURE__ */ jsxs(Fragment$1, { children: [comments.map((comment) => {
6274
+ const displayName = comment.author ? getReviewAuthorDisplayName(comment.author) : getGitIdentityDisplayName(identity);
6275
+ return /* @__PURE__ */ jsx(ReviewCommentEditor, {
6276
+ agentId,
6277
+ agentLabel,
6278
+ comment,
6279
+ displayName,
6280
+ focusCommentId,
6281
+ focusEditorRef,
6282
+ identity,
6283
+ keymap,
6284
+ onAskCodex,
6285
+ onCommentBlur,
6286
+ onCommentDraftChange,
6287
+ onCommentFocus,
6288
+ onDeleteComment,
6289
+ onSaveCommentEdit,
6290
+ onSubmitComment,
6291
+ onUpdateComment,
6292
+ supportsReviewCommentActions
6293
+ }, comment.id);
6294
+ }), hasThreadActions ? /* @__PURE__ */ jsxs("div", {
6295
+ className: "review-comment-thread-footer",
6296
+ children: [resolveError ? /* @__PURE__ */ jsx("span", {
6297
+ className: "review-comment-thread-error",
6298
+ children: resolveError
6299
+ }) : null, /* @__PURE__ */ jsxs("div", {
6300
+ className: "review-comment-thread-actions",
6301
+ children: [canReplyToThread ? /* @__PURE__ */ jsx("button", {
6302
+ className: "review-comment-action",
6303
+ onClick: handleReply,
6304
+ type: "button",
6305
+ children: "Reply"
6306
+ }) : null, canResolveThread ? /* @__PURE__ */ jsx("button", {
6307
+ className: "review-comment-action",
6308
+ disabled: resolving,
6309
+ onClick: handleResolve,
6310
+ type: "button",
6311
+ children: resolving ? "Saving" : effectiveThreadResolved ? "Reopen" : "Resolve"
6312
+ }) : null]
6313
+ })]
6314
+ }) : null] });
6315
+ return effectiveThreadResolved ? /* @__PURE__ */ jsx(ResolvedThreadDisclosure, {
6316
+ commentCount: comments.length,
6317
+ focused: hasFocusedComment,
6318
+ focusRequest: focusCommentRequest,
6319
+ initialExpanded: Boolean(resolveError),
6320
+ saving: resolving,
6321
+ children: threadContent
6322
+ }) : /* @__PURE__ */ jsx("div", {
5708
6323
  className: "review-comment-thread-group",
5709
- children: [comments.map((comment) => {
5710
- return /* @__PURE__ */ jsx(ReviewCommentEditor, {
5711
- agentId,
5712
- agentLabel,
5713
- comment,
5714
- displayName: comment.author ? getReviewAuthorDisplayName(comment.author) : getGitIdentityDisplayName(identity),
5715
- focusCommentId,
5716
- focusEditorRef,
5717
- identity,
5718
- keymap,
5719
- onAskCodex,
5720
- onCommentBlur,
5721
- onCommentDraftChange,
5722
- onCommentFocus,
5723
- onDeleteComment,
5724
- onSaveCommentEdit,
5725
- onSubmitComment,
5726
- onUpdateComment,
5727
- supportsReviewCommentActions
5728
- }, comment.id);
5729
- }), hasThreadActions ? /* @__PURE__ */ jsxs("div", {
5730
- className: "review-comment-thread-footer",
5731
- children: [resolveError ? /* @__PURE__ */ jsx("span", {
5732
- className: "review-comment-thread-error",
5733
- children: resolveError
5734
- }) : null, /* @__PURE__ */ jsxs("div", {
5735
- className: "review-comment-thread-actions",
5736
- children: [canReplyToThread ? /* @__PURE__ */ jsx("button", {
5737
- className: "review-comment-action",
5738
- onClick: handleReply,
5739
- type: "button",
5740
- children: "Reply"
5741
- }) : null, canResolveThread ? /* @__PURE__ */ jsx("button", {
5742
- className: "review-comment-action",
5743
- disabled: resolving,
5744
- onClick: handleResolve,
5745
- type: "button",
5746
- children: resolving ? "Saving" : threadResolved ? "Reopen" : "Resolve"
5747
- }) : null]
5748
- })]
5749
- }) : null]
6324
+ children: threadContent
5750
6325
  });
5751
6326
  }
5752
6327
  function ReviewAnnotation({ agentId, agentLabel, annotation, comments, focusCommentId, focusCommentRequest, identity, keymap, onAskCodex, onCommentBlur, onCommentDraftChange, onCommentFocus, onDeleteComment, onHeightChange, onReplyToThread, onResolveThread = noopResolveThread, onSaveCommentEdit, onSubmitComment, onUpdateComment, supportsReviewCommentActions }) {
@@ -5779,14 +6354,16 @@ function ReviewAnnotation({ agentId, agentLabel, annotation, comments, focusComm
5779
6354
  return () => observer.disconnect();
5780
6355
  }, [annotationComments.length, onHeightChange]);
5781
6356
  if (annotationComments.length === 0) return null;
6357
+ const commentGroups = groupReviewCommentsByThread(annotationComments);
5782
6358
  return /* @__PURE__ */ jsx("div", {
5783
6359
  className: "review-comment-thread",
5784
6360
  ref: threadRef,
5785
- children: groupReviewCommentsByThread(annotationComments).map((group) => /* @__PURE__ */ jsx(ReviewCommentThreadGroup, {
6361
+ children: commentGroups.map((group) => /* @__PURE__ */ jsx(ReviewCommentThreadGroup, {
5786
6362
  agentId,
5787
6363
  agentLabel,
5788
6364
  comments: group.comments,
5789
6365
  focusCommentId,
6366
+ focusCommentRequest,
5790
6367
  focusEditorRef: setFocusEditorRef,
5791
6368
  identity,
5792
6369
  keymap,
@@ -5988,7 +6565,7 @@ const getHunkSelectionRange = (hunk) => {
5988
6565
  } : null;
5989
6566
  };
5990
6567
  const isSameSelection = (a, b) => a.start === b.start && a.end === b.end && a.side === b.side;
5991
- function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle = false, blocks, bottomInset = codeViewLayout.paddingBottom, codeQualityFindings = [], collapsed, comments, commitMetadata, diffLineHeight = 20, diffStyle, disableWorkerPool = false, expandedGenerated = emptyExpandedGenerated, files, focusCommentId, focusCommentRequest, forceExpandedPaths, gitIdentity, hunkNavigation, initialMarkdownPreviewSectionIds = emptyMarkdownPreviewSectionIds, isReadOnly = false, itemVersionByKey, keymap, loadingSectionIds, onActiveBlockChange, onAskCodex, onCommentDraftChange, onCreateComment, onDeleteComment, onLoadImageContent, onLoadSection, onLoadSectionContents, onOpenFile, onRefreshMarkdown, onResolveThread = noopResolveThread, onSaveCommentEdit, onSelectPathFromScroll, onSubmitComment, onToggleCollapsed, onToggleViewed, onUpdateComment, onUpdateSourceDescription, onUpdateSourceTitle, onUploadSourceDescriptionAsset, reviewIdentityByPath, scrollTarget, searchQuery, selectedPath, showSourceDescription = true, showWhitespace, source, sourceDescriptionActions, sourceDescriptionFooter, supportsReviewCommentActions, theme = "system", viewed, walkthroughNotes, wordWrap }) {
6568
+ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle = false, blocks, bottomInset = codeViewLayout.paddingBottom, codeQualityFindings = [], collapsed, comments, commitMetadata, diffLineHeight = 20, diffStyle, disableWorkerPool = false, expandedGenerated = emptyExpandedGenerated, files, focusCommentId, focusCommentRequest, forceExpandedPaths, gitIdentity, hunkNavigation, initialMarkdownPreviewSectionIds = emptyMarkdownPreviewSectionIds, isReadOnly = false, itemVersionByKey, keymap, loadingSectionIds, onActiveBlockChange, onAskCodex, onCommentDraftChange, onCreateComment, onDeleteComment, onFindDefinitions, onLoadImageContent, onLoadSection, onLoadSectionContents, onOpenDefinition, onOpenFile, onRefreshMarkdown, onResolveThread = noopResolveThread, onSaveCommentEdit, onSelectPathFromScroll, onSubmitComment, onToggleCollapsed, onToggleViewed, onUpdateComment, onUpdateSourceDescription, onUpdateSourceTitle, onUploadSourceDescriptionAsset, reviewIdentityByPath, scrollTarget, searchQuery, selectedPath, showSourceDescription = true, showWhitespace, source, sourceDescriptionActions, sourceDescriptionFooter, supportsReviewCommentActions, theme = "system", viewed, walkthroughNotes, wordWrap }) {
5992
6569
  const codeViewRef = useRef(null);
5993
6570
  const markdownEditorRefs = useRef(/* @__PURE__ */ new Map());
5994
6571
  const refreshingMarkdownSectionsRef = useRef(/* @__PURE__ */ new Set());
@@ -5996,6 +6573,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
5996
6573
  const handledScrollRequestRef = useRef(null);
5997
6574
  const handledHunkNavRef = useRef(hunkNavigation?.request ?? null);
5998
6575
  const emptyCommentDeleteTimersRef = useRef(/* @__PURE__ */ new Map());
6576
+ const definitionHighlightFrameRef = useRef(null);
5999
6577
  const highlightFrameRef = useRef(null);
6000
6578
  const ignoreNextLineSelectionEndRef = useRef(false);
6001
6579
  const navigatedSelectionRef = useRef(null);
@@ -6009,6 +6587,15 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6009
6587
  const [imagePreviewLayoutPassBySection, setImagePreviewLayoutPassBySection] = useState({});
6010
6588
  const [commentLayoutPassByItem, setCommentLayoutPassByItem] = useState({});
6011
6589
  const [selectedLines, setSelectedLines] = useState(null);
6590
+ const [definitionLookup, setDefinitionLookup] = useState(null);
6591
+ const definitionLookupRequestRef = useRef(0);
6592
+ const definitionModifierActiveRef = useRef(false);
6593
+ const sourceKey = getSourceKey(source);
6594
+ const [definitionLookupSourceKey, setDefinitionLookupSourceKey] = useState(sourceKey);
6595
+ if (definitionLookupSourceKey !== sourceKey) {
6596
+ setDefinitionLookupSourceKey(sourceKey);
6597
+ setDefinitionLookup(null);
6598
+ }
6012
6599
  const selectedLinesRef = useRef(null);
6013
6600
  const commitMessageMetadata = source.type === "commit" ? commitMetadata : null;
6014
6601
  const shouldShowCommitMessage = commitMessageMetadata != null;
@@ -6058,19 +6645,22 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6058
6645
  [itemId]: (current[itemId] ?? 0) + 1
6059
6646
  }));
6060
6647
  }, []);
6061
- const { firstItemByBlockId, firstItemByPath, itemBlockId, itemMetadata, items, searchTargetsByBaseItemId } = useMemo(() => {
6648
+ const { commentTargetById, firstItemByBlockId, firstItemByPath, itemBlockId, itemMetadata, items, searchTargetsByBaseItemId, selectedHeaderItemIds } = useMemo(() => {
6062
6649
  const nextItems = [];
6650
+ const nextCommentTargetById = /* @__PURE__ */ new Map();
6063
6651
  const nextFirstItemByBlockId = /* @__PURE__ */ new Map();
6064
6652
  const nextFirstItemByPath = /* @__PURE__ */ new Map();
6065
6653
  const nextItemBlockId = /* @__PURE__ */ new Map();
6066
6654
  const nextItemMetadata = /* @__PURE__ */ new Map();
6067
6655
  const nextSearchTargetsByBaseItemId = /* @__PURE__ */ new Map();
6656
+ const nextSelectedHeaderItemIds = /* @__PURE__ */ new Set();
6068
6657
  const fontLayoutKey = `line-height:${diffLineHeight}`;
6069
6658
  for (const block of reviewBlocks) {
6070
6659
  if (block.header) {
6071
6660
  const headerId = getBlockHeaderItemId(block);
6072
6661
  nextFirstItemByBlockId.set(block.id, nextFirstItemByBlockId.get(block.id) ?? headerId);
6073
6662
  nextItemBlockId.set(headerId, block.id);
6663
+ if ((block.headerSelected ?? block.selected) === true) nextSelectedHeaderItemIds.add(headerId);
6074
6664
  nextItems.push({
6075
6665
  annotations: [{
6076
6666
  lineNumber: 1,
@@ -6088,7 +6678,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6088
6678
  },
6089
6679
  id: headerId,
6090
6680
  type: "file",
6091
- version: getItemVersion(`${block.id}:walkthrough-header:${(block.headerSelected ?? block.selected) === true ? "selected" : "idle"}`)
6681
+ version: getItemVersion(`${block.id}:walkthrough-header`)
6092
6682
  });
6093
6683
  }
6094
6684
  if (!block.file) continue;
@@ -6126,6 +6716,11 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6126
6716
  const blockSectionComments = blockCommentsBySection.get(section.id) ?? [];
6127
6717
  const sectionComments = dedupeReviewComments([...visibleGlobalComments, ...blockSectionComments]);
6128
6718
  for (const comment of sectionComments) {
6719
+ if (!nextCommentTargetById.has(comment.id)) nextCommentTargetById.set(comment.id, isLineReviewComment(comment) ? {
6720
+ id,
6721
+ lineNumber: comment.lineNumber,
6722
+ side: comment.side
6723
+ } : { id });
6129
6724
  const key = getCommentKey(comment);
6130
6725
  const existing = annotationMap.get(key);
6131
6726
  if (existing && existing.metadata.type === "review-comment") annotationMap.set(key, {
@@ -6237,12 +6832,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6237
6832
  }
6238
6833
  }
6239
6834
  return {
6835
+ commentTargetById: nextCommentTargetById,
6240
6836
  firstItemByBlockId: nextFirstItemByBlockId,
6241
6837
  firstItemByPath: nextFirstItemByPath,
6242
6838
  itemBlockId: nextItemBlockId,
6243
6839
  itemMetadata: nextItemMetadata,
6244
6840
  items: nextItems,
6245
- searchTargetsByBaseItemId: nextSearchTargetsByBaseItemId
6841
+ searchTargetsByBaseItemId: nextSearchTargetsByBaseItemId,
6842
+ selectedHeaderItemIds: nextSelectedHeaderItemIds
6246
6843
  };
6247
6844
  }, [
6248
6845
  collapsed,
@@ -6295,6 +6892,18 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6295
6892
  itemMetadata,
6296
6893
  searchTargetsByBaseItemId
6297
6894
  ]);
6895
+ const getDefinitionDiffTarget = useCallback((candidate) => {
6896
+ for (const item of items) {
6897
+ if (item.type !== "diff") continue;
6898
+ const metadata = itemMetadata.get(item.id);
6899
+ const path = candidate.side === "deletions" ? metadata?.file.oldPath ?? metadata?.file.path : metadata?.file.path;
6900
+ if (metadata && path === candidate.path && lineIsVisibleInFileDiff(item.fileDiff, candidate.side, candidate.lineNumber)) return {
6901
+ item,
6902
+ metadata
6903
+ };
6904
+ }
6905
+ return null;
6906
+ }, [itemMetadata, items]);
6298
6907
  const setCodeViewSelectedLines = useCallback((selection) => {
6299
6908
  if (selection == null || navigatedSelectionRef.current == null || selection.id !== navigatedSelectionRef.current.id || !isSameSelection(selection.range, navigatedSelectionRef.current.range)) navigatedSelectionRef.current = null;
6300
6909
  setSelectedLines(selection);
@@ -6408,6 +7017,50 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6408
7017
  createCommentForRange(range, context);
6409
7018
  },
6410
7019
  onLineClick: (line, context) => {
7020
+ if (isPrimaryModifier(line.event) && onFindDefinitions) {
7021
+ line.event.preventDefault();
7022
+ line.event.stopPropagation();
7023
+ const lineElement = "lineElement" in line ? line.lineElement : null;
7024
+ const identifier = lineElement ? getIdentifierFromPointerEvent(line.event, lineElement) : null;
7025
+ const meta = itemMetadata.get(context.item.id);
7026
+ const side = "annotationSide" in line ? line.annotationSide : null;
7027
+ if (!identifier || !meta || !side) {
7028
+ setDefinitionLookup(null);
7029
+ return;
7030
+ }
7031
+ const requestId = ++definitionLookupRequestRef.current;
7032
+ setDefinitionLookup({
7033
+ anchor: {
7034
+ x: line.event.clientX,
7035
+ y: line.event.clientY
7036
+ },
7037
+ identifier,
7038
+ result: null,
7039
+ sourceKey
7040
+ });
7041
+ onFindDefinitions({
7042
+ identifier,
7043
+ kind: meta.section.kind,
7044
+ lineNumber: line.lineNumber,
7045
+ path: side === "deletions" ? meta.file.oldPath ?? meta.file.path : meta.file.path,
7046
+ side,
7047
+ source
7048
+ }).then((result) => {
7049
+ if (definitionLookupRequestRef.current === requestId) setDefinitionLookup((current) => current?.identifier === identifier && current.sourceKey === sourceKey ? {
7050
+ ...current,
7051
+ result
7052
+ } : current);
7053
+ }).catch(() => {
7054
+ if (definitionLookupRequestRef.current === requestId) setDefinitionLookup((current) => current?.identifier === identifier && current.sourceKey === sourceKey ? {
7055
+ ...current,
7056
+ result: {
7057
+ reason: "Definition search is unavailable for this repository.",
7058
+ status: "unavailable"
7059
+ }
7060
+ } : current);
7061
+ });
7062
+ return;
7063
+ }
6411
7064
  if (isReadOnly) return;
6412
7065
  if (isInteractiveReviewEvent(line.event)) return;
6413
7066
  const meta = itemMetadata.get(context.item.id);
@@ -6440,11 +7093,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6440
7093
  const metadata = itemMetadata.get(context.item.id);
6441
7094
  const isWalkthroughHeaderItem = context.item.id.endsWith(":walkthrough-header");
6442
7095
  node.classList.toggle("codiff-walkthrough-header-item", isWalkthroughHeaderItem);
6443
- node.classList.toggle("codiff-selected-item", metadata?.isSelected === true);
7096
+ node.classList.toggle("codiff-selected-item", metadata?.isSelected === true || selectedHeaderItemIds.has(context.item.id));
6444
7097
  node.classList.toggle("codiff-markdown-preview-item", metadata?.isMarkdownPreview === true);
6445
7098
  node.classList.toggle("codiff-image-preview-item", context.item.type === "file" && Boolean(metadata && canRenderImagePreview(metadata.file.path, metadata.section)));
6446
7099
  node.classList.toggle("codiff-loadable-summary-item", metadata != null && shouldLoadDiffSectionContents(metadata.section));
6447
7100
  node.classList.toggle("codiff-loading-summary-item", Boolean(metadata && loadingSectionIds.has(metadata.section.id)));
7101
+ if (definitionModifierActiveRef.current) window.requestAnimationFrame(() => {
7102
+ if (definitionModifierActiveRef.current && node.isConnected) applyIdentifierNavigationState([{ element: node }], true);
7103
+ });
6448
7104
  },
6449
7105
  overflow: wordWrap ? "wrap" : "scroll",
6450
7106
  stickyHeaders: true,
@@ -6465,7 +7121,11 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6465
7121
  loadDiffFiles,
6466
7122
  loadingSectionIds,
6467
7123
  onCreateComment,
7124
+ onFindDefinitions,
6468
7125
  onLoadSection,
7126
+ selectedHeaderItemIds,
7127
+ source,
7128
+ sourceKey,
6469
7129
  theme,
6470
7130
  wordWrap
6471
7131
  ]);
@@ -6557,11 +7217,19 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6557
7217
  poolSize: Math.min(3, Math.max(1, navigator.hardwareConcurrency || 3)),
6558
7218
  workerFactory: () => new Worker(new URL("@pierre/diffs/worker/worker.js", import.meta.url), { type: "module" })
6559
7219
  }), []);
6560
- const requestScrollItemHeaderIntoView = useCallback((itemId, behavior = "instant") => {
7220
+ const requestScrollTargetIntoView = useCallback((itemId, behavior = "instant", commentTarget) => {
6561
7221
  const handle = codeViewRef.current;
6562
7222
  const viewer = handle?.getInstance();
6563
7223
  if (!handle || !viewer || viewer.getTopForItem(itemId) == null) return false;
6564
- handle.scrollTo({
7224
+ handle.scrollTo(commentTarget?.lineNumber != null && commentTarget.side ? {
7225
+ align: "center",
7226
+ behavior: getEffectiveScrollBehavior(behavior),
7227
+ id: itemId,
7228
+ lineNumber: commentTarget.lineNumber,
7229
+ offset: 11,
7230
+ side: commentTarget.side,
7231
+ type: "line"
7232
+ } : {
6565
7233
  behavior: getEffectiveScrollBehavior(behavior),
6566
7234
  id: itemId,
6567
7235
  offset: 11,
@@ -6572,14 +7240,15 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6572
7240
  useLayoutEffect(() => {
6573
7241
  if (!scrollTarget || handledScrollRequestRef.current === scrollTarget.request) return;
6574
7242
  const behavior = scrollTarget.behavior ?? "instant";
6575
- const itemId = scrollTarget.blockId ? firstItemByBlockId.get(scrollTarget.blockId) : scrollTarget.path ? firstItemByPath.get(scrollTarget.path) : null;
7243
+ const commentTarget = scrollTarget.commentId ? commentTargetById.get(scrollTarget.commentId) : void 0;
7244
+ const itemId = commentTarget?.id ?? (scrollTarget.blockId ? firstItemByBlockId.get(scrollTarget.blockId) : scrollTarget.path ? firstItemByPath.get(scrollTarget.path) : null);
6576
7245
  if (!itemId) return;
6577
7246
  let frame = null;
6578
7247
  let attempts = 0;
6579
7248
  let canceled = false;
6580
7249
  const tryScroll = () => {
6581
7250
  if (canceled || handledScrollRequestRef.current === scrollTarget.request) return;
6582
- if (requestScrollItemHeaderIntoView(itemId, behavior)) {
7251
+ if (requestScrollTargetIntoView(itemId, behavior, commentTarget)) {
6583
7252
  handledScrollRequestRef.current = scrollTarget.request;
6584
7253
  return;
6585
7254
  }
@@ -6594,9 +7263,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6594
7263
  if (frame != null) window.cancelAnimationFrame(frame);
6595
7264
  };
6596
7265
  }, [
7266
+ commentTargetById,
6597
7267
  firstItemByBlockId,
6598
7268
  firstItemByPath,
6599
- requestScrollItemHeaderIntoView,
7269
+ requestScrollTargetIntoView,
6600
7270
  scrollTarget
6601
7271
  ]);
6602
7272
  useEffect(() => {
@@ -6758,6 +7428,22 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6758
7428
  applySearchHighlights(viewer.getRenderedItems(), searchQuery, resolvedActiveSearchMatch);
6759
7429
  });
6760
7430
  }, [resolvedActiveSearchMatch, searchQuery]);
7431
+ const updateRenderedIdentifierNavigation = useCallback((active, viewer) => {
7432
+ const nextViewer = viewer ?? codeViewRef.current?.getInstance();
7433
+ if (!nextViewer) return;
7434
+ if (definitionHighlightFrameRef.current != null) {
7435
+ window.cancelAnimationFrame(definitionHighlightFrameRef.current);
7436
+ definitionHighlightFrameRef.current = null;
7437
+ }
7438
+ if (!active) {
7439
+ applyIdentifierNavigationState(nextViewer.getRenderedItems(), false);
7440
+ return;
7441
+ }
7442
+ definitionHighlightFrameRef.current = window.requestAnimationFrame(() => {
7443
+ definitionHighlightFrameRef.current = null;
7444
+ applyIdentifierNavigationState(nextViewer.getRenderedItems(), true);
7445
+ });
7446
+ }, []);
6761
7447
  const scheduleStickyHeaderStateUpdate = useCallback((viewer) => {
6762
7448
  const nextViewer = viewer ?? codeViewRef.current?.getInstance();
6763
7449
  if (!nextViewer) return;
@@ -6771,6 +7457,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6771
7457
  for (const timer of deferredTimersRef.current) window.clearTimeout(timer);
6772
7458
  deferredTimersRef.current.clear();
6773
7459
  emptyCommentDeleteTimersRef.current.clear();
7460
+ if (definitionHighlightFrameRef.current != null) window.cancelAnimationFrame(definitionHighlightFrameRef.current);
6774
7461
  if (highlightFrameRef.current != null) window.cancelAnimationFrame(highlightFrameRef.current);
6775
7462
  if (stickyHeaderFrameRef.current != null) window.cancelAnimationFrame(stickyHeaderFrameRef.current);
6776
7463
  }, []);
@@ -6782,6 +7469,38 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6782
7469
  scheduleSearchHighlights,
6783
7470
  scheduleStickyHeaderStateUpdate
6784
7471
  ]);
7472
+ useEffect(() => {
7473
+ if (!onFindDefinitions) return;
7474
+ const updateDefinitionModifierState = (active) => {
7475
+ if (definitionModifierActiveRef.current === active) return;
7476
+ definitionModifierActiveRef.current = active;
7477
+ updateRenderedIdentifierNavigation(active);
7478
+ };
7479
+ const handleModifierChange = (event) => {
7480
+ updateDefinitionModifierState(isPrimaryModifier(event));
7481
+ };
7482
+ const handleModifierPointer = (event) => {
7483
+ updateDefinitionModifierState(isPrimaryModifier(event));
7484
+ };
7485
+ const clearDefinitionModifierState = () => updateDefinitionModifierState(false);
7486
+ window.addEventListener("keydown", handleModifierChange);
7487
+ window.addEventListener("keyup", handleModifierChange);
7488
+ window.addEventListener("pointerdown", handleModifierPointer);
7489
+ window.addEventListener("pointermove", handleModifierPointer);
7490
+ window.addEventListener("blur", clearDefinitionModifierState);
7491
+ return () => {
7492
+ clearDefinitionModifierState();
7493
+ window.removeEventListener("keydown", handleModifierChange);
7494
+ window.removeEventListener("keyup", handleModifierChange);
7495
+ window.removeEventListener("pointerdown", handleModifierPointer);
7496
+ window.removeEventListener("pointermove", handleModifierPointer);
7497
+ window.removeEventListener("blur", clearDefinitionModifierState);
7498
+ };
7499
+ }, [onFindDefinitions, updateRenderedIdentifierNavigation]);
7500
+ const invalidateDefinitionLookup = useCallback(() => {
7501
+ definitionLookupRequestRef.current++;
7502
+ }, []);
7503
+ useEffect(() => invalidateDefinitionLookup, [invalidateDefinitionLookup]);
6785
7504
  useEffect(() => {
6786
7505
  const handle = codeViewRef.current;
6787
7506
  const viewer = handle?.getInstance();
@@ -6963,15 +7682,17 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6963
7682
  }
6964
7683
  scheduleSearchHighlights();
6965
7684
  scheduleStickyHeaderStateUpdate(viewer);
7685
+ if (definitionModifierActiveRef.current) updateRenderedIdentifierNavigation(true, viewer);
6966
7686
  }, [
6967
7687
  itemBlockId,
6968
7688
  items,
6969
7689
  onActiveBlockChange,
6970
7690
  onSelectPathFromScroll,
6971
7691
  scheduleSearchHighlights,
6972
- scheduleStickyHeaderStateUpdate
7692
+ scheduleStickyHeaderStateUpdate,
7693
+ updateRenderedIdentifierNavigation
6973
7694
  ]);
6974
- return disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
7695
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
6975
7696
  className: "code-view",
6976
7697
  disableWorkerPool,
6977
7698
  items: codeViewItems,
@@ -6999,11 +7720,77 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6999
7720
  renderCustomHeader,
7000
7721
  selectedLines: isReadOnly ? null : selectedLines
7001
7722
  })
7723
+ }), definitionLookup?.sourceKey === sourceKey && onOpenDefinition ? /* @__PURE__ */ jsx(DefinitionPopover, {
7724
+ anchor: definitionLookup.anchor,
7725
+ getDestination: (candidate) => getDefinitionDiffTarget(candidate) ? "diff" : candidate.canOpenInEditor ? "editor" : "unavailable",
7726
+ identifier: definitionLookup.identifier,
7727
+ onClose: () => {
7728
+ invalidateDefinitionLookup();
7729
+ setDefinitionLookup(null);
7730
+ },
7731
+ onOpen: (candidate) => {
7732
+ const target = getDefinitionDiffTarget(candidate);
7733
+ if (target) {
7734
+ if (target.metadata.isCollapsed) onToggleCollapsed(target.metadata.file, true, target.metadata.reviewIdentity.key);
7735
+ const scrollToDefinition = () => codeViewRef.current?.scrollTo({
7736
+ align: "center",
7737
+ behavior: "smooth-auto",
7738
+ id: target.item.id,
7739
+ lineNumber: candidate.lineNumber,
7740
+ offset: 11,
7741
+ side: candidate.side,
7742
+ type: "line"
7743
+ });
7744
+ if (target.metadata.isCollapsed) window.requestAnimationFrame(() => window.requestAnimationFrame(scrollToDefinition));
7745
+ else scrollToDefinition();
7746
+ } else if (candidate.canOpenInEditor) onOpenDefinition(candidate);
7747
+ invalidateDefinitionLookup();
7748
+ setDefinitionLookup(null);
7749
+ },
7750
+ result: definitionLookup.result
7751
+ }) : null] });
7752
+ }
7753
+ //#endregion
7754
+ //#region app/components/ReviewModeControl.tsx
7755
+ function ReviewModeControl({ mode, modes, onModeChange }) {
7756
+ return /* @__PURE__ */ jsx("div", {
7757
+ "aria-label": "Review mode",
7758
+ className: "review-mode-control",
7759
+ role: "tablist",
7760
+ children: modes.map((item) => /* @__PURE__ */ jsxs("button", {
7761
+ "aria-label": item.ariaLabel,
7762
+ "aria-selected": mode === item.value,
7763
+ onClick: () => onModeChange(item.value),
7764
+ role: "tab",
7765
+ title: item.title,
7766
+ type: "button",
7767
+ children: [
7768
+ item.icon,
7769
+ /* @__PURE__ */ jsx("span", {
7770
+ className: "review-mode-label",
7771
+ children: item.label
7772
+ }),
7773
+ item.indicator
7774
+ ]
7775
+ }, item.value))
7002
7776
  });
7003
7777
  }
7004
7778
  //#endregion
7005
7779
  //#region app/components/ReviewTopBar.tsx
7006
- function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, onToggleSidebar, repository, repositoryTooltip, sidebarCollapsed, toggleTitle }) {
7780
+ function ReviewTopBar({ actions, center, context, leading, mode, modes, onModeChange, onToggleSidebar, repository, repositoryTooltip, sidebarCollapsed, sidebarPosition = "left", sourceMenu, toggleTitle }) {
7781
+ const sidebarToggle = /* @__PURE__ */ jsx("button", {
7782
+ "aria-label": sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar",
7783
+ className: "review-top-bar-icon-button sidebar-toggle-button",
7784
+ onClick: onToggleSidebar,
7785
+ title: toggleTitle,
7786
+ type: "button",
7787
+ children: /* @__PURE__ */ jsx(SidebarSimpleIcon, {
7788
+ "aria-hidden": true,
7789
+ mirrored: sidebarPosition === "right",
7790
+ size: 18,
7791
+ weight: "bold"
7792
+ })
7793
+ });
7007
7794
  return /* @__PURE__ */ jsxs("header", {
7008
7795
  className: "review-top-bar workspace-top-bar",
7009
7796
  children: [
@@ -7011,18 +7798,8 @@ function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, on
7011
7798
  className: "review-top-bar-left",
7012
7799
  children: [
7013
7800
  leading,
7014
- /* @__PURE__ */ jsx("button", {
7015
- "aria-label": sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar",
7016
- className: "sidebar-toggle-button",
7017
- onClick: onToggleSidebar,
7018
- title: toggleTitle,
7019
- type: "button",
7020
- children: /* @__PURE__ */ jsx(SidebarSimpleIcon, {
7021
- "aria-hidden": true,
7022
- size: 18,
7023
- weight: "bold"
7024
- })
7025
- }),
7801
+ sidebarPosition === "left" ? sidebarToggle : null,
7802
+ sourceMenu,
7026
7803
  /* @__PURE__ */ jsx("div", {
7027
7804
  className: "review-top-bar-repository-slot",
7028
7805
  title: repositoryTooltip,
@@ -7030,36 +7807,27 @@ function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, on
7030
7807
  })
7031
7808
  ]
7032
7809
  }),
7033
- /* @__PURE__ */ jsx("div", {
7034
- "aria-label": "Review mode",
7035
- className: "review-mode-control",
7036
- role: "tablist",
7037
- children: modes.map((item) => /* @__PURE__ */ jsxs("button", {
7038
- "aria-label": item.ariaLabel,
7039
- "aria-selected": mode === item.value,
7040
- onClick: () => onModeChange(item.value),
7041
- role: "tab",
7042
- title: item.title,
7043
- type: "button",
7044
- children: [
7045
- item.icon,
7046
- /* @__PURE__ */ jsx("span", {
7047
- className: "review-mode-label",
7048
- children: item.label
7049
- }),
7050
- item.indicator
7051
- ]
7052
- }, item.value))
7810
+ modes ? /* @__PURE__ */ jsx(ReviewModeControl, {
7811
+ mode,
7812
+ modes,
7813
+ onModeChange
7814
+ }) : /* @__PURE__ */ jsx("div", {
7815
+ className: "review-top-bar-center",
7816
+ children: center
7053
7817
  }),
7054
7818
  /* @__PURE__ */ jsxs("div", {
7055
7819
  className: "review-top-bar-right",
7056
- children: [context ? /* @__PURE__ */ jsx("div", {
7057
- className: "review-top-bar-context",
7058
- children: context
7059
- }) : null, actions ? /* @__PURE__ */ jsx("div", {
7060
- className: "review-top-bar-actions",
7061
- children: actions
7062
- }) : null]
7820
+ children: [
7821
+ context ? /* @__PURE__ */ jsx("div", {
7822
+ className: "review-top-bar-context",
7823
+ children: context
7824
+ }) : null,
7825
+ actions ? /* @__PURE__ */ jsx("div", {
7826
+ className: "review-top-bar-actions",
7827
+ children: actions
7828
+ }) : null,
7829
+ sidebarPosition === "right" ? sidebarToggle : null
7830
+ ]
7063
7831
  })
7064
7832
  ]
7065
7833
  });
@@ -7211,6 +7979,7 @@ function StageFileRow({ file, on, onToggle }) {
7211
7979
  function StageGroupRow({ group, onToggleFile, onToggleGroup, selected }) {
7212
7980
  const paths = group.files.map((file) => file.path);
7213
7981
  const onCount = paths.filter((path) => selected.has(path)).length;
7982
+ const state = onCount === 0 ? "off" : onCount === paths.length ? "on" : "partial";
7214
7983
  return /* @__PURE__ */ jsxs("div", {
7215
7984
  className: "wt-stage-group",
7216
7985
  children: [/* @__PURE__ */ jsxs("button", {
@@ -7218,7 +7987,7 @@ function StageGroupRow({ group, onToggleFile, onToggleGroup, selected }) {
7218
7987
  onClick: () => onToggleGroup(paths),
7219
7988
  type: "button",
7220
7989
  children: [
7221
- /* @__PURE__ */ jsx(CommitCheck, { state: onCount === 0 ? "off" : onCount === paths.length ? "on" : "partial" }),
7990
+ /* @__PURE__ */ jsx(CommitCheck, { state }),
7222
7991
  /* @__PURE__ */ jsx("span", {
7223
7992
  className: "wt-stage-group-icon",
7224
7993
  children: /* @__PURE__ */ jsx(ChapterIcon, {
@@ -7554,14 +8323,14 @@ function CommitView({ branch, draft, model, onCommit, onCommitOutput, onUpdateMe
7554
8323
  }
7555
8324
  //#endregion
7556
8325
  //#region app/components/walkthrough/NarrativeWalkthroughView.tsx
7557
- const getWalkthroughBlockScrollTarget = ({ activeBlockId, firstSupportBlockId, mode, stopScrollRequest, supportScrollRequest }) => mode === "support" && firstSupportBlockId ? {
8326
+ const getWalkthroughBlockScrollTarget = ({ activeBlockId, firstSupportBlockId, scrollTarget }) => scrollTarget.kind === "support" && firstSupportBlockId ? {
7558
8327
  behavior: "smooth",
7559
8328
  blockId: firstSupportBlockId,
7560
- request: supportScrollRequest
7561
- } : mode === "stop" && activeBlockId && stopScrollRequest > 0 ? {
8329
+ request: scrollTarget.nonce
8330
+ } : scrollTarget.kind === "stop" && activeBlockId && scrollTarget.nonce > 0 ? {
7562
8331
  behavior: "smooth",
7563
8332
  blockId: activeBlockId,
7564
- request: stopScrollRequest
8333
+ request: scrollTarget.nonce
7565
8334
  } : null;
7566
8335
  const getFocusedRunDiffs = (item, files) => resolveWalkthroughHunkRuns(item, files).flatMap((run) => {
7567
8336
  const focused = focusChangedFileForHunks(run.resolved.file, run.resolved.section, run.hunks);
@@ -7584,14 +8353,15 @@ const getWalkthroughNavigationKeyDirection = (event) => {
7584
8353
  if (isCtrlOnly && event.key === "ArrowUp") return -1;
7585
8354
  return 0;
7586
8355
  };
8356
+ const getWalkthroughKeyboardForwardIndex = ({ index, scrollRequest }) => index === 0 && scrollRequest === 0 ? 0 : index + 1;
7587
8357
  const emptyWalkthroughBlockSet = {
7588
8358
  blocks: [],
7589
8359
  firstBlockIdByStop: [],
7590
8360
  stopIndexByBlockId: /* @__PURE__ */ new Map()
7591
8361
  };
7592
- function StopHeader({ current, stop }) {
8362
+ function StopHeader({ stop }) {
7593
8363
  return /* @__PURE__ */ jsxs("div", {
7594
- className: `wt-stop-block wt-stop-block-header${current ? " current" : ""}`,
8364
+ className: "wt-stop-block wt-stop-block-header",
7595
8365
  children: [/* @__PURE__ */ jsxs("div", {
7596
8366
  className: "wt-stage-title-row",
7597
8367
  children: [/* @__PURE__ */ jsx("h2", {
@@ -7601,9 +8371,9 @@ function StopHeader({ current, stop }) {
7601
8371
  }), /* @__PURE__ */ jsx(Narration, { prose: stop.prose })]
7602
8372
  });
7603
8373
  }
7604
- function SupportHeader({ current }) {
8374
+ function SupportHeader() {
7605
8375
  return /* @__PURE__ */ jsxs("div", {
7606
- className: `wt-stop-block wt-stop-block-header${current ? " current" : ""}`,
8376
+ className: "wt-stop-block wt-stop-block-header",
7607
8377
  children: [/* @__PURE__ */ jsxs("div", {
7608
8378
  className: "wt-stage-title-row",
7609
8379
  children: [/* @__PURE__ */ jsx("h2", {
@@ -7624,10 +8394,7 @@ const createWalkthroughBlocks = (files, walkthroughView, currentIndex) => {
7624
8394
  firstBlockIdByStop[stop.index] = blockId;
7625
8395
  stopIndexByBlockId.set(blockId, stop.index);
7626
8396
  blocks.push({
7627
- header: /* @__PURE__ */ jsx(StopHeader, {
7628
- current: stop.index === currentIndex,
7629
- stop
7630
- }),
8397
+ header: /* @__PURE__ */ jsx(StopHeader, { stop }),
7631
8398
  headerSelected: stop.index === currentIndex,
7632
8399
  id: blockId
7633
8400
  });
@@ -7639,10 +8406,7 @@ const createWalkthroughBlocks = (files, walkthroughView, currentIndex) => {
7639
8406
  stopIndexByBlockId.set(blockId, stop.index);
7640
8407
  blocks.push({
7641
8408
  file,
7642
- header: runIndex === 0 ? /* @__PURE__ */ jsx(StopHeader, {
7643
- current: stop.index === currentIndex,
7644
- stop
7645
- }) : null,
8409
+ header: runIndex === 0 ? /* @__PURE__ */ jsx(StopHeader, { stop }) : null,
7646
8410
  headerSelected: stop.index === currentIndex,
7647
8411
  id: blockId,
7648
8412
  itemIdPrefix: blockId,
@@ -7671,7 +8435,7 @@ const createSupportBlocks = (files, selected, walkthroughView, showWhitespace) =
7671
8435
  const isFirstBlock = blocks.length === 0;
7672
8436
  blocks.push({
7673
8437
  file,
7674
- header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, { current: selected }) : null,
8438
+ header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, {}) : null,
7675
8439
  headerSelected: selected,
7676
8440
  id: blockId,
7677
8441
  itemIdPrefix: blockId,
@@ -7685,7 +8449,7 @@ const createSupportBlocks = (files, selected, walkthroughView, showWhitespace) =
7685
8449
  const isFirstBlock = blocks.length === 0;
7686
8450
  blocks.push({
7687
8451
  file,
7688
- header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, { current: selected }) : null,
8452
+ header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, {}) : null,
7689
8453
  headerSelected: selected,
7690
8454
  id: blockId,
7691
8455
  itemIdPrefix: blockId,
@@ -7720,8 +8484,10 @@ function Arc({ committable, navigation, onShareWalkthrough, shareWalkthroughDisa
7720
8484
  const goArcPrev = useCallback(() => {
7721
8485
  if (navigation.mode === "stop") navigation.goPrev();
7722
8486
  else if (navigation.mode === "support") navigation.goStop(walkthroughView.sequence.length - 1);
7723
- else if (navigation.mode === "commit") if (supportAvailable) navigation.openSupport();
7724
- else navigation.goStop(walkthroughView.sequence.length - 1);
8487
+ else if (navigation.mode === "commit") {
8488
+ if (supportAvailable) navigation.openSupport();
8489
+ else navigation.goStop(walkthroughView.sequence.length - 1);
8490
+ }
7725
8491
  }, [
7726
8492
  navigation,
7727
8493
  supportAvailable,
@@ -7880,9 +8646,7 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
7880
8646
  const reviewBlockScrollTarget = getWalkthroughBlockScrollTarget({
7881
8647
  activeBlockId,
7882
8648
  firstSupportBlockId,
7883
- mode: navigation.mode,
7884
- stopScrollRequest: navigation.scrollTarget.nonce,
7885
- supportScrollRequest: navigation.supportScrollRequest
8649
+ scrollTarget: navigation.scrollTarget
7886
8650
  });
7887
8651
  const handleActiveBlockChange = useCallback((blockId) => {
7888
8652
  onActiveReviewTargetChange(getBlockReviewTarget(reviewBlocks, blockId));
@@ -7918,7 +8682,11 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
7918
8682
  if (direction === 1) {
7919
8683
  event.preventDefault();
7920
8684
  if (navigation.mode === "stop") {
7921
- if (navigation.index < walkthroughView.sequence.length - 1) navigation.goNext();
8685
+ const targetIndex = getWalkthroughKeyboardForwardIndex({
8686
+ index: navigation.index,
8687
+ scrollRequest: navigation.scrollTarget.nonce
8688
+ });
8689
+ if (targetIndex < walkthroughView.sequence.length) navigation.goStop(targetIndex);
7922
8690
  else if (supportAvailable) navigation.openSupport();
7923
8691
  else if (committable) navigation.enterCommit();
7924
8692
  } else if (navigation.mode === "support" && committable) navigation.enterCommit();
@@ -7926,8 +8694,10 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
7926
8694
  event.preventDefault();
7927
8695
  if (navigation.mode === "stop") navigation.goPrev();
7928
8696
  else if (navigation.mode === "support") navigation.goStop(navigation.index);
7929
- else if (navigation.mode === "commit") if (supportAvailable) navigation.openSupport();
7930
- else navigation.goStop(navigation.index);
8697
+ else if (navigation.mode === "commit") {
8698
+ if (supportAvailable) navigation.openSupport();
8699
+ else navigation.goStop(navigation.index);
8700
+ }
7931
8701
  }
7932
8702
  }, [
7933
8703
  committable,
@@ -8160,9 +8930,9 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8160
8930
  const [index, setIndex] = useState(0);
8161
8931
  const [scrollTarget, setScrollTarget] = useState({
8162
8932
  index: 0,
8933
+ kind: "stop",
8163
8934
  nonce: 0
8164
8935
  });
8165
- const [supportScrollRequest, setSupportScrollRequest] = useState(0);
8166
8936
  const [supportVisited, setSupportVisited] = useState(false);
8167
8937
  const [visited, setVisited] = useState(() => {
8168
8938
  const stopId = firstStopId(walkthrough);
@@ -8193,9 +8963,9 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8193
8963
  setIndex(0);
8194
8964
  setScrollTarget({
8195
8965
  index: 0,
8966
+ kind: "stop",
8196
8967
  nonce: 0
8197
8968
  });
8198
- setSupportScrollRequest(0);
8199
8969
  setSupportVisited(false);
8200
8970
  const stopId = firstStopId(walkthrough);
8201
8971
  setVisited(new Set(stopId ? [stopId] : []));
@@ -8257,6 +9027,7 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8257
9027
  pendingSupportScrollRef.current = null;
8258
9028
  setScrollTarget((current) => ({
8259
9029
  index: clamped,
9030
+ kind: "stop",
8260
9031
  nonce: current.nonce + 1
8261
9032
  }));
8262
9033
  }, [
@@ -8301,7 +9072,11 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8301
9072
  if (walkthroughView?.sequence.length) setIndex(walkthroughView.sequence.length - 1);
8302
9073
  setMode("support");
8303
9074
  pendingSupportScrollRef.current = { walkthrough };
8304
- setSupportScrollRequest((current) => current + 1);
9075
+ setScrollTarget((current) => ({
9076
+ ...current,
9077
+ kind: "support",
9078
+ nonce: current.nonce + 1
9079
+ }));
8305
9080
  setSupportVisited(true);
8306
9081
  }, [
8307
9082
  walkthrough,
@@ -8309,11 +9084,15 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8309
9084
  leaveStopMode
8310
9085
  ]);
8311
9086
  const syncSupportFromScroll = useCallback(() => {
9087
+ const pendingStop = pendingStopScrollRef.current;
9088
+ if (pendingStop) {
9089
+ if (pendingStop.walkthrough === walkthrough) return;
9090
+ pendingStopScrollRef.current = null;
9091
+ }
8312
9092
  pendingSupportScrollRef.current = null;
8313
- pendingStopScrollRef.current = null;
8314
9093
  setMode("support");
8315
9094
  setSupportVisited(true);
8316
- }, []);
9095
+ }, [walkthrough]);
8317
9096
  return {
8318
9097
  commitBody,
8319
9098
  commitSelected,
@@ -8332,7 +9111,6 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8332
9111
  scrollTarget,
8333
9112
  setCommitBody,
8334
9113
  setCommitSubject,
8335
- supportScrollRequest,
8336
9114
  supportVisited,
8337
9115
  syncIndexFromScroll,
8338
9116
  syncSupportFromScroll,
@@ -8431,7 +9209,7 @@ const writeSidebarWidth = (width, storage = localStorage) => {
8431
9209
  };
8432
9210
  //#endregion
8433
9211
  //#region app/hooks/useResizableSidebar.ts
8434
- function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, readWidth }) {
9212
+ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, position, readWidth }) {
8435
9213
  const [sidebarWidth, setSidebarWidth] = useState(readWidth);
8436
9214
  return {
8437
9215
  resizeSidebar: useCallback((event) => {
@@ -8440,7 +9218,7 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8440
9218
  const handle = event.currentTarget;
8441
9219
  const shell = handle.parentElement;
8442
9220
  if (!shell) return;
8443
- const shellLeft = shell.getBoundingClientRect().left;
9221
+ const shellRect = shell.getBoundingClientRect();
8444
9222
  handle.setPointerCapture(event.pointerId);
8445
9223
  handle.classList.add("dragging");
8446
9224
  document.body.style.cursor = "col-resize";
@@ -8454,7 +9232,7 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8454
9232
  document.body.style.cursor = "";
8455
9233
  };
8456
9234
  const handleMove = (moveEvent) => {
8457
- const rawWidth = moveEvent.clientX - shellLeft;
9235
+ const rawWidth = position === "right" ? shellRect.right - moveEvent.clientX : moveEvent.clientX - shellRect.left;
8458
9236
  if (collapseThreshold != null && rawWidth < collapseThreshold) {
8459
9237
  collapsed = true;
8460
9238
  onCollapse?.();
@@ -8476,7 +9254,8 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8476
9254
  }, [
8477
9255
  collapseThreshold,
8478
9256
  onCollapse,
8479
- onWidthCommit
9257
+ onWidthCommit,
9258
+ position
8480
9259
  ]),
8481
9260
  sidebarWidth
8482
9261
  };
@@ -8582,6 +9361,7 @@ function useReviewCommentDrafts({ canCreateComment = true, comments, onCommentFi
8582
9361
  setComments,
8583
9362
  updateActiveReviewCommentDraft
8584
9363
  ]),
9364
+ focusComment,
8585
9365
  focusCommentId,
8586
9366
  focusCommentRequest,
8587
9367
  resetCommentFocus,
@@ -8652,6 +9432,7 @@ function useReviewFileState({ initialSelectedPath = null, onViewedChange } = {})
8652
9432
  const defaultConfigTemplate = {
8653
9433
  settings: {
8654
9434
  "agentBackend": "codex",
9435
+ "checkForUpdates": true,
8655
9436
  "claudeModel": "claude-sonnet-4-6",
8656
9437
  "codeFontFamily": "",
8657
9438
  "codeFontSize": 13,
@@ -8663,6 +9444,7 @@ const defaultConfigTemplate = {
8663
9444
  "opencodeModel": "opencode-default",
8664
9445
  "piModel": "pi-default",
8665
9446
  "reviewCommentsPrefix": "# Address these Review Comments",
9447
+ "sidebarPosition": "left",
8666
9448
  "showOutdated": false,
8667
9449
  "showWhitespace": false,
8668
9450
  "theme": "system",
@@ -8670,6 +9452,7 @@ const defaultConfigTemplate = {
8670
9452
  "wordWrap": false
8671
9453
  },
8672
9454
  keymap: {
9455
+ "askAgent": "Mod+Alt+Enter",
8673
9456
  "closeSearch": "Escape",
8674
9457
  "commandBar": "Mod+Shift+p",
8675
9458
  "diffSearch": "Mod+f",
@@ -8727,10 +9510,35 @@ const emptyReviewComments = [];
8727
9510
  const emptyGeneralCommentThreads = [];
8728
9511
  const emptyPaths = /* @__PURE__ */ new Set();
8729
9512
  const emptyWalkthroughNotes = /* @__PURE__ */ new Map();
9513
+ const reviewSurfacePreferencesKey = "codiff:web-review-surface-preferences:v1";
9514
+ const getLocationHashTarget = () => {
9515
+ const hash = window.location.hash.slice(1);
9516
+ if (!hash) return null;
9517
+ try {
9518
+ return decodeURIComponent(hash);
9519
+ } catch {
9520
+ return hash;
9521
+ }
9522
+ };
9523
+ const commentMatchesHashTarget = (comment, target) => comment.id === target || comment.threadId === target || comment.url?.slice(comment.url.lastIndexOf("#") + 1) === target;
8730
9524
  const readSharedSidebarWidth = () => typeof localStorage === "undefined" ? 292 : readSidebarWidth();
8731
9525
  const writeSharedSidebarWidth = (width) => {
8732
9526
  if (typeof localStorage !== "undefined") writeSidebarWidth(width);
8733
9527
  };
9528
+ const readStoredSidebarCollapsed = () => {
9529
+ if (typeof localStorage === "undefined") return null;
9530
+ try {
9531
+ const stored = JSON.parse(localStorage.getItem(reviewSurfacePreferencesKey) ?? "{}");
9532
+ if (stored && typeof stored === "object" && "sidebarCollapsed" in stored && typeof stored.sidebarCollapsed === "boolean") return stored.sidebarCollapsed;
9533
+ } catch {}
9534
+ return null;
9535
+ };
9536
+ const writeStoredSidebarCollapsed = (sidebarCollapsed) => {
9537
+ if (typeof localStorage === "undefined") return;
9538
+ try {
9539
+ localStorage.setItem(reviewSurfacePreferencesKey, JSON.stringify({ sidebarCollapsed }));
9540
+ } catch {}
9541
+ };
8734
9542
  const getSnapshotReviewComments = (snapshot) => {
8735
9543
  if (!snapshot.reviewComments?.length) return emptyReviewComments;
8736
9544
  return getReviewCommentsFromState({
@@ -8752,7 +9560,9 @@ const disabledCommitMessage = async () => ({
8752
9560
  reason: "Shared walkthroughs are read-only.",
8753
9561
  status: "unavailable"
8754
9562
  });
8755
- function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMode, interactive, onDeleteShare, onModeChange, providerLabel = "provider", repositoryUrl, settingsBar, signInLabel = "Sign in to comment", snapshot, sourceDescriptionFooterAside, title }) {
9563
+ const mobileSidebarMediaQuery = "(max-width: 720px)";
9564
+ const shouldCollapseSidebarInitially = () => readStoredSidebarCollapsed() ?? (typeof window !== "undefined" && window.matchMedia(mobileSidebarMediaQuery).matches);
9565
+ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMode, interactive, onDeleteShare, onModeChange, providerLabel = "provider", repositoryUrl, settingsBar, sidebarPosition = "left", signInLabel = "Sign in to comment", snapshot, sourceDescriptionFooterAside, title }) {
8756
9566
  const canComment = commenting?.canComment ?? Boolean(interactive);
8757
9567
  const deleteShare = useCallback(async () => {
8758
9568
  if (!onDeleteShare || !window.confirm("Delete this shared walkthrough? This cannot be undone.")) return;
@@ -8775,23 +9585,44 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8775
9585
  const keymap = useMemo(() => createDefaultConfig().keymap, []);
8776
9586
  const [fileSearchQuery, setFileSearchQuery] = useState("");
8777
9587
  const [uncontrolledSidebarMode, setUncontrolledSidebarMode] = useState(() => initialMode ?? (interactive ? "tree" : "walkthrough"));
8778
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
9588
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(null);
9589
+ const sidebarCollapsedRef = useRef(null);
9590
+ const sidebarInteractedRef = useRef(false);
9591
+ const updateSidebarCollapsed = useCallback((value, persist) => {
9592
+ sidebarCollapsedRef.current = value;
9593
+ setSidebarCollapsed(value);
9594
+ if (persist) writeStoredSidebarCollapsed(value);
9595
+ }, []);
9596
+ const toggleSidebar = useCallback(() => {
9597
+ sidebarInteractedRef.current = true;
9598
+ updateSidebarCollapsed(!(sidebarCollapsedRef.current ?? shouldCollapseSidebarInitially()), true);
9599
+ }, [updateSidebarCollapsed]);
9600
+ useEffect(() => {
9601
+ const timeout = window.setTimeout(() => {
9602
+ if (!sidebarInteractedRef.current) updateSidebarCollapsed(shouldCollapseSidebarInitially(), false);
9603
+ }, 0);
9604
+ return () => window.clearTimeout(timeout);
9605
+ }, [updateSidebarCollapsed]);
8779
9606
  useEffect(() => {
8780
9607
  const handleKeyDown = (event) => {
8781
9608
  if (event.repeat || !matchesShortcut(event, keymap, "toggleSidebar")) return;
8782
9609
  event.preventDefault();
8783
- setSidebarCollapsed((current) => !current);
9610
+ toggleSidebar();
8784
9611
  };
8785
9612
  window.addEventListener("keydown", handleKeyDown);
8786
9613
  return () => window.removeEventListener("keydown", handleKeyDown);
8787
- }, [keymap]);
9614
+ }, [keymap, toggleSidebar]);
8788
9615
  const sidebarMode = Boolean(initialMode && onModeChange) && initialMode ? initialMode : uncontrolledSidebarMode;
8789
9616
  const [treeScrollTarget, setTreeScrollTarget] = useState(null);
8790
9617
  const { bumpItemVersion, collapsed, expandedGenerated, itemVersionByKey, selectedPath, setSelectedPath, toggleCollapsed, toggleViewed, viewed } = useReviewFileState({ initialSelectedPath: snapshot.files[0]?.path ?? null });
8791
9618
  const { resizeSidebar, sidebarWidth } = useResizableSidebar({
8792
9619
  collapseThreshold: 80,
8793
- onCollapse: () => setSidebarCollapsed(true),
9620
+ onCollapse: () => {
9621
+ sidebarInteractedRef.current = true;
9622
+ updateSidebarCollapsed(true, true);
9623
+ },
8794
9624
  onWidthCommit: writeSharedSidebarWidth,
9625
+ position: sidebarPosition,
8795
9626
  readWidth: readSharedSidebarWidth
8796
9627
  });
8797
9628
  const snapshotReviewComments = useMemo(() => getSnapshotReviewComments(snapshot), [snapshot]);
@@ -8802,7 +9633,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8802
9633
  } : comment), [editedReviewCommentBodies, snapshotReviewComments]);
8803
9634
  const [localReviewComments, setLocalReviewComments] = useState(emptyReviewComments);
8804
9635
  const reviewComments = useMemo(() => mergeReviewComments(visibleSnapshotReviewComments, localReviewComments), [localReviewComments, visibleSnapshotReviewComments]);
8805
- const { activeReviewCommentDraftRef, activeReviewCommentDraftState, clearCommentFocus, createComment, deleteComment: deleteLocalComment, focusCommentId, focusCommentRequest, reviewCommentsRef, updateActiveReviewCommentDraft, updateComment } = useReviewCommentDrafts({
9636
+ const { activeReviewCommentDraftRef, activeReviewCommentDraftState, clearCommentFocus, createComment, deleteComment: deleteLocalComment, focusComment, focusCommentId, focusCommentRequest, reviewCommentsRef, updateActiveReviewCommentDraft, updateComment } = useReviewCommentDrafts({
8806
9637
  canCreateComment: canComment,
8807
9638
  comments: reviewComments,
8808
9639
  onCommentFileChange: bumpItemVersion,
@@ -8828,6 +9659,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8828
9659
  const walkthroughRequestPendingRef = useRef(false);
8829
9660
  const [walkthroughRequestId, setWalkthroughRequestId] = useState(0);
8830
9661
  const interactiveRef = useRef(interactive);
9662
+ const handledHashTargetRef = useRef(null);
8831
9663
  const visibleFiles = useMemo(() => sortFiles(snapshot.files).filter((file) => fuzzyMatches(file.path, fileSearchQuery) && fileHasVisibleDiff(file, snapshot.preferences.showWhitespace)), [
8832
9664
  fileSearchQuery,
8833
9665
  snapshot.files,
@@ -9074,6 +9906,56 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9074
9906
  request: (current?.request ?? 0) + 1
9075
9907
  }));
9076
9908
  }, [setSelectedPath]);
9909
+ const activateReviewComment = useCallback((comment) => {
9910
+ changeSidebarMode("tree");
9911
+ setSelectedPath(comment.filePath);
9912
+ focusComment(comment.id);
9913
+ setTreeScrollTarget((current) => ({
9914
+ behavior: "smooth",
9915
+ commentId: comment.id,
9916
+ path: comment.filePath,
9917
+ request: (current?.request ?? 0) + 1
9918
+ }));
9919
+ }, [
9920
+ changeSidebarMode,
9921
+ focusComment,
9922
+ setSelectedPath
9923
+ ]);
9924
+ const activateHashTarget = useCallback(() => {
9925
+ const target = getLocationHashTarget();
9926
+ if (!target || handledHashTargetRef.current === target) return;
9927
+ const reviewComment = reviewComments.find((comment) => commentMatchesHashTarget(comment, target));
9928
+ if (reviewComment) {
9929
+ handledHashTargetRef.current = target;
9930
+ activateReviewComment(reviewComment);
9931
+ return;
9932
+ }
9933
+ for (const thread of generalCommentThreads) {
9934
+ const generalComment = thread.comments.find((comment) => commentMatchesHashTarget(comment, target)) ?? (thread.id === target ? thread.comments[0] : void 0);
9935
+ if (generalComment) {
9936
+ handledHashTargetRef.current = target;
9937
+ activateGeneralComment(generalComment.id);
9938
+ return;
9939
+ }
9940
+ }
9941
+ }, [
9942
+ activateGeneralComment,
9943
+ activateReviewComment,
9944
+ generalCommentThreads,
9945
+ reviewComments
9946
+ ]);
9947
+ useEffect(() => {
9948
+ const timeout = window.setTimeout(activateHashTarget, 0);
9949
+ return () => window.clearTimeout(timeout);
9950
+ }, [activateHashTarget]);
9951
+ useEffect(() => {
9952
+ const handleHashChange = () => {
9953
+ handledHashTargetRef.current = null;
9954
+ activateHashTarget();
9955
+ };
9956
+ window.addEventListener("hashchange", handleHashChange);
9957
+ return () => window.removeEventListener("hashchange", handleHashChange);
9958
+ }, [activateHashTarget]);
9077
9959
  const updateSelectedPathFromScroll = useCallback((viewer) => {
9078
9960
  const nextPath = getSelectedPathFromScroll(viewer, visibleFiles, snapshot.preferences.showWhitespace);
9079
9961
  if (nextPath) setSelectedPath((current) => current === nextPath ? current : nextPath);
@@ -9181,7 +10063,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9181
10063
  });
9182
10064
  };
9183
10065
  const sourceLabel = snapshot.repository.source.type === "working-tree" ? null : getSourceLabel(snapshot.repository.source);
9184
- const rootLabel = repositoryUrl ? snapshot.repository.root : compactPath(snapshot.repository.root);
10066
+ const rootLabel = repositoryUrl ? snapshot.repository.root : abbreviateHomePath(snapshot.repository.root);
9185
10067
  const sourceExternalUrl = snapshot.repository.source.type === "pull-request" ? externalUrl ?? snapshot.repository.source.url : null;
9186
10068
  const repositoryLinkUrl = repositoryUrl ?? sourceExternalUrl;
9187
10069
  const walkthroughStatus = walkthroughRequestPending && interactive?.walkthroughStatus !== "ready" ? "generating" : interactive?.walkthroughStatus;
@@ -9252,9 +10134,10 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9252
10134
  children: settingsBar
9253
10135
  }) : null] }) : void 0;
9254
10136
  return /* @__PURE__ */ jsxs("div", {
9255
- className: `app-shell share-shell${interactive ? " merge-request-shell" : ""}${sidebarCollapsed ? " sidebar-collapsed" : ""}`,
10137
+ className: `app-shell share-shell${interactive ? " merge-request-shell" : ""}${sidebarCollapsed === null ? " sidebar-auto" : sidebarCollapsed ? " sidebar-collapsed" : ""}`,
10138
+ "data-sidebar-position": sidebarPosition,
9256
10139
  "data-theme": shellTheme,
9257
- style: sidebarCollapsed ? void 0 : { gridTemplateColumns: `${sidebarWidth}px 0 minmax(0, 1fr)` },
10140
+ style: sidebarCollapsed !== false ? void 0 : { gridTemplateColumns: sidebarPosition === "right" ? `minmax(0, 1fr) 0 ${sidebarWidth}px` : `${sidebarWidth}px 0 minmax(0, 1fr)` },
9258
10141
  children: [
9259
10142
  /* @__PURE__ */ jsx(ReviewTopBar, {
9260
10143
  actions: topBarActions,
@@ -9295,7 +10178,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9295
10178
  mode: sidebarMode,
9296
10179
  modes: reviewModes,
9297
10180
  onModeChange: changeSidebarMode,
9298
- onToggleSidebar: () => setSidebarCollapsed((current) => !current),
10181
+ onToggleSidebar: toggleSidebar,
9299
10182
  repository: repositoryLinkUrl ? /* @__PURE__ */ jsx("a", {
9300
10183
  className: "review-top-bar-repository",
9301
10184
  href: repositoryLinkUrl,
@@ -9307,7 +10190,8 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9307
10190
  children: rootLabel
9308
10191
  }),
9309
10192
  repositoryTooltip: snapshot.repository.root,
9310
- sidebarCollapsed,
10193
+ sidebarCollapsed: sidebarCollapsed ?? false,
10194
+ sidebarPosition,
9311
10195
  toggleTitle: `${sidebarCollapsed ? "Expand" : "Collapse"} sidebar`
9312
10196
  }),
9313
10197
  /* @__PURE__ */ jsxs("aside", {
@@ -9385,6 +10269,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9385
10269
  onCancelEdit: cancelEditGeneralComment,
9386
10270
  onChangeDraft: setGeneralCommentDraft,
9387
10271
  onChangeEditDraft: setGeneralCommentEditDraft,
10272
+ onResolveDiscussion: resolveDiscussion,
9388
10273
  onSaveEdit: saveGeneralCommentEdit,
9389
10274
  onStartEdit: startEditGeneralComment,
9390
10275
  onSubmit: submitGeneralComment,