@nkzw/codiff-core 1.10.0 → 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.
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,7 +18,6 @@ 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
22
  import "@phosphor-icons/react/CircleNotch";
23
23
  import { PowerIcon } from "@phosphor-icons/react/Power";
@@ -28,6 +28,8 @@ import { ColumnsIcon } from "@phosphor-icons/react/Columns";
28
28
  import { ImageBrokenIcon } from "@phosphor-icons/react/ImageBroken";
29
29
  import { SquareSplitVerticalIcon } from "@phosphor-icons/react/SquareSplitVertical";
30
30
  import { CodeView, WorkerPoolContextProvider } from "@pierre/diffs/react";
31
+ import { CrosshairSimpleIcon } from "@phosphor-icons/react/CrosshairSimple";
32
+ import { ProhibitIcon } from "@phosphor-icons/react/Prohibit";
31
33
  import { PersistentMarkdownEditor } from "@nkzw/mdx-editor/persistence";
32
34
  import { GitBranchIcon } from "@phosphor-icons/react/GitBranch";
33
35
  import { ShareNetworkIcon } from "@phosphor-icons/react/ShareNetwork";
@@ -294,13 +296,40 @@ const renderInlineMarkdown = (text) => {
294
296
  return nodes.length > 0 ? nodes : text;
295
297
  };
296
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
297
316
  //#region app/components/ReadOnlyMarkdownView.tsx
