Decompose StackView and FileList into compound components#12902
Decompose StackView and FileList into compound components#12902
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
68c88bd to
438f1d4
Compare
There was a problem hiding this comment.
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
StackControllerandFileListControllerto centralize state/behavior and provide it via Svelte context. - Decomposes
StackViewintoStackPanel,StackDetails, andStackCodegen, and decomposes the oldFileListintoFileListProvider,FileListItems, andFileListConflicts. - Moves Codegen data querying + message sending responsibilities into
CodegenMessages, leavingStackCodegenas 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; |
| use:intersectionObserver={{ | ||
| callback: (entry) => { | ||
| onVisible(!!entry?.isIntersecting); | ||
| }, | ||
| options: { | ||
| threshold: 0.5, | ||
| root: lanesScrollableEl, | ||
| }, | ||
| }} |
| // 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); | ||
| } | ||
| }); | ||
| } | ||
| }); |
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.
438f1d4 to
255f1dc
Compare
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.
255f1dc to
bda9dee
Compare
There was a problem hiding this comment.
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 decomposesStackViewintoStackPanel,StackDetails, andStackCodegen. - Introduces
FileListController+ provider/children components (FileListProvider,FileListItems,FileListConflicts) and migrates call sites away from the monolithicFileList.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.
| 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); | ||
| }} |
| 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)); | ||
| } |
| 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.
Uh oh!
There was an error while loading. Please reload this page.