@nkzw/codiff-core 1.10.0 → 1.11.1

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;
@@ -2442,14 +2633,16 @@ function PullRequestReviewAction({ disabled, event, hasPendingComments = false,
2442
2633
  }) : null]
2443
2634
  });
2444
2635
  }
2445
- function PullRequestReviewButtons({ children, disabled, hasPendingComments, onClosePullRequest, onSubmitReview, reviewStatus, showCommentReview = false }) {
2636
+ function PullRequestReviewButtons({ children, disabled, hasPendingComments, onClosePullRequest, onMarkPullRequestReady, onSubmitReview, reviewStatus, showCommentReview = false }) {
2446
2637
  const approveBlocked = isPullRequestReviewActionDisabled(reviewStatus, "APPROVE");
2447
2638
  const commentBlocked = isPullRequestReviewActionDisabled(reviewStatus, "COMMENT");
2448
2639
  const requestChangesBlocked = isPullRequestReviewActionDisabled(reviewStatus, "REQUEST_CHANGES");
2449
2640
  const closeStatus = reviewStatus?.close;
2450
2641
  const closeVisible = onClosePullRequest && closeStatus && closeStatus.disabled !== true;
2642
+ const markReadyStatus = reviewStatus?.markReady;
2643
+ const markReadyVisible = onMarkPullRequestReady && markReadyStatus && markReadyStatus.disabled !== true;
2451
2644
  const commentVisible = showCommentReview && !commentBlocked;
2452
- const hasReviewActions = commentVisible || !approveBlocked || !requestChangesBlocked || closeVisible;
2645
+ const hasReviewActions = commentVisible || !approveBlocked || !requestChangesBlocked || markReadyVisible || closeVisible;
2453
2646
  if (!hasReviewActions && !children) return null;
2454
2647
  return /* @__PURE__ */ jsxs("div", {
2455
2648
  "aria-label": hasReviewActions ? "Pull request review actions" : "Pull request status",
@@ -2496,6 +2689,21 @@ function PullRequestReviewButtons({ children, disabled, hasPendingComments, onCl
2496
2689
  onSubmitReview,
2497
2690
  title: getPullRequestReviewActionTitle(reviewStatus, "REQUEST_CHANGES", "Request changes")
2498
2691
  }) : null,
2692
+ markReadyVisible ? /* @__PURE__ */ jsxs(Button, {
2693
+ action: onMarkPullRequestReady,
2694
+ "aria-label": "Mark merge request as ready",
2695
+ className: "review-submit-button ready",
2696
+ disabled,
2697
+ pendingPlaceholder: "Marking ready…",
2698
+ title: markReadyStatus.reason ?? "Mark merge request as ready",
2699
+ type: "button",
2700
+ children: [/* @__PURE__ */ jsx(CheckCircleIcon, {
2701
+ "aria-hidden": true,
2702
+ className: "review-submit-icon ready",
2703
+ size: 15,
2704
+ weight: "bold"
2705
+ }), /* @__PURE__ */ jsx("span", { children: "Mark ready" })]
2706
+ }) : null,
2499
2707
  closeVisible ? /* @__PURE__ */ jsxs(Button, {
2500
2708
  action: onClosePullRequest,
2501
2709
  "aria-label": "Close merge request",
@@ -3347,6 +3555,7 @@ setCustomExtension("cts", "typescript");
3347
3555
  setCustomExtension("mts", "typescript");
3348
3556
  const statusLabel = {
3349
3557
  added: "Added",
3558
+ conflicted: "Conflicted",
3350
3559
  deleted: "Deleted",
3351
3560
  modified: "Modified",
3352
3561
  renamed: "Renamed",
@@ -3479,6 +3688,20 @@ const codeViewUnsafeCSS = `
3479
3688
  cursor: progress;
3480
3689
  }
3481
3690
 
3691
+ :host([data-codiff-definition-mode]) [data-codiff-identifier] {
3692
+ cursor: pointer;
3693
+ text-decoration-color: color-mix(in srgb, currentColor 38%, transparent);
3694
+ text-decoration-line: underline;
3695
+ text-decoration-style: dotted;
3696
+ text-decoration-thickness: 1px;
3697
+ text-underline-offset: 3px;
3698
+ }
3699
+
3700
+ :host([data-codiff-definition-mode]) [data-codiff-identifier]:hover {
3701
+ text-decoration-color: currentColor;
3702
+ text-decoration-style: solid;
3703
+ }
3704
+
3482
3705
  .codiff-search-mark {
3483
3706
  background: var(--diffs-find-highlight-bg, rgb(255 216 92 / 0.65));
3484
3707
  border-radius: 3px;
@@ -3505,8 +3728,179 @@ const codeViewUnsafeCSS = `
3505
3728
  [data-selected-line] [data-gutter-utility-slot] {
3506
3729
  display: none;
3507
3730
  }
3731
+
3732
+ /* The imperative API wraps annotations in a node that resets white-space, but
3733
+ the React API slots them in bare, so they inherit "pre" from the shadow
3734
+ <pre> and prose never wraps. */
3735
+ [data-annotation-content] slot::slotted(*) {
3736
+ white-space: normal;
3737
+ }
3508
3738
  `;
3509
3739
  //#endregion
3740
+ //#region lib/identifier-navigation.ts
3741
+ const identifierPattern = /[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*/gu;
3742
+ const exactIdentifierPattern = /^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u;
3743
+ const identifierContainedPattern = /[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*/u;
3744
+ const commonKeywords = /* @__PURE__ */ new Set([
3745
+ "abstract",
3746
+ "as",
3747
+ "async",
3748
+ "await",
3749
+ "break",
3750
+ "case",
3751
+ "catch",
3752
+ "class",
3753
+ "const",
3754
+ "continue",
3755
+ "def",
3756
+ "default",
3757
+ "defer",
3758
+ "do",
3759
+ "else",
3760
+ "enum",
3761
+ "export",
3762
+ "extends",
3763
+ "false",
3764
+ "finally",
3765
+ "fn",
3766
+ "for",
3767
+ "from",
3768
+ "func",
3769
+ "function",
3770
+ "if",
3771
+ "implements",
3772
+ "import",
3773
+ "in",
3774
+ "interface",
3775
+ "let",
3776
+ "match",
3777
+ "mod",
3778
+ "namespace",
3779
+ "new",
3780
+ "nil",
3781
+ "none",
3782
+ "null",
3783
+ "package",
3784
+ "pass",
3785
+ "private",
3786
+ "protected",
3787
+ "pub",
3788
+ "public",
3789
+ "return",
3790
+ "self",
3791
+ "static",
3792
+ "struct",
3793
+ "super",
3794
+ "switch",
3795
+ "this",
3796
+ "throw",
3797
+ "throws",
3798
+ "trait",
3799
+ "true",
3800
+ "try",
3801
+ "type",
3802
+ "typeof",
3803
+ "undefined",
3804
+ "use",
3805
+ "var",
3806
+ "void",
3807
+ "while",
3808
+ "with",
3809
+ "yield"
3810
+ ]);
3811
+ const identifierNavigationAttribute = "data-codiff-identifier";
3812
+ const definitionModifierAttribute = "data-codiff-definition-mode";
3813
+ const nonCodeScopePattern = /(^|\s)(comment|string)([.\s]|$)/;
3814
+ const nonCodeTokenColors = new Set([dunkel_default, licht_default].flatMap((theme) => theme.tokenColors.flatMap((token) => {
3815
+ const scopes = Array.isArray(token.scope) ? token.scope : [token.scope];
3816
+ const foreground = token.settings.foreground?.toLowerCase();
3817
+ return foreground && scopes.some((scope) => typeof scope === "string" && nonCodeScopePattern.test(scope)) ? [foreground] : [];
3818
+ })));
3819
+ const isNavigableIdentifierToken = (text) => {
3820
+ return exactIdentifierPattern.test(text) && !commonKeywords.has(text.toLowerCase());
3821
+ };
3822
+ const isNonCodeSyntaxToken = (element) => {
3823
+ const style = element?.getAttribute("style")?.toLowerCase() ?? "";
3824
+ return [...nonCodeTokenColors].some((color) => style.includes(color));
3825
+ };
3826
+ const clearIdentifierNavigationWrappers = (root) => {
3827
+ for (const wrapper of Array.from(root.querySelectorAll(`[${identifierNavigationAttribute}]`))) {
3828
+ const parent = wrapper.parentElement;
3829
+ wrapper.replaceWith(document.createTextNode(wrapper.textContent ?? ""));
3830
+ parent?.normalize();
3831
+ }
3832
+ };
3833
+ const wrapIdentifierNavigationText = (root) => {
3834
+ const textNodes = [];
3835
+ for (const line of Array.from(root.querySelectorAll("[data-line]"))) {
3836
+ const walker = document.createTreeWalker(line, NodeFilter.SHOW_TEXT, { acceptNode: (node) => {
3837
+ const parent = node.parentElement;
3838
+ return node.textContent && identifierContainedPattern.test(node.textContent) && !parent?.closest(`[${identifierNavigationAttribute}], mark.codiff-search-mark`) && !isNonCodeSyntaxToken(parent) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
3839
+ } });
3840
+ let node = walker.nextNode();
3841
+ while (node) {
3842
+ textNodes.push(node);
3843
+ node = walker.nextNode();
3844
+ }
3845
+ }
3846
+ for (const textNode of textNodes) {
3847
+ const text = textNode.textContent ?? "";
3848
+ const fragment = document.createDocumentFragment();
3849
+ let offset = 0;
3850
+ let wrapped = false;
3851
+ for (const match of text.matchAll(identifierPattern)) {
3852
+ const identifier = match[0];
3853
+ if (!isNavigableIdentifierToken(identifier)) continue;
3854
+ if (match.index > offset) fragment.append(document.createTextNode(text.slice(offset, match.index)));
3855
+ const wrapper = document.createElement("span");
3856
+ wrapper.setAttribute(identifierNavigationAttribute, "");
3857
+ wrapper.textContent = identifier;
3858
+ fragment.append(wrapper);
3859
+ offset = match.index + identifier.length;
3860
+ wrapped = true;
3861
+ }
3862
+ if (!wrapped) continue;
3863
+ if (offset < text.length) fragment.append(document.createTextNode(text.slice(offset)));
3864
+ textNode.replaceWith(fragment);
3865
+ }
3866
+ };
3867
+ const applyIdentifierNavigationState = (renderedItems, active) => {
3868
+ for (const { element } of renderedItems) {
3869
+ element.toggleAttribute(definitionModifierAttribute, active);
3870
+ const root = element.shadowRoot;
3871
+ if (!root) continue;
3872
+ clearIdentifierNavigationWrappers(root);
3873
+ if (active) wrapIdentifierNavigationText(root);
3874
+ }
3875
+ };
3876
+ const getIdentifierAtOffset = (text, offset) => {
3877
+ for (const match of text.matchAll(identifierPattern)) {
3878
+ const start = match.index;
3879
+ const end = start + match[0].length;
3880
+ if (offset >= start && offset < end) return {
3881
+ identifier: match[0],
3882
+ start
3883
+ };
3884
+ }
3885
+ return null;
3886
+ };
3887
+ const getIdentifierFromPointerEvent = (event, lineElement) => {
3888
+ const caretRange = document.caretRangeFromPoint?.(event.clientX, event.clientY);
3889
+ if (caretRange && lineElement.contains(caretRange.startContainer)) {
3890
+ const prefix = document.createRange();
3891
+ prefix.selectNodeContents(lineElement);
3892
+ prefix.setEnd(caretRange.startContainer, caretRange.startOffset);
3893
+ const match = getIdentifierAtOffset(lineElement.textContent || "", prefix.toString().length);
3894
+ if (match) return match.identifier;
3895
+ }
3896
+ const target = event.target;
3897
+ if (target && lineElement.contains(target)) {
3898
+ const matches = [...(target.textContent || "").matchAll(identifierPattern)];
3899
+ if (matches.length === 1) return matches[0][0];
3900
+ }
3901
+ return null;
3902
+ };
3903
+ //#endregion
3510
3904
  //#region lib/item-version.ts
3511
3905
  const getItemVersion = (value) => {
3512
3906
  let hash = 0;
@@ -3522,6 +3916,8 @@ const isNativeInputTarget = (target) => {
3522
3916
  const candidate = target;
3523
3917
  return candidate?.closest?.("input, select, textarea") != null || candidate?.isContentEditable === true;
3524
3918
  };
3919
+ const isMacPlatform = (platform = navigator.platform) => platform.toLowerCase().includes("mac");
3920
+ const isPrimaryModifier = (event, platform = navigator.platform) => isMacPlatform(platform) ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
3525
3921
  //#endregion
3526
3922
  //#region lib/review-identity.ts
3527
3923
  const getFileReviewIdentity = (file) => ({
@@ -3601,6 +3997,101 @@ const applySearchHighlights = (renderedItems, query, activeMatch) => {
3601
3997
  }
3602
3998
  };
3603
3999
  //#endregion
4000
+ //#region lib/source.ts
4001
+ const rangeLabel = (source) => `${source.base}${source.symmetric ? "..." : ".."}${source.head}`;
4002
+ 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";
4003
+ const getShortRef = (ref) => ref.slice(0, 7);
4004
+ 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";
4005
+ //#endregion
4006
+ //#region app/components/DefinitionPopover.tsx
4007
+ function DefinitionPopover({ anchor, getDestination, identifier, onClose, onOpen, result }) {
4008
+ const popoverRef = useRef(null);
4009
+ useEffect(() => {
4010
+ const closeOnEscape = (event) => {
4011
+ if (event.key === "Escape") onClose();
4012
+ };
4013
+ const closeOutside = (event) => {
4014
+ const target = event.target;
4015
+ if (target && !popoverRef.current?.contains(target)) onClose();
4016
+ };
4017
+ document.addEventListener("keydown", closeOnEscape);
4018
+ document.addEventListener("pointerdown", closeOutside);
4019
+ return () => {
4020
+ document.removeEventListener("keydown", closeOnEscape);
4021
+ document.removeEventListener("pointerdown", closeOutside);
4022
+ };
4023
+ }, [onClose]);
4024
+ const left = Math.min(anchor.x, Math.max(12, window.innerWidth - 432));
4025
+ const top = Math.min(anchor.y + 10, Math.max(12, window.innerHeight - 320));
4026
+ return createPortal(/* @__PURE__ */ jsxs("div", {
4027
+ "aria-label": `Definitions for ${identifier}`,
4028
+ className: "definition-popover",
4029
+ ref: popoverRef,
4030
+ role: "dialog",
4031
+ style: {
4032
+ left,
4033
+ top
4034
+ },
4035
+ children: [/* @__PURE__ */ jsxs("div", {
4036
+ className: "definition-popover-header",
4037
+ children: [/* @__PURE__ */ jsxs("span", { children: ["Definition of ", /* @__PURE__ */ jsx("code", { children: identifier })] }), /* @__PURE__ */ jsx("button", {
4038
+ "aria-label": "Close definition results",
4039
+ onClick: onClose,
4040
+ type: "button",
4041
+ children: /* @__PURE__ */ jsx(XIcon, { size: 14 })
4042
+ })]
4043
+ }), !result ? /* @__PURE__ */ jsx("div", {
4044
+ className: "definition-popover-message",
4045
+ children: "Searching…"
4046
+ }) : result.status === "unavailable" ? /* @__PURE__ */ jsx("div", {
4047
+ className: "definition-popover-message",
4048
+ children: result.reason
4049
+ }) : result.candidates.length === 0 ? /* @__PURE__ */ jsx("div", {
4050
+ className: "definition-popover-message",
4051
+ children: "No likely definitions found."
4052
+ }) : /* @__PURE__ */ jsx("div", {
4053
+ className: "definition-popover-results",
4054
+ children: result.candidates.map((candidate) => {
4055
+ const destination = getDestination(candidate);
4056
+ const destinationLabel = destination === "diff" ? "Jump within diff" : destination === "editor" ? "Open in editor" : "Unavailable outside this historical diff";
4057
+ return /* @__PURE__ */ jsxs("button", {
4058
+ className: "definition-popover-result",
4059
+ disabled: destination === "unavailable",
4060
+ onClick: () => onOpen(candidate),
4061
+ type: "button",
4062
+ children: [/* @__PURE__ */ jsxs("span", {
4063
+ className: "definition-popover-location",
4064
+ children: [
4065
+ candidate.path,
4066
+ ":",
4067
+ candidate.lineNumber,
4068
+ /* @__PURE__ */ jsxs("span", {
4069
+ className: "definition-popover-meta",
4070
+ children: [/* @__PURE__ */ jsx("span", { children: candidate.kind }), /* @__PURE__ */ jsx("span", {
4071
+ "aria-label": destinationLabel,
4072
+ className: "definition-popover-destination",
4073
+ role: "img",
4074
+ title: destinationLabel,
4075
+ children: destination === "diff" ? /* @__PURE__ */ jsx(CrosshairSimpleIcon, {
4076
+ "aria-hidden": "true",
4077
+ size: 13
4078
+ }) : destination === "editor" ? /* @__PURE__ */ jsx(ArrowSquareOutIcon, {
4079
+ "aria-hidden": "true",
4080
+ size: 13
4081
+ }) : /* @__PURE__ */ jsx(ProhibitIcon, {
4082
+ "aria-hidden": "true",
4083
+ size: 13
4084
+ })
4085
+ })]
4086
+ })
4087
+ ]
4088
+ }), /* @__PURE__ */ jsx("code", { children: candidate.line })]
4089
+ }, `${candidate.path}:${candidate.lineNumber}`);
4090
+ })
4091
+ })]
4092
+ }), document.body);
4093
+ }
4094
+ //#endregion
3604
4095
  //#region app/components/MarkdownDocumentEditor.tsx
3605
4096
  const persistenceAdapter = { save: ({ content, document }) => window.codiff.saveMarkdownDocument({
3606
4097
  baseVersion: document.version,
@@ -3717,12 +4208,6 @@ const RepositoryMarkdownEditor = forwardRef(function RepositoryMarkdownEditor({
3717
4208
  });
3718
4209
  });
3719
4210
  //#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
4211
  //#region lib/narrative-walkthrough.ts
3727
4212
  const walkthroughFileName = (path) => path.split("/").pop() ?? path;
3728
4213
  const uniquePaths = (paths) => {
@@ -4145,7 +4630,8 @@ const chapterIcons = {
4145
4630
  wrench: WrenchIcon
4146
4631
  };
4147
4632
  function ChapterIcon({ icon, size = 13 }) {
4148
- return /* @__PURE__ */ jsx(chapterIcons[icon] ?? chapterIcons.path, { size });
4633
+ const Icon = chapterIcons[icon] ?? chapterIcons.path;
4634
+ return /* @__PURE__ */ jsx(Icon, { size });
4149
4635
  }
4150
4636
  function ImportancePill({ importance }) {
4151
4637
  return /* @__PURE__ */ jsx("span", {
@@ -4430,13 +4916,14 @@ function WalkthroughProgress({ phase, responseLabelIndex, stageRevision }) {
4430
4916
  }, [stageRevision]);
4431
4917
  const elapsedSeconds = timerState.stageRevision === stageRevision ? timerState.elapsedSeconds : 0;
4432
4918
  const showTimer = elapsedSeconds >= TIMER_THRESHOLD_SECONDS;
4919
+ const label = phase === "agent-generation" ? "Analyzing changes…" : phase === "response-received" ? walkthroughResponseLabels[Math.abs(responseLabelIndex) % walkthroughResponseLabels.length] : "Generating walkthrough…";
4433
4920
  return /* @__PURE__ */ jsxs("span", {
4434
4921
  "aria-live": "polite",
4435
4922
  className: "walkthrough-progress",
4436
4923
  role: "status",
4437
4924
  children: [/* @__PURE__ */ jsx("span", {
4438
4925
  className: "walkthrough-progress-label",
4439
- children: phase === "agent-generation" ? "Analyzing changes…" : phase === "response-received" ? walkthroughResponseLabels[Math.abs(responseLabelIndex) % walkthroughResponseLabels.length] : "Generating walkthrough…"
4926
+ children: label
4440
4927
  }), /* @__PURE__ */ jsx("span", {
4441
4928
  "aria-hidden": !showTimer,
4442
4929
  className: `walkthrough-progress-timer${showTimer ? " visible" : ""}`,
@@ -5476,6 +5963,9 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5476
5963
  flushDraft,
5477
5964
  onCommentBlur
5478
5965
  ]);
5966
+ const handleAddComment = useCallback(() => {
5967
+ document.activeElement?.blur();
5968
+ }, []);
5479
5969
  const handleFocus = useCallback(() => {
5480
5970
  onCommentFocus(draftComment);
5481
5971
  }, [draftComment, onCommentFocus]);
@@ -5491,13 +5981,21 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5491
5981
  ]);
5492
5982
  const handleKeyDown = useCallback((event) => {
5493
5983
  if (matchesShortcut(event, keymap, "submitComment")) {
5494
- if (supportsReviewCommentActions && commentCanSubmit) {
5495
- event.preventDefault();
5496
- event.stopPropagation();
5497
- handleSubmitComment();
5984
+ if (supportsReviewCommentActions) {
5985
+ if (commentCanSubmit) {
5986
+ event.preventDefault();
5987
+ event.stopPropagation();
5988
+ handleSubmitComment();
5989
+ }
5498
5990
  return;
5499
5991
  }
5500
- if (!supportsReviewCommentActions && canAskCodex) {
5992
+ event.preventDefault();
5993
+ event.stopPropagation();
5994
+ handleAddComment();
5995
+ return;
5996
+ }
5997
+ if (matchesShortcut(event, keymap, "askAgent")) {
5998
+ if (canAskCodex) {
5501
5999
  event.preventDefault();
5502
6000
  event.stopPropagation();
5503
6001
  handleAskCodex();
@@ -5515,6 +6013,7 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5515
6013
  comment.id,
5516
6014
  comment.isReadOnly,
5517
6015
  draft,
6016
+ handleAddComment,
5518
6017
  handleAskCodex,
5519
6018
  handleSubmitComment,
5520
6019
  keymap,
@@ -5570,7 +6069,7 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5570
6069
  className: "review-comment-action",
5571
6070
  disabled: !canAskCodex,
5572
6071
  onClick: handleAskCodex,
5573
- title: canAskCodex ? `Ask ${agentLabel}` : `Write a note before asking ${agentLabel}`,
6072
+ title: canAskCodex ? `Ask ${agentLabel} (${getShortcutLabel(keymap, "askAgent")})` : `Write a note before asking ${agentLabel}`,
5574
6073
  type: "button",
5575
6074
  children: [/* @__PURE__ */ jsx("img", {
5576
6075
  alt: "",
@@ -5732,7 +6231,7 @@ const groupReviewCommentsByThread = (comments) => {
5732
6231
  };
5733
6232
  const noopResolveThread = () => {};
5734
6233
  const noopLayoutReady = () => {};
5735
- function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentId, focusEditorRef, identity, keymap, onAskCodex, onCommentBlur, onCommentDraftChange, onCommentFocus, onDeleteComment, onReplyToThread, onResolveThread = noopResolveThread, onSaveCommentEdit, onSubmitComment, onUpdateComment, supportsReviewCommentActions }) {
6234
+ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentId, focusCommentRequest, focusEditorRef, identity, keymap, onAskCodex, onCommentBlur, onCommentDraftChange, onCommentFocus, onDeleteComment, onReplyToThread, onResolveThread = noopResolveThread, onSaveCommentEdit, onSubmitComment, onUpdateComment, supportsReviewCommentActions }) {
5736
6235
  const [resolveState, setResolveState] = useState({
5737
6236
  error: null,
5738
6237
  submitting: false,
@@ -5741,10 +6240,13 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5741
6240
  const lastComment = comments.at(-1);
5742
6241
  const threadId = lastComment?.threadId;
5743
6242
  const threadResolved = comments.some((comment) => comment.isThreadResolved === true);
6243
+ const hasFocusedComment = focusCommentId != null && comments.some((comment) => comment.id === focusCommentId);
6244
+ const [resolutionOverride, setResolutionOverride] = useState(null);
6245
+ const effectiveThreadResolved = resolutionOverride?.base === threadResolved ? resolutionOverride.value : threadResolved;
6246
+ const resolving = resolveState.threadId === threadId && resolveState.submitting;
5744
6247
  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;
6248
+ const canReplyToThread = supportsReviewCommentActions && threadId != null && comments.some((comment) => comment.isReadOnly) && !comments.some((comment) => !comment.isReadOnly) && !comments.some((comment) => comment.canReplyThread === false) && !effectiveThreadResolved && !resolving;
5746
6249
  const hasThreadActions = canReplyToThread || canResolveThread;
5747
- const resolving = resolveState.threadId === threadId && resolveState.submitting;
5748
6250
  const resolveError = resolveState.threadId === threadId ? resolveState.error : null;
5749
6251
  const handleReply = useCallback(() => {
5750
6252
  if (!threadId || !lastComment) return;
@@ -5756,16 +6258,22 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5756
6258
  ]);
5757
6259
  const handleResolve = useCallback(() => {
5758
6260
  if (!threadId || resolving) return;
6261
+ const nextResolved = !effectiveThreadResolved;
6262
+ setResolutionOverride({
6263
+ base: threadResolved,
6264
+ value: nextResolved
6265
+ });
5759
6266
  setResolveState({
5760
6267
  error: null,
5761
6268
  submitting: true,
5762
6269
  threadId
5763
6270
  });
5764
- Promise.resolve(onResolveThread(threadId, !threadResolved)).then(() => setResolveState({
6271
+ Promise.resolve(onResolveThread(threadId, nextResolved)).then(() => setResolveState({
5765
6272
  error: null,
5766
6273
  submitting: false,
5767
6274
  threadId
5768
6275
  })).catch((error) => {
6276
+ setResolutionOverride(null);
5769
6277
  setResolveState({
5770
6278
  error: error instanceof Error ? error.message : String(error),
5771
6279
  submitting: false,
@@ -5773,54 +6281,64 @@ function ReviewCommentThreadGroup({ agentId, agentLabel, comments, focusCommentI
5773
6281
  });
5774
6282
  });
5775
6283
  }, [
6284
+ effectiveThreadResolved,
5776
6285
  onResolveThread,
5777
6286
  resolving,
5778
6287
  threadId,
5779
6288
  threadResolved
5780
6289
  ]);
5781
- return /* @__PURE__ */ jsxs("div", {
6290
+ const threadContent = /* @__PURE__ */ jsxs(Fragment$1, { children: [comments.map((comment) => {
6291
+ const displayName = comment.author ? getReviewAuthorDisplayName(comment.author) : getGitIdentityDisplayName(identity);
6292
+ return /* @__PURE__ */ jsx(ReviewCommentEditor, {
6293
+ agentId,
6294
+ agentLabel,
6295
+ comment,
6296
+ displayName,
6297
+ focusCommentId,
6298
+ focusEditorRef,
6299
+ identity,
6300
+ keymap,
6301
+ onAskCodex,
6302
+ onCommentBlur,
6303
+ onCommentDraftChange,
6304
+ onCommentFocus,
6305
+ onDeleteComment,
6306
+ onSaveCommentEdit,
6307
+ onSubmitComment,
6308
+ onUpdateComment,
6309
+ supportsReviewCommentActions
6310
+ }, comment.id);
6311
+ }), hasThreadActions ? /* @__PURE__ */ jsxs("div", {
6312
+ className: "review-comment-thread-footer",
6313
+ children: [resolveError ? /* @__PURE__ */ jsx("span", {
6314
+ className: "review-comment-thread-error",
6315
+ children: resolveError
6316
+ }) : null, /* @__PURE__ */ jsxs("div", {
6317
+ className: "review-comment-thread-actions",
6318
+ children: [canReplyToThread ? /* @__PURE__ */ jsx("button", {
6319
+ className: "review-comment-action",
6320
+ onClick: handleReply,
6321
+ type: "button",
6322
+ children: "Reply"
6323
+ }) : null, canResolveThread ? /* @__PURE__ */ jsx("button", {
6324
+ className: "review-comment-action",
6325
+ disabled: resolving,
6326
+ onClick: handleResolve,
6327
+ type: "button",
6328
+ children: resolving ? "Saving" : effectiveThreadResolved ? "Reopen" : "Resolve"
6329
+ }) : null]
6330
+ })]
6331
+ }) : null] });
6332
+ return effectiveThreadResolved ? /* @__PURE__ */ jsx(ResolvedThreadDisclosure, {
6333
+ commentCount: comments.length,
6334
+ focused: hasFocusedComment,
6335
+ focusRequest: focusCommentRequest,
6336
+ initialExpanded: Boolean(resolveError),
6337
+ saving: resolving,
6338
+ children: threadContent
6339
+ }) : /* @__PURE__ */ jsx("div", {
5782
6340
  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]
6341
+ children: threadContent
5824
6342
  });
5825
6343
  }
5826
6344
  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 +6371,16 @@ function ReviewAnnotation({ agentId, agentLabel, annotation, comments, focusComm
5853
6371
  return () => observer.disconnect();
5854
6372
  }, [annotationComments.length, onHeightChange]);
5855
6373
  if (annotationComments.length === 0) return null;
6374
+ const commentGroups = groupReviewCommentsByThread(annotationComments);
5856
6375
  return /* @__PURE__ */ jsx("div", {
5857
6376
  className: "review-comment-thread",
5858
6377
  ref: threadRef,
5859
- children: groupReviewCommentsByThread(annotationComments).map((group) => /* @__PURE__ */ jsx(ReviewCommentThreadGroup, {
6378
+ children: commentGroups.map((group) => /* @__PURE__ */ jsx(ReviewCommentThreadGroup, {
5860
6379
  agentId,
5861
6380
  agentLabel,
5862
6381
  comments: group.comments,
5863
6382
  focusCommentId,
6383
+ focusCommentRequest,
5864
6384
  focusEditorRef: setFocusEditorRef,
5865
6385
  identity,
5866
6386
  keymap,
@@ -6062,7 +6582,7 @@ const getHunkSelectionRange = (hunk) => {
6062
6582
  } : null;
6063
6583
  };
6064
6584
  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 }) {
6585
+ 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
6586
  const codeViewRef = useRef(null);
6067
6587
  const markdownEditorRefs = useRef(/* @__PURE__ */ new Map());
6068
6588
  const refreshingMarkdownSectionsRef = useRef(/* @__PURE__ */ new Set());
@@ -6070,6 +6590,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6070
6590
  const handledScrollRequestRef = useRef(null);
6071
6591
  const handledHunkNavRef = useRef(hunkNavigation?.request ?? null);
6072
6592
  const emptyCommentDeleteTimersRef = useRef(/* @__PURE__ */ new Map());
6593
+ const definitionHighlightFrameRef = useRef(null);
6073
6594
  const highlightFrameRef = useRef(null);
6074
6595
  const ignoreNextLineSelectionEndRef = useRef(false);
6075
6596
  const navigatedSelectionRef = useRef(null);
@@ -6083,6 +6604,15 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6083
6604
  const [imagePreviewLayoutPassBySection, setImagePreviewLayoutPassBySection] = useState({});
6084
6605
  const [commentLayoutPassByItem, setCommentLayoutPassByItem] = useState({});
6085
6606
  const [selectedLines, setSelectedLines] = useState(null);
6607
+ const [definitionLookup, setDefinitionLookup] = useState(null);
6608
+ const definitionLookupRequestRef = useRef(0);
6609
+ const definitionModifierActiveRef = useRef(false);
6610
+ const sourceKey = getSourceKey(source);
6611
+ const [definitionLookupSourceKey, setDefinitionLookupSourceKey] = useState(sourceKey);
6612
+ if (definitionLookupSourceKey !== sourceKey) {
6613
+ setDefinitionLookupSourceKey(sourceKey);
6614
+ setDefinitionLookup(null);
6615
+ }
6086
6616
  const selectedLinesRef = useRef(null);
6087
6617
  const commitMessageMetadata = source.type === "commit" ? commitMetadata : null;
6088
6618
  const shouldShowCommitMessage = commitMessageMetadata != null;
@@ -6132,19 +6662,22 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6132
6662
  [itemId]: (current[itemId] ?? 0) + 1
6133
6663
  }));
6134
6664
  }, []);
6135
- const { firstItemByBlockId, firstItemByPath, itemBlockId, itemMetadata, items, searchTargetsByBaseItemId } = useMemo(() => {
6665
+ const { commentTargetById, firstItemByBlockId, firstItemByPath, itemBlockId, itemMetadata, items, searchTargetsByBaseItemId, selectedHeaderItemIds } = useMemo(() => {
6136
6666
  const nextItems = [];
6667
+ const nextCommentTargetById = /* @__PURE__ */ new Map();
6137
6668
  const nextFirstItemByBlockId = /* @__PURE__ */ new Map();
6138
6669
  const nextFirstItemByPath = /* @__PURE__ */ new Map();
6139
6670
  const nextItemBlockId = /* @__PURE__ */ new Map();
6140
6671
  const nextItemMetadata = /* @__PURE__ */ new Map();
6141
6672
  const nextSearchTargetsByBaseItemId = /* @__PURE__ */ new Map();
6673
+ const nextSelectedHeaderItemIds = /* @__PURE__ */ new Set();
6142
6674
  const fontLayoutKey = `line-height:${diffLineHeight}`;
6143
6675
  for (const block of reviewBlocks) {
6144
6676
  if (block.header) {
6145
6677
  const headerId = getBlockHeaderItemId(block);
6146
6678
  nextFirstItemByBlockId.set(block.id, nextFirstItemByBlockId.get(block.id) ?? headerId);
6147
6679
  nextItemBlockId.set(headerId, block.id);
6680
+ if ((block.headerSelected ?? block.selected) === true) nextSelectedHeaderItemIds.add(headerId);
6148
6681
  nextItems.push({
6149
6682
  annotations: [{
6150
6683
  lineNumber: 1,
@@ -6162,7 +6695,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6162
6695
  },
6163
6696
  id: headerId,
6164
6697
  type: "file",
6165
- version: getItemVersion(`${block.id}:walkthrough-header:${(block.headerSelected ?? block.selected) === true ? "selected" : "idle"}`)
6698
+ version: getItemVersion(`${block.id}:walkthrough-header`)
6166
6699
  });
6167
6700
  }
6168
6701
  if (!block.file) continue;
@@ -6200,6 +6733,11 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6200
6733
  const blockSectionComments = blockCommentsBySection.get(section.id) ?? [];
6201
6734
  const sectionComments = dedupeReviewComments([...visibleGlobalComments, ...blockSectionComments]);
6202
6735
  for (const comment of sectionComments) {
6736
+ if (!nextCommentTargetById.has(comment.id)) nextCommentTargetById.set(comment.id, isLineReviewComment(comment) ? {
6737
+ id,
6738
+ lineNumber: comment.lineNumber,
6739
+ side: comment.side
6740
+ } : { id });
6203
6741
  const key = getCommentKey(comment);
6204
6742
  const existing = annotationMap.get(key);
6205
6743
  if (existing && existing.metadata.type === "review-comment") annotationMap.set(key, {
@@ -6311,12 +6849,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6311
6849
  }
6312
6850
  }
6313
6851
  return {
6852
+ commentTargetById: nextCommentTargetById,
6314
6853
  firstItemByBlockId: nextFirstItemByBlockId,
6315
6854
  firstItemByPath: nextFirstItemByPath,
6316
6855
  itemBlockId: nextItemBlockId,
6317
6856
  itemMetadata: nextItemMetadata,
6318
6857
  items: nextItems,
6319
- searchTargetsByBaseItemId: nextSearchTargetsByBaseItemId
6858
+ searchTargetsByBaseItemId: nextSearchTargetsByBaseItemId,
6859
+ selectedHeaderItemIds: nextSelectedHeaderItemIds
6320
6860
  };
6321
6861
  }, [
6322
6862
  collapsed,
@@ -6369,6 +6909,18 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6369
6909
  itemMetadata,
6370
6910
  searchTargetsByBaseItemId
6371
6911
  ]);
6912
+ const getDefinitionDiffTarget = useCallback((candidate) => {
6913
+ for (const item of items) {
6914
+ if (item.type !== "diff") continue;
6915
+ const metadata = itemMetadata.get(item.id);
6916
+ const path = candidate.side === "deletions" ? metadata?.file.oldPath ?? metadata?.file.path : metadata?.file.path;
6917
+ if (metadata && path === candidate.path && lineIsVisibleInFileDiff(item.fileDiff, candidate.side, candidate.lineNumber)) return {
6918
+ item,
6919
+ metadata
6920
+ };
6921
+ }
6922
+ return null;
6923
+ }, [itemMetadata, items]);
6372
6924
  const setCodeViewSelectedLines = useCallback((selection) => {
6373
6925
  if (selection == null || navigatedSelectionRef.current == null || selection.id !== navigatedSelectionRef.current.id || !isSameSelection(selection.range, navigatedSelectionRef.current.range)) navigatedSelectionRef.current = null;
6374
6926
  setSelectedLines(selection);
@@ -6482,6 +7034,50 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6482
7034
  createCommentForRange(range, context);
6483
7035
  },
6484
7036
  onLineClick: (line, context) => {
7037
+ if (isPrimaryModifier(line.event) && onFindDefinitions) {
7038
+ line.event.preventDefault();
7039
+ line.event.stopPropagation();
7040
+ const lineElement = "lineElement" in line ? line.lineElement : null;
7041
+ const identifier = lineElement ? getIdentifierFromPointerEvent(line.event, lineElement) : null;
7042
+ const meta = itemMetadata.get(context.item.id);
7043
+ const side = "annotationSide" in line ? line.annotationSide : null;
7044
+ if (!identifier || !meta || !side) {
7045
+ setDefinitionLookup(null);
7046
+ return;
7047
+ }
7048
+ const requestId = ++definitionLookupRequestRef.current;
7049
+ setDefinitionLookup({
7050
+ anchor: {
7051
+ x: line.event.clientX,
7052
+ y: line.event.clientY
7053
+ },
7054
+ identifier,
7055
+ result: null,
7056
+ sourceKey
7057
+ });
7058
+ onFindDefinitions({
7059
+ identifier,
7060
+ kind: meta.section.kind,
7061
+ lineNumber: line.lineNumber,
7062
+ path: side === "deletions" ? meta.file.oldPath ?? meta.file.path : meta.file.path,
7063
+ side,
7064
+ source
7065
+ }).then((result) => {
7066
+ if (definitionLookupRequestRef.current === requestId) setDefinitionLookup((current) => current?.identifier === identifier && current.sourceKey === sourceKey ? {
7067
+ ...current,
7068
+ result
7069
+ } : current);
7070
+ }).catch(() => {
7071
+ if (definitionLookupRequestRef.current === requestId) setDefinitionLookup((current) => current?.identifier === identifier && current.sourceKey === sourceKey ? {
7072
+ ...current,
7073
+ result: {
7074
+ reason: "Definition search is unavailable for this repository.",
7075
+ status: "unavailable"
7076
+ }
7077
+ } : current);
7078
+ });
7079
+ return;
7080
+ }
6485
7081
  if (isReadOnly) return;
6486
7082
  if (isInteractiveReviewEvent(line.event)) return;
6487
7083
  const meta = itemMetadata.get(context.item.id);
@@ -6514,11 +7110,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6514
7110
  const metadata = itemMetadata.get(context.item.id);
6515
7111
  const isWalkthroughHeaderItem = context.item.id.endsWith(":walkthrough-header");
6516
7112
  node.classList.toggle("codiff-walkthrough-header-item", isWalkthroughHeaderItem);
6517
- node.classList.toggle("codiff-selected-item", metadata?.isSelected === true);
7113
+ node.classList.toggle("codiff-selected-item", metadata?.isSelected === true || selectedHeaderItemIds.has(context.item.id));
6518
7114
  node.classList.toggle("codiff-markdown-preview-item", metadata?.isMarkdownPreview === true);
6519
7115
  node.classList.toggle("codiff-image-preview-item", context.item.type === "file" && Boolean(metadata && canRenderImagePreview(metadata.file.path, metadata.section)));
6520
7116
  node.classList.toggle("codiff-loadable-summary-item", metadata != null && shouldLoadDiffSectionContents(metadata.section));
6521
7117
  node.classList.toggle("codiff-loading-summary-item", Boolean(metadata && loadingSectionIds.has(metadata.section.id)));
7118
+ if (definitionModifierActiveRef.current) window.requestAnimationFrame(() => {
7119
+ if (definitionModifierActiveRef.current && node.isConnected) applyIdentifierNavigationState([{ element: node }], true);
7120
+ });
6522
7121
  },
6523
7122
  overflow: wordWrap ? "wrap" : "scroll",
6524
7123
  stickyHeaders: true,
@@ -6539,7 +7138,11 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6539
7138
  loadDiffFiles,
6540
7139
  loadingSectionIds,
6541
7140
  onCreateComment,
7141
+ onFindDefinitions,
6542
7142
  onLoadSection,
7143
+ selectedHeaderItemIds,
7144
+ source,
7145
+ sourceKey,
6543
7146
  theme,
6544
7147
  wordWrap
6545
7148
  ]);
@@ -6631,11 +7234,19 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6631
7234
  poolSize: Math.min(3, Math.max(1, navigator.hardwareConcurrency || 3)),
6632
7235
  workerFactory: () => new Worker(new URL("@pierre/diffs/worker/worker.js", import.meta.url), { type: "module" })
6633
7236
  }), []);
6634
- const requestScrollItemHeaderIntoView = useCallback((itemId, behavior = "instant") => {
7237
+ const requestScrollTargetIntoView = useCallback((itemId, behavior = "instant", commentTarget) => {
6635
7238
  const handle = codeViewRef.current;
6636
7239
  const viewer = handle?.getInstance();
6637
7240
  if (!handle || !viewer || viewer.getTopForItem(itemId) == null) return false;
6638
- handle.scrollTo({
7241
+ handle.scrollTo(commentTarget?.lineNumber != null && commentTarget.side ? {
7242
+ align: "center",
7243
+ behavior: getEffectiveScrollBehavior(behavior),
7244
+ id: itemId,
7245
+ lineNumber: commentTarget.lineNumber,
7246
+ offset: 11,
7247
+ side: commentTarget.side,
7248
+ type: "line"
7249
+ } : {
6639
7250
  behavior: getEffectiveScrollBehavior(behavior),
6640
7251
  id: itemId,
6641
7252
  offset: 11,
@@ -6646,14 +7257,15 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6646
7257
  useLayoutEffect(() => {
6647
7258
  if (!scrollTarget || handledScrollRequestRef.current === scrollTarget.request) return;
6648
7259
  const behavior = scrollTarget.behavior ?? "instant";
6649
- const itemId = scrollTarget.blockId ? firstItemByBlockId.get(scrollTarget.blockId) : scrollTarget.path ? firstItemByPath.get(scrollTarget.path) : null;
7260
+ const commentTarget = scrollTarget.commentId ? commentTargetById.get(scrollTarget.commentId) : void 0;
7261
+ const itemId = commentTarget?.id ?? (scrollTarget.blockId ? firstItemByBlockId.get(scrollTarget.blockId) : scrollTarget.path ? firstItemByPath.get(scrollTarget.path) : null);
6650
7262
  if (!itemId) return;
6651
7263
  let frame = null;
6652
7264
  let attempts = 0;
6653
7265
  let canceled = false;
6654
7266
  const tryScroll = () => {
6655
7267
  if (canceled || handledScrollRequestRef.current === scrollTarget.request) return;
6656
- if (requestScrollItemHeaderIntoView(itemId, behavior)) {
7268
+ if (requestScrollTargetIntoView(itemId, behavior, commentTarget)) {
6657
7269
  handledScrollRequestRef.current = scrollTarget.request;
6658
7270
  return;
6659
7271
  }
@@ -6668,9 +7280,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6668
7280
  if (frame != null) window.cancelAnimationFrame(frame);
6669
7281
  };
6670
7282
  }, [
7283
+ commentTargetById,
6671
7284
  firstItemByBlockId,
6672
7285
  firstItemByPath,
6673
- requestScrollItemHeaderIntoView,
7286
+ requestScrollTargetIntoView,
6674
7287
  scrollTarget
6675
7288
  ]);
6676
7289
  useEffect(() => {
@@ -6832,6 +7445,22 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6832
7445
  applySearchHighlights(viewer.getRenderedItems(), searchQuery, resolvedActiveSearchMatch);
6833
7446
  });
6834
7447
  }, [resolvedActiveSearchMatch, searchQuery]);
7448
+ const updateRenderedIdentifierNavigation = useCallback((active, viewer) => {
7449
+ const nextViewer = viewer ?? codeViewRef.current?.getInstance();
7450
+ if (!nextViewer) return;
7451
+ if (definitionHighlightFrameRef.current != null) {
7452
+ window.cancelAnimationFrame(definitionHighlightFrameRef.current);
7453
+ definitionHighlightFrameRef.current = null;
7454
+ }
7455
+ if (!active) {
7456
+ applyIdentifierNavigationState(nextViewer.getRenderedItems(), false);
7457
+ return;
7458
+ }
7459
+ definitionHighlightFrameRef.current = window.requestAnimationFrame(() => {
7460
+ definitionHighlightFrameRef.current = null;
7461
+ applyIdentifierNavigationState(nextViewer.getRenderedItems(), true);
7462
+ });
7463
+ }, []);
6835
7464
  const scheduleStickyHeaderStateUpdate = useCallback((viewer) => {
6836
7465
  const nextViewer = viewer ?? codeViewRef.current?.getInstance();
6837
7466
  if (!nextViewer) return;
@@ -6845,6 +7474,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6845
7474
  for (const timer of deferredTimersRef.current) window.clearTimeout(timer);
6846
7475
  deferredTimersRef.current.clear();
6847
7476
  emptyCommentDeleteTimersRef.current.clear();
7477
+ if (definitionHighlightFrameRef.current != null) window.cancelAnimationFrame(definitionHighlightFrameRef.current);
6848
7478
  if (highlightFrameRef.current != null) window.cancelAnimationFrame(highlightFrameRef.current);
6849
7479
  if (stickyHeaderFrameRef.current != null) window.cancelAnimationFrame(stickyHeaderFrameRef.current);
6850
7480
  }, []);
@@ -6856,6 +7486,38 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6856
7486
  scheduleSearchHighlights,
6857
7487
  scheduleStickyHeaderStateUpdate
6858
7488
  ]);
7489
+ useEffect(() => {
7490
+ if (!onFindDefinitions) return;
7491
+ const updateDefinitionModifierState = (active) => {
7492
+ if (definitionModifierActiveRef.current === active) return;
7493
+ definitionModifierActiveRef.current = active;
7494
+ updateRenderedIdentifierNavigation(active);
7495
+ };
7496
+ const handleModifierChange = (event) => {
7497
+ updateDefinitionModifierState(isPrimaryModifier(event));
7498
+ };
7499
+ const handleModifierPointer = (event) => {
7500
+ updateDefinitionModifierState(isPrimaryModifier(event));
7501
+ };
7502
+ const clearDefinitionModifierState = () => updateDefinitionModifierState(false);
7503
+ window.addEventListener("keydown", handleModifierChange);
7504
+ window.addEventListener("keyup", handleModifierChange);
7505
+ window.addEventListener("pointerdown", handleModifierPointer);
7506
+ window.addEventListener("pointermove", handleModifierPointer);
7507
+ window.addEventListener("blur", clearDefinitionModifierState);
7508
+ return () => {
7509
+ clearDefinitionModifierState();
7510
+ window.removeEventListener("keydown", handleModifierChange);
7511
+ window.removeEventListener("keyup", handleModifierChange);
7512
+ window.removeEventListener("pointerdown", handleModifierPointer);
7513
+ window.removeEventListener("pointermove", handleModifierPointer);
7514
+ window.removeEventListener("blur", clearDefinitionModifierState);
7515
+ };
7516
+ }, [onFindDefinitions, updateRenderedIdentifierNavigation]);
7517
+ const invalidateDefinitionLookup = useCallback(() => {
7518
+ definitionLookupRequestRef.current++;
7519
+ }, []);
7520
+ useEffect(() => invalidateDefinitionLookup, [invalidateDefinitionLookup]);
6859
7521
  useEffect(() => {
6860
7522
  const handle = codeViewRef.current;
6861
7523
  const viewer = handle?.getInstance();
@@ -7037,15 +7699,17 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7037
7699
  }
7038
7700
  scheduleSearchHighlights();
7039
7701
  scheduleStickyHeaderStateUpdate(viewer);
7702
+ if (definitionModifierActiveRef.current) updateRenderedIdentifierNavigation(true, viewer);
7040
7703
  }, [
7041
7704
  itemBlockId,
7042
7705
  items,
7043
7706
  onActiveBlockChange,
7044
7707
  onSelectPathFromScroll,
7045
7708
  scheduleSearchHighlights,
7046
- scheduleStickyHeaderStateUpdate
7709
+ scheduleStickyHeaderStateUpdate,
7710
+ updateRenderedIdentifierNavigation
7047
7711
  ]);
7048
- return disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
7712
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
7049
7713
  className: "code-view",
7050
7714
  disableWorkerPool,
7051
7715
  items: codeViewItems,
@@ -7073,11 +7737,77 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7073
7737
  renderCustomHeader,
7074
7738
  selectedLines: isReadOnly ? null : selectedLines
7075
7739
  })
7740
+ }), definitionLookup?.sourceKey === sourceKey && onOpenDefinition ? /* @__PURE__ */ jsx(DefinitionPopover, {
7741
+ anchor: definitionLookup.anchor,
7742
+ getDestination: (candidate) => getDefinitionDiffTarget(candidate) ? "diff" : candidate.canOpenInEditor ? "editor" : "unavailable",
7743
+ identifier: definitionLookup.identifier,
7744
+ onClose: () => {
7745
+ invalidateDefinitionLookup();
7746
+ setDefinitionLookup(null);
7747
+ },
7748
+ onOpen: (candidate) => {
7749
+ const target = getDefinitionDiffTarget(candidate);
7750
+ if (target) {
7751
+ if (target.metadata.isCollapsed) onToggleCollapsed(target.metadata.file, true, target.metadata.reviewIdentity.key);
7752
+ const scrollToDefinition = () => codeViewRef.current?.scrollTo({
7753
+ align: "center",
7754
+ behavior: "smooth-auto",
7755
+ id: target.item.id,
7756
+ lineNumber: candidate.lineNumber,
7757
+ offset: 11,
7758
+ side: candidate.side,
7759
+ type: "line"
7760
+ });
7761
+ if (target.metadata.isCollapsed) window.requestAnimationFrame(() => window.requestAnimationFrame(scrollToDefinition));
7762
+ else scrollToDefinition();
7763
+ } else if (candidate.canOpenInEditor) onOpenDefinition(candidate);
7764
+ invalidateDefinitionLookup();
7765
+ setDefinitionLookup(null);
7766
+ },
7767
+ result: definitionLookup.result
7768
+ }) : null] });
7769
+ }
7770
+ //#endregion
7771
+ //#region app/components/ReviewModeControl.tsx
7772
+ function ReviewModeControl({ mode, modes, onModeChange }) {
7773
+ return /* @__PURE__ */ jsx("div", {
7774
+ "aria-label": "Review mode",
7775
+ className: "review-mode-control",
7776
+ role: "tablist",
7777
+ children: modes.map((item) => /* @__PURE__ */ jsxs("button", {
7778
+ "aria-label": item.ariaLabel,
7779
+ "aria-selected": mode === item.value,
7780
+ onClick: () => onModeChange(item.value),
7781
+ role: "tab",
7782
+ title: item.title,
7783
+ type: "button",
7784
+ children: [
7785
+ item.icon,
7786
+ /* @__PURE__ */ jsx("span", {
7787
+ className: "review-mode-label",
7788
+ children: item.label
7789
+ }),
7790
+ item.indicator
7791
+ ]
7792
+ }, item.value))
7076
7793
  });
7077
7794
  }
7078
7795
  //#endregion
7079
7796
  //#region app/components/ReviewTopBar.tsx
7080
- function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, onToggleSidebar, repository, repositoryTooltip, sidebarCollapsed, sourceMenu, toggleTitle }) {
7797
+ function ReviewTopBar({ actions, center, context, leading, mode, modes, onModeChange, onToggleSidebar, repository, repositoryTooltip, sidebarCollapsed, sidebarPosition = "left", sourceMenu, toggleTitle }) {
7798
+ const sidebarToggle = /* @__PURE__ */ jsx("button", {
7799
+ "aria-label": sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar",
7800
+ className: "review-top-bar-icon-button sidebar-toggle-button",
7801
+ onClick: onToggleSidebar,
7802
+ title: toggleTitle,
7803
+ type: "button",
7804
+ children: /* @__PURE__ */ jsx(SidebarSimpleIcon, {
7805
+ "aria-hidden": true,
7806
+ mirrored: sidebarPosition === "right",
7807
+ size: 18,
7808
+ weight: "bold"
7809
+ })
7810
+ });
7081
7811
  return /* @__PURE__ */ jsxs("header", {
7082
7812
  className: "review-top-bar workspace-top-bar",
7083
7813
  children: [
@@ -7085,18 +7815,7 @@ function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, on
7085
7815
  className: "review-top-bar-left",
7086
7816
  children: [
7087
7817
  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
- }),
7818
+ sidebarPosition === "left" ? sidebarToggle : null,
7100
7819
  sourceMenu,
7101
7820
  /* @__PURE__ */ jsx("div", {
7102
7821
  className: "review-top-bar-repository-slot",
@@ -7105,36 +7824,27 @@ function ReviewTopBar({ actions, context, leading, mode, modes, onModeChange, on
7105
7824
  })
7106
7825
  ]
7107
7826
  }),
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))
7827
+ modes ? /* @__PURE__ */ jsx(ReviewModeControl, {
7828
+ mode,
7829
+ modes,
7830
+ onModeChange
7831
+ }) : /* @__PURE__ */ jsx("div", {
7832
+ className: "review-top-bar-center",
7833
+ children: center
7128
7834
  }),
7129
7835
  /* @__PURE__ */ jsxs("div", {
7130
7836
  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]
7837
+ children: [
7838
+ context ? /* @__PURE__ */ jsx("div", {
7839
+ className: "review-top-bar-context",
7840
+ children: context
7841
+ }) : null,
7842
+ actions ? /* @__PURE__ */ jsx("div", {
7843
+ className: "review-top-bar-actions",
7844
+ children: actions
7845
+ }) : null,
7846
+ sidebarPosition === "right" ? sidebarToggle : null
7847
+ ]
7138
7848
  })
7139
7849
  ]
7140
7850
  });
@@ -7286,6 +7996,7 @@ function StageFileRow({ file, on, onToggle }) {
7286
7996
  function StageGroupRow({ group, onToggleFile, onToggleGroup, selected }) {
7287
7997
  const paths = group.files.map((file) => file.path);
7288
7998
  const onCount = paths.filter((path) => selected.has(path)).length;
7999
+ const state = onCount === 0 ? "off" : onCount === paths.length ? "on" : "partial";
7289
8000
  return /* @__PURE__ */ jsxs("div", {
7290
8001
  className: "wt-stage-group",
7291
8002
  children: [/* @__PURE__ */ jsxs("button", {
@@ -7293,7 +8004,7 @@ function StageGroupRow({ group, onToggleFile, onToggleGroup, selected }) {
7293
8004
  onClick: () => onToggleGroup(paths),
7294
8005
  type: "button",
7295
8006
  children: [
7296
- /* @__PURE__ */ jsx(CommitCheck, { state: onCount === 0 ? "off" : onCount === paths.length ? "on" : "partial" }),
8007
+ /* @__PURE__ */ jsx(CommitCheck, { state }),
7297
8008
  /* @__PURE__ */ jsx("span", {
7298
8009
  className: "wt-stage-group-icon",
7299
8010
  children: /* @__PURE__ */ jsx(ChapterIcon, {
@@ -7629,14 +8340,14 @@ function CommitView({ branch, draft, model, onCommit, onCommitOutput, onUpdateMe
7629
8340
  }
7630
8341
  //#endregion
7631
8342
  //#region app/components/walkthrough/NarrativeWalkthroughView.tsx
7632
- const getWalkthroughBlockScrollTarget = ({ activeBlockId, firstSupportBlockId, mode, stopScrollRequest, supportScrollRequest }) => mode === "support" && firstSupportBlockId ? {
8343
+ const getWalkthroughBlockScrollTarget = ({ activeBlockId, firstSupportBlockId, scrollTarget }) => scrollTarget.kind === "support" && firstSupportBlockId ? {
7633
8344
  behavior: "smooth",
7634
8345
  blockId: firstSupportBlockId,
7635
- request: supportScrollRequest
7636
- } : mode === "stop" && activeBlockId && stopScrollRequest > 0 ? {
8346
+ request: scrollTarget.nonce
8347
+ } : scrollTarget.kind === "stop" && activeBlockId && scrollTarget.nonce > 0 ? {
7637
8348
  behavior: "smooth",
7638
8349
  blockId: activeBlockId,
7639
- request: stopScrollRequest
8350
+ request: scrollTarget.nonce
7640
8351
  } : null;
7641
8352
  const getFocusedRunDiffs = (item, files) => resolveWalkthroughHunkRuns(item, files).flatMap((run) => {
7642
8353
  const focused = focusChangedFileForHunks(run.resolved.file, run.resolved.section, run.hunks);
@@ -7659,14 +8370,15 @@ const getWalkthroughNavigationKeyDirection = (event) => {
7659
8370
  if (isCtrlOnly && event.key === "ArrowUp") return -1;
7660
8371
  return 0;
7661
8372
  };
8373
+ const getWalkthroughKeyboardForwardIndex = ({ index, scrollRequest }) => index === 0 && scrollRequest === 0 ? 0 : index + 1;
7662
8374
  const emptyWalkthroughBlockSet = {
7663
8375
  blocks: [],
7664
8376
  firstBlockIdByStop: [],
7665
8377
  stopIndexByBlockId: /* @__PURE__ */ new Map()
7666
8378
  };
7667
- function StopHeader({ current, stop }) {
8379
+ function StopHeader({ stop }) {
7668
8380
  return /* @__PURE__ */ jsxs("div", {
7669
- className: `wt-stop-block wt-stop-block-header${current ? " current" : ""}`,
8381
+ className: "wt-stop-block wt-stop-block-header",
7670
8382
  children: [/* @__PURE__ */ jsxs("div", {
7671
8383
  className: "wt-stage-title-row",
7672
8384
  children: [/* @__PURE__ */ jsx("h2", {
@@ -7676,9 +8388,9 @@ function StopHeader({ current, stop }) {
7676
8388
  }), /* @__PURE__ */ jsx(Narration, { prose: stop.prose })]
7677
8389
  });
7678
8390
  }
7679
- function SupportHeader({ current }) {
8391
+ function SupportHeader() {
7680
8392
  return /* @__PURE__ */ jsxs("div", {
7681
- className: `wt-stop-block wt-stop-block-header${current ? " current" : ""}`,
8393
+ className: "wt-stop-block wt-stop-block-header",
7682
8394
  children: [/* @__PURE__ */ jsxs("div", {
7683
8395
  className: "wt-stage-title-row",
7684
8396
  children: [/* @__PURE__ */ jsx("h2", {
@@ -7699,10 +8411,7 @@ const createWalkthroughBlocks = (files, walkthroughView, currentIndex) => {
7699
8411
  firstBlockIdByStop[stop.index] = blockId;
7700
8412
  stopIndexByBlockId.set(blockId, stop.index);
7701
8413
  blocks.push({
7702
- header: /* @__PURE__ */ jsx(StopHeader, {
7703
- current: stop.index === currentIndex,
7704
- stop
7705
- }),
8414
+ header: /* @__PURE__ */ jsx(StopHeader, { stop }),
7706
8415
  headerSelected: stop.index === currentIndex,
7707
8416
  id: blockId
7708
8417
  });
@@ -7714,10 +8423,7 @@ const createWalkthroughBlocks = (files, walkthroughView, currentIndex) => {
7714
8423
  stopIndexByBlockId.set(blockId, stop.index);
7715
8424
  blocks.push({
7716
8425
  file,
7717
- header: runIndex === 0 ? /* @__PURE__ */ jsx(StopHeader, {
7718
- current: stop.index === currentIndex,
7719
- stop
7720
- }) : null,
8426
+ header: runIndex === 0 ? /* @__PURE__ */ jsx(StopHeader, { stop }) : null,
7721
8427
  headerSelected: stop.index === currentIndex,
7722
8428
  id: blockId,
7723
8429
  itemIdPrefix: blockId,
@@ -7746,7 +8452,7 @@ const createSupportBlocks = (files, selected, walkthroughView, showWhitespace) =
7746
8452
  const isFirstBlock = blocks.length === 0;
7747
8453
  blocks.push({
7748
8454
  file,
7749
- header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, { current: selected }) : null,
8455
+ header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, {}) : null,
7750
8456
  headerSelected: selected,
7751
8457
  id: blockId,
7752
8458
  itemIdPrefix: blockId,
@@ -7760,7 +8466,7 @@ const createSupportBlocks = (files, selected, walkthroughView, showWhitespace) =
7760
8466
  const isFirstBlock = blocks.length === 0;
7761
8467
  blocks.push({
7762
8468
  file,
7763
- header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, { current: selected }) : null,
8469
+ header: isFirstBlock ? /* @__PURE__ */ jsx(SupportHeader, {}) : null,
7764
8470
  headerSelected: selected,
7765
8471
  id: blockId,
7766
8472
  itemIdPrefix: blockId,
@@ -7795,8 +8501,10 @@ function Arc({ committable, navigation, onShareWalkthrough, shareWalkthroughDisa
7795
8501
  const goArcPrev = useCallback(() => {
7796
8502
  if (navigation.mode === "stop") navigation.goPrev();
7797
8503
  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);
8504
+ else if (navigation.mode === "commit") {
8505
+ if (supportAvailable) navigation.openSupport();
8506
+ else navigation.goStop(walkthroughView.sequence.length - 1);
8507
+ }
7800
8508
  }, [
7801
8509
  navigation,
7802
8510
  supportAvailable,
@@ -7955,9 +8663,7 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
7955
8663
  const reviewBlockScrollTarget = getWalkthroughBlockScrollTarget({
7956
8664
  activeBlockId,
7957
8665
  firstSupportBlockId,
7958
- mode: navigation.mode,
7959
- stopScrollRequest: navigation.scrollTarget.nonce,
7960
- supportScrollRequest: navigation.supportScrollRequest
8666
+ scrollTarget: navigation.scrollTarget
7961
8667
  });
7962
8668
  const handleActiveBlockChange = useCallback((blockId) => {
7963
8669
  onActiveReviewTargetChange(getBlockReviewTarget(reviewBlocks, blockId));
@@ -7993,7 +8699,11 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
7993
8699
  if (direction === 1) {
7994
8700
  event.preventDefault();
7995
8701
  if (navigation.mode === "stop") {
7996
- if (navigation.index < walkthroughView.sequence.length - 1) navigation.goNext();
8702
+ const targetIndex = getWalkthroughKeyboardForwardIndex({
8703
+ index: navigation.index,
8704
+ scrollRequest: navigation.scrollTarget.nonce
8705
+ });
8706
+ if (targetIndex < walkthroughView.sequence.length) navigation.goStop(targetIndex);
7997
8707
  else if (supportAvailable) navigation.openSupport();
7998
8708
  else if (committable) navigation.enterCommit();
7999
8709
  } else if (navigation.mode === "support" && committable) navigation.enterCommit();
@@ -8001,8 +8711,10 @@ function NarrativeWalkthroughView({ allowCommit = true, files, navigation, onAct
8001
8711
  event.preventDefault();
8002
8712
  if (navigation.mode === "stop") navigation.goPrev();
8003
8713
  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);
8714
+ else if (navigation.mode === "commit") {
8715
+ if (supportAvailable) navigation.openSupport();
8716
+ else navigation.goStop(navigation.index);
8717
+ }
8006
8718
  }
8007
8719
  }, [
8008
8720
  committable,
@@ -8235,9 +8947,9 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8235
8947
  const [index, setIndex] = useState(0);
8236
8948
  const [scrollTarget, setScrollTarget] = useState({
8237
8949
  index: 0,
8950
+ kind: "stop",
8238
8951
  nonce: 0
8239
8952
  });
8240
- const [supportScrollRequest, setSupportScrollRequest] = useState(0);
8241
8953
  const [supportVisited, setSupportVisited] = useState(false);
8242
8954
  const [visited, setVisited] = useState(() => {
8243
8955
  const stopId = firstStopId(walkthrough);
@@ -8268,9 +8980,9 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8268
8980
  setIndex(0);
8269
8981
  setScrollTarget({
8270
8982
  index: 0,
8983
+ kind: "stop",
8271
8984
  nonce: 0
8272
8985
  });
8273
- setSupportScrollRequest(0);
8274
8986
  setSupportVisited(false);
8275
8987
  const stopId = firstStopId(walkthrough);
8276
8988
  setVisited(new Set(stopId ? [stopId] : []));
@@ -8332,6 +9044,7 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8332
9044
  pendingSupportScrollRef.current = null;
8333
9045
  setScrollTarget((current) => ({
8334
9046
  index: clamped,
9047
+ kind: "stop",
8335
9048
  nonce: current.nonce + 1
8336
9049
  }));
8337
9050
  }, [
@@ -8376,7 +9089,11 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8376
9089
  if (walkthroughView?.sequence.length) setIndex(walkthroughView.sequence.length - 1);
8377
9090
  setMode("support");
8378
9091
  pendingSupportScrollRef.current = { walkthrough };
8379
- setSupportScrollRequest((current) => current + 1);
9092
+ setScrollTarget((current) => ({
9093
+ ...current,
9094
+ kind: "support",
9095
+ nonce: current.nonce + 1
9096
+ }));
8380
9097
  setSupportVisited(true);
8381
9098
  }, [
8382
9099
  walkthrough,
@@ -8384,11 +9101,15 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8384
9101
  leaveStopMode
8385
9102
  ]);
8386
9103
  const syncSupportFromScroll = useCallback(() => {
9104
+ const pendingStop = pendingStopScrollRef.current;
9105
+ if (pendingStop) {
9106
+ if (pendingStop.walkthrough === walkthrough) return;
9107
+ pendingStopScrollRef.current = null;
9108
+ }
8387
9109
  pendingSupportScrollRef.current = null;
8388
- pendingStopScrollRef.current = null;
8389
9110
  setMode("support");
8390
9111
  setSupportVisited(true);
8391
- }, []);
9112
+ }, [walkthrough]);
8392
9113
  return {
8393
9114
  commitBody,
8394
9115
  commitSelected,
@@ -8407,7 +9128,6 @@ const useNarrativeNavigation = (walkthrough, files, resetKey = "") => {
8407
9128
  scrollTarget,
8408
9129
  setCommitBody,
8409
9130
  setCommitSubject,
8410
- supportScrollRequest,
8411
9131
  supportVisited,
8412
9132
  syncIndexFromScroll,
8413
9133
  syncSupportFromScroll,
@@ -8506,7 +9226,7 @@ const writeSidebarWidth = (width, storage = localStorage) => {
8506
9226
  };
8507
9227
  //#endregion
8508
9228
  //#region app/hooks/useResizableSidebar.ts
8509
- function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, readWidth }) {
9229
+ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, position, readWidth }) {
8510
9230
  const [sidebarWidth, setSidebarWidth] = useState(readWidth);
8511
9231
  return {
8512
9232
  resizeSidebar: useCallback((event) => {
@@ -8515,7 +9235,7 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8515
9235
  const handle = event.currentTarget;
8516
9236
  const shell = handle.parentElement;
8517
9237
  if (!shell) return;
8518
- const shellLeft = shell.getBoundingClientRect().left;
9238
+ const shellRect = shell.getBoundingClientRect();
8519
9239
  handle.setPointerCapture(event.pointerId);
8520
9240
  handle.classList.add("dragging");
8521
9241
  document.body.style.cursor = "col-resize";
@@ -8529,7 +9249,7 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8529
9249
  document.body.style.cursor = "";
8530
9250
  };
8531
9251
  const handleMove = (moveEvent) => {
8532
- const rawWidth = moveEvent.clientX - shellLeft;
9252
+ const rawWidth = position === "right" ? shellRect.right - moveEvent.clientX : moveEvent.clientX - shellRect.left;
8533
9253
  if (collapseThreshold != null && rawWidth < collapseThreshold) {
8534
9254
  collapsed = true;
8535
9255
  onCollapse?.();
@@ -8551,7 +9271,8 @@ function useResizableSidebar({ collapseThreshold, onCollapse, onWidthCommit, rea
8551
9271
  }, [
8552
9272
  collapseThreshold,
8553
9273
  onCollapse,
8554
- onWidthCommit
9274
+ onWidthCommit,
9275
+ position
8555
9276
  ]),
8556
9277
  sidebarWidth
8557
9278
  };
@@ -8657,6 +9378,7 @@ function useReviewCommentDrafts({ canCreateComment = true, comments, onCommentFi
8657
9378
  setComments,
8658
9379
  updateActiveReviewCommentDraft
8659
9380
  ]),
9381
+ focusComment,
8660
9382
  focusCommentId,
8661
9383
  focusCommentRequest,
8662
9384
  resetCommentFocus,
@@ -8739,6 +9461,7 @@ const defaultConfigTemplate = {
8739
9461
  "opencodeModel": "opencode-default",
8740
9462
  "piModel": "pi-default",
8741
9463
  "reviewCommentsPrefix": "# Address these Review Comments",
9464
+ "sidebarPosition": "left",
8742
9465
  "showOutdated": false,
8743
9466
  "showWhitespace": false,
8744
9467
  "theme": "system",
@@ -8746,6 +9469,7 @@ const defaultConfigTemplate = {
8746
9469
  "wordWrap": false
8747
9470
  },
8748
9471
  keymap: {
9472
+ "askAgent": "Mod+Alt+Enter",
8749
9473
  "closeSearch": "Escape",
8750
9474
  "commandBar": "Mod+Shift+p",
8751
9475
  "diffSearch": "Mod+f",
@@ -8803,10 +9527,35 @@ const emptyReviewComments = [];
8803
9527
  const emptyGeneralCommentThreads = [];
8804
9528
  const emptyPaths = /* @__PURE__ */ new Set();
8805
9529
  const emptyWalkthroughNotes = /* @__PURE__ */ new Map();
9530
+ const reviewSurfacePreferencesKey = "codiff:web-review-surface-preferences:v1";
9531
+ const getLocationHashTarget = () => {
9532
+ const hash = window.location.hash.slice(1);
9533
+ if (!hash) return null;
9534
+ try {
9535
+ return decodeURIComponent(hash);
9536
+ } catch {
9537
+ return hash;
9538
+ }
9539
+ };
9540
+ const commentMatchesHashTarget = (comment, target) => comment.id === target || comment.threadId === target || comment.url?.slice(comment.url.lastIndexOf("#") + 1) === target;
8806
9541
  const readSharedSidebarWidth = () => typeof localStorage === "undefined" ? 292 : readSidebarWidth();
8807
9542
  const writeSharedSidebarWidth = (width) => {
8808
9543
  if (typeof localStorage !== "undefined") writeSidebarWidth(width);
8809
9544
  };
9545
+ const readStoredSidebarCollapsed = () => {
9546
+ if (typeof localStorage === "undefined") return null;
9547
+ try {
9548
+ const stored = JSON.parse(localStorage.getItem(reviewSurfacePreferencesKey) ?? "{}");
9549
+ if (stored && typeof stored === "object" && "sidebarCollapsed" in stored && typeof stored.sidebarCollapsed === "boolean") return stored.sidebarCollapsed;
9550
+ } catch {}
9551
+ return null;
9552
+ };
9553
+ const writeStoredSidebarCollapsed = (sidebarCollapsed) => {
9554
+ if (typeof localStorage === "undefined") return;
9555
+ try {
9556
+ localStorage.setItem(reviewSurfacePreferencesKey, JSON.stringify({ sidebarCollapsed }));
9557
+ } catch {}
9558
+ };
8810
9559
  const getSnapshotReviewComments = (snapshot) => {
8811
9560
  if (!snapshot.reviewComments?.length) return emptyReviewComments;
8812
9561
  return getReviewCommentsFromState({
@@ -8828,7 +9577,9 @@ const disabledCommitMessage = async () => ({
8828
9577
  reason: "Shared walkthroughs are read-only.",
8829
9578
  status: "unavailable"
8830
9579
  });
8831
- function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMode, interactive, onDeleteShare, onModeChange, providerLabel = "provider", repositoryUrl, settingsBar, signInLabel = "Sign in to comment", snapshot, sourceDescriptionFooterAside, title }) {
9580
+ const mobileSidebarMediaQuery = "(max-width: 720px)";
9581
+ const shouldCollapseSidebarInitially = () => readStoredSidebarCollapsed() ?? (typeof window !== "undefined" && window.matchMedia(mobileSidebarMediaQuery).matches);
9582
+ 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
9583
  const canComment = commenting?.canComment ?? Boolean(interactive);
8833
9584
  const deleteShare = useCallback(async () => {
8834
9585
  if (!onDeleteShare || !window.confirm("Delete this shared walkthrough? This cannot be undone.")) return;
@@ -8851,23 +9602,44 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8851
9602
  const keymap = useMemo(() => createDefaultConfig().keymap, []);
8852
9603
  const [fileSearchQuery, setFileSearchQuery] = useState("");
8853
9604
  const [uncontrolledSidebarMode, setUncontrolledSidebarMode] = useState(() => initialMode ?? (interactive ? "tree" : "walkthrough"));
8854
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
9605
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(null);
9606
+ const sidebarCollapsedRef = useRef(null);
9607
+ const sidebarInteractedRef = useRef(false);
9608
+ const updateSidebarCollapsed = useCallback((value, persist) => {
9609
+ sidebarCollapsedRef.current = value;
9610
+ setSidebarCollapsed(value);
9611
+ if (persist) writeStoredSidebarCollapsed(value);
9612
+ }, []);
9613
+ const toggleSidebar = useCallback(() => {
9614
+ sidebarInteractedRef.current = true;
9615
+ updateSidebarCollapsed(!(sidebarCollapsedRef.current ?? shouldCollapseSidebarInitially()), true);
9616
+ }, [updateSidebarCollapsed]);
9617
+ useEffect(() => {
9618
+ const timeout = window.setTimeout(() => {
9619
+ if (!sidebarInteractedRef.current) updateSidebarCollapsed(shouldCollapseSidebarInitially(), false);
9620
+ }, 0);
9621
+ return () => window.clearTimeout(timeout);
9622
+ }, [updateSidebarCollapsed]);
8855
9623
  useEffect(() => {
8856
9624
  const handleKeyDown = (event) => {
8857
9625
  if (event.repeat || !matchesShortcut(event, keymap, "toggleSidebar")) return;
8858
9626
  event.preventDefault();
8859
- setSidebarCollapsed((current) => !current);
9627
+ toggleSidebar();
8860
9628
  };
8861
9629
  window.addEventListener("keydown", handleKeyDown);
8862
9630
  return () => window.removeEventListener("keydown", handleKeyDown);
8863
- }, [keymap]);
9631
+ }, [keymap, toggleSidebar]);
8864
9632
  const sidebarMode = Boolean(initialMode && onModeChange) && initialMode ? initialMode : uncontrolledSidebarMode;
8865
9633
  const [treeScrollTarget, setTreeScrollTarget] = useState(null);
8866
9634
  const { bumpItemVersion, collapsed, expandedGenerated, itemVersionByKey, selectedPath, setSelectedPath, toggleCollapsed, toggleViewed, viewed } = useReviewFileState({ initialSelectedPath: snapshot.files[0]?.path ?? null });
8867
9635
  const { resizeSidebar, sidebarWidth } = useResizableSidebar({
8868
9636
  collapseThreshold: 80,
8869
- onCollapse: () => setSidebarCollapsed(true),
9637
+ onCollapse: () => {
9638
+ sidebarInteractedRef.current = true;
9639
+ updateSidebarCollapsed(true, true);
9640
+ },
8870
9641
  onWidthCommit: writeSharedSidebarWidth,
9642
+ position: sidebarPosition,
8871
9643
  readWidth: readSharedSidebarWidth
8872
9644
  });
8873
9645
  const snapshotReviewComments = useMemo(() => getSnapshotReviewComments(snapshot), [snapshot]);
@@ -8878,7 +9650,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8878
9650
  } : comment), [editedReviewCommentBodies, snapshotReviewComments]);
8879
9651
  const [localReviewComments, setLocalReviewComments] = useState(emptyReviewComments);
8880
9652
  const reviewComments = useMemo(() => mergeReviewComments(visibleSnapshotReviewComments, localReviewComments), [localReviewComments, visibleSnapshotReviewComments]);
8881
- const { activeReviewCommentDraftRef, activeReviewCommentDraftState, clearCommentFocus, createComment, deleteComment: deleteLocalComment, focusCommentId, focusCommentRequest, reviewCommentsRef, updateActiveReviewCommentDraft, updateComment } = useReviewCommentDrafts({
9653
+ const { activeReviewCommentDraftRef, activeReviewCommentDraftState, clearCommentFocus, createComment, deleteComment: deleteLocalComment, focusComment, focusCommentId, focusCommentRequest, reviewCommentsRef, updateActiveReviewCommentDraft, updateComment } = useReviewCommentDrafts({
8882
9654
  canCreateComment: canComment,
8883
9655
  comments: reviewComments,
8884
9656
  onCommentFileChange: bumpItemVersion,
@@ -8899,11 +9671,13 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
8899
9671
  const [generalCommentSubmitting, setGeneralCommentSubmitting] = useState(false);
8900
9672
  const [pullRequestReviewSubmitting, setPullRequestReviewSubmitting] = useState(null);
8901
9673
  const [pullRequestCloseSubmitting, setPullRequestCloseSubmitting] = useState(false);
9674
+ const [pullRequestReadySubmitting, setPullRequestReadySubmitting] = useState(false);
8902
9675
  const [pullRequestMergeSubmitting, setPullRequestMergeSubmitting] = useState(false);
8903
9676
  const [walkthroughRequestPending, setWalkthroughRequestPending] = useState(false);
8904
9677
  const walkthroughRequestPendingRef = useRef(false);
8905
9678
  const [walkthroughRequestId, setWalkthroughRequestId] = useState(0);
8906
9679
  const interactiveRef = useRef(interactive);
9680
+ const handledHashTargetRef = useRef(null);
8907
9681
  const visibleFiles = useMemo(() => sortFiles(snapshot.files).filter((file) => fuzzyMatches(file.path, fileSearchQuery) && fileHasVisibleDiff(file, snapshot.preferences.showWhitespace)), [
8908
9682
  fileSearchQuery,
8909
9683
  snapshot.files,
@@ -9041,6 +9815,18 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9041
9815
  pullRequestCloseSubmitting,
9042
9816
  snapshot.repository.source
9043
9817
  ]);
9818
+ const markPullRequestReady = useCallback(() => {
9819
+ const source = snapshot.repository.source;
9820
+ if (!interactive?.onMarkPullRequestReady || pullRequestReadySubmitting || source.type !== "pull-request" || source.reviewStatus?.markReady?.disabled === true || !source.reviewStatus?.markReady) return;
9821
+ setPullRequestReadySubmitting(true);
9822
+ return Promise.resolve(interactive.onMarkPullRequestReady()).catch((error) => {
9823
+ window.alert(error instanceof Error ? error.message : String(error));
9824
+ }).finally(() => setPullRequestReadySubmitting(false));
9825
+ }, [
9826
+ interactive,
9827
+ pullRequestReadySubmitting,
9828
+ snapshot.repository.source
9829
+ ]);
9044
9830
  const mergePullRequest = useCallback((options) => {
9045
9831
  if (!interactive?.onMergePullRequest || pullRequestMergeSubmitting) return;
9046
9832
  setPullRequestMergeSubmitting(true);
@@ -9150,6 +9936,56 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9150
9936
  request: (current?.request ?? 0) + 1
9151
9937
  }));
9152
9938
  }, [setSelectedPath]);
9939
+ const activateReviewComment = useCallback((comment) => {
9940
+ changeSidebarMode("tree");
9941
+ setSelectedPath(comment.filePath);
9942
+ focusComment(comment.id);
9943
+ setTreeScrollTarget((current) => ({
9944
+ behavior: "smooth",
9945
+ commentId: comment.id,
9946
+ path: comment.filePath,
9947
+ request: (current?.request ?? 0) + 1
9948
+ }));
9949
+ }, [
9950
+ changeSidebarMode,
9951
+ focusComment,
9952
+ setSelectedPath
9953
+ ]);
9954
+ const activateHashTarget = useCallback(() => {
9955
+ const target = getLocationHashTarget();
9956
+ if (!target || handledHashTargetRef.current === target) return;
9957
+ const reviewComment = reviewComments.find((comment) => commentMatchesHashTarget(comment, target));
9958
+ if (reviewComment) {
9959
+ handledHashTargetRef.current = target;
9960
+ activateReviewComment(reviewComment);
9961
+ return;
9962
+ }
9963
+ for (const thread of generalCommentThreads) {
9964
+ const generalComment = thread.comments.find((comment) => commentMatchesHashTarget(comment, target)) ?? (thread.id === target ? thread.comments[0] : void 0);
9965
+ if (generalComment) {
9966
+ handledHashTargetRef.current = target;
9967
+ activateGeneralComment(generalComment.id);
9968
+ return;
9969
+ }
9970
+ }
9971
+ }, [
9972
+ activateGeneralComment,
9973
+ activateReviewComment,
9974
+ generalCommentThreads,
9975
+ reviewComments
9976
+ ]);
9977
+ useEffect(() => {
9978
+ const timeout = window.setTimeout(activateHashTarget, 0);
9979
+ return () => window.clearTimeout(timeout);
9980
+ }, [activateHashTarget]);
9981
+ useEffect(() => {
9982
+ const handleHashChange = () => {
9983
+ handledHashTargetRef.current = null;
9984
+ activateHashTarget();
9985
+ };
9986
+ window.addEventListener("hashchange", handleHashChange);
9987
+ return () => window.removeEventListener("hashchange", handleHashChange);
9988
+ }, [activateHashTarget]);
9153
9989
  const updateSelectedPathFromScroll = useCallback((viewer) => {
9154
9990
  const nextPath = getSelectedPathFromScroll(viewer, visibleFiles, snapshot.preferences.showWhitespace);
9155
9991
  if (nextPath) setSelectedPath((current) => current === nextPath ? current : nextPath);
@@ -9207,9 +10043,10 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9207
10043
  const isTerminalMergeState = sourceMergeState ? isTerminalPullRequestMergeState(sourceMergeState) : false;
9208
10044
  const sourceMergeStatusBadge = sourceMergeState && isTerminalMergeState ? /* @__PURE__ */ jsx(PullRequestMergeStatusBadge, { mergeState: sourceMergeState }) : null;
9209
10045
  const sourceDescriptionActions = interactive && source.type === "pull-request" ? /* @__PURE__ */ jsx(PullRequestReviewButtons, {
9210
- disabled: pullRequestReviewSubmitting != null || pullRequestCloseSubmitting,
10046
+ disabled: pullRequestReviewSubmitting != null || pullRequestCloseSubmitting || pullRequestReadySubmitting,
9211
10047
  hasPendingComments: getPendingPullRequestReviewComments(localReviewComments, activeReviewCommentDraftState).length > 0,
9212
10048
  onClosePullRequest: closePullRequest,
10049
+ onMarkPullRequestReady: markPullRequestReady,
9213
10050
  onSubmitReview: submitReview,
9214
10051
  reviewStatus: source.reviewStatus,
9215
10052
  showCommentReview: source.provider === "github" || source.host === "github.com",
@@ -9328,9 +10165,10 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9328
10165
  children: settingsBar
9329
10166
  }) : null] }) : void 0;
9330
10167
  return /* @__PURE__ */ jsxs("div", {
9331
- className: `app-shell share-shell${interactive ? " merge-request-shell" : ""}${sidebarCollapsed ? " sidebar-collapsed" : ""}`,
10168
+ className: `app-shell share-shell${interactive ? " merge-request-shell" : ""}${sidebarCollapsed === null ? " sidebar-auto" : sidebarCollapsed ? " sidebar-collapsed" : ""}`,
10169
+ "data-sidebar-position": sidebarPosition,
9332
10170
  "data-theme": shellTheme,
9333
- style: sidebarCollapsed ? void 0 : { gridTemplateColumns: `${sidebarWidth}px 0 minmax(0, 1fr)` },
10171
+ style: sidebarCollapsed !== false ? void 0 : { gridTemplateColumns: sidebarPosition === "right" ? `minmax(0, 1fr) 0 ${sidebarWidth}px` : `${sidebarWidth}px 0 minmax(0, 1fr)` },
9334
10172
  children: [
9335
10173
  /* @__PURE__ */ jsx(ReviewTopBar, {
9336
10174
  actions: topBarActions,
@@ -9371,7 +10209,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9371
10209
  mode: sidebarMode,
9372
10210
  modes: reviewModes,
9373
10211
  onModeChange: changeSidebarMode,
9374
- onToggleSidebar: () => setSidebarCollapsed((current) => !current),
10212
+ onToggleSidebar: toggleSidebar,
9375
10213
  repository: repositoryLinkUrl ? /* @__PURE__ */ jsx("a", {
9376
10214
  className: "review-top-bar-repository",
9377
10215
  href: repositoryLinkUrl,
@@ -9383,7 +10221,8 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9383
10221
  children: rootLabel
9384
10222
  }),
9385
10223
  repositoryTooltip: snapshot.repository.root,
9386
- sidebarCollapsed,
10224
+ sidebarCollapsed: sidebarCollapsed ?? false,
10225
+ sidebarPosition,
9387
10226
  toggleTitle: `${sidebarCollapsed ? "Expand" : "Collapse"} sidebar`
9388
10227
  }),
9389
10228
  /* @__PURE__ */ jsxs("aside", {
@@ -9461,6 +10300,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9461
10300
  onCancelEdit: cancelEditGeneralComment,
9462
10301
  onChangeDraft: setGeneralCommentDraft,
9463
10302
  onChangeEditDraft: setGeneralCommentEditDraft,
10303
+ onResolveDiscussion: resolveDiscussion,
9464
10304
  onSaveEdit: saveGeneralCommentEdit,
9465
10305
  onStartEdit: startEditGeneralComment,
9466
10306
  onSubmit: submitGeneralComment,