docs: add repo import list helper design spec
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
1. **Help menu** — the title-bar dropdown in `MainWindow.axaml` that contains `About…`,
|
||||||
|
`Worktrees…`, etc. Add a new `MenuItem` `Add repos as lists…` wired to a command on
|
||||||
|
`MainWindowViewModel`.
|
||||||
|
2. **Lists island** — a small folder icon button beside the existing `+ New list` button in
|
||||||
|
`ListsIslandView.axaml`, wired to a command on `ListsIslandViewModel`.
|
||||||
|
|
||||||
|
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).
|
||||||
|
- `.git` may 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' `WorkingDir` values (for the
|
||||||
|
"already added" check) and create new `ListEntity` rows. Same dependency
|
||||||
|
`ListsIslandViewModel` already 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 with `RepoScanner`, appends new candidates. De-dupes by full path
|
||||||
|
(case-insensitive) against rows already present.
|
||||||
|
- `CreateAsync` — for each ticked, non-existing row, create a `ListEntity` via
|
||||||
|
`ListRepository.AddAsync` (Name = folder name, WorkingDir = full path,
|
||||||
|
DefaultCommitType = `CommitTypeRegistry.DefaultType`, fresh `Guid` id, `CreatedAt` = now).
|
||||||
|
Then `CloseAction()`.
|
||||||
|
- `Cancel` — `CloseAction()`.
|
||||||
|
|
||||||
|
On load, fetch all existing lists once and capture their `WorkingDir`s 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]) — defaults `true` for new repos. For already-added rows it
|
||||||
|
is forced `true` and the checkbox is disabled.
|
||||||
|
- `CanToggle` => `!AlreadyAdded` (binds to checkbox `IsEnabled`).
|
||||||
|
|
||||||
|
### `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 `ItemsControl` over `Repos`. Each row = `CheckBox` (IsChecked two-way,
|
||||||
|
IsEnabled = `CanToggle`) + repo name + dim full path + `(already added)` label when
|
||||||
|
`AlreadyAdded`.
|
||||||
|
- **Footer:** `Create {CreateCount} lists` button (disabled when `CreateCount == 0`) + `Cancel`.
|
||||||
|
- Folder picker lives in the code-behind (mirrors `ListSettingsModalView.BrowseClicked`):
|
||||||
|
`OpenFolderPickerAsync` with `AllowMultiple = true`, results handed to the VM's
|
||||||
|
`AddFolderAsync`.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. User opens the modal from either entry point → modal loads existing lists' `WorkingDir`s.
|
||||||
|
2. User clicks `Add folder…` → picks one or more parent folders → `RepoScanner` finds repos →
|
||||||
|
rows appended (de-duped), already-added rows shown ticked+disabled.
|
||||||
|
3. User adjusts ticks → clicks `Create N lists`.
|
||||||
|
4. VM creates one `ListEntity` per ticked-new row via `ListRepository`.
|
||||||
|
5. Modal closes → the **caller reloads the Lists island** so new lists appear:
|
||||||
|
- Lists-island entry point: `ListsIslandViewModel.LoadAsync()`.
|
||||||
|
- Help-menu entry point: `MainWindowViewModel` reloads its `Lists` (the
|
||||||
|
`ListsIslandViewModel` instance) after the modal closes.
|
||||||
|
|
||||||
|
## DI / Wiring
|
||||||
|
|
||||||
|
- Register `RepoImportModalViewModel` (transient) alongside other modal VMs.
|
||||||
|
- Register `RepoScanner` if implemented as an injected service; a static helper needs no
|
||||||
|
registration.
|
||||||
|
- `ListsIslandViewModel` gains `Func<RepoImportModalViewModel, Task>? ShowRepoImportModal` and
|
||||||
|
an `OpenRepoImportCommand`, wired in `ListsIslandView.axaml.cs` (mirrors
|
||||||
|
`ShowListSettingsModal`).
|
||||||
|
- `MainWindowViewModel` gains the same `Func` + an `OpenRepoImportCommand`, wired in
|
||||||
|
`MainWindow.axaml.cs`.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Unreadable / missing folders: `RepoScanner` returns 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
|
||||||
|
|
||||||
|
- `RepoScanner` unit tests (the testable seam): a temp directory tree with a mix of git repos
|
||||||
|
(`.git` dir), 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 `CreateCount` can 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).
|
||||||
Reference in New Issue
Block a user