-
Notifications
You must be signed in to change notification settings - Fork 761
Changes list and diffs preview redesign #11729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@PavelLaptev there's a few more things I need to do here, but could you have a quick look at this and provide feedback if you have any? |
186079e to
7b4172f
Compare
|
@krlvi could you give this a quick spin and share your impressions? Pavel is ready to work on it if we agree this is the right direction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This WIP pull request refactors the diff viewing experience to show scrollable diffs rather than one at a time. The changes introduce a new MultiDiffView component that uses VirtualList for efficient rendering of multiple file diffs in a single scrollable view, replacing the previous drawer-based single-file preview approach.
Key changes:
- Introduced
MultiDiffViewcomponent for displaying multiple file diffs in a scrollable container - Removed intersection observer-based lazy loading from
HunkDiffBody(all rows now render immediately) - Simplified
BranchReffrom a branded type to a plain string - Enhanced several components (
FileViewHeader,Drawer,ChangedFiles) with new props for better layout control
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ui/src/lib/index.ts | Exports new VirtualList component |
| packages/ui/src/lib/components/hunkDiff/HunkDiffBody.svelte | Removes chunking/lazy loading logic, renders all rows immediately |
| packages/ui/src/lib/components/file/FileViewHeader.svelte | Replaces "transparent" prop with "solid" (inverted logic), adds sticky/border props |
| packages/ui/src/lib/components/VirtualList.svelte | Adds scrollToIndex function, removes overflow:hidden, inlines updateRange logic |
| apps/desktop/src/lib/utils/branch.ts | Simplifies BranchRef from branded type to plain string |
| apps/desktop/src/lib/stacks/stackService.svelte.ts | Updates to use string instead of BranchRef type |
| apps/desktop/src/lib/selection/fileSelectionUtils.ts | Changes return type from boolean|undefined to void |
| apps/desktop/src/components/WorktreeChanges.svelte | Updates onselect callback to include index parameter |
| apps/desktop/src/components/StackView.svelte | Major refactor replacing drawer-based previews with MultiDiffView |
| apps/desktop/src/components/ReduxResult.svelte | Adds hideLoading prop for conditional spinner display |
| apps/desktop/src/components/PreviewHeader.svelte | Adds transparent and sticky props |
| apps/desktop/src/components/MultiDiffView.svelte | New component for rendering multiple file diffs in scrollable list |
| apps/desktop/src/components/FileListItemWrapper.svelte | Whitespace cleanup |
| apps/desktop/src/components/FileList.svelte | Adds reactivity workaround, updates onselect to include index |
| apps/desktop/src/components/Drawer.svelte | Adds topBorder, transparent, and stickyHeader props |
| apps/desktop/src/components/CommitView.svelte | Removes bottom border, adds padding |
| apps/desktop/src/components/ChangedFiles.svelte | Adds maxHeight, border, and header transparency props |
| apps/desktop/src/components/BranchList.svelte | Integrates changed files display inline with branch |
| apps/desktop/src/components/BranchHeader.svelte | Adds changedFiles snippet slot |
| apps/desktop/src/components/BranchCommitList.svelte | Adds inline changed files for expanded commits |
| apps/desktop/src/components/BranchCard.svelte | Adds changedFiles snippet slot |
| e2e/playwright/tests/commitActions.spec.ts | Updates test IDs from stack-preview to stack |
|
I am running this locally and I have to say, at least in the first 30 mins... I really enjoy this. You are right that the visual distinction between files is not very clear, this can probably be improved by tweaking the design |
|
Alright, let's move ahead. @PavelLaptev I'll add the same behaviour for the unassigned view! |
f2b531f to
fefce12
Compare
fefce12 to
7566dc4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.
@mtsgrd cool! I'll continue to work in this PR then. |
|
I’ll also convert it to draft for now, so Copilot can stop commenting for a while. |
7566dc4 to
e22999f
Compare
|
@PavelLaptev sorry for force pushing, but I wanted to fix one thing and add highlighting as an example for you. |
3fb5ee2 to
540110e
Compare
|
@mtsgrd, no problem. I’ll likely begin working on this today in the evening. |
Moves the highlighted state and animation from FileViewHeader to PreviewHeader and Drawer components. MultiDiffView and StackView now pass the highlighted prop to the correct header components, and FileViewHeader no longer handles highlighting. This centralizes the visual indication of active or highlighted headers, improving maintainability and consistency.
Adjusted the width of the vertical line from 4px to 6px in the CommitRow component for improved visibility.
Set the background color of the diff view container using the --clr-bg-1 CSS variable for improved visual consistency.
Eliminates the childrenWrapHeight and childrenWrapDisplay props from Drawer.svelte and their usage in ChangedFiles.svelte, simplifying the component interface and usage.
Replaces the ChangedFiles component with the new NestedChangedFiles component in BranchCommitList, updating CommitRow to support rendering changed files in a nested layout. Refactors CommitLine to use a dotOnTop prop instead of alignDot, and updates related styles and props for improved UI consistency. Adds the NestedChangedFiles.svelte component for a more flexible and modern file list display.
Adjusted the CSS gap property in the StackView component's container to create a more compact layout.
Moved the changed files indicator in BranchHeader to display only when the branch is selected, and updated its styling and structure. Replaced ChangedFiles with NestedChangedFiles in BranchList for a more consistent display. Adjusted CommitRow and NestedChangedFiles to improve layout and visual consistency for changed files containers.
Added an EmptyStatePlaceholder with an empty folder illustration and message when there are no changed files to display. This improves the user experience by providing clear feedback in the absence of file changes.
Only render the empty-state placeholder in NestedChangedFiles when there are no change entries and there are no conflict entries. Previously the empty state would appear even when conflicts existed because conflictEntries being an empty object or undefined wasn't considered. This prevents hiding conflicts from the user and ensures the UI properly reflects when conflict entries are present. Use derived store for conflict presence check Use a derived value to determine whether there are any conflict entries instead of recalculating Object.keys(conflictEntries).length > 0 inline. This simplifies the template condition, avoids repeated calculations, and makes the intent clearer when checking for an empty change list. - Add hasConflicts derived store - Replace template condition with !hasConflicts as part of the empty-state check
This makes the initial render use the previously measured height.
Set the startIndex prop on MultiDiffView instances mounted from StackView so that if there are one or more selected files (tracked via $activeLastAdded), the diff view opens at that selected file. This restores the previously reverted behavior in a cleaner way by passing startIndex={$activeLastAdded?.index} to the component where it is rendered.
This ensures that branch/commit gets selected when clicking one of the files.
acad881 to
1bfd687
Compare
1bfd687 to
3ed889a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 47 out of 48 changed files in this pull request and generated 6 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
apps/desktop/src/components/LazyList.svelte:35
displayCountis declared with$derived(...)but later reassigned inloadMore().$derivedvalues are read-only in runes mode, so this will fail to compile. Use$statefordisplayCount(and reset it whenitemschanges via$effect) instead of$derived.
| A helper component for testing the VirtuaList. It would be good to find a | ||
| better location for this file, something accessible to both tests as well |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in comment: "VirtuaList" should be "VirtualList".
| {#snippet branchGroup(props: { title: string; children: SidebarEntrySubject[] })} | ||
| {#if props.children.length > 0} | ||
| <BranchesListGroup title={props.title}> | ||
| <ChunkyList items={props.children} item={sidebarEntry}></ChunkyList> | ||
| <LazyList items={props.children} template={sidebarEntry}></LazyList> | ||
| </BranchesListGroup> |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LazyList calls its template snippet with two arguments (item, context). Here it is passed sidebarEntry, which in this component is used elsewhere as a 1-arg snippet. If sidebarEntry is still typed/defined as Snippet<[SidebarEntrySubject]>, this will cause a TS mismatch and/or require updating callers to accept the extra context parameter. Consider adapting sidebarEntry (and its call sites) to accept the context arg, or making LazyList's context parameter optional in its template type.
| /** | ||
| * The initial index to start rendering from when the list first loads. | ||
| * If provided, the list will initialize at this position instead of top or bottom. | ||
| * Takes precedence over stickToBottom for initial render. | ||
| */ | ||
| startIndex?: number; | ||
| renderDistance?: number; | ||
| }; |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renderDistance is typed as a required prop in Props, but the destructuring provides a default (renderDistance = 0) and most call sites don't pass it. This makes the component require renderDistance at type-level and will break consumers. Mark renderDistance as optional (renderDistance?: number) in Props.
| const index = indexAttr ? parseInt(indexAttr) : undefined; | ||
| if (index !== undefined) { | ||
| const oldHeight = heightMap[index] || defaultHeight; | ||
| if (target.clientHeight === heightMap[index]) { | ||
| return; | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inside the ResizeObserver loop, return is used when an observed element's height hasn't changed. That exits the entire observer callback and skips processing any remaining entries, which can leave heightMap and offsets stale. Replace this return with continue (or restructure the logic) so all entries are handled.
| const scrollTop = viewport?.scrollTop; | ||
| if (lastScrollTop && lastScrollTop > scrollTop) { | ||
| lastScrollDirection = 'up'; | ||
| } else if (lastScrollTop && lastScrollTop < scrollTop) { | ||
| lastScrollDirection = 'down'; | ||
| } else { | ||
| lastScrollDirection = undefined; | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scroll direction detection uses if (lastScrollTop && ...) which fails when lastScrollTop is 0 (falsy). This causes lastScrollDirection to stay undefined for the first scrolls starting at the top, and can break the resize/scroll adjustment logic that relies on direction. Use an explicit lastScrollTop !== undefined check instead of truthiness.
3ed889a to
c1275ac
Compare
No description provided.