6.2 KiB
Repo Import List Helper — Design
Date: 2026-05-29 Status: Approved (pending spec review)
Problem
Creating lists is one-at-a-time: click + New list, then open List Settings to set the
working directory. Users with many repos under a few parent folders want to wire them all up
in one pass.
Goal
A "list helper" that scans one or more parent folders for git repos, presents them as a
checklist, and bulk-creates a list (with WorkingDir pre-filled) for each ticked repo.
Entry Points
- Help menu — the title-bar dropdown in
MainWindow.axamlthat containsAbout…,Worktrees…, etc. Add a newMenuItemAdd repos as lists…wired to a command onMainWindowViewModel. - Lists island — a small folder icon button beside the existing
+ New listbutton inListsIslandView.axaml, wired to a command onListsIslandViewModel.
Both open the same modal.
Components
RepoScanner (new, ClaudeDo.Ui/Services or ClaudeDo.Data)
Pure filesystem helper, no git library. Given a parent folder path, enumerates immediate
subdirectories and returns those that contain a .git entry (directory or file). Kept
separate from the VM so it is unit-testable.
IReadOnlyList<RepoCandidate> Scan(string parentFolder)
record RepoCandidate(string Name, string FullPath)
- Skips the parent itself; only immediate children are considered (non-recursive).
.gitmay be a directory (normal repo) or a file (worktree/submodule) — both count.- Returns empty on missing/unreadable folder rather than throwing.
RepoImportModalViewModel (new, ClaudeDo.Ui/ViewModels/Modals)
Follows the existing modal-VM pattern (CloseAction, resolved from DI).
Dependencies:
IDbContextFactory<ClaudeDoDbContext>— load existing lists'WorkingDirvalues (for the "already added" check) and create newListEntityrows. Same dependencyListsIslandViewModelalready uses.
State:
ObservableCollection<RepoImportItemViewModel> Repos— the combined checklist.- A set of parent folder paths already scanned (to de-dupe re-adds).
CreateCount— computed count of ticked-and-new rows (drives the confirm button label).
Commands:
AddFolderAsync— invokes the folder picker (via view code-behind callback, see below), scans each chosen folder withRepoScanner, appends new candidates. De-dupes by full path (case-insensitive) against rows already present.CreateAsync— for each ticked, non-existing row, create aListEntityviaListRepository.AddAsync(Name = folder name, WorkingDir = full path, DefaultCommitType =CommitTypeRegistry.DefaultType, freshGuidid,CreatedAt= now). ThenCloseAction().Cancel—CloseAction().
On load, fetch all existing lists once and capture their WorkingDirs into a case-insensitive
set; each appended candidate whose path is in that set is marked AlreadyAdded.
RepoImportItemViewModel (new)
Name,FullPath(display).AlreadyAdded(bool) — true if a list already points at this path.IsChecked([ObservableProperty]) — defaultstruefor new repos. For already-added rows it is forcedtrueand the checkbox is disabled.CanToggle=>!AlreadyAdded(binds to checkboxIsEnabled).
RepoImportModalView (new, ClaudeDo.Ui/Views/Modals)
A Window styled like the other modals (header bar, body, footer), shown via
ShowDialog(owner).
- Header: title
ADD REPOS AS LISTS+ close button. - Top of body:
Add folder…button. - Body: scrollable
ItemsControloverRepos. Each row =CheckBox(IsChecked two-way, IsEnabled =CanToggle) + repo name + dim full path +(already added)label whenAlreadyAdded. - Footer:
Create {CreateCount} listsbutton (disabled whenCreateCount == 0) +Cancel. - Folder picker lives in the code-behind (mirrors
ListSettingsModalView.BrowseClicked):OpenFolderPickerAsyncwithAllowMultiple = true, results handed to the VM'sAddFolderAsync.
Data Flow
- User opens the modal from either entry point → modal loads existing lists'
WorkingDirs. - User clicks
Add folder…→ picks one or more parent folders →RepoScannerfinds repos → rows appended (de-duped), already-added rows shown ticked+disabled. - User adjusts ticks → clicks
Create N lists. - VM creates one
ListEntityper ticked-new row viaListRepository. - Modal closes → the caller reloads the Lists island so new lists appear:
- Lists-island entry point:
ListsIslandViewModel.LoadAsync(). - Help-menu entry point:
MainWindowViewModelreloads itsLists(theListsIslandViewModelinstance) after the modal closes.
- Lists-island entry point:
DI / Wiring
- Register
RepoImportModalViewModel(transient) alongside other modal VMs. - Register
RepoScannerif implemented as an injected service; a static helper needs no registration. ListsIslandViewModelgainsFunc<RepoImportModalViewModel, Task>? ShowRepoImportModaland anOpenRepoImportCommand, wired inListsIslandView.axaml.cs(mirrorsShowListSettingsModal).MainWindowViewModelgains the sameFunc+ anOpenRepoImportCommand, wired inMainWindow.axaml.cs.
Error Handling
- Unreadable / missing folders:
RepoScannerreturns empty, no crash. - Re-adding a folder already scanned: de-duped by path, no duplicate rows.
- Two ticked repos sharing a folder name: both created (list names are not unique) — acceptable.
- List creation failure (rare): best-effort per the existing pattern; do not block remaining creations.
Testing
RepoScannerunit tests (the testable seam): a temp directory tree with a mix of git repos (.gitdir), a.git-file repo, plain folders, and an empty/missing parent. Assert only the repo subfolders are returned and missing folders yield empty.- VM-level "already added" logic and
CreateCountcan be exercised if a test seam is convenient, but the filesystem scanner is the primary unit under test. UI wiring verified manually.
Out of Scope (YAGNI)
- Recursive / deep scanning.
- Inline editing of the list name before creation.
- Setting model / system prompt / agent during import (tuned later per-list in List Settings).
- Picking repo folders directly (only parent-folder scan, per decision).