Skip to content

feat: Add existing folder as repository#405

Open
PureWeen wants to merge 3 commits intomainfrom
feature/add-existing-folder
Open

feat: Add existing folder as repository#405
PureWeen wants to merge 3 commits intomainfrom
feature/add-existing-folder

Conversation

@PureWeen
Copy link
Owner

Summary

Allows users to add an already-cloned local folder as a managed repository in PolyPilot. Once added, the folder gets full feature parity with repos cloned through the app: worktree creation, branch checkout, PR checkout, squad file discovery, and sessions.

How it works

The '+ Repo' button in the sidebar now shows two tabs on desktop:

  • 🔗 URL — existing flow (clone by GitHub URL/shorthand)
  • 📂 Existing Folder — new flow (pick a local folder that's already cloned)

For the folder flow, the user either types a path or clicks to open the native folder picker. PolyPilot then:

  1. Validates the folder exists
  2. Confirms it's a git repository (git rev-parse --git-dir)
  3. Reads the origin remote URL
  4. Runs the normal bare-clone/fetch pipeline using that URL (same as URL mode)

The result is a RepositoryInfo entry in repos.json and a bare clone in ~/.polypilot/repos/ — identical to any other managed repo.

Changes

  • RepoManager.AddRepositoryFromLocalAsync — enhanced with validation (folder exists, is git repo, has origin remote), progress callback, and clear error messages
  • CopilotService.AddRepoFromLocalFolderAsync — new public method, local mode only
  • SessionSidebar.razor — URL/Folder mode tabs, folder path input + browse button (platform-gated), CloseAddRepoForm helper
  • SessionSidebar.razor.css — styles for tabs, folder row, browse button
  • Tests — 3 new AddRepositoryFromLocal_* tests covering non-existent folder, non-git folder, and git repo with no origin remote

@PureWeen
Copy link
Owner Author

🔍 Multi-Model Consensus Review — PR #405 (Round 1)

PR: feat: Add existing folder as repository
CI Status: ⚠️ No CI checks configured on this branch
Models: claude-opus-4.6 ×2, claude-sonnet-4.6, gemini-3-pro-preview, gpt-5.3-codex


Findings (consensus ≥ 2 models)

🟡 F1: RunProcess test helper — race condition (RepoManagerTests.cs:69-75)
Process.Start() is called on line 69, but p.Exited is subscribed on line 70 and p.EnableRaisingEvents = true is set on line 75. If git init completes between Start() and EnableRaisingEvents = true, the Exited event never fires and the TaskCompletionSource blocks forever — no timeout, no cancellation token. Fix: set EnableRaisingEvents = true and subscribe Exited before Start(), or use await p.WaitForExitAsync(ct).

🟡 F2: Bare catch swallows OperationCanceledException (RepoManager.cs, AddRepositoryFromLocalAsync)

catch { remoteUrl = ""; }

RunGitAsync throws InvalidOperationException on non-zero exit, but if the user cancels via CancellationToken, OperationCanceledException is also caught. The caller then throws a misleading "No 'origin' remote found" instead of propagating cancellation. Fix: catch (Exception ex) when (ex is not OperationCanceledException).

🟡 F3: Same bare catch pattern in IsGitRepositoryAsync (RepoManager.cs)

catch { return false; }

A cancelled git rev-parse reports "not a git repository" instead of propagating cancellation. Same fix as F2.

🟢 F4: Fragile state reset on form open (SessionSidebar.razor:341)
showAddRepo = true inline handler does not reset addRepoFolderMode or addRepoFolderPath. All current close paths go through CloseAddRepoForm() which resets them, but any future close path that sets showAddRepo = false directly would leak stale folder-mode state. Low risk — all current paths are correct.


✅ Positive Notes

  • AddRepoFromLocalFolderAsync in CopilotService.Bridge.cs correctly guards remote mode, follows GetOrCreateRepoGroup pattern, and state persistence is handled (save + notify)
  • AddRepositoryAsync already handles duplicate repos gracefully (returns existing)
  • CloseAddRepoForm() properly resets all new state variables
  • Test coverage for error paths (non-existent folder, no git, no origin) is good

Test Coverage

  • ✅ Three failure-mode tests added (NonExistentFolder, NoGit, NoOrigin)
  • ⚠️ No success-path test for AddRepositoryFromLocalAsync (bare clone from local)
  • ⚠️ No test for AddRepoFromLocalFolderAsync remote-mode guard (InvalidOperationException)

Verdict: ⚠️ Request Changes

Required: Fix F1 (test hang risk) and F2/F3 (cancellation swallowing)
Nice-to-have: F4 state reset, success-path test

PureWeen added a commit that referenced this pull request Mar 18, 2026
F1 (test race): RunProcess now sets EnableRaisingEvents=true and subscribes
Exited BEFORE Process.Start(), eliminating the window where a fast process
(like 'git init') exits before the event handler is registered.

F2 (bare catch swallows cancellation): AddRepositoryFromLocalAsync now uses
'catch (Exception ex) when (ex is not OperationCanceledException)' so
cancellation propagates instead of yielding a misleading 'No origin remote' error.

F3 (bare catch in IsGitRepositoryAsync): Added explicit re-throw for
OperationCanceledException so cancellation propagates from the git rev-parse
call instead of returning 'false' (not a git repo).

F4 (stale form state on re-open): Replaced inline 'showAddRepo = true' handler
with OpenAddRepoForm() method that resets addRepoFolderMode, addRepoFolderPath,
addRepoError, confirmRepoReplace, confirmRepoName, and newRepoUrl on every open.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen added a commit that referenced this pull request Mar 18, 2026
F1 (test race): RunProcess now sets EnableRaisingEvents=true and subscribes
Exited BEFORE Process.Start(), eliminating the window where a fast process
(like 'git init') exits before the event handler is registered.

F2 (bare catch swallows cancellation): AddRepositoryFromLocalAsync now uses
'catch (Exception ex) when (ex is not OperationCanceledException)' so
cancellation propagates instead of yielding a misleading 'No origin remote' error.

F3 (bare catch in IsGitRepositoryAsync): Added explicit re-throw for
OperationCanceledException so cancellation propagates from the git rev-parse
call instead of returning 'false' (not a git repo).

F4 (stale form state on re-open): Replaced inline 'showAddRepo = true' handler
with OpenAddRepoForm() method that resets addRepoFolderMode, addRepoFolderPath,
addRepoError, confirmRepoReplace, confirmRepoName, and newRepoUrl on every open.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen force-pushed the feature/add-existing-folder branch from 775ea1b to c4e2e56 Compare March 18, 2026 15:13
PureWeen added a commit that referenced this pull request Mar 18, 2026
Replace `Dictionary` with `ConcurrentDictionary` for `_cache` in
ExternalSessionScanner. The cache is read/written from a Timer callback
(ThreadPool thread) in `Scan()`, and `Dictionary` is not thread-safe for
concurrent operations.

Found during PR #405 code review — the scanner code is from PR #370.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen and others added 2 commits March 18, 2026 12:06
Skip dead event-stream disk fallback when SessionId is empty to avoid
reading from an invalid path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allows users to add an already-cloned local folder as a managed repository
in PolyPilot, giving it full feature parity with repos cloned through the app
(worktrees, branch creation, PR checkout, squad discovery, sessions, etc.).

## RepoManager.AddRepositoryFromLocalAsync (enhanced)
- Added Directory.Exists check with clear error message
- Added IsGitRepositoryAsync helper (runs git rev-parse --git-dir)
- Added origin-remote check with actionable error message
- Added Action<string>? onProgress parameter forwarded to the clone step
- All three validations throw InvalidOperationException with user-friendly messages

## CopilotService.AddRepoFromLocalFolderAsync (new)
- Public surface that delegates to RepoManager then creates the repo group
- Desktop (local) mode only — throws clearly when connected to a remote server

## SessionSidebar UI (updated)
- '+ Repo' form now shows URL / Existing Folder tabs on desktop
- Folder tab: path text input + Browse (…) button that opens FolderPickerService
- Browse button is platform-gated (#if MACCATALYST || WINDOWS)
- CloseAddRepoForm resets both modes; AddRepositoryFromFolder wires progress display
- CSS: .add-repo-mode-tabs, .add-repo-tab, .add-repo-folder-row, .btn-repo-browse

## Tests (3 new in RepoManagerTests)
- AddRepositoryFromLocal_NonExistentFolder_ThrowsWithClearMessage
- AddRepositoryFromLocal_FolderWithNoGit_ThrowsWithClearMessage
- AddRepositoryFromLocal_GitRepoWithNoOrigin_ThrowsWithClearMessage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen force-pushed the feature/add-existing-folder branch from 0760d4d to 76ac48e Compare March 18, 2026 17:06
…into existing repo)

Root cause of ongoing confusion: when the user added ~/Projects/maui3, the code
extracted its origin (https://github.com/dotnet/maui) and merged it into the
existing 'maui' group. The user saw no visible change.

The user's expectation: adding a local folder should create a NEW, DISTINCT entry
in the sidebar -- like '📁 maui3' -- that they can see and use.

Fix: complete redesign of the local-folder flow:

1. SessionGroup.LocalPath (new field): marks a group as a 'pinned local folder'.
   IsLocalFolder computed property. Not linked to a PolyPilot bare clone.

2. GetOrCreateLocalFolderGroup() (new): creates a distinct group named after the
   folder (e.g. 'maui3'), idempotent by path. Un-collapses if already exists.

3. AddRepoFromLocalFolderAsync: now does path normalization (including ~ expansion)
   itself, validates the folder, ALSO registers the bare clone (for worktree support),
   then creates a LOCAL FOLDER GROUP -- not the existing repo group.

4. Sidebar rendering: local-folder groups show a 📁 icon, have their own '...' menu
   with 'New Session Here' (creates CWD=localPath session) and 'Remove Folder'.

5. QuickCreateSessionForFolder: creates a timestamped session in the local folder
   group, with working directory set to the folder path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant