Add worktree labels to the UI and worktree switching to the dialog - #24
Conversation
DiffViewer already opened linked worktrees correctly, but nothing on screen distinguished one from another. Recent-context rows were titled from the path leaf, which for a worktree is usually the branch name and never names the repository, so two worktrees of one repo rendered near-identically. Adds a libgit2-free GitWorktreeLayout helper that resolves the commondir pointer, plus IGitWorktreeEnumerator for listing every worktree attached to a repository. Two details are non-obvious: libgit2 omits the main worktree from its worktree list, so it is synthesized from commondir; and a pruned worktree is still listed but throws on every property access, so names come from the administrative directory instead. RepositoryShape now carries CommonGitDirectory and WorktreeName, and recent rows persist the repository and worktree names captured at launch time. Rows written by earlier versions load with null labels and fall back to the old path-leaf rendering, healing on next launch. AI-Local-Session: 80032277-bc2c-4613-a0d3-72ad20d3621b AI-Cloud-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64
Pointing a diff at a different worktree meant remembering where `git worktree add` put it and typing the path. Both the repo-path row in the New diff dialog and the context bar now offer a worktree list: main first, each entry showing its checked-out branch and path, with pruned worktrees listed as unavailable rather than silently dropped. The context-bar switcher reuses WorktreePickerViewModel with a write-back that runs an in-place context switch instead of editing a text box, carrying both diff sides over unchanged. The picker is offered on every local form, not just working-tree ones. Two worktrees share an object database and all shared refs, so `main..feature` resolves identically from either - but HEAD is per-worktree, and the dialog advertises HEAD~3 as valid input, so the choice changes the answer for commit-ish forms too. Extracts LocalRepoFormViewModelBase along the way. The five local forms each carried their own copy of the repo path, its canonicalization, and the deferred-error handling - three of them verbatim identical - and the worktree picker would have been a sixth copy of the same pattern. The repo-path row in NewDiffDialog.xaml was duplicated five times likewise, and is now one RepoPathField control, which also retires the Browse handler's Tag="RepoPath" visual-tree walk in favour of a named element. AI-Local-Session: 80032277-bc2c-4613-a0d3-72ad20d3621b AI-Cloud-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64
CommonGitDirectory is documented as identical across every worktree of a repository, which is what makes "same repo, different checkout" recognizable. It wasn't: libgit2 reports a main worktree's git directory with a trailing separator, while the commondir pointer resolves through Path.GetFullPath without one, so the two forms compared unequal. No consumer relied on it yet, so this is a trap rather than a live bug. The repository-service test now compares the raw strings instead of normalizing both sides, which is what let the gap through. AI-Local-Session: 80032277-bc2c-4613-a0d3-72ad20d3621b AI-Cloud-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64
There was a problem hiding this comment.
🟡 Changes recommended
There are a couple of correctness/documentation issues in new worktree-related helpers that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds first-class Git worktree awareness to DiffViewer’s UI by persisting stable worktree/repository labels for recents, appending worktree names to the window title, and enabling switching the current diff or new-diff dialog to a different checkout via a shared worktree picker control and view-model.
Changes:
- Persist repository/worktree display labels into recents and surface them in the recents dropdown and window title
- Add a reusable Worktree picker popup and integrate it into the New Diff dialog and the context bar
- Extract local-repo-path handling into
LocalRepoFormViewModelBaseand introduce a LibGit2-based worktree enumerator plus test coverage
File summaries
| File | Description |
|---|---|
| DiffViewer/Views/WorktreePicker.xaml.cs | Adds code-behind wiring for the worktree picker popup |
| DiffViewer/Views/WorktreePicker.xaml | Defines the worktree picker popup UI and row styling |
| DiffViewer/Views/RepoPathField.xaml.cs | Adds browse-folder behavior for the shared repo-path field control |
| DiffViewer/Views/RepoPathField.xaml | Introduces reusable repo-path field with Browse and Worktree toggle |
| DiffViewer/Views/RecentsBarView.xaml | Adds a context-bar Worktree switch toggle and embeds WorktreePicker |
| DiffViewer/Views/NewDiffDialog.xaml.cs | Removes repo-path browse logic now handled by RepoPathField |
| DiffViewer/Views/NewDiffDialog.xaml | Replaces repeated repo-path grids with the shared RepoPathField control |
| DiffViewer/ViewModels/WorktreePickerViewModel.cs | Adds VM to enumerate/pick worktrees and write back selected path |
| DiffViewer/ViewModels/WorkingTreeVsHeadFormViewModel.cs | Moves to LocalRepoFormViewModelBase for shared repo-path + picker support |
| DiffViewer/ViewModels/WorkingTreeVsCommitFormViewModel.cs | Moves to LocalRepoFormViewModelBase and re-points ref picker on repo resolution |
| DiffViewer/ViewModels/ViewStashFormViewModel.cs | Moves to LocalRepoFormViewModelBase and keeps stash enumeration behavior |
| DiffViewer/ViewModels/RecentContextsViewModel.cs | Adds worktree switcher integration via WorktreePickerViewModel |
| DiffViewer/ViewModels/NewDiffDialogViewModel.cs | Wires IGitWorktreeEnumerator into form dependencies |
| DiffViewer/ViewModels/MainViewModel.cs | Builds window title with optional worktree name; wires worktree switching |
| DiffViewer/ViewModels/LocalRepoFormViewModelBase.cs | Extracts local repo-path canonicalization, validation deferral, and worktree picker |
| DiffViewer/ViewModels/CommitVsCommitFormViewModel.cs | Moves to LocalRepoFormViewModelBase and syncs both ref pickers on repo resolution |
| DiffViewer/ViewModels/BranchVsMergeBaseFormViewModel.cs | Moves to LocalRepoFormViewModelBase and keeps merge-base seeding behavior |
| DiffViewer/Utility/GitWorktreeLayout.cs | Adds libgit2-free worktree layout reasoning and label derivation |
| DiffViewer/Services/RepositoryService.cs | Extends RepositoryShape with common-git-dir + worktree name data |
| DiffViewer/Services/RecentsJsonSerializer.cs | Serializes/deserializes repository/worktree label metadata for recents |
| DiffViewer/Services/RecentContextsService.cs | Captures worktree labels at record time via GitWorktreeLayout |
| DiffViewer/Services/NewDiffDialogHost.cs | Supplies worktree enumerator to the New Diff dialog VM |
| DiffViewer/Services/LibGit2GitWorktreeEnumerator.cs | Implements worktree enumeration with libgit2 quirks handled |
| DiffViewer/Services/IGitWorktreeEnumerator.cs | Introduces worktree enumeration interface and WorktreeEntry model |
| DiffViewer/Services/IDiffModeProvider.cs | Adds worktree enumerator to FormDependencies |
| DiffViewer/Models/RepositoryShape.cs | Adds CommonGitDirectory and WorktreeName and computed IsLinkedWorktree |
| DiffViewer/Models/RecentLaunchContext.cs | Adds repository/worktree label fields to recent context rows |
| DiffViewer/CompositionRoot.cs | Wires worktree enumerator into MainViewModel construction |
| DiffViewer/AppServices.cs | Adds optional WorktreeEnumerator to app service bundle |
| DiffViewer/App.xaml.cs | Instantiates and wires the libgit2 worktree enumerator into services |
| DiffViewer.Tests/ViewModels/WorktreePickerViewModelTests.cs | Adds unit tests for worktree picker VM behavior |
| DiffViewer.Tests/ViewModels/ViewStashFormViewModelTests.cs | Updates dependencies to include a stub worktree enumerator |
| DiffViewer.Tests/ViewModels/NewDiffFormViewModelTests.cs | Updates dependencies to include a stub worktree enumerator |
| DiffViewer.Tests/ViewModels/NewDiffDialogViewModelTests.cs | Updates VM construction for required worktree enumerator |
| DiffViewer.Tests/ViewModels/MainViewModelWindowTitleTests.cs | Adds tests for window title formatting with and without worktree name |
| DiffViewer.Tests/ViewModels/MainViewModelKeyboardShortcutTests.cs | Updates RepositoryShape construction for new required fields |
| DiffViewer.Tests/ViewModels/MainViewModelContextMenuTests.cs | Updates RepositoryShape construction for new required fields |
| DiffViewer.Tests/ViewModels/MainViewModelCommitMetadataTests.cs | Updates RepositoryShape construction for new required fields |
| DiffViewer.Tests/ViewModels/DiffPaneViewModelTests.cs | Updates RepositoryShape construction for new required fields |
| DiffViewer.Tests/ViewModels/BranchVsMergeBaseFormViewModelTests.cs | Updates dependencies to include a stub worktree enumerator |
| DiffViewer.Tests/Utility/GitWorktreeLayoutTests.cs | Adds tests for pure worktree layout reasoning and label derivation |
| DiffViewer.Tests/StubWorktreeEnumerator.cs | Adds test double for worktree enumeration |
| DiffViewer.Tests/Services/TempRepo.cs | Adds helpers for creating and cleaning up real linked worktrees in tests |
| DiffViewer.Tests/Services/RepositoryServiceTests.cs | Adds coverage for shape worktree name and common-git-dir invariants |
| DiffViewer.Tests/Services/PreDiffPassTests.cs | Updates RepositoryShape construction for new required fields |
| DiffViewer.Tests/Services/NewDiffDialogHostTests.cs | Updates host construction for required worktree enumerator |
| DiffViewer.Tests/Services/LibGit2GitWorktreeEnumeratorTests.cs | Adds behavioral tests for main and prunable worktrees enumeration |
| DiffViewer.Tests/RecentContexts/RecentsJsonSerializerTests.cs | Adds tests for label round-trip and backward compatibility |
| DiffViewer.Tests/RecentContexts/RecentContextsViewModelWorktreeTests.cs | Adds tests for context-bar worktree switching behavior |
| DiffViewer.Tests/RecentContexts/RecentContextItemTests.cs | Adds tests for new title formatting with repository/worktree labels |
| CHANGELOG.md | Documents worktree legibility and switching under Unreleased |
Review details
- Files reviewed: 51/51 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ResolveCommonDirectory normalized every success path but returned the raw string from its catch block, which reintroduces exactly the trailing-separator mismatch the method exists to eliminate: a probing failure would silently make two worktrees of one repository compare as unrelated. Normalize is now best-effort itself, so the failure path can use it without risking a throw from a helper documented to degrade. Also corrects the WorktreePicker comment that claimed EnsureLoadedAsync swallows enumerator failures. It does not - it has try/finally, not try/catch, and relies on the enumerator returning an empty list. The view-level handler is the actual safety net, being the async void boundary where an unexpected exception would tear the dialog down. Both found by Copilot code review on PR #24. AI-Local-Session: 80032277-bc2c-4613-a0d3-72ad20d3621b AI-Cloud-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64
There was a problem hiding this comment.
🟡 Changes recommended
Worktree picker loading, bare-repository detection, and popup scrolling issues remain, and label persistence lacks service-level coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
DiffViewer/ViewModels/WorktreePickerViewModel.cs:84
- This assumes every enumeration contains a main entry, but the enumerator intentionally omits it for a bare hub. When a bare repository has exactly one linked worktree, that row is an available alternative yet this returns false and the popup reports that no other worktrees exist. Base the result on whether any entry is not current instead of the total count.
DiffViewer/Views/WorktreePicker.xaml:60 - A vertical
StackPanelmeasures thisScrollViewerwith unbounded height, so the border'sMaxHeightdoes not establish a viewport and the scrollbar will not make lower rows reachable when many worktrees exist. Use a constrainedGridrow for the list or give theScrollVieweran explicit maximum height.
DiffViewer/Views/WorktreePicker.xaml:104 HasAlternativeWorktreesis false before the asynchronous load finishes, so this empty-state message is visible at the same time as “Loading…”. Gate it onIsLoadedas well, so users are not told that no alternatives exist before enumeration has produced a result.
- Files reviewed: 51/51 changed files
- Comments generated: 2
- Review effort level: Balanced
Five issues from the second Copilot review pass, all confirmed against the code: EnsureLoadedAsync dropped a stale result and returned. Because the IsLoading guard turns away any caller arriving mid-load, a repo-path change during enumeration could leave an already-open popup empty with nothing left to trigger a reload. It now loops and re-enumerates the new path, which is what the turned-away caller was relying on. HasAlternativeWorktrees counted rows, but the enumerator deliberately omits the main worktree for a bare hub, so a single linked worktree read as "no alternatives". It now asks whether any entry is not the current one. A listed-but-unreachable pruned worktree still counts, so the popup never claims there is nowhere to go while visibly listing somewhere. The empty-state message was bound to the negation of that flag, so it rendered alongside "Loading..." asserting an answer enumeration had not produced yet. Added ShowsEmptyState, gated on IsLoaded. The popup body was a StackPanel, which measures children with unbounded height - the ScrollViewer never established a viewport, so the Border's MaxHeight clipped lower rows out of reach entirely. Now a Grid with a star-sized list row, matching RefPicker. RecordLaunchAsync captures the worktree labels but had no service-level coverage; the existing tests use nonexistent paths. Added tests over a synthetic on-disk layout covering main and linked worktrees, disk round-trip, healing of a legacy unlabeled row, and the non-repository path. AI-Local-Session: 80032277-bc2c-4613-a0d3-72ad20d3621b AI-Cloud-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64
There was a problem hiding this comment.
🔵 Needs a closer look
Synchronous filesystem probing can block the WPF UI thread during recent-context recording.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
DiffViewer/Services/RecentContextsService.cs:92
- This label lookup performs synchronous filesystem probes before
RecordLaunchAsynchas necessarily yielded. In the normal uncontended case, the coordinator calls this on the UI thread andWaitAsynccompletes synchronously, so an offline or slow network repository path can freeze the window during launch. Please move the probe to a background task, consistent with the repository's rule that filesystem I/O must not block WPF's dispatcher.
- Files reviewed: 52/52 changed files
- Comments generated: 0 new
- Review effort level: Balanced
RecordLaunchAsync captured worktree labels synchronously. The coordinator awaits it from the UI thread during a context swap, and _gate.WaitAsync completes synchronously when uncontended, so everything up to the first real suspension ran on the dispatcher - including GitWorktreeLayout.Describe, which stats the repo path. A repo on an offline or slow network share would block that stat until SMB timed out and freeze the window mid-launch, violating the rule in AGENTS.md section 9 that filesystem IO must not block WPF's dispatcher. The probe now runs through a runner that defaults to Task.Run, awaited before the gate is taken - which both guarantees the method yields before touching the file system and shortens the critical section. The runner is an internal seam mirroring IRootScanRunner in LocalRepoLocator so tests can observe which thread the probe lands on; two tests cover that it runs off the caller and that no probing happens synchronously. Found by Copilot code review on PR #24. AI-Local-Session: 80032277-bc2c-4613-a0d3-72ad20d3621b AI-Cloud-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64
Confirmed and fixed in 51e83ee. The analysis was exactly right, including the subtle part: The probe now runs through a runner defaulting to The runner is an internal seam mirroring Noting for the record that this arrived as a review-body finding with no inline thread, so there is nothing to resolve on this one — replying here instead so the fix is on the record. |
There was a problem hiding this comment.
🔵 Needs a closer look
Mid-flight stash enumeration can prevent the selected worktree’s stash list from ever loading.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
DiffViewer/ViewModels/ViewStashFormViewModel.cs:71
- Switching the stash form to another worktree while its initial stash enumeration is still running can leave the new checkout permanently unloaded.
OnRepoPathResolvedstarts a new enumeration, butEnumerateStashesAsyncimmediately returns whileIsLoadingis true; the old request then detects the changed path and drops its result, with nothing retrying the new path. Coalesce or loop stale loads asWorktreePickerViewModel.EnsureLoadedAsyncdoes, and cover the mid-flight path-change sequence.
- Files reviewed: 52/52 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Same defect the worktree picker had, in the other view-model that loads on a repo-path change. EnumerateStashesAsync turned away any caller arriving while IsLoading was true, then dropped its own result when the path had moved - so nothing loaded the new checkout and the stash list stayed empty until the user edited the path again. It was latent before: changing the repo path meant typing. The worktree picker makes it a single click, which is what makes it worth fixing here rather than leaving as pre-existing. Now loops on a stale result, mirroring WorktreePickerViewModel.EnsureLoadedAsync. Covered by a test that re-points the form from inside the enumerate runner and asserts both paths were enumerated and the new checkout's stashes landed. Also replaces Task.Yield with a TaskCompletionSource gate in RecordLaunchAsync_YieldsBeforeProbingTheFileSystem. Task.Yield only schedules the continuation, so whether the probe had run by the time the assertion executed was a race; the test failed on its second run. The gate makes it deterministic - the probe provably cannot have run. Found by Copilot code review on PR #24. AI-Local-Session: 80032277-bc2c-4613-a0d3-72ad20d3621b AI-Cloud-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6d4fbf2-3954-4138-a5fb-aa9eaecbff64
Confirmed and fixed in 96ca109. This is the same defect you found in Worth noting why it belongs in this PR rather than being waved off as pre-existing: the guard and the stale-drop both predate these changes, but reaching them used to require typing a new path mid-enumeration. The worktree picker this PR adds turns that into a single click, so the change is what makes the bug readily reachable. Fixed the same way as the picker — loop on a stale result instead of discarding it — with a test that re-points the form from inside the enumerate runner and asserts both paths were enumerated and the new checkout's stashes landed. Separately, while validating this I found that a test I added in the previous round, This one also arrived as a review-body finding with no inline thread, so there's nothing to resolve — replying here to keep the fix on the record. |
Two user-facing features since v1.9.0 (worktree switching in the New diff dialog and context bar; worktree identification in recents and the window title), so a minor bump under the post-1.0 rules in AGENTS.md 12. Checked against the six breaking surfaces: no CLI argv change, recents rows written by older versions still parse, install footprint untouched. Also logs the stale stash-enumeration fix (96ca109) under Fixed. It landed as a review follow-up on PR #24 without a changelog entry, but it repairs a shipped feature - the View stash form going blank after the repository path moved mid-load - so it belongs in the release notes. AI-Local-Session: c7aecf5c-ca18-4b85-94db-c1325614b602 AI-Cloud-Session: 6f972cbf-b409-428f-8d6a-9f764839e4b0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Why
DiffViewer already worked when pointed at a linked worktree, but nothing on screen acknowledged it and there was no way to get to one except typing its path from memory.
The visible symptom was in the recents dropdown. Row titles came from
Path.GetFileName(repoPath), which for a worktree is usually the branch name and never mentions the repository. A worktree at...\copilot-worktrees\DiffViewer\feature-xrendered asfeature-x - HEAD -> WT, so two worktrees of one repo were effectively indistinguishable and the word "DiffViewer" appeared nowhere.What changed
Legibility. Recent rows now render as
DiffViewer [feature-x] - HEAD -> WT, and the window title appends the worktree name after the path. The labels are captured at launch time and persisted alongside the existing display-only fields, so the dropdown does no disk IO at render time. Rows written by earlier versions load with null labels, fall back to the old rendering, and heal on next launch.Switching. The repo-path row in the New diff dialog gained a Worktree... button, and the context bar gained a matching Worktree button that re-opens the comparison you are already looking at rooted at a different checkout, with both sides unchanged. Both list the main worktree first with each entry's checked-out branch and path.
New seams.
GitWorktreeLayoutdoes libgit2-free path reasoning about git's on-disk worktree layout (commondir pointers,.git-as-a-file pointers, repository naming).IGitWorktreeEnumeratorlists worktrees;WorktreePickerViewModelmirrors the existingRefPickerViewModelshape, and the context-bar switcher reuses it with a write-back that runs a context switch instead of editing a text box.Things worth a careful look
Two libgit2 quirks drove the enumerator's design. Both were verified empirically against a real repo rather than assumed:
commondirpointer.NullReferenceException. Names therefore come from the<common>/worktrees/*administrative directories, which survive the deletion. Such worktrees are shown as unavailable rather than silently dropped.The picker is offered on every local form, not just working-tree ones. I initially argued against this. Two worktrees share an object database and all shared refs, so
main..featureresolves identically from either. ButHEADis per-worktree and the dialog advertisesHEAD~3as valid input, so the choice genuinely changes the answer for commit-ish forms too.LocalRepoFormViewModelBaseis an extraction, and it is the largest part of the diff. The five local New-diff forms each carried their own copy of the repo path, its canonicalization, and the deferred-error handling, three of them verbatim identical, and the worktree picker would have been a sixth copy.NewDiffDialog.xamlduplicated the repo-path Grid five times likewise and is now oneRepoPathFieldcontrol, which also retires the Browse handler'sTag="RepoPath"visual-tree walk in favour of a named element. The extraction is intended to be behavior-preserving; the full pre-existing suite passing unchanged is the main evidence for that, and the ordering of canonicalization vsValidate()vs side effects likeTrySeedDefaultPartneris the part most worth a second pair of eyes.The third commit fixes a latent trap found in review.
RepositoryShape.CommonGitDirectoryis documented as identical across every worktree of a repository, which is what makes "same repo, different checkout" recognizable. It was not: libgit2 reports a main worktree's git dir with a trailing separator while the commondir pointer resolves without one. Nothing consumed the field yet, so it was a trap for the first caller rather than a live bug.Not included
True cross-worktree diffing (worktree A's files against worktree B's) is tracked separately in #23. It needs a model change, since
DiffSide.WorkingTreeis payload-free andParsedCommandLinecarries a single repo path.Review findings addressed
Several findings arrived in review bodies rather than as inline comments. GitHub has no resolve state for those, so they stay visible at the top of the conversation and read as open forever. Tracking them here, since there is nowhere else to mark them done.
ResolveCommonDirectoryreturned an unnormalized path from its catch blocka36c3a2WorktreePickercomment claimedEnsureLoadedAsyncswallows failuresa36c3a2a1844b5a1844b5HasAlternativeWorktreesmis-reported a bare hub with one linked worktreea1844b5a1844b5StackPaneldenied the popup'sScrollViewera viewporta1844b551e83ee96ca109All inline review threads are replied to and resolved.
Verification
dotnet build -c Releaseclean, 0 warnings.dotnet test: 1644 passed, 1 skipped. The 1608 pre-existing tests pass unchanged.Session
80032277-bc2c-4613-a0d3-72ad20d3621bb6d4fbf2-3954-4138-a5fb-aa9eaecbff64