Conversation
|
/review |
4 similar comments
|
/review |
|
/review |
|
/review |
|
/review |
There was a problem hiding this comment.
This PR upgrades @pierre/diffs from 1.1.15 to 1.2.7 and introduces a major architectural shift from single-file diff views to multi-file CodeView diff panes for Changes, PR review, and PR files screens. It also adds git error logging, LSP multi-document support, and improved repo action error dialogs.
I found several issues worth addressing:
- Concurrency risk in git error logging:
lastGitCommandErrorLogis a module-level singleton. If multiple git commands fail concurrently (or if one fails while another is awaiting the log path), the stored log path can be overwritten, causing the error dialog to point to the wrong log file. - Patch version mismatch: The patch file
patches/@pierre__diffs@1.2.4.patchtargets version 1.2.4, but the installed version is 1.2.7. Without explicitpnpm.patchedDependenciesconfiguration, this patch is likely not applied, meaning theforceRenderOverridefix is missing. - Performance regression from eager fetching: The new
PullRequestCodeViewDiffPaneandChangesCodeViewDiffPanesubscribe to file-version queries for all files simultaneously, rather than just the active file. For large PRs or snapshots with many changes, this creates numerous concurrent RTK Query subscriptions and refetches all of them on window focus/reconnect. - Security concern: Git error logs are written to a predictable world-readable temp directory (
os.tmpdir()) and may contain sensitive repository paths or credentials from stderr/stdout. - Missing tests: Several large new files (695–756 line components, complex hooks) have no test coverage.
- Minor: Root
package.jsonaddselectronas a production dependency, which is atypical for a monorepo workspace root.
General comments
- Missing test coverage: The PR adds several large, complex new modules with zero tests:
PullRequestCodeViewDiffPane.tsx(695 lines)ChangesCodeViewDiffPane.tsx(756 lines)DiffScrollbarMarkers.tsxuseMultiDiffCodeViewOptions.tsuseMultiDiffDiagnostics.tsRepoActionErrorDialog.tsx
At minimum, useMultiDiffCodeViewOptions.ts and useMultiDiffDiagnostics.ts deserve unit tests since they manage refs, caches, and DOM decoration logic that is easy to regress.
API contract note: The old useCurrentLspDocument hook was renamed to useCurrentLspDocuments and its signature changed from (repoPath, relPath, text) to ([{repoPath, relPath, text}]). Callers in GeneralFileViewer.tsx were updated, but verify there are no other consumers in the codebase that still import the old hook.
- Behavioral change in diff fetching: The switch from on-demand single-file fetching to all-files eager fetching is a significant architectural change. While it may improve perceived performance for small PRs, it risks degrading performance for large PRs (100+ files) due to:
- Memory pressure from holding all file versions in Redux cache
- IPC/main-thread contention from concurrent git operations
- Slower window-focus recovery due to mass refetching
Consider adding a threshold or virtualization strategy to limit active subscriptions to visible/nearby files.
Findings not posted inline
- apps/desktop/electron/git.ts:44 (RIGHT) — line is not commentable in the GitHub diff
lastGitCommandErrorLogis a module-level singleton. If two git commands fail concurrently (or interleave during the asyncgetLastGitCommandErrorLogPathIPC call), the second failure overwrites the first before the caller can read it. Consider scoping the error log by repoPath+timestamp or returning the logPath directly in the thrown error so callers don't need a second round-trip. - apps/desktop/electron/git.ts:42 (RIGHT) — line is not commentable in the GitHub diff
Writing error logs toos.tmpdir()with a predictable directory name can leak sensitive git output (repo paths, file names, or credentials from hooks) on multi-user systems where/tmpis world-readable. Consider using a user-private directory (e.g., inside the app's user data path) and restricting file permissions. - apps/desktop/src/features/diff-view/hooks/useMultiDiffCodeViewOptions.ts:1145 (RIGHT) — line is not commentable in the GitHub diff
queueMicrotask(() => forceUpdate())can fire after the component unmounts. While React 19 generally handles this gracefully, it is safer to guard with a mounted ref or use a batched state update inside the existing effect cleanup. - apps/desktop/src/features/pull-requests/components/PullRequestCodeViewDiffPane.tsx:2431 (RIGHT) — line is not commentable in the GitHub diff
useEnsureBranchFileVersionQueriessubscribes to alltargetsat once. For large PRs this creates many concurrent RTK Query subscriptions withrefetchOnFocus: true, causing a burst of IPC requests on every window focus. The previous architecture fetched only the active file. Consider limiting eager fetching or paginating the target list. - apps/desktop/src/features/source-control/components/ChangesCodeViewDiffPane.tsx:3730 (RIGHT) — line is not commentable in the GitHub diff
Same eager-fetching concern:useEnsureFileVersionQueriessubscribes to queries for every changed file in the snapshot. In repos with hundreds of changes, this will create excessive concurrent subscriptions and refetch them all on focus/reconnect.
| @@ -22,10 +25,18 @@ | |||
| "format:check": "pnpm fmt:check", | |||
| "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm fmt:check" | |||
| }, | |||
There was a problem hiding this comment.
Adding electron as a root dependencies entry is unusual for a monorepo. It forces every workspace package to resolve electron and can bloat non-desktop packages. It should typically remain a devDependency or live only in apps/desktop/package.json.
| @@ -0,0 +1,21 @@ | |||
| diff --git a/dist/components/VirtualizedFileDiff.js b/dist/components/VirtualizedFileDiff.js | |||
There was a problem hiding this comment.
This patch targets @pierre/diffs@1.2.4, but apps/desktop/package.json installs 1.2.7. Unless pnpm.patchedDependencies is explicitly configured (not visible in the diff), pnpm will not apply this patch to the newer version, so the forceRenderOverride fix will be missing at runtime.
|
/review |
5 similar comments
|
/review |
|
/review |
|
/review |
|
/review |
|
/review |
There was a problem hiding this comment.
This PR upgrades @pierre/diffs from 1.1.15 to 1.2.7 and introduces a multi-diff code view for PR and changes screens, replacing the single-file DiffWorkspace approach. It adds git error logging, a RepoActionErrorDialog, multi-file LSP diagnostics, and refactors LSP document syncing to support multiple files simultaneously.
Issues found:
- Git error log global singleton –
lastGitCommandErrorLogis a module-level variable. Concurrent or rapid git failures from different repos/commands overwrite it, causinggetLastGitCommandErrorLogPathto potentially return the wrong log (or a log from a different repo). This is a concurrency risk. - Git error log temp directory accumulation – Failed git command logs are written to
os.tmpdir()with no cleanup. Logs contain full stderr/stdout which may include sensitive data. Over time, this leaks data and accumulates disk usage. - Unnecessary scroll jumps in PR diff pane –
PullRequestCodeViewDiffPaneincludesparsedDiffs.lengthin its scrolluseEffectdeps. When new diffs load incrementally, the effect re-runs and re-scrolls to the active item, causing disruptive scroll jumps. - DiffScrollbarMarkers code duplication –
DiffScrollbarMarkersandbuildDiffScrollbarMarkersare duplicated in bothDiffViewer.tsxand the newDiffScrollbarMarkers.tsx. They have already diverged slightly (e.g.,hover:w-4vshover:w-3). This is a maintenance risk. - LSP documents key uses text length –
useCurrentLspDocumentsderivesdocumentsKeyfromdocument.text.length. If a file is edited and the character count happens to stay the same, the key won't change. Whiledocumentsis also in the effect deps today, the key is misleading and could cause missed re-syncs if the deps are refactored. - Accessibility gap in scrollbar markers – The diff scrollbar marker buttons only have
onPointerDown. Keyboard users cannot activate them with Enter/Space. - Missing tests – None of the new major components (
ChangesCodeViewDiffPane,PullRequestCodeViewDiffPane,RepoActionErrorDialog,DiffScrollbarMarkers,useMultiDiffDiagnostics,useMultiDiffCodeViewOptions) have tests. The PR only adds one test forLspSymbolPeekpositioning.
General comments
- Missing tests: The PR introduces several large new components and hooks (
ChangesCodeViewDiffPane,PullRequestCodeViewDiffPane,RepoActionErrorDialog,DiffScrollbarMarkers,useMultiDiffDiagnostics,useMultiDiffCodeViewOptions,useCurrentLspDocuments) but does not include any tests for them. The only test added is forLspSymbolPeekpositioning. Consider adding at least unit tests for the scrollbar marker math, the error log path extraction, and the LSP document sync logic. - Git error log cleanup:
writeGitCommandErrorLogcreates a new log file on every failed git command but never deletes old files. ThelastGitCommandErrorLogonly stores the most recent path, so previous files are orphaned. Consider adding a cleanup step that deletes the log file after a timeout or on the next successful git command. - Code duplication:
DiffScrollbarMarkersandbuildDiffScrollbarMarkersexist in bothDiffViewer.tsxandDiffScrollbarMarkers.tsx. The old inline version should be removed andDiffViewer.tsxshould import from the new shared module to avoid drift.
Findings not posted inline
- apps/desktop/electron/git.ts:44 (RIGHT) — line is not commentable in the GitHub diff
lastGitCommandErrorLogis a global module-level variable. If two git commands fail concurrently (or in quick succession from different repos), the last one wins. This meansgetLastGitCommandErrorLogPathcan return a log from a different repo or a more recent unrelated command. Consider making the log path part of the error object and avoiding the global singleton. - apps/desktop/electron/git.ts:42 (RIGHT) — line is not commentable in the GitHub diff
Logs are written toos.tmpdir()with no cleanup. On shared systems, this directory may be readable by other users. The logs contain full git stderr/stdout which may include sensitive data (e.g., file paths, error messages). Consider using a user-specific temp directory and periodically cleaning up old logs. - apps/desktop/src/features/pull-requests/components/PullRequestCodeViewDiffPane.tsx:2639 (RIGHT) — line is not commentable in the GitHub diff
IncludingparsedDiffs.lengthin thisuseEffectdependency array causes the effect to re-run whenever new diffs are loaded. If the user is already viewing the active file, this will force a re-scroll toactiveItemId, creating disruptive scroll jumps while incremental diffs are still loading. RemoveparsedDiffs.lengthfrom the deps;activeItemIdandfocusedLineNumberare sufficient for the intended behavior. - apps/desktop/src/features/diff-view/components/DiffViewer.tsx:449 (RIGHT) — line is not commentable in the GitHub diff
This function is duplicated in the newDiffScrollbarMarkers.tsxfile. The same duplication applies toDiffScrollbarMarkertype,clampPercent,getDiffTotalLines, and theDiffScrollbarMarkerscomponent. Maintenance risk: fixes to one will not apply to the other. Consider importing from the shared module instead. - apps/desktop/src/features/diff-view/components/DiffScrollbarMarkers.tsx:623 (RIGHT) — line is not commentable in the GitHub diff
These marker buttons only haveonPointerDown. Keyboard users cannot activate them with Enter/Space. AddonClick(oronKeyDownhandling Enter/Space) to satisfy accessibility requirements.
| const activeDocumentRef = useRef<ActiveDocument | null>(null); | ||
| const activeDocumentsRef = useRef(new Map<string, CurrentLspDocument>()); | ||
| const documentsKey = documents | ||
| .map((document) => `${documentKey(document)}\u0000${document.text.length}`) |
There was a problem hiding this comment.
documentsKey is derived from document.text.length rather than the actual text content. If a file is edited and the character count happens to stay the same, the key won't change. While documents is also in the deps array today, this key is misleading and would miss changes if the effect deps were ever refactored to rely on it. Use a hash of the text or the text itself instead.
|
/review |
There was a problem hiding this comment.
This PR upgrades the @pierre/diffs library from 1.1.15 to 1.2.7 and introduces multi-diff code view support via new CodeView-based panes for Changes and Pull Request screens. It also adds git error logging, a repo action error dialog, and improves LSP diagnostics handling across multiple diff items.
Critical / High issues found:
-
Concurrency risk in git error logging (
apps/desktop/electron/git.ts):lastGitCommandErrorLogis a global mutable variable. If multiple git commands fail concurrently (e.g., across different repos or rapid operations), the stored log path can be overwritten by a later error beforegetLastGitCommandErrorLogPathis called. This means the error dialog may show or open the wrong log file. -
Dead patch file (
patches/@pierre__diffs@1.2.4.patch): The patch is for version1.2.4butapps/desktop/package.jsoninstalls1.2.7. There is nopatchedDependenciesconfiguration inpackage.json, so pnpm will not apply this patch. The patch fixes virtualized diff force-render behavior; without it, the new multi-diff views may exhibit stale rendering or scroll-jump issues. -
Code duplication (
DiffScrollbarMarkers): The newDiffScrollbarMarkers.tsxfile exportsbuildDiffScrollbarMarkersandDiffScrollbarMarkers, butDiffViewer.tsx(lines 418–566) still contains an identical copy. The oldDiffViewer.tsxshould be updated to import from the new shared file to avoid drift. -
Missing tests: Several new, large components and hooks have no test coverage:
ChangesCodeViewDiffPane.tsx(756 lines)PullRequestCodeViewDiffPane.tsx(695 lines)useMultiDiffDiagnostics.tsuseMultiDiffCodeViewOptions.tsRepoActionErrorDialog.tsxsetRepoActionErrorreducer insourceControlSlice
-
Electron version mismatch: The root
package.jsonaddselectron: ^42.3.0as a dependency, whileapps/desktop/package.jsonpinselectron: "41.0.2"as a devDependency. This creates a version mismatch and may cause runtime or build issues in the monorepo. -
Missing scrollbar markers in changes view:
PullRequestCodeViewDiffPane.tsxrenders<DiffScrollbarMarkers markers={scrollbarMarkers} viewportRef={viewportRef} />, butChangesCodeViewDiffPane.tsxdoes not. This is an inconsistent UX omission. -
Potential memory leak in
useMultiDiffDiagnostics:renderedRootNodesRefstores DOM node references per item. If a file is removed from the view (e.g., staged and then committed), its entry is never deleted becauseonPostRenderis not called for removed items, and the cleanup effect only runs whendiagnosticsByLineByItemchanges. Over time, detached DOM nodes can accumulate. -
Placeholder diff injection risk:
createPlaceholderDiffinuseMultiDiffCodeViewOptions.tsbuilds a git patch string withdiff --git a/${fileName} b/${fileName}. IffileNamecontains spaces or special characters, the generated patch is malformed. Although this is display-only, it could causeprocessFileto throw or produce garbled output. -
Pre-commit hook change: The pre-commit hook was changed from
pnpm lint && pnpm fmt:checktopnpm precommit. The newprecommitscript ispnpm lint && pnpm fmt:check. This is functionally equivalent, but the change itself is harmless.
Overall: The PR is a large refactor with good architectural direction (shared multi-diff hooks, centralized error handling), but it has critical correctness and maintenance issues that should be addressed before merging.
General comments
- The
DiffScrollbarMarkerscomponent was extracted to a new file butDiffViewer.tsxstill contains its own copy. Consider a follow-up PR to deduplicate by havingDiffViewer.tsximport from the shared file. - The
useMultiDiffDiagnosticshook usesuseEffectto iterate overrenderedRootNodesRef.currentand deletes entries while iterating. WhileMapiteration handles this safely in JavaScript, it is worth documenting or adding a cleanup function that is called when items are explicitly removed. - The
ChangesCodeViewDiffPaneandPullRequestCodeViewDiffPaneboth useuseMemo(() => getDiffTheme(), [])which creates a new theme object on every render (because[]means it only runs once, but the function is called insideuseMemo). Wait, actuallyuseMemo(() => getDiffTheme(), [])is correct - it only runs once. But thegetDiffTheme()function might return a new object each time. This is fine as long as the theme is memoized.
Findings not posted inline
- apps/desktop/src/features/diff-view/components/DiffViewer.tsx:449 (RIGHT) — line is not commentable in the GitHub diff
Code duplication:buildDiffScrollbarMarkersis already defined inDiffScrollbarMarkers.tsx. TheDiffViewer.tsxshould import it from the shared file instead of maintaining its own copy. - apps/desktop/src/features/diff-view/components/DiffViewer.tsx:503 (RIGHT) — line is not commentable in the GitHub diff
Code duplication:DiffScrollbarMarkerscomponent is already defined inDiffScrollbarMarkers.tsx. TheDiffViewer.tsxshould import and use the shared component to avoid maintenance drift.
| const GIT_WRITE_RETRY_DELAY_MS = 120; | ||
| const GIT_ERROR_LOG_DIR = path.join(os.tmpdir(), "open-warden-git-logs"); | ||
|
|
||
| let lastGitCommandErrorLog: { repoPath: string; path: string } | null = null; |
There was a problem hiding this comment.
Concurrency risk: lastGitCommandErrorLog is a global mutable variable. If two git commands fail concurrently, the second error overwrites the first before getLastGitCommandErrorLogPath can retrieve it. Consider storing the log path in a per-repo or per-promise map, or returning it directly with the error instead of using shared global state.
| const stderr = rawStderr.trim() || error.message; | ||
| const code = typeof rawCode === "number" ? rawCode : null; | ||
| const commandError = new GitCommandError(args, stderr, code); | ||
| const logPath = await writeGitCommandErrorLog({ repoPath, args, stderr, stdout, code }); |
There was a problem hiding this comment.
The logPath is computed asynchronously and stored globally. If another runGit fails in between, the log path for this error will be lost. This makes the error dialog potentially show the wrong log or no log at all.
| @@ -0,0 +1,21 @@ | |||
| diff --git a/dist/components/VirtualizedFileDiff.js b/dist/components/VirtualizedFileDiff.js | |||
There was a problem hiding this comment.
Dead patch: this patch targets @pierre/diffs@1.2.4, but apps/desktop/package.json installs 1.2.7. There is no patchedDependencies configuration, so pnpm will not apply this patch. The patch should be renamed to match the installed version or pnpm.patchedDependencies should be configured in package.json.
| "@hookform/resolvers": "^5.2.2", | ||
| "@m2d/react-markdown": "^1.0.0", | ||
| "@pierre/diffs": "1.1.15", | ||
| "@pierre/diffs": "1.2.7", |
There was a problem hiding this comment.
Version mismatch with patch: package is 1.2.7 but patch file is 1.2.4. Ensure the patch is applied or the fix is already included in 1.2.7.
| export function useMultiDiffDiagnostics(diagnosticsByItem: Map<string, LspDiagnostic[]>) { | ||
| const diagnosticCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
| const isDiagnosticPopoverHoveredRef = useRef(false); | ||
| const renderedRootNodesRef = useRef(new Map<string, HTMLElement>()); |
There was a problem hiding this comment.
Potential memory leak: renderedRootNodesRef stores DOM nodes. If an item is removed from the view (e.g., file is staged/committed), its entry is never deleted because onPostRender is not called for removed items, and the cleanup effect only runs when diagnosticsByLineByItem changes. Consider pruning disconnected nodes in the effect or on item removal.
| @@ -0,0 +1,133 @@ | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |||
There was a problem hiding this comment.
Missing tests: this hook manages popover state, timers, and DOM decorations across multiple items. It should have unit tests for timer cleanup, popover open/close logic, and decoration application.
| @@ -0,0 +1,429 @@ | |||
| import { useEffect, useMemo, useReducer, useRef } from "react"; | |||
There was a problem hiding this comment.
Missing tests: this hook builds CodeViewOptions with many callbacks via refs. It should have unit tests for createPlaceholderDiff, getAnnotationsKey, useParsedMultiFileDiffs, and updateSelection behavior.
| @@ -0,0 +1,104 @@ | |||
| import { useState } from "react"; | |||
There was a problem hiding this comment.
Missing tests: this dialog component handles error display, log opening, and output toggling. It should have tests for rendering, closing, and opening the log file.
| @@ -159,6 +162,9 @@ const sourceControlSlice = createSlice({ | |||
| state.runningAction = action.payload; | |||
There was a problem hiding this comment.
Missing tests: setRepoActionError and resetRepoViewState (which now clears repoActionError) are not covered in sourceControlSlice.test.ts. Add tests to verify the reducer correctly sets and clears the error state.
| "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm fmt:check" | ||
| }, | ||
| "dependencies": { | ||
| "electron": "^42.3.0" |
There was a problem hiding this comment.
Version mismatch: root package.json adds electron: ^42.3.0 as a dependency, but apps/desktop/package.json has electron: "41.0.2" as a devDependency. This mismatch can cause build/runtime issues. Either remove the root dependency or align the versions.
|
/review |
There was a problem hiding this comment.
This PR updates the Pierre diffs library to 1.2.7 and introduces a multi-diff CodeView architecture across the Changes, Pull Request, and Review screens. It also adds git error logging, LSP document multi-sync, diagnostic multi-diff support, scrollbar change markers, and a repo action error dialog. The code is generally well-structured and follows the project patterns, but there are several correctness, robustness, and testing gaps.
Correctness / Security:
useCurrentLspDocumentsusesdocument.text.lengthas part of its effect key. If the text changes but the length stays the same and the array identity is preserved, the effect will not re-run and the LSP document will not be re-synced. This is a latent correctness bug.createPlaceholderDiffconstructs a raw git diff patch string using unsanitizedpathandmessageinputs. If these contain newlines or special diff characters, the generated patch could be malformed, potentially causing downstream parsing errors or producing invalid diff metadata.lastGitCommandErrorLogis a module-level singleton. If two asyncrunGitcalls fail concurrently, the second failure can overwrite the log path before the first error's consumer reads it, causing the wrong log to be associated with an error. This is a latent concurrency risk.
Memory / Lifecycle:
4. useMultiDiffDiagnostics stores rendered DOM nodes in a useRef Map (renderedRootNodesRef). It only removes disconnected nodes during the diagnostics update effect. If the component unmounts without a diagnostics update, the HTMLElements are leaked. The onTokenLeave callback also sets a setTimeout without cleanup on unmount. While React 18 handles post-unmount state setters safely, this is an anti-pattern.
Accessibility:
5. DiffScrollbarMarkers uses role="scrollbar" on the track container without the required ARIA attributes (aria-valuenow, aria-valuemax, aria-valuemin, aria-controls). This is misleading to assistive technologies and may not convey the component's purpose correctly.
Missing Tests:
6. All new complex units—useMultiDiffDiagnostics, useMultiDiffCodeViewOptions, DiffScrollbarMarkers, ChangesCodeViewDiffPane, PullRequestCodeViewDiffPane, and RepoActionErrorDialog—are added without tests. The ChangesCodeViewDiffPane and PullRequestCodeViewDiffPane components are large, stateful, and imperative (mutating CodeView items via refs), making them high-risk for regressions without automated coverage.
General comments
- Missing tests: All new complex units (
useMultiDiffDiagnostics,useMultiDiffCodeViewOptions,DiffScrollbarMarkers,ChangesCodeViewDiffPane,PullRequestCodeViewDiffPane,RepoActionErrorDialog) are added without any tests. The two*CodeViewDiffPanecomponents are large (~700–800 lines), use imperativeCodeViewref mutations, and coordinate multiple async data sources. They should have at least basic mount/update/unmount tests and coverage for the item update effect logic. - Patch dependency: The PR adds a
patches/@pierre__diffs@1.2.4.patchfile but thepackage.jsonreferences@pierre/diffs1.2.7. Verify that the patch is still compatible with the 1.2.7 release and that the patched paths in thedist/tree match the installed version. If the library was updated after the patch was authored, the patch may fail to apply or may silently patch the wrong lines.
| const dispatch = useAppDispatch(); | ||
| const activeDocumentRef = useRef<ActiveDocument | null>(null); | ||
| const activeDocumentsRef = useRef(new Map<string, CurrentLspDocument>()); | ||
| const documentsKey = documents |
There was a problem hiding this comment.
Correctness risk: The effect key uses document.text.length instead of the text content. If the text changes but the length stays the same and the documents array identity is preserved, the effect will not re-run and the LSP document will not be re-synced. Consider hashing the text or using the text itself in the key.
| return typeof item.version === "number" ? item.version + 1 : 1; | ||
| } | ||
|
|
||
| export function createPlaceholderDiff(path: string, message: string): FileDiffMetadata { |
There was a problem hiding this comment.
Robustness issue: createPlaceholderDiff constructs a raw git diff patch string with unsanitized path and message. If either contains newlines or special characters (e.g. \n, \t, backslash), the generated patch could be malformed. Consider escaping or sanitizing these inputs before interpolating them.
| export function useMultiDiffDiagnostics(diagnosticsByItem: Map<string, LspDiagnostic[]>) { | ||
| const diagnosticCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
| const isDiagnosticPopoverHoveredRef = useRef(false); | ||
| const renderedRootNodesRef = useRef(new Map<string, HTMLElement>()); |
There was a problem hiding this comment.
Memory leak: renderedRootNodesRef stores DOM nodes in a Map. Disconnected nodes are only cleaned up in the diagnostics update effect. If the component unmounts without a diagnostics update, the HTMLElements are retained. Add a cleanup effect that clears the ref on unmount.
| [closePopover, diagnosticsByLineByItem], | ||
| ); | ||
|
|
||
| const onTokenLeave = useCallback(() => { |
There was a problem hiding this comment.
Anti-pattern: onTokenLeave sets a setTimeout that calls closePopover (and thus setState) without a cleanup function. Add a cleanup effect that clears the timer on unmount to avoid post-unmount state updates.
| if (markers.length === 0) return null; | ||
|
|
||
| return ( | ||
| <div |
There was a problem hiding this comment.
Accessibility: The container uses role="scrollbar" without required ARIA attributes (aria-valuenow, aria-valuemax, aria-valuemin, aria-controls). A scrollbar role requires these to be meaningful. Consider using a different role (e.g. region) or adding the required ARIA properties.
| const GIT_WRITE_RETRY_DELAY_MS = 120; | ||
| const GIT_ERROR_LOG_DIR = path.join(os.tmpdir(), "open-warden-git-logs"); | ||
|
|
||
| let lastGitCommandErrorLog: { repoPath: string; path: string } | null = null; |
There was a problem hiding this comment.
Concurrency risk: lastGitCommandErrorLog is a module-level singleton. If two runGit calls fail concurrently, the second writeGitCommandErrorLog can overwrite the log before the first error's consumer reads it, causing the wrong log to be associated with the first error.
|
/review |
5 similar comments
|
/review |
|
/review |
|
/review |
|
/review |
|
/review |
There was a problem hiding this comment.
Large refactor that migrates the Changes and Pull-Request diff panes from the single-file DiffWorkspace to a multi-file @pierre/diffs CodeView (with a 1.1.15 -> 1.2.7 bump), plus a new git error-log pipeline (temp log files + getLastGitCommandErrorLogPath) surfaced through a RepoActionErrorDialog, and assorted LSP/diagnostics/symbol-peek/scrollbar-marker improvements.
Overall the migration looks coherent and the existing flows are preserved, but there are a few real issues worth addressing before merge:
-
The new
patches/@pierre__diffs@1.2.4.patchis orphaned — there is nopnpm.patchedDependenciesentry anywhere (package.json/pnpm-workspace.yaml) and the filename version (1.2.4) does not match the installed version (1.2.7), so pnpm will never apply it. Either register the patch against 1.2.7 or remove the file. -
The git error-log path is retrieved via a process-global singleton (
lastGitCommandErrorLog) plus a separate IPC round-trip, which is racy against any other concurrent git failure in the same repo (notably theSnapshotrefetch thatcommitStagedinvalidates). The commit error dialog can therefore surface the wrong command's log. Prefer threading the log path through the IPC error result directly. -
useCurrentLspDocumentskeys its sync effect ondocument.text.length, which is inconsistent with the exacttext === textcomparison inside the effect; equal-length content edits are only caught because thedocumentsarray identity happens to change. That coupling is fragile and should be hardened. -
Git error logs are written under the OS tmpdir but never cleaned up, so they accumulate indefinitely.
-
The bulk of the new logic (scrollbar-marker math, error handling/cleaning, multi-file diff parsing, multi-diff diagnostics) ships without unit tests; only minor test stubs were added.
Smaller notes: electron was added as a root production dependency (unusual — normally a devDependency in apps/desktop), commitStaged is the only mutation wired to the new log-path flow (other repo-action errors won't get the "Open Git Log" button even though logs are written for them), and there are now two DiffScrollbarMarkers components (one local in DiffViewer.tsx, one new exported module).
General comments
- Test coverage: this PR adds ~2400 LOC of new logic (PullRequestCodeViewDiffPane, ChangesCodeViewDiffPane, useMultiDiffCodeViewOptions, useMultiDiffDiagnostics, the git error-log pipeline, RepoActionErrorDialog, useCurrentLspDocuments) but ships essentially no tests for it — only a mock stub in actions.test.ts and a scroll-position case in LspSymbolPeek.test.tsx. The scrollbar-marker builder, ANSI/error-message cleaning (
stripAnsi/cleanDesktopErrorMessage/firstErrorLine),buildRepoActionError, andcreatePlaceholderDiffare all pure functions and are good unit-test candidates. Please add at least baseline coverage before merge. - RepoActionError lifecycle:
runRepoActionsetsrepoActionErroron failure but never clears it on success. If a user dismisses the dialog and the same action later succeeds, that's fine (dialog controls clearing), but if a different action fails while the dialog is already open the error is silently swapped. Consider clearingrepoActionErrorat the start ofrunRepoAction(or on success in thefinally) to avoid showing stale/overwritten content. - The new multi-file CodeView relies on a
useRefmutated during render (interactionRef.current = {...}in useMultiDiffCodeViewOptions) to keep callbacks fresh without re-creating the memoizedoptions. That works and is idempotent under StrictMode, but it sidesteps the dependency list — any future handler that closes over props/state won't update unless also funneled through the ref. Worth a brief code comment so the invariant is preserved. useMultiDiffDiagnosticsrepaints ALL rendered root nodes (renderedRootNodesRef) on any change todiagnosticsByLineByItem(its useEffect dep). For large multi-file reviews this is O(total tokens across every file) on each diagnostic update; if diagnostics stream in per-file this will do a lot of redundantquerySelectorAllwork. Consider scoping the repaint to the changed item ids.
Findings not posted inline
- apps/desktop/src/features/diff-view/components/DiffViewer.tsx:503 (RIGHT) — line is not commentable in the GitHub diff
There's now a secondDiffScrollbarMarkershere in DiffViewer.tsx alongside the newly extracted/exported one incomponents/DiffScrollbarMarkers.tsx. Both were updated in lockstep (right-6, hover:w-3,.matches('.diff-viewport-scroll')fallback) — this duplication will drift. Consider having DiffViewer.tsx import the shared component, or note why the single-diff path needs its own copy.
| const GIT_WRITE_RETRY_DELAY_MS = 120; | ||
| const GIT_ERROR_LOG_DIR = path.join(os.tmpdir(), "open-warden-git-logs"); | ||
|
|
||
| let lastGitCommandErrorLog: { repoPath: string; path: string } | null = null; |
There was a problem hiding this comment.
This module-level singleton is the root of a concurrency hazard. runGit overwrites lastGitCommandErrorLog on every git failure (status/diff/log reads included, not just writes), and getLastGitCommandErrorLogPath(repoPath) only filters by repo path. Between a commit failing and the renderer's separate IPC call to fetch the log path (see toGitErrorResult in api.ts), commitStaged invalidates the Snapshot tag which triggers a getGitSnapshot refetch; if that refetch also fails in the same repo it silently overwrites this global, so the commit error dialog can display the wrong command's log.
The GitCommandError already carries the correct logPath for the failing command — the robust fix is to return it through the IPC error result (e.g. via toGitErrorResult reading error.logPath instead of re-querying the global) rather than relying on a shared mutable singleton.
| return { data: await commitStaged(repoPath, message) }; | ||
| } catch (error) { | ||
| return { error: toErrorResult(error) }; | ||
| return { error: await toGitErrorResult(error, repoPath) }; |
There was a problem hiding this comment.
toGitErrorResult ignores the logPath already attached to the thrown GitCommandError and instead re-queries the process-global lastGitCommandErrorLog via another IPC hop. That round-trip is the window in which a concurrent git failure in the same repo can swap the log (see git.ts). Since the goal here is just to recover a value that doesn't survive Electron IPC serialization, prefer returning the log path as structured data from the commitStaged service (or embedding it in the error payload) and read it directly here.
| return { message, details: message, logPath: options?.logPath ?? null }; | ||
| } | ||
|
|
||
| async function toGitErrorResult(error: unknown, repoPath: string): Promise<ErrorResult> { |
There was a problem hiding this comment.
Only commitStaged is routed through toGitErrorResult, so only commit errors get a logPath. runGit writes a log file for every git failure (stage/unstage/discard/etc.), but those errors still go through toErrorResult(error) with logPath: null, so their RepoActionErrorDialog never shows the "Open Git Log" button. If logs are only meant for commits, writeGitCommandErrorLog should be scoped to write commands; otherwise route the other write mutations through toGitErrorResult too for consistency.
| }) { | ||
| try { | ||
| await fs.mkdir(GIT_ERROR_LOG_DIR, { recursive: true }); | ||
| const logPath = path.join(GIT_ERROR_LOG_DIR, `git-${Date.now()}-${process.pid}.log`); |
There was a problem hiding this comment.
Log files (git-<ts>-<pid>.log) are written for every git failure but never deleted. Over time these accumulate in $TMPDIR/open-warden-git-logs. Consider a bounded rotation (e.g. keep last N, or clean on app quit), and note the content includes the full command line + repo path so a cleanup story also limits that exposure.
| const activeDocumentRef = useRef<ActiveDocument | null>(null); | ||
| const activeDocumentsRef = useRef(new Map<string, CurrentLspDocument>()); | ||
| const documentsKey = documents | ||
| .map((document) => `${documentKey(document)}\u0000${document.text.length}`) |
There was a problem hiding this comment.
documentsKey incorporates only document.text.length, while the effect body compares full text === text. Today this happens to work only because the documents array identity changes whenever text changes (callers memoize on targetResults). If a caller ever passes a stable documents array, an equal-length content edit (e.g. replacing a character) won't change documentsKey and the LSP document won't be re-synced, leaving diagnostics/hovers stale. The previous single-document hook keyed on the full text; consider hashing or including a content version here for robustness.
| @@ -0,0 +1,21 @@ | |||
| diff --git a/dist/components/VirtualizedFileDiff.js b/dist/components/VirtualizedFileDiff.js | |||
There was a problem hiding this comment.
This patch is orphaned. There is no pnpm.patchedDependencies entry in either package.json or pnpm-workspace.yaml, so pnpm won't apply it at all; and even if registered, the filename targets 1.2.4 while apps/desktop/package.json now pins @pierre/diffs to 1.2.7, so the version would not match. The patch tweaks VirtualizedFileDiff force-render behavior — if it's still needed, register it against 1.2.7 (via pnpm patch @pierre/diffs@1.2.7); if not, delete it so it doesn't read as a silently-applied dependency.
| "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm fmt:check" | ||
| }, | ||
| "dependencies": { | ||
| "electron": "^42.3.0" |
There was a problem hiding this comment.
electron is now a root-level production dependency (and added to pnpm.onlyBuiltDependencies). Electron is a build/runtime tool for the desktop app, not a dependency of the root workspace package — it's almost certainly already declared (as a devDependency) in apps/desktop. Promoting it to a root prod dep drags a large platform binary into the root install and changes the dependency graph. Move it to apps/desktop (devDependency) unless there's a specific root-level reason.
| fileDiff: FileDiffMetadata, | ||
| diffStyle: DiffStyle, | ||
| ): DiffScrollbarMarker[] { | ||
| const totalLines = getDiffTotalLines(fileDiff, diffStyle); |
There was a problem hiding this comment.
The marker math reads fileDiff.splitLineCount/unifiedLineCount, hunk.splitLineStart/unifiedLineStart, and content.additions/deletions/lines directly off @pierre/diffs' FileDiffMetadata. These are untested and any change in their semantics between 1.1.15 and 1.2.7 would silently skew the scrollbar markers (or, if undefined, produce NaN% positions). Worth a small unit test covering a known hunk (split + unified) to lock in the contract now that the version was bumped.
|
/review |
No description provided.