What's wrong
The Clone button (ProjectDirector/ProjectDirector.cs, ~lines 707–712) does this:
Task.Run(() => QueueGitLog(..., GitCli.Run("clone", ...)))
.ContinueWith((t) => RefreshPage(), new CancellationToken(),
TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Current);
The app installs no SynchronizationContext, so TaskScheduler.Current at that point is the default thread-pool scheduler. With ExecuteSynchronously, RefreshPage() then runs on the worker thread that did the clone. From that thread it:
- mutates
Options.ClonedRepos (UpdateClonedStatus);
- enumerates
Options.Repos (PairSiblings);
- clears and replaces
BrowserContentsBase (SwitchRepoBrowserPath);
- writes
Options.BaseRepo and related state.
Meanwhile the render thread is enumerating BrowserContentsBase and calling ClonedRepos.ContainsKey/ContainsValue every frame, and the debounced save serialises Options.
Failure scenario
The user clicks Clone, and the clone finishes while a frame is rendering the repo browser. The render thread can then throw InvalidOperationException: Collection was modified, or the non-thread-safe Dictionary can be corrupted by concurrent read and write. Nothing guards against a clone already being in flight either, so clicking Clone again while the first is running starts a duplicate git clone into the same folder.
(Traced from the code; it's a race, so it can't be reproduced deterministically.)
Suggested fix
Do all UI and state mutation on the render thread. In the continuation, set a flag or enqueue a request, then call RefreshPage() from Tick, the same way other background results such as OwnerPendingTokenPopup are handed over. Also track the in-flight clone per repo and disable the button while it runs.
What's wrong
The Clone button (
ProjectDirector/ProjectDirector.cs, ~lines 707–712) does this:The app installs no
SynchronizationContext, soTaskScheduler.Currentat that point is the default thread-pool scheduler. WithExecuteSynchronously,RefreshPage()then runs on the worker thread that did the clone. From that thread it:Options.ClonedRepos(UpdateClonedStatus);Options.Repos(PairSiblings);BrowserContentsBase(SwitchRepoBrowserPath);Options.BaseRepoand related state.Meanwhile the render thread is enumerating
BrowserContentsBaseand callingClonedRepos.ContainsKey/ContainsValueevery frame, and the debounced save serialisesOptions.Failure scenario
The user clicks Clone, and the clone finishes while a frame is rendering the repo browser. The render thread can then throw
InvalidOperationException: Collection was modified, or the non-thread-safeDictionarycan be corrupted by concurrent read and write. Nothing guards against a clone already being in flight either, so clicking Clone again while the first is running starts a duplicategit cloneinto the same folder.(Traced from the code; it's a race, so it can't be reproduced deterministically.)
Suggested fix
Do all UI and state mutation on the render thread. In the continuation, set a flag or enqueue a request, then call
RefreshPage()fromTick, the same way other background results such asOwnerPendingTokenPopupare handed over. Also track the in-flight clone per repo and disable the button while it runs.