Skip to content

Decompose StackView and FileList into compound components#12902

Open
mtsgrd wants to merge 10 commits intomasterfrom
refactor/filelist-controller
Open

Decompose StackView and FileList into compound components#12902
mtsgrd wants to merge 10 commits intomasterfrom
refactor/filelist-controller

Conversation

@mtsgrd
Copy link
Contributor

@mtsgrd mtsgrd commented Mar 18, 2026

  • Extract the monolithic StackView.svelte (~800 lines) into compound components: StackPanel , StackDetails , and StackCodegen , coordinated by a StackController reactive class
  • Extract FileList.svelte into compound components: FileListProvider , FileListItems , and FileListConflicts , coordinated by a FileListController reactive class
  • Replace prop drilling of identity and UI state in BranchList and BranchCommitList with getStackContext()
  • Move codegen data queries and message sending from StackCodegen into CodegenMessages , making StackCodegen a thin wrapper
  • Fix a reactivity bug where switching commits caused the details panel CSS to flash by memoizing isDetailsViewOpen with $derived
  • Rename file selection tracking APIs for clarity ( activeLastAdded → focusedFileStore , assignedKey → stagedFocusedFile , etc.)

Split FileList into a provider and compound children (FileListProvider,
FileListItems, FileListConflicts) backed by a FileListController that
owns selection, keyboard navigation, and focus state. Consumers compose
only the pieces they need and wire callbacks (onselect, extraKeyHandlers)
on the child that uses them.
Copilot AI review requested due to automatic review settings March 18, 2026 11:35
@vercel
Copy link

vercel bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
gitbutler-web Skipped Skipped Mar 18, 2026 3:32pm

Request Review

Copy link
Contributor

Copilot AI left a 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 PR refactors the desktop stack and file list UI into compound components coordinated via reactive controller classes, reducing prop drilling and moving shared selection/preview orchestration into context-backed controllers.

Changes:

  • Introduces StackController and FileListController to centralize state/behavior and provide it via Svelte context.
  • Decomposes StackView into StackPanel, StackDetails, and StackCodegen, and decomposes the old FileList into FileListProvider, FileListItems, and FileListConflicts.
  • Moves Codegen data querying + message sending responsibilities into CodegenMessages, leaving StackCodegen as a thin wrapper.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/desktop/src/lib/stack/stackController.svelte.ts New reactive controller for StackView compound components (selection, preview, diff coordination).
apps/desktop/src/lib/selection/fileListController.svelte.ts New reactive controller for file list selection, keyboard nav, and focus syncing.
apps/desktop/src/components/codegen/CodegenMessages.svelte Refactored to read stack state via context and to own message sending/query orchestration.
apps/desktop/src/components/WorktreeChanges.svelte Migrated from monolithic FileList to provider/items compound components; adds AI keyboard handlers integration.
apps/desktop/src/components/StackView.svelte Composition root rewritten to use StackController + StackPanel/StackDetails.
apps/desktop/src/components/StackPanel.svelte New left-panel compound component (worktree changes, start commit, IRC row, branch list).
apps/desktop/src/components/StackDetails.svelte New right-panel/details compound component (commit/branch/worktree diff, codegen, IRC) + diff coordination.
apps/desktop/src/components/StackCodegen.svelte New wrapper around CodegenMessages plus MCP config modal.
apps/desktop/src/components/SnapshotCard.svelte Migrated snapshot file rendering to the new FileList compound components.
apps/desktop/src/components/NestedChangedFiles.svelte Migrated nested changed files to FileListProvider + FileListItems + FileListConflicts.
apps/desktop/src/components/IrcCommit.svelte Migrated IRC commit file list to the new FileList compound components.
apps/desktop/src/components/FileListProvider.svelte New context provider that instantiates and exposes FileListController.
apps/desktop/src/components/FileListItems.svelte New compound component rendering list/tree file entries with keyboard + click selection.
apps/desktop/src/components/FileListConflicts.svelte New compound component rendering unrepresented conflicts + resolve action.
apps/desktop/src/components/FileList.svelte Removed monolithic FileList implementation in favor of compound components.
apps/desktop/src/components/BranchList.svelte Updated to use getStackContext() instead of prop drilling, and to coordinate selection via controller.
apps/desktop/src/components/BranchCommitList.svelte Updated to use getStackContext() and to internalize commit actions previously passed via props.

You can also share your feedback on Copilot code review. Take the survey.

}