298
- const detailsBlockPattern = /<details\b([^>]*)>\s*<summary\b[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi;
299
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;
300
322
  const hasOpenAttribute = (attributes) => /(?:^|\s)open(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?(?:\s|$)/i.test(attributes);
301
323
  const stripHtmlComments$1 = (value) => value.replaceAll(htmlCommentPattern$1, "");
302
- const getFenceMarker = (line) => {
303
- 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;
304
333
  };
305
334
  const normalizeReadOnlyMarkdownValue = (value) => {
306
335
  const normalizedLineEndings = value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
@@ -310,10 +339,9 @@ const normalizeReadOnlyMarkdownValue = (value) => {
310
339
  let pendingBlankLine = false;
311
340
  let fenceMarker = null;
312
341
  for (const line of lines) {
313
- const currentFenceMarker = getFenceMarker(line);
314
342
  if (fenceMarker) {
315
343
  normalizedLines.push(line);
316
- if (currentFenceMarker?.startsWith(fenceMarker[0]) && currentFenceMarker.length >= fenceMarker.length) fenceMarker = null;
344
+ if (isClosingFence(fenceMarker, line)) fenceMarker = null;
317
345
  continue;
318
346
  }
319
347
  if (!line.trim()) {
@@ -323,27 +351,83 @@ const normalizeReadOnlyMarkdownValue = (value) => {
323
351
  if (pendingBlankLine && normalizedLines.length > 0) normalizedLines.push("");
324
352
  pendingBlankLine = false;
325
353
  normalizedLines.push(line);
326
- if (currentFenceMarker) fenceMarker = currentFenceMarker;
354
+ const openingFenceMarker = getOpeningFenceMarker(line);
355
+ if (openingFenceMarker) fenceMarker = openingFenceMarker;
327
356
  }
328
357
  const normalizedValue = normalizedLines.join("\n");
329
358
  return preserveSingleTrailingNewline && normalizedValue ? `${normalizedValue}\n` : normalizedValue;
330
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
+ };
331
382
  const parseMarkdownDetails = (value) => {
332
383
  const parts = [];
333
384
  let lastIndex = 0;
334
- let match;
335
- while (match = detailsBlockPattern.exec(value)) {
336
- 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({
337
420
  type: "markdown",
338
- value: value.slice(lastIndex, match.index)
421
+ value: value.slice(lastIndex, detailsOpen.index)
339
422
  });
340
423
  parts.push({
341
- body: match[3] ?? "",
342
- open: hasOpenAttribute(match[1] ?? ""),
343
- 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(),
344
427
  type: "details"
345
428
  });
346
- lastIndex = detailsBlockPattern.lastIndex;
429
+ lastIndex = detailsTagPattern.lastIndex;
430
+ searchIndex = lastIndex;
347
431
  }
348
432
  if (lastIndex < value.length) parts.push({
349
433
  type: "markdown",
@@ -352,6 +436,42 @@ const parseMarkdownDetails = (value) => {
352
436
  return parts;
353
437
  };
354
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
+ }
355
475
  function MarkdownSegment({ additionalPlugins, ariaLabel, contentClassName, editorClassName, onHeightChange, value }) {
356
476
  const normalizedValue = normalizeReadOnlyMarkdownValue(value);
357
477
  if (!normalizedValue.trim()) return null;
@@ -383,17 +503,22 @@ function MarkdownParts({ additionalPlugins, ariaLabel, contentClassName, editorC
383
503
  onHeightChange,
384
504
  value: part.value
385
505
  }, `markdown:${index}`);
506
+ const bodyParts = parseMarkdownDetails(part.body);
507
+ const code = getOwnFencedCode(bodyParts);
386
508
  return /* @__PURE__ */ jsxs("details", {
387
509
  className: "codiff-markdown-details",
388
510
  onToggle: (event) => onHeightChange?.(event.currentTarget.getBoundingClientRect().height),
389
511
  open: part.open,
390
- 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, {
391
516
  additionalPlugins,
392
517
  ariaLabel: `${ariaLabel} details`,
393
518
  contentClassName,
394
519
  editorClassName,
395
520
  onHeightChange,
396
- parts: parseMarkdownDetails(part.body)
521
+ parts: bodyParts
397
522
  })]
398
523
  }, `details:${index}`);
399
524
  });
@@ -497,8 +622,7 @@ const getPlanCommentTargetLabel = (anchor) => {
497
622
  const entity = anchor.kind === "text" ? "Selection" : planCommentEntityLabels[anchor.block.type] ?? "Block";
498
623
  const targetText = (anchor.kind === "text" ? anchor.quote?.exact : anchor.block.text)?.replaceAll(/\s+/g, " ").trim();
499
624
  if (!targetText) return entity;
500
- const maximumLength = 48;
501
- return `${entity} · ${targetText.length > maximumLength ? `${targetText.slice(0, maximumLength - 1).trimEnd()}…` : targetText}`;
625
+ return `${entity} · ${targetText.length > 48 ? `${targetText.slice(0, 47).trimEnd()}…` : targetText}`;
502
626
  };
503
627
  const escapeInlineMarkdown = (value) => value.replaceAll("\\", String.raw`\\`).replaceAll(/([`*_[\]<>])/g, String.raw`\$1`);
504
628
  const getPlanCommentAuthorLabel = (author) => {
@@ -1420,6 +1544,7 @@ const getFirstVisibleSection = (file, showWhitespace) => getVisibleDiffSections(
1420
1544
  //#region lib/files.ts
1421
1545
  const statusForTree = {
1422
1546
  added: "added",
1547
+ conflicted: "modified",
1423
1548
  deleted: "deleted",
1424
1549
  modified: "modified",
1425
1550
  renamed: "renamed",
@@ -1558,22 +1683,23 @@ function ReviewFileTree({ files, onActivatePath, reloadDeltaPaths = emptyPaths$2
1558
1683
  scrollSelectedPathIntoView,
1559
1684
  selectedPath
1560
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]);
1561
1696
  return /* @__PURE__ */ jsx("div", {
1562
1697
  className: "file-tree-shell",
1563
1698
  ref: treeHostRef,
1564
1699
  children: /* @__PURE__ */ jsx(FileTree, {
1565
1700
  className: "file-tree",
1566
1701
  model,
1567
- onClick: useCallback((event) => {
1568
- for (const target of event.nativeEvent.composedPath()) {
1569
- if (!("getAttribute" in target) || typeof target.getAttribute !== "function") continue;
1570
- const path = target.getAttribute("data-item-path");
1571
- if (path && filePathSet.has(path)) {
1572
- onActivatePath(path);
1573
- return;
1574
- }
1575
- }
1576
- }, [filePathSet, onActivatePath])
1702
+ onClick: handleTreeClick
1577
1703
  })
1578
1704
  });
1579
1705
  }
@@ -1727,6 +1853,66 @@ const matchesCombo = (event, combo, matchesKey) => {
1727
1853
  const matchesBinding = (event, binding, matchesKey) => getBindingCombos(binding).some((combo) => matchesCombo(event, combo, matchesKey));
1728
1854
  const canFallBackToPhysicalKey = (event, keymap) => event.altKey && isMac() && isDeadOrCharacterKey(event.key) && !Object.values(keymap).some((binding) => matchesBinding(event, binding, matchesTypedKey));
1729
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
+ }
1730
1916
  //#endregion
1731
1917
  //#region app/components/merge-request/GeneralComments.tsx
1732
1918
  const getAuthorDisplayName = (author) => author.name || author.login;
@@ -1770,13 +1956,14 @@ function SubmittedAtTime({ submittedAt }) {
1770
1956
  }
1771
1957
  function ReadOnlyGeneralCommentCard({ className = "", comment, focused = false }) {
1772
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(" ");
1773
1965
  return /* @__PURE__ */ jsxs("article", {
1774
- className: [
1775
- "review-comment",
1776
- "general-comment-card",
1777
- focused ? "focused" : "",
1778
- className
1779
- ].filter(Boolean).join(" "),
1966
+ className: classes,
1780
1967
  id: getGeneralCommentElementId(comment.id),
1781
1968
  children: [/* @__PURE__ */ jsx(Avatar, {
1782
1969
  name: displayName,
@@ -1913,13 +2100,16 @@ function GeneralCommentCard({ comment, editDraft, editError, editing, editSubmit
1913
2100
  })]
1914
2101
  });
1915
2102
  }
1916
- 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 }) {
1917
2104
  const [replyDraft, setReplyDraft] = useState("");
1918
2105
  const [replyError, setReplyError] = useState(null);
1919
2106
  const [replying, setReplying] = useState(false);
1920
2107
  const [showReply, setShowReply] = useState(false);
1921
2108
  const [resolving, setResolving] = useState(false);
1922
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;
1923
2113
  const submitReply = useCallback(() => {
1924
2114
  const body = replyDraft.trim();
1925
2115
  if (!body || replying) return;
@@ -1939,62 +2129,79 @@ function GeneralCommentThreadCard({ canComment, editDraft, editError, editingCom
1939
2129
  ]);
1940
2130
  const toggleResolved = useCallback(() => {
1941
2131
  if (resolving) return;
2132
+ const nextResolved = !effectiveResolved;
2133
+ setResolutionOverride({
2134
+ base: resolved,
2135
+ value: nextResolved
2136
+ });
1942
2137
  setResolving(true);
1943
- onResolve(thread.id, !resolved).catch((error) => {
2138
+ onResolve(thread.id, nextResolved).catch((error) => {
2139
+ setResolutionOverride(null);
1944
2140
  window.alert(error instanceof Error ? error.message : String(error));
1945
2141
  }).finally(() => setResolving(false));
1946
2142
  }, [
2143
+ effectiveResolved,
1947
2144
  onResolve,
1948
2145
  resolved,
1949
2146
  resolving,
1950
2147
  thread.id
1951
2148
  ]);
1952
- 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", {
1953
2194
  className: "general-comment-thread",
1954
- children: [
1955
- thread.comments.map((comment) => /* @__PURE__ */ jsx(GeneralCommentCard, {
1956
- comment,
1957
- editDraft,
1958
- editError: editingCommentId === comment.id ? editError : null,
1959
- editing: editingCommentId === comment.id,
1960
- editSubmitting: editSubmitting && editingCommentId === comment.id,
1961
- focused: comment.id === focusedCommentId,
1962
- keymap,
1963
- onCancelEdit,
1964
- onChangeEditDraft,
1965
- onDelete,
1966
- onSaveEdit,
1967
- onStartEdit
1968
- }, comment.id)),
1969
- thread.canReply && canComment && !resolved ? showReply ? /* @__PURE__ */ jsx(GeneralCommentComposer, {
1970
- disabled: false,
1971
- draft: replyDraft,
1972
- error: replyError,
1973
- gitIdentity: null,
1974
- keymap,
1975
- onChangeDraft: setReplyDraft,
1976
- onSubmit: submitReply,
1977
- submitting: replying
1978
- }) : /* @__PURE__ */ jsx("div", {
1979
- className: "review-comment-thread-footer",
1980
- children: /* @__PURE__ */ jsx("button", {
1981
- className: "review-comment-action",
1982
- onClick: () => setShowReply(true),
1983
- type: "button",
1984
- children: "Reply"
1985
- })
1986
- }) : null,
1987
- thread.canResolve ? /* @__PURE__ */ jsx("div", {
1988
- className: "review-comment-thread-footer",
1989
- children: /* @__PURE__ */ jsx("button", {
1990
- className: "review-comment-action",
1991
- disabled: resolving,
1992
- onClick: toggleResolved,
1993
- type: "button",
1994
- children: resolving ? "Saving" : resolved ? "Reopen" : "Resolve"
1995
- })
1996
- }) : null
1997
- ]
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
1998
2205
  });
1999
2206
  }
2000
2207
  function SidebarGeneralCommentList({ comments, focusedCommentId, onActivateComment }) {
@@ -2102,7 +2309,7 @@ function GeneralCommentComposer({ disabled, draft, error, gitIdentity, keymap, o
2102
2309
  })
2103
2310
  });
2104
2311
  }
2105
- 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 }) {
2106
2313
  const commentsRef = useRef(null);
2107
2314
  useEffect(() => {
2108
2315
  if (focusedCommentId == null) return;
@@ -2128,6 +2335,7 @@ function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, ed
2128
2335
  editingCommentId,
2129
2336
  editSubmitting,
2130
2337
  focusedCommentId,
2338
+ focusedCommentRequest,
2131
2339
  keymap,
2132
2340
  onCancelEdit,
2133
2341
  onChangeEditDraft,
@@ -2137,7 +2345,7 @@ function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, ed
2137
2345
  });
2138
2346
  },
2139
2347
  onReply: (threadId, body) => commenting?.onReplyGeneralComment(threadId, body) ?? Promise.reject(/* @__PURE__ */ new Error("Replying is unavailable.")),
2140
- 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.")),
2141
2349
  onSaveEdit,
2142
2350
  onStartEdit,
2143
2351
  thread
@@ -2283,23 +2491,6 @@ const getReviewCommentsFromState = (state) => (state.reviewComments ?? []).flatM
2283
2491
  });
2284
2492
  const shouldDiscardReviewCommentOnEscape = (body, confirmDiscard = window.confirm) => body.trim().length === 0 || confirmDiscard("Discard this review comment?");
2285
2493
  //#endregion
2286
- //#region app/components/useCopiedState.ts
2287
- function useCopiedState(timeoutMs) {
2288
- const [copied, setCopied] = useState(false);
2289
- const copiedTimerRef = useRef(null);
2290
- useEffect(() => () => {
2291
- if (copiedTimerRef.current != null) window.clearTimeout(copiedTimerRef.current);
2292
- }, []);
2293
- return [copied, useCallback(() => {
2294
- setCopied(true);
2295
- if (copiedTimerRef.current != null) window.clearTimeout(copiedTimerRef.current);
2296
- copiedTimerRef.current = window.setTimeout(() => {
2297
- setCopied(false);
2298
- copiedTimerRef.current = null;
2299
- }, timeoutMs);
2300
- }, [timeoutMs])];
2301
- }
2302
- //#endregion
2303
2494
  //#region app/components/Panels.tsx
2304
2495
  const getPullRequestReviewActionStatus = (reviewStatus, event) => event === "APPROVE" ? reviewStatus?.approve : event === "COMMENT" ? reviewStatus?.comment : reviewStatus?.requestChanges;
2305
2496
  const isPullRequestReviewActionDisabled = (reviewStatus, event) => getPullRequestReviewActionStatus(reviewStatus, event)?.disabled === true;
@@ -3347,6 +3538,7 @@ setCustomExtension("cts", "typescript");
3347
3538
  setCustomExtension("mts", "typescript");
3348
3539
  const statusLabel = {
3349
3540
  added: "Added",
3541
+ conflicted: "Conflicted",
3350
3542
  deleted: "Deleted",
3351
3543
  modified: "Modified",
3352
3544
  renamed: "Renamed",
@@ -3479,6 +3671,20 @@ const codeViewUnsafeCSS = `
3479
3671
  cursor: progress;
3480
3672
  }
3481
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
+
3482
3688
  .codiff-search-mark {
3483
3689
  background: var(--diffs-find-highlight-bg, rgb(255 216 92 / 0.65));
3484
3690
  border-radius: 3px;
@@ -3505,8 +3711,179 @@ const codeViewUnsafeCSS = `
3505
3711
  [data-selected-line] [data-gutter-utility-slot] {
3506
3712
  display: none;
3507
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
+ }
3508
3721
  `;
3509
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
3510
3887
  //#region lib/item-version.ts
3511
3888
  const getItemVersion = (value) => {
3512
3889
  let hash = 0;
@@ -3522,6 +3899,8 @@ const isNativeInputTarget = (target) => {
3522
3899
  const candidate = target;
3523
3900
  return candidate?.closest?.("input, select, textarea") != null || candidate?.isContentEditable === true;
3524
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;
3525
3904
  //#endregion
3526
3905
  //#region lib/review-identity.ts
3527
3906
  const getFileReviewIdentity = (file) => ({
@@ -3601,6 +3980,101 @@ const applySearchHighlights = (renderedItems, query, activeMatch) => {
3601
3980
  }
3602
3981
  };
3603
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
3604
4078
  //#region app/components/MarkdownDocumentEditor.tsx
3605
4079
  const persistenceAdapter = { save: ({ content, document }) => window.codiff.saveMarkdownDocument({
3606
4080
  baseVersion: document.version,
@@ -3717,12 +4191,6 @@ const RepositoryMarkdownEditor = forwardRef(function RepositoryMarkdownEditor({
3717
4191
  });
3718
4192
  });
3719
4193
  //#endregion
3720
- //#region lib/source.ts
3721
- const rangeLabel = (source) => `${source.base}${source.symmetric ? "..." : ".."}${source.head}`;
3722
- 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";
3723
- const getShortRef = (ref) => ref.slice(0, 7);
3724
- 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";
3725
- //#endregion
3726
4194
  //#region lib/narrative-walkthrough.ts
3727
4195
  const walkthroughFileName = (path) => path.split("/").pop() ?? path;
3728
4196
  const uniquePaths = (paths) => {
@@ -4145,7 +4613,8 @@ const chapterIcons = {
4145
4613
  wrench: WrenchIcon
4146
4614
  };
4147
4615
  function ChapterIcon({ icon, size = 13 }) {
4148
- return /* @__PURE__ */ jsx(chapterIcons[icon] ?? chapterIcons.path, { size });
4616
+ const Icon = chapterIcons[icon] ?? chapterIcons.path;
4617
+ return /* @__PURE__ */ jsx(Icon, { size });
4149
4618
  }
4150
4619
  function ImportancePill({ importance }) {
4151
4620
  return /* @__PURE__ */ jsx("span", {
@@ -4430,13 +4899,14 @@ function WalkthroughProgress({ phase, responseLabelIndex, stageRevision }) {
4430
4899
  }, [stageRevision]);
4431
4900
  const elapsedSeconds = timerState.stageRevision === stageRevision ? timerState.elapsedSeconds : 0;
4432
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…";
4433
4903
  return /* @__PURE__ */ jsxs("span", {
4434
4904
  "aria-live": "polite",
4435
4905
  className: "walkthrough-progress",
4436
4906
  role: "status",
4437
4907
  children: [/* @__PURE__ */ jsx("span", {
4438
4908
  className: "walkthrough-progress-label",
4439
- children: phase === "agent-generation" ? "Analyzing changes…" : phase === "response-received" ? walkthroughResponseLabels[Math.abs(responseLabelIndex) % walkthroughResponseLabels.length] : "Generating walkthrough…"
4909
+ children: label
4440
4910
  }), /* @__PURE__ */ jsx("span", {
4441
4911
  "aria-hidden": !showTimer,
4442
4912
  className: `walkthrough-progress-timer${showTimer ? " visible" : ""}`,
@@ -5476,6 +5946,9 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5476
5946
  flushDraft,
5477
5947
  onCommentBlur
5478
5948
  ]);
5949
+ const handleAddComment = useCallback(() => {
5950
+ document.activeElement?.blur();
5951
+ }, []);
5479
5952
  const handleFocus = useCallback(() => {
5480
5953
  onCommentFocus(draftComment);
5481
5954
  }, [draftComment, onCommentFocus]);
@@ -5491,13 +5964,21 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5491
5964
  ]);
5492
5965
  const handleKeyDown = useCallback((event) => {
5493
5966
  if (matchesShortcut(event, keymap, "submitComment")) {
5494
- if (supportsReviewCommentActions && commentCanSubmit) {
5495
- event.preventDefault();
5496
- event.stopPropagation();
5497
- handleSubmitComment();
5967
+ if (supportsReviewCommentActions) {
5968
+ if (commentCanSubmit) {
5969
+ event.preventDefault();
5970
+ event.stopPropagation();
5971
+ handleSubmitComment();
5972
+ }
5498
5973
  return;
5499
5974
  }
5500
- if (!supportsReviewCommentActions && canAskCodex) {
5975
+ event.preventDefault();
5976
+ event.stopPropagation();
5977
+ handleAddComment();
5978
+ return;
5979
+ }
5980
+ if (matchesShortcut(event, keymap, "askAgent")) {
5981
+ if (canAskCodex) {
5501
5982
  event.preventDefault();
5502
5983
  event.stopPropagation();
5503
5984
  handleAskCodex();
@@ -5515,6 +5996,7 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5515
5996
  comment.id,
5516
5997
  comment.isReadOnly,
5517
5998
  draft,
5999
+ handleAddComment,
5518
6000
  handleAskCodex,
5519
6001
  handleSubmitComment,
5520
6002
  keymap,
@@ -5570,7 +6052,7 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5570
6052
  className: "review-comment-action",
5571
6053
  disabled: !canAskCodex,
5572
6054
  onClick: handleAskCodex,
5573
- title: canAskCodex ? `Ask ${agentLabel}` : `Write a note before asking ${agentLabel}`,
6055
+ title: canAskCodex ? `Ask ${agentLabel} (${getShortcutLabel(keymap, "askAgent")})` : `Write a note before asking ${agentLabel}`,
5574
6056
  type: "button",
5575
6057
  children: [/* @__PURE__ */ jsx("img", {
5576
6058
  alt: "",
@@ -5732,7 +6214,7 @@ const groupReviewCommentsByThread = (comments) => {
5732
6214
  };
5733
6215
  const noopResolveThread = () => {};
5734
6216
  const noopLayoutReady = () => {};
5735
- 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 }) {
5736
6218
  const [resolveState, setResolveState] = useState({
5737
6219
  error: null,
5738
6220
  submitting: false,
@@ -5741,10 +6223,13 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5741
6223
  const lastComment = comments.at(-1);
5742
6224
  const threadId = lastComment?.threadId;
5743
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;
5744
6230
  const canResolveThread = supportsReviewCommentActions && threadId != null && comments.some((comment) => comment.canResolveThread === true);
5745
- 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;
5746
6232
  const hasThreadActions = canReplyToThread || canResolveThread;
5747
- const resolving = resolveState.threadId === threadId && resolveState.submitting;
5748
6233
  const resolveError = resolveState.threadId === threadId ? resolveState.error : null;
5749
6234
  const handleReply = useCallback(() => {
5750
6235
  if (!threadId || !lastComment) return;
@@ -5756,16 +6241,22 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5756
6241
  ]);
5757
6242
  const handleResolve = useCallback(() => {
5758
6243
  if (!threadId || resolving) return;
6244
+ const nextResolved = !effectiveThreadResolved;
6245
+ setResolutionOverride({
6246
+ base: threadResolved,
6247
+ value: nextResolved
6248
+ });
5759
6249
  setResolveState({
5760
6250
  error: null,
5761
6251
  submitting: true,
5762
6252
  threadId
5763
6253
  });
5764
- Promise.resolve(onResolveThread(threadId, !threadResolved)).then(() => setResolveState({
6254
+ Promise.resolve(onResolveThread(threadId, nextResolved)).then(() => setResolveState({
5765
6255
  error: null,
5766
6256
  submitting: false,
5767
6257
  threadId
5768
6258
  })).catch((error) => {
6259
+ setResolutionOverride(null);
5769
6260
  setResolveState({
5770
6261
  error: error instanceof Error ? error.message : String(error),
5771
6262
  submitting: false,
@@ -5773,54 +6264,64 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5773
6264
  });
5774
6265
  });
5775
6266
  }, [
6267
+ effectiveThreadResolved,
5776
6268
  onResolveThread,
5777
6269
  resolving,
5778
6270
  threadId,
5779
6271
  threadResolved
5780
6272
  ]);
5781
- 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", {
5782
6323
  className: "review-comment-thread-group",
5783
- children: [comments.map((comment) => {
5784
- return /* @__PURE__ */ jsx(ReviewCommentEditor, {
5785
- agentId,
5786
- agentLabel,
5787
- comment,
5788
- displayName: comment.author ? getReviewAuthorDisplayName(comment.author) : getGitIdentityDisplayName(identity),
5789
- focusCommentId,
5790
- focusEditorRef,
5791
- identity,
5792
- keymap,
5793
- onAskCodex,
5794
- onCommentBlur,
5795
- onCommentDraftChange,
5796
- onCommentFocus,
5797
- onDeleteComment,
5798
- onSaveCommentEdit,
5799
- onSubmitComment,
5800
- onUpdateComment,
5801
- supportsReviewCommentActions
5802
- }, comment.id);
5803
- }), hasThreadActions ? /* @__PURE__ */ jsxs("div", {
5804
- className: "review-comment-thread-footer",
5805
- children: [resolveError ? /* @__PURE__ */ jsx("span", {
5806
- className: "review-comment-thread-error",
5807
- children: resolveError
5808
- }) : null, /* @__PURE__ */ jsxs("div", {
5809
- className: "review-comment-thread-actions",
5810
- children: [canReplyToThread ? /* @__PURE__ */ jsx("button", {
5811
- className: "review-comment-action",
5812
- onClick: handleReply,
5813
- type: "button",
5814
- children: "Reply"
5815
- }) : null, canResolveThread ? /* @__PURE__ */ jsx("button", {
5816
- className: "review-comment-action",
5817
- disabled: resolving,
5818
- onClick: handleResolve,
5819
- type: "button",
5820
- children: resolving ? "Saving" : threadResolved ? "Reopen" : "Resolve"
5821
- }) : null]
5822
- })]
5823
- }) : null]
6324
+ children: threadContent
5824
6325
  });
5825
6326
  }
5826
6327
  function ReviewAnnotation({ agentId, agentLabel, annotation, comments, focusCommentId, focusCommentRequest, identity, keymap, onAskCodex, onCommentBlur, onCommentDraftChange, onCommentFocus, onDeleteComment, onHeightChange, onReplyToThread, onResolveThread = noopResolveThread, onSaveCommentEdit, onSubmitComment, onUpdateComment, supportsReviewCommentActions }) {
@@ -5853,14 +6354,16 @@ function ReviewAnnotation({ agentId, agentLabel, annotation, comments, focusComm
5853
6354
  return () => observer.disconnect();
5854
6355
  }, [annotationComments.length, onHeightChange]);
5855
6356
  if (annotationComments.length === 0) return null;
6357
+ const commentGroups = groupReviewCommentsByThread(annotationComments);
5856
6358
  return /* @__PURE__ */ jsx("div", {
5857
6359
  className: "review-comment-thread",
5858
6360
  ref: threadRef,
5859
- children: groupReviewCommentsByThread(annotationComments).map((group) => /* @__PURE__ */ jsx(ReviewCommentThreadGroup, {
6361
+ children: commentGroups.map((group) => /* @__PURE__ */ jsx(ReviewCommentThreadGroup, {
5860
6362
  agentId,
5861
6363
  agentLabel,
5862
6364
  comments: group.comments,
5863
6365
  focusCommentId,
6366
+ focusCommentRequest,
5864
6367
  focusEditorRef: setFocusEditorRef,
5865
6368
  identity,
5866
6369
  keymap,
@@ -6062,7 +6565,7 @@ const getHunkSelectionRange = (hunk) => {
6062
6565
  } : null;
6063
6566
  };
6064
6567
  const isSameSelection = (a, b) => a.start === b.start && a.end === b.end && a.side === b.side;
6065
- 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 }) {
6066
6569
  const codeViewRef = useRef(null);
6067
6570
  const markdownEditorRefs = useRef(/* @__PURE__ */ new Map());
6068
6571
  const refreshingMarkdownSectionsRef = useRef(/* @__PURE__ */ new Set());
@@ -6070,6 +6573,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6070
6573
  const handledScrollRequestRef = useRef(null);
6071
6574
  const handledHunkNavRef = useRef(hunkNavigation?.request ?? null);
6072
6575
  const emptyCommentDeleteTimersRef = useRef(/* @__PURE__ */ new Map());
6576
+ const definitionHighlightFrameRef = useRef(null);
6073
6577
  const highlightFrameRef = useRef(null);
6074
6578
  const ignoreNextLineSelectionEndRef = useRef(false);
6075
6579
  const navigatedSelectionRef = useRef(null);
@@ -6083,6 +6587,15 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6083
6587
  const [imagePreviewLayoutPassBySection, setImagePreviewLayoutPassBySection] = useState({});
6084
6588
  const [commentLayoutPassByItem, setCommentLayoutPassByItem] = useState({});
6085
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
+ }
6086
6599
  const selectedLinesRef = useRef(null);
6087
6600
  const commitMessageMetadata = source.type === "commit" ? commitMetadata : null;
6088
6601
  const shouldShowCommitMessage = commitMessageMetadata != null;
@@ -6132,19 +6645,22 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6132
6645
  [itemId]: (current[itemId] ?? 0) + 1
6133
6646
  }));
6134
6647
  }, []);
6135
- const { firstItemByBlockId, firstItemByPath, itemBlockId, itemMetadata, items, searchTargetsByBaseItemId } = useMemo(() => {
6648
+ const { commentTargetById, firstItemByBlockId, firstItemByPath, itemBlockId, itemMetadata, items, searchTargetsByBaseItemId, selectedHeaderItemIds } = useMemo(() => {
6136
6649
  const nextItems = [];
6650
+ const nextCommentTargetById = /* @__PURE__ */ new Map();
6137
6651
  const nextFirstItemByBlockId = /* @__PURE__ */ new Map();
6138
6652
  const nextFirstItemByPath = /* @__PURE__ */ new Map();
6139
6653
  const nextItemBlockId = /* @__PURE__ */ new Map();
6140
6654
  const nextItemMetadata = /* @__PURE__ */ new Map();
6141
6655
  const nextSearchTargetsByBaseItemId = /* @__PURE__ */ new Map();
6656
+ const nextSelectedHeaderItemIds = /* @__PURE__ */ new Set();
6142
6657
  const fontLayoutKey = `line-height:${diffLineHeight}`;
6143
6658
  for (const block of reviewBlocks) {
6144
6659
  if (block.header) {
6145
6660
  const headerId = getBlockHeaderItemId(block);
6146
6661
  nextFirstItemByBlockId.set(block.id, nextFirstItemByBlockId.get(block.id) ?? headerId);
6147
6662
  nextItemBlockId.set(headerId, block.id);
6663
+ if ((block.headerSelected ?? block.selected) === true) nextSelectedHeaderItemIds.add(headerId);
6148
6664
  nextItems.push({
6149
6665
  annotations: [{
6150
6666
  lineNumber: 1,
@@ -6162,7 +6678,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6162
6678
  },
6163
6679
  id: headerId,
6164
6680
  type: "file",
6165
- version: getItemVersion(`${block.id}:walkthrough-header:${(block.headerSelected ?? block.selected) === true ? "selected" : "idle"}`)
6681
+ version: getItemVersion(`${block.id}:walkthrough-header`)
6166
6682
  });
6167
6683
  }
6168
6684
  if (!block.file) continue;
@@ -6200,6 +6716,11 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6200
6716
  const blockSectionComments = blockCommentsBySection.get(section.id) ?? [];
6201
6717
  const sectionComments = dedupeReviewComments([...visibleGlobalComments, ...blockSectionComments]);
6202
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 });
6203
6724
  const key = getCommentKey(comment);
6204
6725
  const existing = annotationMap.get(key);
6205
6726
  if (existing && existing.metadata.type === "review-comment") annotationMap.set(key, {
@@ -6311,12 +6832,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6311
6832
  }
6312
6833
  }
6313
6834
  return {
6835
+ commentTargetById: nextCommentTargetById,
6314
6836
  firstItemByBlockId: nextFirstItemByBlockId,
6315
6837
  firstItemByPath: nextFirstItemByPath,
6316
6838
  itemBlockId: nextItemBlockId,
6317
6839
  itemMetadata: nextItemMetadata,
6318
6840
  items: nextItems,
6319
- searchTargetsByBaseItemId: nextSearchTargetsByBaseItemId
6841
+ searchTargetsByBaseItemId: nextSearchTargetsByBaseItemId,
6842
+ selectedHeaderItemIds: nextSelectedHeaderItemIds
6320
6843
  };
6321
6844
  }, [
6322
6845
  collapsed,
@@ -6369,6 +6892,18 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6369
6892
  itemMetadata,
6370
6893
  searchTargetsByBaseItemId
6371
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]);
6372
6907
  const setCodeViewSelectedLines = useCallback((selection) => {
6373
6908
  if (selection == null || navigatedSelectionRef.current == null || selection.id !== navigatedSelectionRef.current.id || !isSameSelection(selection.range, navigatedSelectionRef.current.range)) navigatedSelectionRef.current = null;
6374
6909
  setSelectedLines(selection);
@@ -6482,6 +7017,50 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6482
7017
  createCommentForRange(range, context);
6483
7018
  },
6484
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
+ }
6485
7064
  if (isReadOnly) return;
6486
7065
  if (isInteractiveReviewEvent(line.event)) return;
6487
7066
  const meta = itemMetadata.get(context.item.id);
@@ -6514,11 +7093,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6514
7093
  const metadata = itemMetadata.get(context.item.id);
6515
7094
  const isWalkthroughHeaderItem = context.item.id.endsWith(":walkthrough-header");
6516
7095
  node.classList.toggle("codiff-walkthrough-header-item", isWalkthroughHeaderItem);
6517
- node.classList.toggle("codiff-selected-item", metadata?.isSelected === true);
7096
+ node.classList.toggle("codiff-selected-item", metadata?.isSelected === true || selectedHeaderItemIds.has(context.item.id));
6518
7097
  node.classList.toggle("codiff-markdown-preview-item", metadata?.isMarkdownPreview === true);
6519
7098
  node.classList.toggle("codiff-image-preview-item", context.item.type === "file" && Boolean(metadata && canRenderImagePreview(metadata.file.path, metadata.section)));
6520
7099
  node.classList.toggle("codiff-loadable-summary-item", metadata != null && shouldLoadDiffSectionContents(metadata.section));
6521
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
+ });
6522
7104
  },
6523
7105
  overflow: wordWrap ? "wrap" : "scroll",
6524
7106
  stickyHeaders: true,
@@ -6539,7 +7121,11 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6539
7121
  loadDiffFiles,
6540
7122
  loadingSectionIds,
6541
7123
  onCreateComment,
7124
+ onFindDefinitions,
6542
7125
  onLoadSection,
7126
+ selectedHeaderItemIds,
7127
+ source,
7128
+ sourceKey,
6543
7129
  theme,
6544
7130
  wordWrap
6545
7131
  ]);
@@ -6631,11 +7217,19 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6631
7217
  poolSize: Math.min(3, Math.max(1, navigator.hardwareConcurrency || 3)),
6632
7218
  workerFactory: () => new Worker(new URL("@pierre/diffs/worker/worker.js", import.meta.url), { type: "module" })
6633
7219
  }), []);
6634
- const requestScrollItemHeaderIntoView = useCallback((itemId, behavior = "instant") => {
7220
+ const requestScrollTargetIntoView = useCallback((itemId, behavior = "instant", commentTarget) => {
6635
7221
  const handle = codeViewRef.current;
6636
7222
  const viewer = handle?.getInstance();
6637
7223
  if (!handle || !viewer || viewer.getTopForItem(itemId) == null) return false;
6638
- 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
+ } : {
6639
7233
  behavior: getEffectiveScrollBehavior(behavior),
6640
7234
  id: itemId,
6641
7235
  offset: 11,
@@ -6646,14 +7240,15 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6646
7240
  useLayoutEffect(() => {
6647
7241
  if (!scrollTarget || handledScrollRequestRef.current === scrollTarget.request) return;
6648
7242
  const behavior = scrollTarget.behavior ?? "instant";
6649
- 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);
6650
7245
  if (!itemId) return;
6651
7246
  let frame = null;
6652
7247
  let attempts = 0;
6653
7248
  let canceled = false;
6654
7249
  const tryScroll = () => {
6655
7250
  if (canceled || handledScrollRequestRef.current === scrollTarget.request) return;
6656
- if (requestScrollItemHeaderIntoView(itemId, behavior)) {
7251
+ if (requestScrollTargetIntoView(itemId, behavior, commentTarget)) {
6657
7252
  handledScrollRequestRef.current = scrollTarget.request;
6658
7253
  return;
6659
7254
  }
@@ -6668,9 +7263,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6668
7263
  if (frame != null) window.cancelAnimationFrame(frame);
6669
7264
  };
6670
7265
  }, [
7266
+ commentTargetById,
6671
7267
  firstItemByBlockId,
6672
7268
  firstItemByPath,
6673
- requestScrollItemHeaderIntoView,
7269
+ requestScrollTargetIntoView,
6674
7270
  scrollTarget
6675
7271
  ]);
6676
7272
  useEffect(() => {
@@ -6832,6 +7428,22 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6832
7428
  applySearchHighlights(viewer.getRenderedItems(), searchQuery, resolvedActiveSearchMatch);
6833
7429
  });
6834
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
+ }, []);
6835
7447
  const scheduleStickyHeaderStateUpdate = useCallback((viewer) => {
6836
7448
  const nextViewer = viewer ?? codeViewRef.current?.getInstance();
6837
7449
  if (!nextViewer) return;
@@ -6845,6 +7457,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6845
7457
  for (const timer of deferredTimersRef.current) window.clearTimeout(timer);
6846
7458
  deferredTimersRef.current.clear();
6847
7459
  emptyCommentDeleteTimersRef.current.clear();
7460
+ if (definitionHighlightFrameRef.current != null) window.cancelAnimationFrame(definitionHighlightFrameRef.current);
6848
7461
  if (highlightFrameRef.current != null) window.cancelAnimationFrame(highlightFrameRef.current);
6849
7462
  if (stickyHeaderFrameRef.current != null) window.cancelAnimationFrame(stickyHeaderFrameRef.current);
6850
7463
  }, []);
@@ -6856,6 +7469,38 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6856
7469
  scheduleSearchHighlights,
6857
7470
  scheduleStickyHeaderStateUpdate
6858
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]);
6859
7504
  useEffect(() => {
6860
7505
  const handle = codeViewRef.current;
6861
7506
  const viewer = handle?.getInstance();
@@ -7037,15 +7682,17 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7037
7682
  }
7038
7683
  scheduleSearchHighlights();
7039
7684
  scheduleStickyHeaderStateUpdate(viewer);
7685
+ if (definitionModifierActiveRef.current) updateRenderedIdentifierNavigation(true, viewer);
7040
7686
  }, [
7041
7687
  itemBlockId,
7042
7688
  items,
7043
7689
  onActiveBlockChange,
7044
7690
  onSelectPathFromScroll,
7045
7691
  scheduleSearchHighlights,
7046
- scheduleStickyHeaderStateUpdate
7692
+ scheduleStickyHeaderStateUpdate,
7693
+ updateRenderedIdentifierNavigation
7047
7694
  ]);
7048
- return disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
7695
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
7049
7696
  className: "code-view",
7050
7697
  disableWorkerPool,
7051
7698
  items: codeViewItems,
@@ -7073,11 +7720,77 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7073
7720
  renderCustomHeader,
7074
7721
  selectedLines: isReadOnly ? null : selectedLines
7075
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))
7076
7776
  });
7077
7777
  }
7078
7778
  //#endregion
7079
7779
  //#region app/components/ReviewTopBar.tsx
7080
- function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, onToggleSidebar, repository, repositoryTooltip, sidebarCollapsed, sourceMenu, 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
+ });
7081
7794
  return /* @__PURE__ */ jsxs("header", {
7082
7795
  className: "review-top-bar workspace-top-bar",
7083
7796
  children: [
@@ -7085,18 +7798,7 @@ function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, on
7085
7798
  className: "review-top-bar-left",
7086
7799
  children: [
7087
7800
  leading,
7088
- /* @__PURE__ */ jsx("button", {
7089
- "aria-label": sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar",
7090
- className: "review-top-bar-icon-button sidebar-toggle-button",
7091
- onClick: onToggleSidebar,
7092
- title: toggleTitle,
7093
- type: "button",
7094
- children: /* @__PURE__ */ jsx(SidebarSimpleIcon, {
7095
- "aria-hidden": true,
7096
- size: 18,
7097
- weight: "bold"
7098
- })
7099
- }),
7801
+ sidebarPosition === "left" ? sidebarToggle : null,
7100
7802
  sourceMenu,
7101
7803
  /* @__PURE__ */ jsx("div", {
7102
7804
  className: "review-top-bar-repository-slot",
@@ -7105,36 +7807,27 @@ function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, on
7105
7807
  })
7106
7808
  ]
7107
7809
  }),
7108
- /* @__PURE__ */ jsx("div", {
7109
- "aria-label": "Review mode",
7110
- className: "review-mode-control",
7111
- role: "tablist",
7112
- children: modes.map((item) => /* @__PURE__ */ jsxs("button", {
7113
- "aria-label": item.ariaLabel,
7114
- "aria-selected": mode === item.value,
7115
- onClick: () => onModeChange(item.value),
7116
- role: "tab",
7117
- title: item.title,
7118
- type: "button",
7119
- children: [
7120
- item.icon,
7121
- /* @__PURE__ */ jsx("span", {
7122
- className: "review-mode-label",
7123
- children: item.label
7124
- }),
7125
- item.indicator
7126
- ]
7127
- }, 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
7128
7817
  }),
7129
7818
  /* @__PURE__ */ jsxs("div", {
7130
7819
  className: "review-top-bar-right",
7131
- children: [context ? /* @__PURE__ */ jsx("div", {
7132
- className: "review-top-bar-context",
7133
- children: context
7134
- }) : null, actions ? /* @__PURE__ */ jsx("div", {
7135
- className: "review-top-bar-actions",
7136
- children: actions
7137
- }) : 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
+ ]
7138
7831
  })
7139
7832
  ]
7140
7833
  });
@@ -7286,6 +7979,7 @@ function StageFileRow({ file, on, onToggle }) {
7286
7979
  function StageGroupRow({ group, onToggleFile, onToggleGroup, selected }) {
7287
7980
  const paths = group.files.map((file) => file.path);
7288
7981
  const onCount = paths.filter((path) => selected.has(path)).length;
7982
+ const state = onCount === 0 ? "off" : onCount === paths.length ? "on" : "partial";
7289
7983
  return /* @__PURE__ */ jsxs("div", {
7290
7984
  className: "wt-stage-group",
7291
7985
  children: [/* @__PURE__ */ jsxs("button", {
@@ -7293,7 +7987,7 @@ function StageGroupRow({ group, onToggleFile, onToggleGroup, selected }) {
7293
7987
  onClick: () => onToggleGroup(paths),
7294
7988
  type: "button",
7295
7989
  children: [
7296
- /* @__PURE__ */ jsx(CommitCheck, { state: onCount === 0 ? "off" : onCount === paths.length ? "on" : "partial" }),
7990
+ /* @__PURE__ */ jsx(CommitCheck, { state }),
7297
7991
  /* @__PURE__ */ jsx("span", {
7298
7992
  className: "wt-stage-group-icon",
7299
7993
  children: /* @__PURE__ */ jsx(ChapterIcon, {
@@ -7629,14 +8323,14 @@ function CommitView({ branch, draft, model, onCommit, onCommitOutput, onUpdateMe
7629
8323
  }
7630
8324
  //#endregion
7631
8325
  //#region app/components/walkthrough/NarrativeWalkthroughView.tsx
7632
- const getWalkthroughBlockScrollTarget = ({ activeBlockId, firstSupportBlockId, mode, stopScrollRequest, supportScrollRequest }) => mode === "support" && firstSupportBlockId ? {
8326
+ const getWalkthroughBlockScrollTarget = ({ activeBlockId, firstSupportBlockId, scrollTarget }) => scrollTarget.kind === "support" && firstSupportBlockId ? {
7633
8327
  behavior: "smooth",
7634
8328
  blockId: firstSupportBlockId,
7635
- request: supportScrollRequest
7636
- } : mode === "stop" && activeBlockId && stopScrollRequest > 0 ? {
8329
+ request: scrollTarget.nonce
8330
+ } : scrollTarget.kind === "stop" && activeBlockId && scrollTarget.nonce > 0 ? {
7637
8331
  behavior: "smooth",
7638
8332
  blockId: activeBlockId,
7639
- request: stopScrollRequest
8333
+ request: scrollTarget.nonce
7640
8334
  } : null;
7641
8335
  const getFocusedRunDiffs = (item, files) => resolveWalkthroughHunkRuns(item, files).flatMap((run) => {
7642
8336
  const focused = focusChangedFileForHunks(run.resolved.file, run.resolved.section, run.hunks);
@@ -7659,14 +8353,15 @@ const getWalkthroughNavigationKeyDirection = (event) => {
7659
8353
  if (isCtrlOnly && event.key === "ArrowUp") return -1;
7660
8354
  return 0;
7661
8355
  };
8356
+ const getWalkthroughKeyboardForwardIndex = ({ index, scrollRequest }) => index === 0 && scrollRequest === 0 ? 0 : index + 1;
7662
8357
  const emptyWalkthroughBlockSet = {
7663
8358
  blocks: [],
7664
8359
  firstBlockIdByStop: [],
7665
8360
  stopIndexByBlockId: /* @__PURE__ */ new Map()
7666
8361
  };
7667
- function StopHeader({ current, stop }) {
8362
+ function StopHeader({ stop }) {
7668
8363
  return /* @__PURE__ */ jsxs("div", {
7669
- className: `wt-stop-block wt-stop-block-header${current ? " current" : ""}`,
8364
+ className: "wt-stop-block wt-stop-block-header",
7670
8365
  children: [/* @__PURE__ */ jsxs("div", {
7671
8366
  className: "wt-stage-title-row",
7672
8367
  children: [/* @__PURE__ */ jsx("h2", {
@@ -7676,9 +8371,9 @@ function StopHeader({ current, stop }) {
7676
8371
  }), /* @__PURE__ */ jsx(Narration, { prose: stop.prose })]
7677
8372
  });
7678
8373
  }
7679
- function SupportHeader({ current }) {
8374
+ function SupportHeader() {
7680
8375
  return /* @__PURE__ */ jsxs("div", {
7681
- className: `wt-stop-block wt-stop-block-header${current ? " current" : ""}`,
8376
+ className: "wt-stop-block wt-stop-block-header",
7682
8377
  children: [/* @__PURE__ */ jsxs("div", {
7683
8378
  className: "wt-stage-title-row",
7684
8379
  children: [/* @__PURE__ */ jsx("h2", {
@@ -7699,10 +8394,7 @@ const createWalkthroughBlocks = (files, walkthroughView, currentIndex) => {
7699
8394
  firstBlockIdByStop[stop.index] = blockId;
7700
8395
  stopIndexByBlockId.set(blockId, stop.index);
7701
8396
  blocks.push({
7702
- header: /* @__PURE__ */ jsx(StopHeader, {
7703
- current: stop.index === currentIndex,
7704
- stop
7705
- }),
8397
+ header: /* @__PURE__ */ jsx(StopHeader, { stop }),
7706
8398
  headerSelected: stop.index === currentIndex,
7707
8399
  id: blockId
7708
8400
  });
@@ -7714,10 +8406,7 @@ const createWalkthroughBlocks = (files, walkthroughView, currentIndex) => {
7714
8406
  stopIndexByBlockId.set(blockId, stop.index);
7715
8407
  blocks.push({
7716
8408
  file,
7717
- header: runIndex === 0 ? /* @__PURE__ */ jsx(StopHeader, {
7718
- current: stop.index === currentIndex,
7719
- stop
7720
- }) : null,
8409
+ header: runIndex === 0 ? /* @__PURE__ */ jsx(StopHeader, { stop }) : null,
7721
8410
  headerSelected: stop.index === currentIndex,
7722
8411
  id: blockId,
7723
8412
  itemIdPrefix: blockId,
@@ -7746,7 +8435,7 @@ const createSupportBlocks = (files, selected, walkthroughView, showWhitespace) =
7746
8435
  const isFirstBlock = blocks.length === 0;
7747
8436
  blocks.push({
7748
8437
  file,
7749
- header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, { current: selected }) : null,
8438
+ header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, {}) : null,
7750
8439
  headerSelected: selected,
7751
8440
  id: blockId,
7752
8441
  itemIdPrefix: blockId,
@@ -7760,7 +8449,7 @@ const createSupportBlocks = (files, selected, walkthroughView, showWhitespace) =
7760
8449
  const isFirstBlock = blocks.length === 0;
7761
8450
  blocks.push({
7762
8451
  file,
7763
- header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, { current: selected }) : null,
8452
+ header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, {}) : null,
7764
8453
  headerSelected: selected,
7765
8454
  id: blockId,
7766
8455
  itemIdPrefix: blockId,
@@ -7795,8 +8484,10 @@ function Arc({ committable, navigation, onShareWalkthrough, shareWalkthroughDisa
7795
8484
  const goArcPrev = useCallback(() => {
7796
8485
  if (navigation.mode === "stop") navigation.goPrev();
7797
8486
  else if (navigation.mode === "support") navigation.goStop(walkthroughView.sequence.length - 1);
7798
- else if (navigation.mode === "commit") if (supportAvailable) navigation.openSupport();
7799
- 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
+ }
7800
8491
  }, [
7801
8492
  navigation,
7802
8493
  supportAvailable,
@@ -7955,9 +8646,7 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
7955
8646
  const reviewBlockScrollTarget = getWalkthroughBlockScrollTarget({
7956
8647
  activeBlockId,
7957
8648
  firstSupportBlockId,
7958
- mode: navigation.mode,
7959
- stopScrollRequest: navigation.scrollTarget.nonce,
7960
- supportScrollRequest: navigation.supportScrollRequest
8649
+ scrollTarget: navigation.scrollTarget
7961
8650
  });
7962
8651
  const handleActiveBlockChange = useCallback((blockId) => {
7963
8652
  onActiveReviewTargetChange(getBlockReviewTarget(reviewBlocks, blockId));
@@ -7993,7 +8682,11 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
7993
8682
  if (direction === 1) {
7994
8683
  event.preventDefault();
7995
8684
  if (navigation.mode === "stop") {
7996
- 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);
7997
8690
  else if (supportAvailable) navigation.openSupport();
7998
8691
  else if (committable) navigation.enterCommit();
7999
8692
  } else if (navigation.mode === "support" && committable) navigation.enterCommit();
@@ -8001,8 +8694,10 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
8001
8694
  event.preventDefault();
8002
8695
  if (navigation.mode === "stop") navigation.goPrev();
8003
8696
  else if (navigation.mode === "support") navigation.goStop(navigation.index);
8004
- else if (navigation.mode === "commit") if (supportAvailable) navigation.openSupport();
8005
- else navigation.goStop(navigation.index);
8697
+ else if (navigation.mode === "commit") {
8698
+ if (supportAvailable) navigation.openSupport();
8699
+ else navigation.goStop(navigation.index);
8700
+ }
8006
8701
  }
8007
8702
  }, [
8008
8703
  committable,
@@ -8235,9 +8930,9 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8235
8930
  const [index, setIndex] = useState(0);
8236
8931
  const [scrollTarget, setScrollTarget] = useState({
8237
8932
  index: 0,
8933
+ kind: "stop",
8238
8934
  nonce: 0
8239
8935
  });
8240
- const [supportScrollRequest, setSupportScrollRequest] = useState(0);
8241
8936
  const [supportVisited, setSupportVisited] = useState(false);
8242
8937
  const [visited, setVisited] = useState(() => {
8243
8938
  const stopId = firstStopId(walkthrough);
@@ -8268,9 +8963,9 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8268
8963
  setIndex(0);
8269
8964
  setScrollTarget({
8270
8965
  index: 0,
8966
+ kind: "stop",
8271
8967
  nonce: 0
8272
8968
  });
8273
- setSupportScrollRequest(0);
8274
8969
  setSupportVisited(false);
8275
8970
  const stopId = firstStopId(walkthrough);
8276
8971
  setVisited(new Set(stopId ? [stopId] : []));
@@ -8332,6 +9027,7 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8332
9027
  pendingSupportScrollRef.current = null;
8333
9028
  setScrollTarget((current) => ({
8334
9029
  index: clamped,
9030
+ kind: "stop",
8335
9031
  nonce: current.nonce + 1
8336
9032
  }));
8337
9033
  }, [
@@ -8376,7 +9072,11 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8376
9072
  if (walkthroughView?.sequence.length) setIndex(walkthroughView.sequence.length - 1);
8377
9073
  setMode("support");
8378
9074
  pendingSupportScrollRef.current = { walkthrough };
8379
- setSupportScrollRequest((current) => current + 1);
9075
+ setScrollTarget((current) => ({
9076
+ ...current,
9077
+ kind: "support",
9078
+ nonce: current.nonce + 1
9079
+ }));
8380
9080
  setSupportVisited(true);
8381
9081
  }, [
8382
9082
  walkthrough,
@@ -8384,11 +9084,15 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8384
9084
  leaveStopMode
8385
9085
  ]);
8386
9086
  const syncSupportFromScroll = useCallback(() => {
9087
+ const pendingStop = pendingStopScrollRef.current;
9088
+ if (pendingStop) {
9089
+ if (pendingStop.walkthrough === walkthrough) return;
9090
+ pendingStopScrollRef.current = null;
9091
+ }
8387
9092
  pendingSupportScrollRef.current = null;
8388
- pendingStopScrollRef.current = null;
8389
9093
  setMode("support");
8390
9094
  setSupportVisited(true);
8391
- }, []);
9095
+ }, [walkthrough]);
8392
9096
  return {
8393
9097
  commitBody,
8394
9098
  commitSelected,
@@ -8407,7 +9111,6 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8407
9111
  scrollTarget,
8408
9112
  setCommitBody,
8409
9113
  setCommitSubject,
8410
- supportScrollRequest,
8411
9114
  supportVisited,
8412
9115
  syncIndexFromScroll,
8413
9116
  syncSupportFromScroll,
@@ -8506,7 +9209,7 @@ const writeSidebarWidth = (width, storage = localStorage) => {
8506
9209
  };
8507
9210
  //#endregion
8508
9211
  //#region app/hooks/useResizableSidebar.ts
8509
- function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, readWidth }) {
9212
+ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, position, readWidth }) {
8510
9213
  const [sidebarWidth, setSidebarWidth] = useState(readWidth);
8511
9214
  return {
8512
9215
  resizeSidebar: useCallback((event) => {
@@ -8515,7 +9218,7 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8515
9218
  const handle = event.currentTarget;
8516
9219
  const shell = handle.parentElement;
8517
9220
  if (!shell) return;
8518
- const shellLeft = shell.getBoundingClientRect().left;
9221
+ const shellRect = shell.getBoundingClientRect();
8519
9222
  handle.setPointerCapture(event.pointerId);
8520
9223
  handle.classList.add("dragging");
8521
9224
  document.body.style.cursor = "col-resize";
@@ -8529,7 +9232,7 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8529
9232
  document.body.style.cursor = "";
8530
9233
  };
8531
9234
  const handleMove = (moveEvent) => {
8532
- const rawWidth = moveEvent.clientX - shellLeft;
9235
+ const rawWidth = position === "right" ? shellRect.right - moveEvent.clientX : moveEvent.clientX - shellRect.left;
8533
9236
  if (collapseThreshold != null && rawWidth < collapseThreshold) {
8534
9237
  collapsed = true;
8535
9238
  onCollapse?.();
@@ -8551,7 +9254,8 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8551
9254
  }, [
8552
9255
  collapseThreshold,
8553
9256
  onCollapse,
8554
- onWidthCommit
9257
+ onWidthCommit,
9258
+ position
8555
9259
  ]),
8556
9260
  sidebarWidth
8557
9261
  };
@@ -8657,6 +9361,7 @@ function useReviewCommentDrafts({ canCreateComment = true, comments, onCommentFi
8657
9361
  setComments,
8658
9362
  updateActiveReviewCommentDraft
8659
9363
  ]),
9364
+ focusComment,
8660
9365
  focusCommentId,
8661
9366
  focusCommentRequest,
8662
9367
  resetCommentFocus,
@@ -8739,6 +9444,7 @@ const defaultConfigTemplate = {
8739
9444
  "opencodeModel": "opencode-default",
8740
9445
  "piModel": "pi-default",
8741
9446
  "reviewCommentsPrefix": "# Address these Review Comments",
9447
+ "sidebarPosition": "left",
8742
9448
  "showOutdated": false,
8743
9449
  "showWhitespace": false,
8744
9450
  "theme": "system",
@@ -8746,6 +9452,7 @@ const defaultConfigTemplate = {
8746
9452
  "wordWrap": false
8747
9453
  },
8748
9454
  keymap: {
9455
+ "askAgent": "Mod+Alt+Enter",
8749
9456
  "closeSearch": "Escape",
8750
9457
  "commandBar": "Mod+Shift+p",
8751
9458
  "diffSearch": "Mod+f",
@@ -8803,10 +9510,35 @@ const emptyReviewComments = [];
8803
9510
  const emptyGeneralCommentThreads = [];
8804
9511
  const emptyPaths = /* @__PURE__ */ new Set();
8805
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;
8806
9524
  const readSharedSidebarWidth = () => typeof localStorage === "undefined" ? 292 : readSidebarWidth();
8807
9525
  const writeSharedSidebarWidth = (width) => {
8808
9526
  if (typeof localStorage !== "undefined") writeSidebarWidth(width);
8809
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
+ };
8810
9542
  const getSnapshotReviewComments = (snapshot) => {
8811
9543
  if (!snapshot.reviewComments?.length) return emptyReviewComments;
8812
9544
  return getReviewCommentsFromState({
@@ -8828,7 +9560,9 @@ const disabledCommitMessage = async () => ({
8828
9560
  reason: "Shared walkthroughs are read-only.",
8829
9561
  status: "unavailable"
8830
9562
  });
8831
- 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 }) {
8832
9566
  const canComment = commenting?.canComment ?? Boolean(interactive);
8833
9567
  const deleteShare = useCallback(async () => {
8834
9568
  if (!onDeleteShare || !window.confirm("Delete this shared walkthrough? This cannot be undone.")) return;
@@ -8851,23 +9585,44 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8851
9585
  const keymap = useMemo(() => createDefaultConfig().keymap, []);
8852
9586
  const [fileSearchQuery, setFileSearchQuery] = useState("");
8853
9587
  const [uncontrolledSidebarMode, setUncontrolledSidebarMode] = useState(() => initialMode ?? (interactive ? "tree" : "walkthrough"));
8854
- 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]);
8855
9606
  useEffect(() => {
8856
9607
  const handleKeyDown = (event) => {
8857
9608
  if (event.repeat || !matchesShortcut(event, keymap, "toggleSidebar")) return;
8858
9609
  event.preventDefault();
8859
- setSidebarCollapsed((current) => !current);
9610
+ toggleSidebar();
8860
9611
  };
8861
9612
  window.addEventListener("keydown", handleKeyDown);
8862
9613
  return () => window.removeEventListener("keydown", handleKeyDown);
8863
- }, [keymap]);
9614
+ }, [keymap, toggleSidebar]);
8864
9615
  const sidebarMode = Boolean(initialMode && onModeChange) && initialMode ? initialMode : uncontrolledSidebarMode;
8865
9616
  const [treeScrollTarget, setTreeScrollTarget] = useState(null);
8866
9617
  const { bumpItemVersion, collapsed, expandedGenerated, itemVersionByKey, selectedPath, setSelectedPath, toggleCollapsed, toggleViewed, viewed } = useReviewFileState({ initialSelectedPath: snapshot.files[0]?.path ?? null });
8867
9618
  const { resizeSidebar, sidebarWidth } = useResizableSidebar({
8868
9619
  collapseThreshold: 80,
8869
- onCollapse: () => setSidebarCollapsed(true),
9620
+ onCollapse: () => {
9621
+ sidebarInteractedRef.current = true;
9622
+ updateSidebarCollapsed(true, true);
9623
+ },
8870
9624
  onWidthCommit: writeSharedSidebarWidth,
9625
+ position: sidebarPosition,
8871
9626
  readWidth: readSharedSidebarWidth
8872
9627
  });
8873
9628
  const snapshotReviewComments = useMemo(() => getSnapshotReviewComments(snapshot), [snapshot]);
@@ -8878,7 +9633,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8878
9633
  } : comment), [editedReviewCommentBodies, snapshotReviewComments]);
8879
9634
  const [localReviewComments, setLocalReviewComments] = useState(emptyReviewComments);
8880
9635
  const reviewComments = useMemo(() => mergeReviewComments(visibleSnapshotReviewComments, localReviewComments), [localReviewComments, visibleSnapshotReviewComments]);
8881
- 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({
8882
9637
  canCreateComment: canComment,
8883
9638
  comments: reviewComments,
8884
9639
  onCommentFileChange: bumpItemVersion,
@@ -8904,6 +9659,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8904
9659
  const walkthroughRequestPendingRef = useRef(false);
8905
9660
  const [walkthroughRequestId, setWalkthroughRequestId] = useState(0);
8906
9661
  const interactiveRef = useRef(interactive);
9662
+ const handledHashTargetRef = useRef(null);
8907
9663
  const visibleFiles = useMemo(() => sortFiles(snapshot.files).filter((file) => fuzzyMatches(file.path, fileSearchQuery) && fileHasVisibleDiff(file, snapshot.preferences.showWhitespace)), [
8908
9664
  fileSearchQuery,
8909
9665
  snapshot.files,
@@ -9150,6 +9906,56 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9150
9906
  request: (current?.request ?? 0) + 1
9151
9907
  }));
9152
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]);
9153
9959
  const updateSelectedPathFromScroll = useCallback((viewer) => {
9154
9960
  const nextPath = getSelectedPathFromScroll(viewer, visibleFiles, snapshot.preferences.showWhitespace);
9155
9961
  if (nextPath) setSelectedPath((current) => current === nextPath ? current : nextPath);
@@ -9328,9 +10134,10 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9328
10134
  children: settingsBar
9329
10135
  }) : null] }) : void 0;
9330
10136
  return /* @__PURE__ */ jsxs("div", {
9331
- 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,
9332
10139
  "data-theme": shellTheme,
9333
- 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)` },
9334
10141
  children: [
9335
10142
  /* @__PURE__ */ jsx(ReviewTopBar, {
9336
10143
  actions: topBarActions,
@@ -9371,7 +10178,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9371
10178
  mode: sidebarMode,
9372
10179
  modes: reviewModes,
9373
10180
  onModeChange: changeSidebarMode,
9374
- onToggleSidebar: () => setSidebarCollapsed((current) => !current),
10181
+ onToggleSidebar: toggleSidebar,
9375
10182
  repository: repositoryLinkUrl ? /* @__PURE__ */ jsx("a", {
9376
10183
  className: "review-top-bar-repository",
9377
10184
  href: repositoryLinkUrl,
@@ -9383,7 +10190,8 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9383
10190
  children: rootLabel
9384
10191
  }),
9385
10192
  repositoryTooltip: snapshot.repository.root,
9386
- sidebarCollapsed,
10193
+ sidebarCollapsed: sidebarCollapsed ?? false,
10194
+ sidebarPosition,
9387
10195
  toggleTitle: `${sidebarCollapsed ? "Expand" : "Collapse"} sidebar`
9388
10196
  }),
9389
10197
  /* @__PURE__ */ jsxs("aside", {
@@ -9461,6 +10269,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9461
10269
  onCancelEdit: cancelEditGeneralComment,
9462
10270
  onChangeDraft: setGeneralCommentDraft,
9463
10271
  onChangeEditDraft: setGeneralCommentEditDraft,
10272
+ onResolveDiscussion: resolveDiscussion,
9464
10273
  onSaveEdit: saveGeneralCommentEdit,
9465
10274
  onStartEdit: startEditGeneralComment,
9466
10275
  onSubmit: submitGeneralComment,