@nkzw/codiff-core 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,7 +39,7 @@ type HeaderReviewDiffBlock = {
39
39
  selected?: boolean;
40
40
  };
41
41
  export type ReviewDiffBlock = FileReviewDiffBlock | HeaderReviewDiffBlock;
42
- export declare function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle, blocks, bottomInset, codeQualityFindings, collapsed, comments, commitMetadata, diffLineHeight, diffStyle, disableWorkerPool, expandedGenerated, files, focusCommentId, focusCommentRequest, forceExpandedPaths, gitIdentity, hunkNavigation, initialMarkdownPreviewSectionIds, isReadOnly, itemVersionByKey, keymap, loadingSectionIds, onActiveBlockChange, onAskCodex, onCommentDraftChange, onCreateComment, onDeleteComment, onFindDefinitions, onLoadImageContent, onLoadSection, onLoadSectionContents, onOpenDefinition, onOpenFile, onRefreshMarkdown, onResolveThread, onSaveCommentEdit, onSelectPathFromScroll, onSubmitComment, onToggleCollapsed, onToggleViewed, onUpdateComment, onUpdateSourceDescription, onUpdateSourceTitle, onUploadSourceDescriptionAsset, reviewIdentityByPath, scrollTarget, searchQuery, selectedPath, showSourceDescription, showWhitespace, source, sourceDescriptionActions, sourceDescriptionFooter, supportsReviewCommentActions, theme, viewed, walkthroughNotes, wordWrap, }: {
42
+ export declare function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle, blocks, bottomInset, codeQualityFindings, collapsed, comments, commitMetadata, diffLineHeight, diffStyle, disableWorkerPool, expandedReviewKeys, files, focusCommentId, focusCommentRequest, forceExpandedPaths, gitIdentity, hunkNavigation, initialMarkdownPreviewSectionIds, isReadOnly, itemVersionByKey, keymap, loadingSectionIds, onActiveBlockChange, onAskCodex, onCommentDraftChange, onCreateComment, onDeleteComment, onFindDefinitions, onLoadImageContent, onLoadSection, onLoadSectionContents, onOpenDefinition, onOpenFile, onRefreshMarkdown, onResolveThread, onSaveCommentEdit, onSelectPathFromScroll, onSubmitComment, onToggleCollapsed, onToggleViewed, onUpdateComment, onUpdateSourceDescription, onUpdateSourceTitle, onUploadSourceDescriptionAsset, reviewIdentityByPath, scrollTarget, searchQuery, selectedPath, showSourceDescription, showWhitespace, source, sourceDescriptionActions, sourceDescriptionFooter, supportsReviewCommentActions, theme, viewed, walkthroughNotes, wordWrap, }: {
43
43
  activeSearchMatch: DiffSearchMatch | null;
44
44
  agentId: 'codex' | 'claude' | 'opencode' | 'pi';
45
45
  agentLabel: string;
@@ -53,7 +53,7 @@ export declare function ReviewCodeView({ activeSearchMatch, agentId, agentLabel,
53
53
  diffLineHeight?: number;
54
54
  diffStyle: CodiffDiffStyle;
55
55
  disableWorkerPool?: boolean;
56
- expandedGenerated?: ReadonlySet<string>;
56
+ expandedReviewKeys?: ReadonlySet<string>;
57
57
  files: ReadonlyArray<ChangedFile>;
58
58
  focusCommentId: string | null;
59
59
  focusCommentRequest: number;
@@ -7,11 +7,11 @@ type UseReviewFileStateOptions = {
7
7
  export declare function useReviewFileState({ initialSelectedPath, onViewedChange, }?: UseReviewFileStateOptions): {
8
8
  bumpItemVersion: (key: string) => void;
9
9
  collapsed: Set<string>;
10
- expandedGenerated: Set<string>;
10
+ expandedReviewKeys: Set<string>;
11
11
  itemVersionByKey: Record<string, number>;
12
12
  selectedPath: string | null;
13
13
  setCollapsed: import("react").Dispatch<import("react").SetStateAction<Set<string>>>;
14
- setExpandedGenerated: import("react").Dispatch<import("react").SetStateAction<Set<string>>>;
14
+ setExpandedReviewKeys: import("react").Dispatch<import("react").SetStateAction<Set<string>>>;
15
15
  setItemVersionByKey: import("react").Dispatch<import("react").SetStateAction<Record<string, number>>>;
16
16
  setSelectedPath: import("react").Dispatch<import("react").SetStateAction<string | null>>;
17
17
  setViewed: import("react").Dispatch<import("react").SetStateAction<Record<string, string>>>;
@@ -52,6 +52,14 @@ export type ReviewScrollTarget = {
52
52
  request: number;
53
53
  };
54
54
  export type ReviewIdentity = {
55
+ coverage?: {
56
+ allHunkIds: ReadonlyArray<string>;
57
+ file: {
58
+ fingerprint: string;
59
+ key: string;
60
+ };
61
+ hunkIds: ReadonlyArray<string>;
62
+ };
55
63
  fingerprint: string;
56
64
  key: string;
57
65
  };
@@ -106,7 +114,7 @@ export type WalkthroughNote = {
106
114
  };
107
115
  export type SourceSession = {
108
116
  collapsed: Set<string>;
109
- expandedGenerated: Set<string>;
117
+ expandedReviewKeys: Set<string>;
110
118
  /** Populated by a generated or pre-authored narrative walkthrough document. */
111
119
  narrativeWalkthrough?: NarrativeWalkthrough | null;
112
120
  reviewComments: ReadonlyArray<ReviewComment>;
@@ -26,6 +26,7 @@ declare const extractPatchHunks: (patch: string) => {
26
26
  added: number;
27
27
  deleted: number;
28
28
  header: string;
29
+ patch: string;
29
30
  }[];
30
31
  /** @param {ReadonlyArray<{added: number; deleted: number}>} hunks */
31
32
  declare const sumHunkLineCounts: (hunks: ReadonlyArray<{
@@ -95,6 +96,7 @@ declare const getSectionWalkthroughHunks: (file: {
95
96
  added: number;
96
97
  deleted: number;
97
98
  header: string;
99
+ patch: string;
98
100
  id: string;
99
101
  index: number;
100
102
  kind: string;
@@ -102,6 +102,7 @@ const extractPatchHunks = (patch) => {
102
102
  index += 1;
103
103
  continue;
104
104
  }
105
+ const hunkStart = index;
105
106
  let additions = 0;
106
107
  let deletions = 0;
107
108
  index += 1;
@@ -115,7 +116,8 @@ const extractPatchHunks = (patch) => {
115
116
  ...parsed,
116
117
  added: additions,
117
118
  deleted: deletions,
118
- header
119
+ header,
120
+ patch: lines.slice(hunkStart, index).join("\n")
119
121
  });
120
122
  }
121
123
  return hunks;
@@ -42,6 +42,7 @@ export declare const formatWalkthroughFileLineRows: (items: ReadonlyArray<{
42
42
  deleted: number;
43
43
  path: string;
44
44
  }>, maxVisibleFiles?: number) => ReadonlyArray<WalkthroughFileLineRow>;
45
+ export declare const getUncoveredWalkthroughReviewIdentity: (file: ChangedFile, view: WalkthroughView, showWhitespace: boolean) => import("./app-types.ts").ReviewIdentity;
45
46
  export declare const getUncoveredWalkthroughFiles: (files: ReadonlyArray<ChangedFile>, view: WalkthroughView, showWhitespace: boolean) => ReadonlyArray<ChangedFile>;
46
47
  export declare const getUncoveredWalkthroughFileLineItems: (files: ReadonlyArray<ChangedFile>, view: WalkthroughView, showWhitespace: boolean) => ReadonlyArray<{
47
48
  added: number;
@@ -1,7 +1,7 @@
1
1
  import type { ChangedFile, PullRequestExistingReviewComment, PullRequestReviewComment, RepositoryState } from '../types.ts';
2
2
  import type { CodeViewInstance, ReviewComment } from './app-types.ts';
3
3
  export declare const isInteractiveReviewEvent: (event: PointerEvent) => boolean;
4
- export declare const hasActiveTextSelection: () => boolean;
4
+ export declare const hasActiveTextSelection: (target?: Node | null) => boolean;
5
5
  export declare const isFileReviewComment: (comment: Pick<ReviewComment, 'anchor' | 'lineNumber' | 'side'>) => boolean;
6
6
  export declare const isLineReviewComment: (comment: ReviewComment) => comment is ReviewComment & {
7
7
  lineNumber: number;
@@ -1,6 +1,9 @@
1
1
  import type { ChangedFile } from '../types.ts';
2
2
  import type { ReviewIdentity } from './app-types.ts';
3
3
  export declare const getFileReviewIdentity: (file: ChangedFile) => ReviewIdentity;
4
+ export declare const getWalkthroughReviewKeyPrefix: (path: string) => string;
5
+ export declare const getSectionReviewHunkIds: (file: ChangedFile, section: ChangedFile['sections'][number]) => ReadonlyArray<string>;
6
+ export declare const getWalkthroughReviewIdentity: (file: ChangedFile, hunkIds: ReadonlyArray<string>) => ReviewIdentity;
4
7
  export declare const getReviewIdentity: (file: ChangedFile, identityByPath?: ReadonlyMap<string, ReviewIdentity>) => ReviewIdentity;
5
8
  export declare const isReviewIdentityViewed: (viewed: Readonly<Record<string, string>>, identity: ReviewIdentity) => boolean;
6
9
  export declare const updateReviewIdentityViewed: (viewed: Readonly<Record<string, string>>, identity: ReviewIdentity, currentlyViewed: boolean) => Record<string, string>;
package/dist/react.mjs CHANGED
@@ -44,7 +44,6 @@ import { ArrowRightIcon } from "@phosphor-icons/react/ArrowRight";
44
44
  import { CaretLeftIcon } from "@phosphor-icons/react/CaretLeft";
45
45
  import { CaretRightIcon } from "@phosphor-icons/react/CaretRight";
46
46
  import { ArrowsClockwiseIcon } from "@phosphor-icons/react/ArrowsClockwise";
47
- import { Terminal, init } from "ghostty-web";
48
47
  //#region app/components/Button.tsx
49
48
  const buttonVariants = cva("codiff-button", {
50
49
  defaultVariants: {
@@ -2380,9 +2379,31 @@ function MergeRequestCommentsView({ canComment, commenting, draft, editDraft, ed
2380
2379
  //#endregion
2381
2380
  //#region lib/review-comments.ts
2382
2381
  const isInteractiveReviewEvent = (event) => event.composedPath().some((target) => target instanceof HTMLElement && (target.closest("button, textarea, input, select, a") || target.closest(".review-comment-thread")));
2383
- const hasActiveTextSelection = () => {
2382
+ const rangeIntersectsTarget = (range, target) => {
2383
+ const targetRoot = target.getRootNode();
2384
+ if (range.startContainer.getRootNode() !== targetRoot || range.endContainer.getRootNode() !== targetRoot) return false;
2385
+ const selectionRange = (target.ownerDocument ?? document).createRange();
2386
+ selectionRange.setStart(range.startContainer, range.startOffset);
2387
+ selectionRange.setEnd(range.endContainer, range.endOffset);
2388
+ return selectionRange.intersectsNode(target);
2389
+ };
2390
+ const isExpandedSelection = (selection, target) => {
2391
+ if (selection == null || selection.rangeCount === 0) return false;
2392
+ const range = selection.getRangeAt(0);
2393
+ return (!selection.isCollapsed || selection.toString().length > 0 || !range.collapsed) && (!target || rangeIntersectsTarget(range, target));
2394
+ };
2395
+ const isExpandedRange = (range, target) => range != null && !range.collapsed && (!target || rangeIntersectsTarget(range, target));
2396
+ const hasActiveTextSelection = (target) => {
2384
2397
  const selection = window.getSelection();
2385
- return selection != null && selection.rangeCount > 0 && !selection.isCollapsed;
2398
+ const root = target?.getRootNode();
2399
+ const shadowRoot = root && "host" in root ? root : null;
2400
+ if (shadowRoot) {
2401
+ const composedRange = selection?.getComposedRanges?.({ shadowRoots: [shadowRoot] })[0];
2402
+ if (isExpandedRange(composedRange, target)) return true;
2403
+ const shadowSelection = shadowRoot.getSelection?.();
2404
+ if (isExpandedSelection(shadowSelection, target)) return true;
2405
+ }
2406
+ return isExpandedSelection(selection, target);
2386
2407
  };
2387
2408
  const isFileReviewComment = (comment) => comment.anchor === "file" || comment.lineNumber == null || comment.side == null;
2388
2409
  const isLineReviewComment = (comment) => !isFileReviewComment(comment);
@@ -3924,18 +3945,46 @@ const getFileReviewIdentity = (file) => ({
3924
3945
  fingerprint: file.fingerprint,
3925
3946
  key: file.path
3926
3947
  });
3948
+ const getWalkthroughReviewKeyPrefix = (path) => `walkthrough:${JSON.stringify(path)}:`;
3949
+ const getHunkViewedKey = (path, hunkId) => `${getWalkthroughReviewKeyPrefix(path)}${JSON.stringify(hunkId)}`;
3950
+ const getSectionReviewHunkIds = (file, section) => {
3951
+ const ids = getSectionWalkthroughHunks(file, section).map((hunk) => hunk.id);
3952
+ return ids.length > 0 ? ids : [section.id];
3953
+ };
3954
+ const getWalkthroughReviewIdentity = (file, hunkIds) => ({
3955
+ coverage: {
3956
+ allHunkIds: file.sections.flatMap((section) => getSectionReviewHunkIds(file, section)),
3957
+ file: getFileReviewIdentity(file),
3958
+ hunkIds
3959
+ },
3960
+ fingerprint: file.fingerprint,
3961
+ key: `${getWalkthroughReviewKeyPrefix(file.path)}${JSON.stringify(hunkIds)}`
3962
+ });
3927
3963
  const getReviewIdentity = (file, identityByPath) => identityByPath?.get(file.path) ?? getFileReviewIdentity(file);
3928
- const isReviewIdentityViewed = (viewed, identity) => viewed[identity.key] === identity.fingerprint;
3964
+ const isReviewIdentityViewed = (viewed, identity) => {
3965
+ const { coverage } = identity;
3966
+ return coverage ? viewed[coverage.file.key] === coverage.file.fingerprint || coverage.hunkIds.length > 0 && coverage.hunkIds.every((id) => viewed[getHunkViewedKey(coverage.file.key, id)] === coverage.file.fingerprint) : viewed[identity.key] === identity.fingerprint;
3967
+ };
3929
3968
  const updateReviewIdentityViewed = (viewed, identity, currentlyViewed) => {
3930
- if (currentlyViewed) {
3931
- const next = { ...viewed };
3932
- delete next[identity.key];
3969
+ const next = { ...viewed };
3970
+ const { coverage } = identity;
3971
+ if (coverage) {
3972
+ const { allHunkIds, file, hunkIds } = coverage;
3973
+ if (viewed[file.key] === file.fingerprint) for (const id of allHunkIds) next[getHunkViewedKey(file.key, id)] = file.fingerprint;
3974
+ for (const id of hunkIds) {
3975
+ const key = getHunkViewedKey(file.key, id);
3976
+ if (currentlyViewed) delete next[key];
3977
+ else next[key] = file.fingerprint;
3978
+ }
3979
+ if (allHunkIds.length > 0 && allHunkIds.every((id) => next[getHunkViewedKey(file.key, id)] === file.fingerprint)) next[file.key] = file.fingerprint;
3980
+ else delete next[file.key];
3933
3981
  return next;
3934
3982
  }
3935
- return {
3936
- ...viewed,
3937
- [identity.key]: identity.fingerprint
3938
- };
3983
+ const prefix = getWalkthroughReviewKeyPrefix(identity.key);
3984
+ for (const key of Object.keys(next)) if (key.startsWith(prefix)) delete next[key];
3985
+ if (currentlyViewed) delete next[identity.key];
3986
+ else next[identity.key] = identity.fingerprint;
3987
+ return next;
3939
3988
  };
3940
3989
  const updateReviewIdentityCollapsed = (collapsed, identity, currentlyCollapsed) => {
3941
3990
  const next = new Set(collapsed);
@@ -4287,6 +4336,7 @@ const getUncoveredWalkthroughSection = (file, section, coveredHunkIds, coveredSe
4287
4336
  if (coveredSyntheticSectionIds.has(section.id)) return null;
4288
4337
  const hunkIds = getSectionHunkIds(file, section);
4289
4338
  if (hunkIds.length === 0) return coveredSectionIds.has(section.id) ? null : {
4339
+ hunkIds: [section.id],
4290
4340
  identity: section.id,
4291
4341
  section
4292
4342
  };
@@ -4294,11 +4344,13 @@ const getUncoveredWalkthroughSection = (file, section, coveredHunkIds, coveredSe
4294
4344
  if (uncoveredHunkIds.length === 0) return null;
4295
4345
  const identity = `${section.id}:${uncoveredHunkIds.join(",")}`;
4296
4346
  if (uncoveredHunkIds.length === hunkIds.length) return {
4347
+ hunkIds: uncoveredHunkIds,
4297
4348
  identity,
4298
4349
  section
4299
4350
  };
4300
4351
  const patch = filterPatchToHunkIds(section.patch, section.id, uncoveredHunkIds);
4301
4352
  return patch ? {
4353
+ hunkIds: uncoveredHunkIds,
4302
4354
  identity,
4303
4355
  section: {
4304
4356
  ...section,
@@ -4306,6 +4358,13 @@ const getUncoveredWalkthroughSection = (file, section, coveredHunkIds, coveredSe
4306
4358
  }
4307
4359
  } : null;
4308
4360
  };
4361
+ const getUncoveredWalkthroughReviewIdentity = (file, view, showWhitespace) => {
4362
+ const coveredHunkIds = walkthroughCoveredHunkIds(view);
4363
+ const coveredSectionIds = walkthroughCoveredSectionIds(view);
4364
+ const coveredSyntheticSectionIds = walkthroughCoveredSyntheticSectionIds(view);
4365
+ const hunkIds = getVisibleDiffSections(file, showWhitespace).flatMap(({ section }) => getUncoveredWalkthroughSection(file, section, coveredHunkIds, coveredSectionIds, coveredSyntheticSectionIds)?.hunkIds ?? []);
4366
+ return getWalkthroughReviewIdentity(file, hunkIds);
4367
+ };
4309
4368
  const getUncoveredWalkthroughFiles = (files, view, showWhitespace) => {
4310
4369
  const coveredHunkIds = walkthroughCoveredHunkIds(view);
4311
4370
  const coveredSectionIds = walkthroughCoveredSectionIds(view);
@@ -4952,7 +5011,7 @@ function DiffLineCountBadge({ ariaLabelPrefix, className = "codiff-line-count",
4952
5011
  //#endregion
4953
5012
  //#region app/components/ReviewCodeView.tsx
4954
5013
  const emptyMarkdownPreviewSectionIds = /* @__PURE__ */ new Set();
4955
- const emptyExpandedGenerated = /* @__PURE__ */ new Set();
5014
+ const emptyExpandedReviewKeys = /* @__PURE__ */ new Set();
4956
5015
  const markdownPreviewPlugins = [frontmatterPlugin(), imagePlugin({
4957
5016
  disableImageResize: true,
4958
5017
  disableImageSettingsButton: true
@@ -5957,8 +6016,9 @@ function ReviewCommentEditor({ agentId, agentLabel, comment, displayName, focusC
5957
6016
  onSaveCommentEdit
5958
6017
  ]);
5959
6018
  const handleBlur = useCallback(() => {
5960
- onCommentBlur(flushDraft(), draft);
6019
+ onCommentBlur(comment, draft, flushDraft);
5961
6020
  }, [
6021
+ comment,
5962
6022
  draft,
5963
6023
  flushDraft,
5964
6024
  onCommentBlur
@@ -6582,7 +6642,7 @@ const getHunkSelectionRange = (hunk) => {
6582
6642
  } : null;
6583
6643
  };
6584
6644
  const isSameSelection = (a, b) => a.start === b.start && a.end === b.end && a.side === b.side;
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 }) {
6645
+ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle = false, blocks, bottomInset = codeViewLayout.paddingBottom, codeQualityFindings = [], collapsed, comments, commitMetadata, diffLineHeight = 20, diffStyle, disableWorkerPool = false, expandedReviewKeys = emptyExpandedReviewKeys, 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 }) {
6586
6646
  const codeViewRef = useRef(null);
6587
6647
  const markdownEditorRefs = useRef(/* @__PURE__ */ new Map());
6588
6648
  const refreshingMarkdownSectionsRef = useRef(/* @__PURE__ */ new Set());
@@ -6590,6 +6650,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6590
6650
  const handledScrollRequestRef = useRef(null);
6591
6651
  const handledHunkNavRef = useRef(hunkNavigation?.request ?? null);
6592
6652
  const emptyCommentDeleteTimersRef = useRef(/* @__PURE__ */ new Map());
6653
+ const activePointerIdsRef = useRef(/* @__PURE__ */ new Set());
6654
+ const pendingCommentBlursRef = useRef(/* @__PURE__ */ new Map());
6655
+ const pendingCommentBlurTimerRef = useRef(null);
6656
+ const focusedCommentEditorIdRef = useRef(null);
6593
6657
  const definitionHighlightFrameRef = useRef(null);
6594
6658
  const highlightFrameRef = useRef(null);
6595
6659
  const ignoreNextLineSelectionEndRef = useRef(false);
@@ -6703,7 +6767,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6703
6767
  const reviewIdentity = block.reviewIdentity ?? getReviewIdentity(file, reviewIdentityByPath);
6704
6768
  const reviewKey = reviewIdentity.key;
6705
6769
  const isViewed = isReviewIdentityViewed(viewed, reviewIdentity);
6706
- const isCollapsed = !forceExpandedPaths.has(file.path) && !expandedGenerated.has(reviewKey) && (collapsed.has(reviewKey) || isGeneratedWalkthroughFile(file));
6770
+ const isCollapsed = !forceExpandedPaths.has(file.path) && !expandedReviewKeys.has(reviewKey) && (isViewed || collapsed.has(reviewKey) || isGeneratedWalkthroughFile(file));
6707
6771
  const visibleSections = getVisibleDiffSections(file, showWhitespace);
6708
6772
  const lineCount = getDiffLineCountFromVisibleSections(visibleSections);
6709
6773
  const sections = isCollapsed ? visibleSections.slice(0, 1) : visibleSections;
@@ -6865,7 +6929,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6865
6929
  commentsBySection,
6866
6930
  diffLineHeight,
6867
6931
  diffStyle,
6868
- expandedGenerated,
6932
+ expandedReviewKeys,
6869
6933
  forceExpandedPaths,
6870
6934
  imagePreviewLayoutPassBySection,
6871
6935
  isReadOnly,
@@ -6939,6 +7003,66 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6939
7003
  }
6940
7004
  emptyCommentDeleteTimersRef.current.clear();
6941
7005
  }, []);
7006
+ const flushPendingCommentBlurs = useCallback(() => {
7007
+ if (activePointerIdsRef.current.size > 0) return;
7008
+ const callbacks = [...pendingCommentBlursRef.current.values()];
7009
+ pendingCommentBlursRef.current.clear();
7010
+ for (const callback of callbacks) callback();
7011
+ }, []);
7012
+ const schedulePendingCommentBlurFlush = useCallback(() => {
7013
+ if (activePointerIdsRef.current.size > 0 || pendingCommentBlursRef.current.size === 0 || pendingCommentBlurTimerRef.current != null) return;
7014
+ const timer = window.setTimeout(() => {
7015
+ deferredTimersRef.current.delete(timer);
7016
+ pendingCommentBlurTimerRef.current = null;
7017
+ flushPendingCommentBlurs();
7018
+ }, 0);
7019
+ pendingCommentBlurTimerRef.current = timer;
7020
+ deferredTimersRef.current.add(timer);
7021
+ }, [flushPendingCommentBlurs]);
7022
+ useEffect(() => {
7023
+ const activePointerIds = activePointerIdsRef.current;
7024
+ const deferredTimers = deferredTimersRef.current;
7025
+ const pendingCommentBlurs = pendingCommentBlursRef.current;
7026
+ const handlePointerDown = (event) => {
7027
+ const timer = pendingCommentBlurTimerRef.current;
7028
+ if (timer != null) {
7029
+ window.clearTimeout(timer);
7030
+ deferredTimers.delete(timer);
7031
+ pendingCommentBlurTimerRef.current = null;
7032
+ }
7033
+ activePointerIds.add(event.pointerId);
7034
+ };
7035
+ const handlePointerEnd = (event) => {
7036
+ activePointerIds.delete(event.pointerId);
7037
+ schedulePendingCommentBlurFlush();
7038
+ };
7039
+ const handleWindowBlur = () => {
7040
+ activePointerIds.clear();
7041
+ schedulePendingCommentBlurFlush();
7042
+ };
7043
+ window.addEventListener("pointerdown", handlePointerDown, true);
7044
+ window.addEventListener("pointercancel", handlePointerEnd, true);
7045
+ window.addEventListener("pointerup", handlePointerEnd, true);
7046
+ window.addEventListener("blur", handleWindowBlur);
7047
+ return () => {
7048
+ window.removeEventListener("pointerdown", handlePointerDown, true);
7049
+ window.removeEventListener("pointercancel", handlePointerEnd, true);
7050
+ window.removeEventListener("pointerup", handlePointerEnd, true);
7051
+ window.removeEventListener("blur", handleWindowBlur);
7052
+ activePointerIds.clear();
7053
+ pendingCommentBlurs.clear();
7054
+ const timer = pendingCommentBlurTimerRef.current;
7055
+ if (timer != null) {
7056
+ window.clearTimeout(timer);
7057
+ deferredTimers.delete(timer);
7058
+ pendingCommentBlurTimerRef.current = null;
7059
+ }
7060
+ };
7061
+ }, [schedulePendingCommentBlurFlush]);
7062
+ const deferCommentBlurUntilPointerEnd = useCallback((commentId, callback) => {
7063
+ if (activePointerIdsRef.current.size > 0 || pendingCommentBlurTimerRef.current != null) pendingCommentBlursRef.current.set(commentId, callback);
7064
+ else callback();
7065
+ }, []);
6942
7066
  const scrollFileItemToTop = useCallback((itemId) => {
6943
7067
  const handle = codeViewRef.current;
6944
7068
  const viewer = handle?.getInstance();
@@ -7034,10 +7158,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7034
7158
  createCommentForRange(range, context);
7035
7159
  },
7036
7160
  onLineClick: (line, context) => {
7161
+ const lineElement = "lineElement" in line ? line.lineElement : null;
7037
7162
  if (isPrimaryModifier(line.event) && onFindDefinitions) {
7038
7163
  line.event.preventDefault();
7039
7164
  line.event.stopPropagation();
7040
- const lineElement = "lineElement" in line ? line.lineElement : null;
7041
7165
  const identifier = lineElement ? getIdentifierFromPointerEvent(line.event, lineElement) : null;
7042
7166
  const meta = itemMetadata.get(context.item.id);
7043
7167
  const side = "annotationSide" in line ? line.annotationSide : null;
@@ -7086,7 +7210,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7086
7210
  onLoadSection(meta.file, meta.section);
7087
7211
  return;
7088
7212
  }
7089
- if (hasActiveTextSelection()) return;
7213
+ if (hasActiveTextSelection(lineElement)) return;
7090
7214
  const side = "annotationSide" in line ? line.annotationSide : null;
7091
7215
  if (!side) return;
7092
7216
  cancelPendingEmptyCommentDeletes();
@@ -7147,6 +7271,8 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7147
7271
  wordWrap
7148
7272
  ]);
7149
7273
  const focusComment = useCallback((comment) => {
7274
+ focusedCommentEditorIdRef.current = comment.id;
7275
+ pendingCommentBlursRef.current.delete(comment.id);
7150
7276
  onCommentDraftChange?.({
7151
7277
  body: comment.body,
7152
7278
  id: comment.id
@@ -7157,25 +7283,32 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7157
7283
  deferredTimersRef.current.delete(timer);
7158
7284
  emptyCommentDeleteTimersRef.current.delete(comment.id);
7159
7285
  }, [onCommentDraftChange]);
7160
- const blurComment = useCallback((comment, body) => {
7161
- onCommentDraftChange?.(null);
7162
- clearCommentLineHighlight();
7163
- if (!comment.isReadOnly && body.trim().length === 0) {
7164
- const existingTimer = emptyCommentDeleteTimersRef.current.get(comment.id);
7165
- if (existingTimer != null) {
7166
- window.clearTimeout(existingTimer);
7167
- deferredTimersRef.current.delete(existingTimer);
7286
+ const blurComment = useCallback((comment, body, flushDraft) => {
7287
+ deferCommentBlurUntilPointerEnd(comment.id, () => {
7288
+ if (focusedCommentEditorIdRef.current === comment.id) {
7289
+ focusedCommentEditorIdRef.current = null;
7290
+ onCommentDraftChange?.(null);
7168
7291
  }
7169
- const timer = window.setTimeout(() => {
7170
- deferredTimersRef.current.delete(timer);
7171
- emptyCommentDeleteTimersRef.current.delete(comment.id);
7172
- onDeleteComment(comment.id);
7173
- }, 120);
7174
- deferredTimersRef.current.add(timer);
7175
- emptyCommentDeleteTimersRef.current.set(comment.id, timer);
7176
- }
7292
+ flushDraft();
7293
+ clearCommentLineHighlight();
7294
+ if (!comment.isReadOnly && body.trim().length === 0) {
7295
+ const existingTimer = emptyCommentDeleteTimersRef.current.get(comment.id);
7296
+ if (existingTimer != null) {
7297
+ window.clearTimeout(existingTimer);
7298
+ deferredTimersRef.current.delete(existingTimer);
7299
+ }
7300
+ const timer = window.setTimeout(() => {
7301
+ deferredTimersRef.current.delete(timer);
7302
+ emptyCommentDeleteTimersRef.current.delete(comment.id);
7303
+ onDeleteComment(comment.id);
7304
+ }, 120);
7305
+ deferredTimersRef.current.add(timer);
7306
+ emptyCommentDeleteTimersRef.current.set(comment.id, timer);
7307
+ }
7308
+ });
7177
7309
  }, [
7178
7310
  clearCommentLineHighlight,
7311
+ deferCommentBlurUntilPointerEnd,
7179
7312
  onCommentDraftChange,
7180
7313
  onDeleteComment
7181
7314
  ]);
@@ -7883,6 +8016,12 @@ function resolveBackdrop(element) {
7883
8016
  }
7884
8017
  return "#00000000";
7885
8018
  }
8019
+ let ghosttyModule = null;
8020
+ const loadGhostty = async () => {
8021
+ const ghostty = await (ghosttyModule ??= import("ghostty-web"));
8022
+ await ghostty.init();
8023
+ return ghostty;
8024
+ };
7886
8025
  /**
7887
8026
  * Read-only ghostty-web terminal that replays the streamed commit output, so
7888
8027
  * ANSI colors and cursor movement from pre-commit hooks render as they would
@@ -7897,7 +8036,7 @@ function CommitLogTerminal({ output }) {
7897
8036
  if (!container) return;
7898
8037
  let disposed = false;
7899
8038
  let instance = null;
7900
- init().then(() => {
8039
+ loadGhostty().then(({ Terminal }) => {
7901
8040
  if (disposed) return;
7902
8041
  const style = getComputedStyle(container);
7903
8042
  instance = new Terminal({
@@ -8354,10 +8493,7 @@ const getFocusedRunDiffs = (item, files) => resolveWalkthroughHunkRuns(item, fil
8354
8493
  return focused ? [{
8355
8494
  file: focused,
8356
8495
  note: getWalkthroughRunNote(item, run),
8357
- reviewIdentity: {
8358
- fingerprint: focused.fingerprint,
8359
- key: `walkthrough:${run.key}`
8360
- }
8496
+ reviewIdentity: getWalkthroughReviewIdentity(run.resolved.file, run.hunks.flatMap((hunk) => hunk.kind === "synthetic" ? getSectionReviewHunkIds(run.resolved.file, run.resolved.section) : [hunk.id]))
8361
8497
  }] : [];
8362
8498
  });
8363
8499
  const getWalkthroughNavigationKeyDirection = (event) => {
@@ -8471,10 +8607,7 @@ const createSupportBlocks = (files, selected, walkthroughView, showWhitespace) =
8471
8607
  id: blockId,
8472
8608
  itemIdPrefix: blockId,
8473
8609
  note: "Not included in the generated walkthrough.",
8474
- reviewIdentity: {
8475
- fingerprint: file.fingerprint,
8476
- key: blockId
8477
- }
8610
+ reviewIdentity: getUncoveredWalkthroughReviewIdentity(files.find((candidate) => candidate.path === file.path), walkthroughView, showWhitespace)
8478
8611
  });
8479
8612
  }
8480
8613
  return blocks;
@@ -9391,7 +9524,7 @@ function useReviewCommentDrafts({ canCreateComment = true, comments, onCommentFi
9391
9524
  //#region app/hooks/useReviewState.ts
9392
9525
  function useReviewFileState({ initialSelectedPath = null, onViewedChange } = {}) {
9393
9526
  const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
9394
- const [expandedGenerated, setExpandedGenerated] = useState(() => /* @__PURE__ */ new Set());
9527
+ const [expandedReviewKeys, setExpandedReviewKeys] = useState(() => /* @__PURE__ */ new Set());
9395
9528
  const [itemVersionByKey, setItemVersionByKey] = useState({});
9396
9529
  const [selectedPath, setSelectedPath] = useState(initialSelectedPath);
9397
9530
  const [viewed, setViewed] = useState({});
@@ -9404,11 +9537,11 @@ function useReviewFileState({ initialSelectedPath = null, onViewedChange } = {})
9404
9537
  return {
9405
9538
  bumpItemVersion,
9406
9539
  collapsed,
9407
- expandedGenerated,
9540
+ expandedReviewKeys,
9408
9541
  itemVersionByKey,
9409
9542
  selectedPath,
9410
9543
  setCollapsed,
9411
- setExpandedGenerated,
9544
+ setExpandedReviewKeys,
9412
9545
  setItemVersionByKey,
9413
9546
  setSelectedPath,
9414
9547
  setViewed,
@@ -9419,9 +9552,9 @@ function useReviewFileState({ initialSelectedPath = null, onViewedChange } = {})
9419
9552
  else next.add(reviewKey);
9420
9553
  return next;
9421
9554
  });
9422
- setExpandedGenerated((current) => {
9555
+ setExpandedReviewKeys((current) => {
9423
9556
  const next = new Set(current);
9424
- if (isCollapsed && isGeneratedWalkthroughFile(file)) next.add(reviewKey);
9557
+ if (isCollapsed) next.add(reviewKey);
9425
9558
  else next.delete(reviewKey);
9426
9559
  return next;
9427
9560
  });
@@ -9433,12 +9566,20 @@ function useReviewFileState({ initialSelectedPath = null, onViewedChange } = {})
9433
9566
  onViewedChange?.(next);
9434
9567
  return next;
9435
9568
  });
9436
- setCollapsed((current) => updateReviewIdentityCollapsed(current, reviewIdentity, isViewed));
9437
- if (!isViewed) setExpandedGenerated((current) => {
9438
- const next = new Set(current);
9439
- next.delete(reviewIdentity.key);
9440
- return next;
9441
- });
9569
+ const fileKey = reviewIdentity.coverage?.file.key ?? file.path;
9570
+ const prefix = getWalkthroughReviewKeyPrefix(fileKey);
9571
+ const clearRelatedKeys = (current) => new Set([...current].filter((key) => key !== fileKey && (reviewIdentity.coverage ? key !== reviewIdentity.key : !key.startsWith(prefix))));
9572
+ if (reviewIdentity.coverage || reviewIdentity.key === file.path) {
9573
+ setCollapsed(clearRelatedKeys);
9574
+ setExpandedReviewKeys(clearRelatedKeys);
9575
+ } else {
9576
+ setCollapsed((current) => updateReviewIdentityCollapsed(current, reviewIdentity, isViewed));
9577
+ setExpandedReviewKeys((current) => {
9578
+ const next = new Set(current);
9579
+ next.delete(reviewIdentity.key);
9580
+ return next;
9581
+ });
9582
+ }
9442
9583
  bumpItemVersion(reviewIdentity.key);
9443
9584
  }, [bumpItemVersion, onViewedChange]),
9444
9585
  viewed
@@ -9471,13 +9612,13 @@ const defaultConfigTemplate = {
9471
9612
  keymap: {
9472
9613
  "askAgent": "Mod+Alt+Enter",
9473
9614
  "closeSearch": "Escape",
9474
- "commandBar": "Mod+Shift+p",
9615
+ "commandBar": "Mod+k",
9475
9616
  "diffSearch": "Mod+f",
9476
9617
  "discardComment": "Escape",
9477
9618
  "fileFilter": "Mod+p",
9478
9619
  "nextHunk": ["Ctrl+ArrowDown", "j"],
9479
9620
  "nextSearchMatch": "Enter",
9480
- "openFile": "Mod+k",
9621
+ "openFile": "Mod+Shift+o",
9481
9622
  "prevHunk": ["Ctrl+ArrowUp", "k"],
9482
9623
  "prevSearchMatch": "Shift+Enter",
9483
9624
  "shortcutsHelp": "Shift+?",
@@ -9631,7 +9772,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
9631
9772
  }, [keymap, toggleSidebar]);
9632
9773
  const sidebarMode = Boolean(initialMode && onModeChange) && initialMode ? initialMode : uncontrolledSidebarMode;
9633
9774
  const [treeScrollTarget, setTreeScrollTarget] = useState(null);
9634
- const { bumpItemVersion, collapsed, expandedGenerated, itemVersionByKey, selectedPath, setSelectedPath, toggleCollapsed, toggleViewed, viewed } = useReviewFileState({ initialSelectedPath: snapshot.files[0]?.path ?? null });
9775
+ const { bumpItemVersion, collapsed, expandedReviewKeys, itemVersionByKey, selectedPath, setSelectedPath, toggleCollapsed, toggleViewed, viewed } = useReviewFileState({ initialSelectedPath: snapshot.files[0]?.path ?? null });
9635
9776
  const { resizeSidebar, sidebarWidth } = useResizableSidebar({
9636
9777
  collapseThreshold: 80,
9637
9778
  onCollapse: () => {
@@ -10006,7 +10147,7 @@ function ReviewSurface({ commenting, externalUrl, gitIdentity = null, initialMod
10006
10147
  diffLineHeight,
10007
10148
  diffStyle: snapshot.preferences.diffStyle,
10008
10149
  disableWorkerPool: true,
10009
- expandedGenerated,
10150
+ expandedReviewKeys,
10010
10151
  focusCommentId,
10011
10152
  focusCommentRequest,
10012
10153
  gitIdentity,
package/dist/style.css CHANGED
@@ -5797,6 +5797,10 @@ html[data-codiff-platform="darwin"] .sidebar {
5797
5797
  background: var(--sidebar-vibrancy-tint);
5798
5798
  }
5799
5799
 
5800
+ .app-shell.command-bar-open > :not(.command-bar-overlay) {
5801
+ filter: blur(4px);
5802
+ }
5803
+
5800
5804
  .merge-request-nav-button {
5801
5805
  color: var(--muted);
5802
5806
  cursor: pointer;
@@ -8601,7 +8605,6 @@ diffs-container .review-comment-thread {
8601
8605
  }
8602
8606
 
8603
8607
  .command-bar-overlay, .open-review-source-overlay {
8604
- backdrop-filter: blur(4px);
8605
8608
  background: #ffffff47;
8606
8609
  justify-content: center;
8607
8610
  align-items: flex-start;
@@ -8611,6 +8614,10 @@ diffs-container .review-comment-thread {
8611
8614
  inset: 0;
8612
8615
  }
8613
8616
 
8617
+ .open-review-source-overlay {
8618
+ backdrop-filter: blur(4px);
8619
+ }
8620
+
8614
8621
  .command-bar-overlay {
8615
8622
  z-index: 100;
8616
8623
  }
@@ -8638,6 +8645,7 @@ diffs-container .review-comment-thread {
8638
8645
  flex-direction: column;
8639
8646
  width: min(520px, 90vw);
8640
8647
  max-height: 400px;
8648
+ padding-top: 6px;
8641
8649
  display: flex;
8642
8650
  overflow: hidden;
8643
8651
  }
@@ -8645,15 +8653,16 @@ diffs-container .review-comment-thread {
8645
8653
  .command-bar-input {
8646
8654
  color: var(--text);
8647
8655
  corner-shape: squircle;
8648
- font: 14px/1.35 var(--font-sans);
8656
+ font: 13px/1.35 var(--font-sans);
8649
8657
  background: #7f7f7f1a;
8650
8658
  border: 0;
8651
8659
  border-radius: 16px;
8652
8660
  outline: none;
8653
8661
  width: calc(100% - 20px);
8654
8662
  height: 34px;
8655
- margin: 10px;
8656
- padding: 0 12px;
8663
+ min-height: 34px;
8664
+ margin: 4px 10px 8px;
8665
+ padding: 0 10px;
8657
8666
  }
8658
8667
 
8659
8668
  .command-bar-input:focus {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/codiff-core",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
4
4
  "description": "A fast diff viewer.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -51,7 +51,7 @@
51
51
  }
52
52
  },
53
53
  "dependencies": {
54
- "@nkzw/mdx-editor": "^0.2.2",
54
+ "@nkzw/mdx-editor": "^1.0.1",
55
55
  "@nkzw/use-relative-time": "^1.2.3",
56
56
  "@phosphor-icons/react": "^2.1.10",
57
57
  "@pierre/diffs": "1.3.6",
@@ -59,7 +59,7 @@
59
59
  "@radix-ui/react-slot": "^1.3.3",
60
60
  "class-variance-authority": "^0.7.1",
61
61
  "ghostty-web": "^0.4.0",
62
- "lucide-react": "^1.34.0",
62
+ "lucide-react": "^1.39.0",
63
63
  "valibot": "^1.4.2"
64
64
  },
65
65
  "peerDependencies": {