export class StackController {
private uiState;
Comment on lines 184 to 191
use:intersectionObserver={{
callback: (entry) => {
onVisible(!!entry?.isIntersecting);
},
options: {
threshold: 0.5,
root: lanesScrollableEl,
},
}}
Comment on lines +73 to +86
// Sync focus to the last-added selection item.
const currentSelection = $derived(this.idSelection.getById(this.getSelectionId()));
const lastAdded = $derived(currentSelection.lastAdded);
const lastAddedIndex = $derived(get(lastAdded)?.index);

$effect(() => {
if (lastAddedIndex !== undefined) {
untrack(() => {
if (this.active) {
this.focusManager.focusNthSibling(lastAddedIndex);
}
});
}
});
mtsgrd added 7 commits March 18, 2026 16:00
Split StackView into StackPanel, StackDetails, and StackCodegen with a
StackController that owns shared reactive state (selection, preview,
cross-panel coordination). Centralizes lifecycle concerns and reduces
prop drilling between panels.
Replace prop drilling of identity and UI state in BranchList and
BranchCommitList with getStackContext(). Move callback definitions
into the components that use them and remove dead conflict resolution UI.
Have CodegenMessages read identity and state from StackController
context and own its data queries (events, session, permissions,
attachments) and message sender. Simplifies StackCodegen to a thin
MCP modal wrapper.
Replace snapshot-based selection reads with reactive tracked properties
so templates and derived stores receive updates properly. Subscribe to
the active selection's lastAdded and the worktree-assigned lastAdded
stores using $effect, mapping their keys to SelectedFile via readKey and
storing results in private reactive fields. This removes the use of
get() for store snapshots and encapsulates uiState as a private field.
Add openProjectSettingsModal to stack controller

Introduce openProjectSettingsModal to set the project-settings modal
state from the stack controller. This provides a clear dropzone-friendly
handler for opening the project settings modal with an optional
selectedId and moves toward making UI_STATE usage explicit and reactive
in handlers.
Memoize details-open check to avoid unnecessary re-renders

Prevent the details panel binding from being treated as changed when
derived boolean state remains the same. By using a memoized derived
value (isDetailsOpen) instead of repeatedly reading
controller.isDetailsViewOpen, effects, class bindings, and keyboard
handlers no longer retrigger and unmount the bound stackViewEl element
when unrelated selection changes occur. This stabilizes the DOM and
avoids the element being unmounted on commit switches.
Replace references to "activeLastAdded" and related "assigned" naming
with focused/staged-oriented names to centralize file focus handling.
This updates StackDetails.svelte to read startIndex from
focusedFileStore, and refactors StackController to rename idSelection to
fileSelection and selected/assigned stores to focused/staged variants,
updating getters and helper methods accordingly. The change was needed
to make the UI use a consistent "focused file" concept for previewing
and multi-diff start positions, and to clarify/stabilize
selection-related APIs and variable names.
Inject the UI_STATE service into several components (BranchCommitList,
BranchList, StackCodegen, StackDetails) and use it directly when
constructing dropzone handlers and resolving lane state, instead of
accessing controller.uiState. This centralizes UI state access, making
dropzone construction independent of the controller and enabling
injecting UI state where needed. Also mark StackController.uiState as
private to reflect that components should use the injected UI_STATE
rather than the controller's property.
Inject UI_STATE into drop handlers and remove uiState args

Inject UI_STATE directly inside drop handler classes and update call
sites to stop passing uiState. This encapsulates UI state access within
the handlers, fixes type errors caused by exposing controller.uiState,
and simplifies constructor/signature usage across BranchCard,
BranchCommitList, StackDetails, and WorktreeChanges. Consumer call sites
no longer import or inject UI_STATE where unnecessary, and
createCommitDropHandlers no longer accepts uiState in its args.
…handler

Remove unused UI_STATE and UNCOMMITTED_SERVICE injections from
BranchList.svelte and instead inject them directly where needed in
dropHandler.ts. This cleans up unused imports and simplifies component
props by moving the service injections into StartCommitDzHandler,
ensuring the drop handler has direct access to uiState and
uncommittedService without passing them through the BranchList
component.
@mtsgrd mtsgrd force-pushed the refactor/filelist-controller branch from 438f1d4 to 255f1dc Compare March 18, 2026 14:45
Remove StackService/HooksService parameters from various components and
handlers and inject STACK_SERVICE (and HOOKS_SERVICE where needed)
directly within drop handler modules. This simplifies signatures and
usage by treating stack service as a singleton injectable rather than
passing it through many component props and handler constructors,
reducing prop drilling and aligning with the stack service being a
singleton.
Copilot AI review requested due to automatic review settings March 18, 2026 14:54
@mtsgrd mtsgrd force-pushed the refactor/filelist-controller branch from 255f1dc to bda9dee Compare March 18, 2026 14:54
Copy link
Contributor

Copilot AI left a 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 PR refactors the desktop app’s stack and file list UIs into compound components coordinated via reactive controller classes stored in Svelte context, reducing prop drilling and centralizing cross-panel coordination/state.

Changes:

  • Introduces StackController + getStackContext() and decomposes StackView into StackPanel, StackDetails, and StackCodegen.
  • Introduces FileListController + provider/children components (FileListProvider, FileListItems, FileListConflicts) and migrates call sites away from the monolithic FileList.svelte.
  • Updates drop handlers and codegen message composition to rely on injected services/context rather than prop drilling.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/desktop/src/lib/stack/stackController.svelte.ts New reactive controller + Svelte context helpers for StackView compound components.
apps/desktop/src/lib/selection/fileListController.svelte.ts New reactive controller + context helpers for file list compound components (selection, focus, keyboard nav).
apps/desktop/src/lib/commits/dropHandler.ts Refactors commit drop handlers to inject UI_STATE/services internally instead of receiving them via ctor/args.
apps/desktop/src/lib/branches/dropHandler.ts Refactors branch drop handlers to inject UI_STATE/services internally.
apps/desktop/src/components/codegen/CodegenMessages.svelte Switches to stack context/controller-driven IDs/state and moves message sending into MessageSender.
apps/desktop/src/components/WorktreeChanges.svelte Migrates to new file list compound components and adds extra keyboard handlers for AI actions.
apps/desktop/src/components/StackView.svelte Replaces monolithic implementation with controller + composed children (StackPanel/StackDetails).
apps/desktop/src/components/StackPanel.svelte New left-panel compound child (worktree changes, start commit, IRC row, branch list).
apps/desktop/src/components/StackDetails.svelte New details/preview compound child (commit/branch views, diffs, codegen, IRC).
apps/desktop/src/components/StackCodegen.svelte New wrapper handling MCP config modal + rendering CodegenMessages in details panel.
apps/desktop/src/components/SnapshotCard.svelte Migrates snapshot file list rendering to FileListProvider + FileListItems.
apps/desktop/src/components/NestedChangedFiles.svelte Migrates nested file list rendering to FileListProvider + FileListItems + FileListConflicts.
apps/desktop/src/components/IrcCommit.svelte Migrates IRC commit file list rendering to FileListProvider + FileListItems.
apps/desktop/src/components/FileListProvider.svelte New provider that instantiates FileListController and sets context.
apps/desktop/src/components/FileListItems.svelte New file list renderer (list/tree) using controller state, selection, keyboard handling.
apps/desktop/src/components/FileListConflicts.svelte New compound child rendering unrepresented conflicts + “Resolve conflicts” action.
apps/desktop/src/components/FileList.svelte Deletes the previous monolithic file list component.
apps/desktop/src/components/BranchList.svelte Removes prop drilling by sourcing lane/stack state from getStackContext().
apps/desktop/src/components/BranchInsertion.svelte Updates MoveBranch drop handler construction after handler refactor.
apps/desktop/src/components/BranchCommitList.svelte Removes prop drilling via stack context; updates commit interactions and drop handler creation.
apps/desktop/src/components/BranchCard.svelte Updates MoveCommit drop handler construction after handler refactor.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +126 to +151
onKeydown: (e) => {
// 1. Activation keys (Enter/Space/l)
if (controller.handleActivation(change, idx, e)) {
onselect?.(change, idx);
return;
}
// 2. Extra handlers (e.g. AI shortcuts)
if (extraKeyHandlers) {
for (const handler of extraKeyHandlers) {
if (handler(change, idx, e)) return;
}
}
// 3. Arrow/vim navigation
const navigatedIndex = controller.handleNavigation(e);
if (navigatedIndex !== undefined) {
const navigatedChange = controller.changes[navigatedIndex];
if (navigatedChange) onselect?.(navigatedChange, navigatedIndex);
}
},
focusable: true,
}}
onclick={(e) => {
e.stopPropagation();
controller.select(e, change, idx);
onselect?.(change, idx);
}}
Comment on lines +101 to +111
get selectedFileIds(): SelectedFile[] {
return this.idSelection.values(this.selectionId);
}

get selectedPaths(): Set<string> {
return new Set(this.selectedFileIds.map((f) => f.path));
}

get hasSelectionInList(): boolean {
return this.changes.some((change) => this.selectedPaths.has(change.path));
}
Comment on lines +57 to +66
selectedFilePath = filePath;
editPatchModal?.show();
}

function handleConfirmEditPatch() {
editPatchModal?.hide();
editPatch({
modeService,
commitId: ancestorMostConflictedCommitId!,
stackId: stackId!,
Convert computed getters for selected paths and list selection into
derived stores and replace the derived-lastAdded setup with a direct
subscription. This aligns selection state with reactive stores, avoids
creating intermediate derived getters, and ensures focus sync uses the
latest lastAdded value via subscribe so focus updates happen reliably
when active.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants