Skip to content

Refactor: move per-workspace-pane management (incl. per-pane tab→document registry and Phase-2 navigation) out of MainWindowViewModel into WorkspacePaneViewModel #1341

Description

@JoshuaRowePhantom

Summary

MainWindowViewModel (features\Phantom.Workspaces\ViewModels\MainWindowViewModel.cs, ~4,790 lines) is a god-object that owns a large amount of logic that is really per-workspace-pane concern: restoring/populating a pane's tabs, walking and migrating the pane's ContentLayout, opening/closing/cycling tabs within a pane, serializing a pane's persisted layout, per-pane tab lifecycle, and — critically — the per-pane tabId → WorkspaceDocument registry and the pane-scoped part of navigate-to-tab-by-id. All of this should move onto the per-pane view-model WorkspacePaneViewModel (features\Phantom.Workspaces\ViewModels\WorkspacePaneViewModel.cs), which already owns Id, Tabs, SelectedTab, ContentLayout, aggregations, SaveCommand, and Populated — making it the natural owner of the per-pane document registry and per-pane navigation. WorkspacePaneDocument remains the thin outer-dock Document wrapper for the pane VM. MainWindowViewModel keeps only window-global collection management (the WorkspacePanes collection, selection, outer-dock ordering, the shared WorkspaceDockFactory, Phase-1 pane resolution for navigation, and thin command delegations that XAML binds). This is a structural refactor with no intended behaviour change (except it structurally eliminates one of the mechanisms tracked in #1340 — see below); existing per-pane tests should relocate to a new WorkspacePaneViewModelTests file.

Current per-pane types (containment)

Outer DockControl (MainWindow) → WorkspacesPaneDock (ItemsSource = MainWindowViewModel.WorkspacePanes) → per-pane WorkspacePaneDocument (extends Dock's Document; created by WorkspacePaneDocumentGenerator, WorkspacePaneDocumentGenerator.cs:25-48) → each holds a WorkspacePaneViewModel (WorkspacePaneDocument.WorkspacePane, WorkspacePaneDocument.cs:54) → WorkspacePaneViewModel.ContentLayout is an inner IRootDock whose primary region is a WorkspaceContentDock bound to pane.TabsWorkspaceDocument(s) (the tabs), created by WorkspaceDocumentGenerator.

  • WorkspacePaneDocument (WorkspacePaneDocument.cs, 75 lines) wraps the pane VM, caches a TabHeaderViewModel (running/notification indicators), forwards Title/AnyTabIsRunning/AnyTabHasUnreadNotification PropertyChanged, and cascades DisposeAsync into the pane. It stays a thin outer-dock Document wrapper — it is NOT the target of the move.
  • WorkspacePaneViewModel (WorkspacePaneViewModel.cs, 277 lines) already owns Id/Title/Entity/IsReadOnly/IsSaving/CloseCommand/SaveCommand, the Tabs collection, SelectedTab, ContentLayout, aggregation flags, the Populated TCS, tab-subscription bookkeeping, and recursive DisposeAsync. Because it already owns Tabs + ContentLayout + Id, it is the natural home for the per-pane tab→document registry and per-pane navigation.

Recommended target

Everything that can be moved off MainWindowViewModel moves onto WorkspacePaneViewModel. That includes not only the per-pane logic previously enumerated below but also (new): the per-pane tabId → WorkspaceDocument registry and Phase-2 of navigate-to-tab-by-id. MainWindowViewModel keeps only window-global concerns (the WorkspacePanes collection, SelectedWorkspacePane, outer-dock Layout, the shared WorkspaceDockFactory, the outer-dock paneId → WorkspacePaneDocument map, Phase-1 pane resolution by workspace tab id, and thin command delegations). WorkspacePaneDocument stays a thin outer-dock Document wrapper.

Per-pane tab→document registry (key structural change)

Today WorkspaceDockFactory.documentsByTabId (features\Phantom.Workspaces\ViewModels\WorkspaceDockFactory.cs:29) is a single window-scoped dictionary that outlives individual panes. WorkspaceDocumentGenerator calls factory.RegisterDocument(tab.Id, document) / UnregisterDocument(tab.Id) (see WorkspaceDockFactory.cs:62-67, 88-89, 107, 190-191) and, on materialization, guards against duplicates with existing = factory.GetDocumentForTab(tab.Id) + !ReferenceEquals(existing.Owner, dock) (WorkspaceDocumentGenerator.cs:40-42). Because the registry outlives panes, a closed-then-reopened pane can still find a stale prior-owner entry and skip materialization — the mechanism (A) of #1340.

Move it. Each WorkspacePaneViewModel gets its own Dictionary<string, WorkspaceDocument> (its tabId → WorkspaceDocument registry). The pane's inner-dock WorkspaceDocumentGenerator registers/unregisters into the owning pane's registry (the generator is already constructed per pane and can be handed a reference to the pane VM at construction time).

Consequences to state explicitly:

  • WorkspaceDockFactory.paneDocumentsByPaneId (paneId → WorkspacePaneDocument, WorkspaceDockFactory.cs:35) STAYS window-scoped — it is the outer-dock pane-resolution concern (needed for Phase-1 of navigation and for restoring WorkspacePaneDocuments by id), not a per-pane concern.
  • The WorkspaceDocumentGenerator collision guard (WorkspaceDocumentGenerator.cs:40-42) is retained but resolves against the owning pane's registry. It still prevents duplicate materialization when the same tab id appears in multiple split regions of the same pane's ContentLayout, but it no longer false-positives across panes.
  • The DockableLocator coupling in WorkspaceDockFactory.RegisterDocument/UnregisterDocument (WorkspaceDockFactory.cs:54, 65-66) — the id→IDockable locator used by Dock's restore-by-id — must be repointed to the per-pane registry (or made pane-scoped: each pane's inner-dock has its own ContextLocator/DockableLocator, so this is a local change). Nothing outside the pane should be resolving tab ids by direct dictionary lookup after this refactor.

Relationship to #1340

Closing a pane (RemoveWorkspacePaneAsync, MainWindowViewModel.cs:2288) currently leaves entries in documentsByTabId for all tabs the pane owned; #1340 mechanism (A) is that those stale entries cause the reopened pane's WorkspaceDocumentGenerator to short-circuit (existing != null && !ReferenceEquals(existing.Owner, dock)) and skip materialization — the reopened pane shows "no documents open" even though its Tabs collection is populated. With the registry moved onto WorkspacePaneViewModel, closing the pane discards its registry wholesale, and a reopened pane starts with an empty registry — so the collision guard cannot trip against a stale prior-owner entry and the restored documents materialize.

State clearly: this move eliminates #1340's mechanism (A) by construction — no per-document eviction is needed, and this is preferred over #1340's previously-proposed explicit UnregisterDocument-enumeration-on-close workaround. #1340 should be marked as resolved-by / dependent-on #1341 for mechanism (A). This does NOT address #1340's autosave-on-close concern, which is explicitly out of scope here and tracked separately (see the "Out of scope" note below).

Navigate-to-tab by id (two-phase request)

Today MainWindowViewModel.ActivateTabByIdAsync/ActivateTabWhenLoaded and OpenTabAsync's dedup path do a window-global dockFactory.GetDocumentForTab(tabId) lookup (MainWindowViewModel.cs:2212, 2240, 2261, 2278, 2399), then FindDocumentDock + SetActiveDockable / SetFocusedDockable. Moving the registry per-pane removes the global lookup. Navigation becomes two-phase, driven by a request that carries both a workspace tab id (= pane id) and a document tab id (= tab id):

Request shape (realign existing scaffolding, do not reinvent — see features\Phantom.Workspaces\Services\Navigation\NavigationHistoryService.cs:10, 23, 54, which already has NavigationTarget with TabId/WorkspacePaneId and NavigationEntry(string tabId, string paneId); and MainWindowTabNavigator.NavigateAsync at MainWindowTabNavigator.cs:29-59 which already pushes new NavigationEntry(tabId, paneId)):

public record NavigationRequest(string WorkspaceTabId, string DocumentTabId);
// where WorkspaceTabId  == the pane id (aka WorkspacePaneDocument id, WorkspacePaneViewModel.Id)
//       DocumentTabId   == the WorkspaceDocument tab id inside that pane

Phase 1 — MWVM identifies the workspace pane by WorkspaceTabId. Uses WorkspacePanes / WorkspaceDockFactory.paneDocumentsByPaneId to resolve the WorkspacePaneDocument, opening the pane first if needed (per #1157). MWVM activates the pane in the outer dock.

Phase 2 — the pane resolves the document tab id against its OWN registry and activates/focuses it within its own ContentLayout. New method on WorkspacePaneViewModel:

public Task<bool> NavigateToDocumentTabAsync(string documentTabId)
{
    // 1. Look up documentTabId in THIS pane's tabId -> WorkspaceDocument registry.
    // 2. If found: FindDocumentDock over ContentLayout, SetActiveDockable, SetFocusedDockable.
    // 3. If not-yet-materialized but present in Tabs (deferred), install ActivateTabWhenLoaded
    //    hook on THIS pane and return true.
    // 4. Otherwise return false.
}

ITabNavigatorHost.ActivateTabByIdAsync(string tabId, string? workspacePaneId) (ITabNavigatorHost.cs:16) is refactored so the host only does Phase-1 pane resolution and then delegates Phase-2 to WorkspacePaneViewModel.NavigateToDocumentTabAsync. The signature is realigned to NavigationRequest:

Task<bool> ActivateTabByRequestAsync(NavigationRequest request);

MainWindowTabNavigator.NavigateAsync (MainWindowTabNavigator.cs:29-59) is updated to build a NavigationRequest(WorkspaceTabId: paneId, DocumentTabId: tabId) from the current signals; NavigationHistoryService.NavigationTarget/NavigationEntry are renamed to use WorkspaceTabId/DocumentTabId for clarity but keep the same on-disk semantics (paneId, tabId).

Fallback for unknown WorkspaceTabId. Today the "search all panes" fallback lives at MainWindowViewModel.cs:2234-2249. It becomes: ask each pane whether it owns the DocumentTabId (each pane exposes bool OwnsDocumentTab(string documentTabId) that consults its own registry). This remains possible without a global registry.

Deferred activation. ActivateTabWhenLoaded (MainWindowViewModel.cs:2252-2286) — currently a MWVM-level hook that awaits pane.Tabs change — moves onto WorkspacePaneViewModel and is triggered from NavigateToDocumentTabAsync when the document tab id is present in Tabs but not yet materialized in ContentLayout.

Members to move / keep / split

Classification: (a) clearly per-pane → move; (b) shared/global → stays; (c) coordination → split (part moves, thin delegation stays). All line numbers are in MainWindowViewModel.cs unless noted.

Member Lines Description Class
OnCloseActiveTab 1995–2041 Closes active tab via FindFocusedDocumentDock/FindDocumentDock/dockFactory.CloseDockable; MRU nav (a) → WorkspacePaneViewModel.CloseActiveTabAsync
OnCycleTab 2043–2079 Cycles active dockable in pane's ContentLayout (a) → WorkspacePaneViewModel.CycleTab(delta)
ActivateTabByIdAsync (per-pane portions) 2212, 2234–2249 Global GetDocumentForTab + per-pane search (c) → Phase-1 stays on MWVM; Phase-2 (per-pane lookup + activate/focus) → WorkspacePaneViewModel.NavigateToDocumentTabAsync
ActivateTabWhenLoaded 2252–2286 Deferred activation once a tab materialises in pane.Tabs (a) → move to WorkspacePaneViewModel (private helper invoked from NavigateToDocumentTabAsync)
SubscribeToInnerDockChanges / Unsubscribe 2766–2779 Empty stubs (#1107) still called on restore/remove (a) → move as stubs, then delete
WriteBackWorkspaceTabs 2786–2903 Serializes pane tabs + dock layout (c) — serialization body → WorkspacePaneViewModel.BuildPersistedTabsSnapshotAsync; the entityBroker.UpdateAsync write stays on MWVM
SaveWorkspacePaneAsync 2905–2906 Wraps WriteBackWorkspaceTabs; already injected as saveAsync (ctor at 3865) (c) — becomes pane.SaveAsync
AppendWorkspaceTabRelationshipChanges 2908–~2970 Reconciles pane tab entities → relationship changes (a) → move
FindDocumentDock 3017–3037 DFS to first IDocumentDock, always over pane.ContentLayout (a) → move
FindFocusedDocumentDock 3048–3066 Same (a) → move
EnumerateAllDocuments / EnumerateContentDocks 3078–3121 Static DFS over a pane layout tree (a) → move (static)
MigrateBaseDocumentDocksToWorkspaceContentDock, ConvertToWorkspaceContentDock, s_documentDockCopyableProperties, BuildDocumentDockCopyableProperties 3141–3278 Per-pane restore-time layout migration (a) → move (static)
PopulateWorkspacePaneTabsAsync 3470–3565 Loads tabs into pane.Tabs, activates saved active tab (a) → WorkspacePaneViewModel.PopulateTabsAsync
TryRestoreFromDockLayoutAsync 3573–3694 Rebuilds ContentLayout from JSON, migrates, wires content docks, populates tabs (a) → move
CreateTabViewModelFromDescriptorAsync 3700–3777 Descriptor → tab VM (c) → behind injected IWorkspaceTabFactory
TryFetchWorkspaceTabAsync, CreateTabFromEntityAsync ~3779–3859 Same (c) → IWorkspaceTabFactory
TryFocusExistingWebTabAsync (per-pane URL lookup) Focus existing web tab by URL (a) → move
OnDockableTabClosed 3296–3334 Loops panes to find owner, removes tab, MRU nav (c) — owner-find stays on MWVM (window-global concern); "remove one tab + MRU-nav-if-active" → WorkspacePaneViewModel.HandleChildTabClosed
OpenTabAsync 2376–~2500+ Pane resolution + per-pane tab insert with anchor + dedup via GetDocumentForTab (2399) (c) — pane resolution stays; dedup uses per-pane registry (via WorkspacePaneViewModel.OwnsDocumentTab); per-pane insert body → WorkspacePaneViewModel.OpenTabAsync
AddWorkspacePaneToDock 1945–1967 Activates the pane's document in the outer dock (c) — keep on MWVM, call SelectedWorkspacePane.PaneDocument.Activate()
CreateWorkspacePaneAsync 3861–~3910 Constructs pane, seeds ContentLayout, defers to populate/restore (c) — pane-construction body → pane-VM factory (Populate/Restore now live on the pane VM)
ActiveTabId / ActiveAgentViewModel 321–343 Query selectedWorkspacePane.ContentLayout (c) — trivial delegations to SelectedWorkspacePane.*

Stays on MainWindowViewModel (window-global): WorkspacePanes, selectedWorkspacePane/SelectedWorkspacePane, Layout, the shared dockFactory (WorkspaceDockFactory), SyncWorkspacePanesOrderFromDock + suppressWorkspaceDockOrderSync (987–1040), RemoveWorkspacePaneAsync collection-removal + re-select (2288–2329), DismissLoadingPane (1918–1943), GetOrCreateLoadingWorkspacePane (2331–2353), FindWorkspacePaneIdForTab (3280–3288) — now implemented as "ask each pane via OwnsDocumentTab", OnCloseWorkspace/CanCloseWorkspace command wiring (1969–1993), NavigationHistoryService interaction, ActivateTabByRequestAsync Phase-1 pane resolution, and the CloseWorkspaceCommand/CloseActiveTabCommand/CycleTab*Command RelayCommand objects (bodies delegate into the pane VM).

Stays on WorkspacePaneDocument (thin outer-dock wrapper): wraps WorkspacePaneViewModel, caches TabHeaderViewModel, forwards a few property-changed events, and cascades DisposeAsync into the pane VM. No new logic is added here.

Dependencies that complicate the move (must be injected into WorkspacePaneViewModel)

Needed by MWVM member Injection target
Populate/Restore/CreateTab/Fetch entityBroker (field 53, entityBrokerTask 46) EntityBroker
CreateTabFromDescriptor / restore openAgentSessionShortcutHandler (63), shortcutManager (62), entityTypeViewCatalog (56), fieldEditorFactory (57) an IWorkspaceTabFactory bundling these
Open/Restore/active-dock resolution dockFactory (75): SetActiveDockable, SetFocusedDockable, ContextLocator, DockState, WireContentDock, CreateWorkspaceContentLayout (note: GetDocumentForTab removed in favour of the pane's own registry) WorkspaceDockFactory (window-scoped; pass in)
Tab close / MRU nav notificationService (93), navigationHistoryService (95), navigatingViaHistory (96), IsTabOpen INotificationService, NavigationHistoryService, Func<string,bool> isTabOpen
WriteBack write entityBroker.UpdateAsync keep write on MWVM (or inject IWorkspacePersistence)
Cross-pane concerns WorkspacePanes collection must remain on MWVM

Registry location. After this refactor, WorkspaceDockFactory.documentsByTabId (WorkspaceDockFactory.cs:29) is removed and its contents live per-pane on WorkspacePaneViewModel. paneDocumentsByPaneId (WorkspaceDockFactory.cs:35) stays on the factory. The WorkspaceDocumentGenerator (constructed per pane) is handed a reference to the owning WorkspacePaneViewModel and registers/unregisters into pane.RegisterDocument/pane.UnregisterDocument.

Construction hookup. WorkspacePaneViewModel is currently constructed by MainWindowViewModel.CreateWorkspacePaneAsync (~3861). Pass a WorkspacePaneEnvironment (bundling EntityBroker, WorkspaceDockFactory, INotificationService, NavigationHistoryService, IWorkspaceTabFactory, dispatcher) into the pane VM's constructor. WorkspacePaneDocumentGenerator remains unchanged in shape; the pane VM already reaches its WorkspacePaneDocument via existing wiring.

XAML impact

Confirmed bindings in MainWindow.axaml: CloseActiveTabCommand (line 21), CycleTabForwardCommand/CycleTabBackwardCommand (22–23) are already MainWindowViewModel RelayCommands — leave the command objects on MWVM and delegate their bodies to SelectedWorkspacePane.*, so no XAML changes are required. SelectedWorkspacePane.SaveCommand/CanSaveWorkspace (28, 161–162) are unchanged. The outer DockControl binds Layout and WorkspacePanes — unchanged. No template files reference the moved internals.

Out of scope

Autosave-on-close is NOT part of this refactor. Do not mention or depend on WriteBackWorkspaceTabs-on-close as a companion change. It is being filed as its own separate bug and must not be conflated with this move. #1340's autosave-on-close concern (mechanism (B)) is explicitly not addressed by #1341; only mechanism (A) (stale registry entries) is resolved structurally.

Affected Files

File Contribution
features\Phantom.Workspaces\ViewModels\MainWindowViewModel.cs Source of all moved members; keeps Phase-1 navigation, window-global concerns
features\Phantom.Workspaces\ViewModels\WorkspacePaneViewModel.cs Destination for per-pane logic AND the per-pane tab→document registry AND NavigateToDocumentTabAsync/OwnsDocumentTab
features\Phantom.Workspaces\ViewModels\WorkspacePaneDocument.cs Unchanged in role — thin outer-dock Document wrapper
features\Phantom.Workspaces\ViewModels\WorkspacePaneDocumentGenerator.cs Unchanged in shape
features\Phantom.Workspaces\ViewModels\WorkspaceDocumentGenerator.cs Register/Unregister targets the owning pane's registry (line 40–42 collision guard rewired)
features\Phantom.Workspaces\ViewModels\WorkspaceDockFactory.cs documentsByTabId (line 29) removed; paneDocumentsByPaneId (line 35) stays; RegisterDocument/UnregisterDocument (lines 54, 62–67, 88–89) removed or repointed; DockableLocator coupling (lines 54, 65–66) made pane-scoped
features\Phantom.Workspaces\Services\Navigation\NavigationHistoryService.cs NavigationTarget/NavigationEntry renamed to WorkspaceTabId/DocumentTabId (lines 10, 23, 54)
features\Phantom.Workspaces\Services\Navigation\ITabNavigatorHost.cs ActivateTabByIdAsync refactored to ActivateTabByRequestAsync(NavigationRequest) (line 16); Phase-1 only in host
features\Phantom.Workspaces\Services\Navigation\MainWindowTabNavigator.cs Builds NavigationRequest; delegates Phase-2 to pane VM (lines 29–59)
(new) IWorkspaceTabFactory Bundles descriptor→tab creation services
(new) NavigationRequest(string WorkspaceTabId, string DocumentTabId) Two-phase navigation payload

Tests to relocate

A per-pane test class does not exist yet; create features\Phantom.Workspaces.Tests\WorkspacePaneViewModelTests.cs for the moved logic (the existing WorkspacePaneViewModelTests.cs file, if present today, already mixes some pane-VM assertions — the moved-behaviour tests below live in the same file or a companion partial as appropriate). WorkspacePaneViewModelTests.cs already contains WorkspacePaneDocument_* assertions (lines 167, 178, 189, 203, 217, 234, 252, 270) — retarget those to the pane VM.

Relocate from features\Phantom.Workspaces.Tests\MainWindowViewModelTests.csWorkspacePaneViewModelTests.cs:
lines 28, 100, 130 (Alt/badge ordering per-pane), 213, 228, 245, 265, 296, 320, 347 (multi-region restore/open-new-window/navigation), 681, 702, 728, 742, 757, 771, 781 (TryFocusExistingWebTabAsync_*), 802, 816, 841, 859, 877 (pane dispose/close/reopen).

Relocate from features\Phantom.Workspaces.Tests\MainWindowDockTemplateTests.csWorkspacePaneViewModelTests.cs:
lines 471, 500, 530, 567, 599, 666, 698 (close-active-tab / split-region / MRU), 1161, 1219, 1259 (migrate base DocumentDock), 1457 (multi-region restore), 1875 (ctrl-click new window in restored region), 1958 (uniform wiring). Keep the pure XAML-template tests where they are.

Stays on MainWindowViewModelTests.cs (window-global): MainWindow_AltShiftDigit_ActivatesWorkspacePaneHostTab (57), MainWindow_F7F8_NotificationNavigation_StillWorks (180), LeftPane/collapser tests (559–626), refresh-tick tests (424, 442), NavigateToHistoryEntry_* (914), template tests. Leave WorkspaceDockPersistenceTests.cs / WorkspaceDockPersistenceIntegrationTests.cs in place (they target DockLayoutCanonicalizer directly, not MWVM).

Expected Tests

New/relocated tests in WorkspacePaneViewModelTests:

Test Name Class What It Verifies
WorkspacePaneViewModel_OwnsDocumentRegistry_RegistersTabDocumentOnAdd WorkspacePaneViewModelTests Adding a tab registers its WorkspaceDocument in the pane's own registry
WorkspacePaneViewModel_Dispose_DiscardsDocumentRegistry WorkspacePaneViewModelTests Disposing the pane drops all its registrations (no window-scoped residue)
WorkspaceReopen_AfterClose_WithRestoredAgentSessionTab_MaterializesDocument_NoStaleEntry MainWindowIntegrationTests Close→reopen materializes the restored tab because the fresh pane has an empty registry (structural #1340(A) guard)
WorkspacePaneViewModel_NavigateToDocumentTabAsync_ActivatesAndFocusesWithinOwnLayout WorkspacePaneViewModelTests Phase-2 navigation activates/focuses the document tab using the pane's own registry
MainWindowViewModel_NavigateByRequest_ResolvesPaneByWorkspaceTabId_ThenDelegatesToPane MainWindowViewModelTests Phase-1 resolves the pane by workspace tab id and delegates to the pane
MainWindowViewModel_NavigateByRequest_UnknownDocumentTabId_FallsBackToAllPanesOwnershipQuery MainWindowViewModelTests Fallback asks each pane whether it owns the document tab id
WorkspacePaneViewModel_CloseActiveTab_WhenActiveTabExists_RoutesThroughFactoryCloseDockable WorkspacePaneViewModelTests Close-active-tab routes through dockFactory.CloseDockable
WorkspacePaneViewModel_CloseActiveTab_WhenLastTabInSplitDockClosed_RemovesEmptyDockAndSplitter WorkspacePaneViewModelTests Closing the last tab in a split region removes the empty dock + splitter
WorkspacePaneViewModel_CloseActiveTab_WhenFocusedInSplitRegion_ClosesOnlyFocusedRegionActiveTab WorkspacePaneViewModelTests Focus-scoped close affects only the focused region
WorkspacePaneViewModel_CycleTab_WhenTwoTabsExist_ActivatesNextInVisibleOrder WorkspacePaneViewModelTests Cycle activates next dockable in visible order
WorkspacePaneViewModel_PopulateTabsAsync_WithLegacyRegionsJson_AddsTabsInDeclarationOrder WorkspacePaneViewModelTests Legacy regions JSON populates tabs in order
WorkspacePaneViewModel_PopulateTabsAsync_WithActiveTabId_ActivatesSavedTab WorkspacePaneViewModelTests Saved active-tab id is activated
WorkspacePaneViewModel_PopulateTabsAsync_WhenPaneClosedDuringLoad_DisposesLoadedTabs WorkspacePaneViewModelTests Racing close during load disposes loaded tabs
WorkspacePaneViewModel_TryRestoreFromDockLayout_WithMultiRegionSplit_RestoresAllRegionsAsWorkspaceContentDock WorkspacePaneViewModelTests Multi-region split restore yields typed docks for every region
WorkspacePaneViewModel_TryRestoreFromDockLayout_WithLegacyBaseDocumentDock_MigratesToWorkspaceContentDock WorkspacePaneViewModelTests Legacy base DocumentDock migrates
WorkspacePaneViewModel_TryRestoreFromDockLayout_CanonicalizesDuplicateDocuments WorkspacePaneViewModelTests Duplicate documents are canonicalised on restore
WorkspacePaneViewModel_OpenTabAsync_WhenAnchorTabProvided_InsertsAfterAnchorInSameRegion WorkspacePaneViewModelTests Anchor insert lands in the same region after the anchor
WorkspacePaneViewModel_OpenTabAsync_WhenTabAlreadyOpen_JustActivates WorkspacePaneViewModelTests Re-open of an open tab just activates (dedup via per-pane registry)
WorkspacePaneViewModel_ActivateTabWhenLoaded_WhenTabAddedLater_ActivatesOnce WorkspacePaneViewModelTests Deferred activation fires exactly once
WorkspacePaneViewModel_TryFocusExistingWebTabAsync_SameUrlOpen_ActivatesAndReturnsTrue WorkspacePaneViewModelTests Same-URL web tab is focused
WorkspacePaneViewModel_TryFocusExistingWebTabAsync_DifferentUrl_ReturnsFalse WorkspacePaneViewModelTests Different URL returns false
WorkspacePaneViewModel_BuildPersistedTabsSnapshot_SerializesLayoutCanonically WorkspacePaneViewModelTests Snapshot serializes the layout canonically
WorkspacePaneViewModel_BuildPersistedTabsSnapshot_IncludesActiveTabIdAndDropsLegacyFocusedTabId WorkspacePaneViewModelTests Snapshot includes active-tab id, drops legacy focused-tab id
WorkspacePaneViewModel_BuildPersistedTabsSnapshot_AppendsRelationshipChangesForLiveTabEntities WorkspacePaneViewModelTests Snapshot appends relationship changes for live tab entities
WorkspacePaneViewModel_DisposeAsync_CascadesToPaneAndReleasesAgentLeases WorkspacePaneViewModelTests Dispose cascades to pane and releases agent leases (relocated)
WorkspacePaneViewModel_HandleChildTabClosed_WhenActive_ActivatesMruTab WorkspacePaneViewModelTests Closing the active child tab activates the MRU tab

Considered / Background

An earlier revision of this issue proposed relocating the moved logic onto WorkspacePaneDocument (the outer-dock Document facade) rather than WorkspacePaneViewModel. The containment analysis behind that proposal is still valid and is preserved here for context:

  • WorkspacePaneDocument is the outer-dock facade for a pane (it is what the outer DockControl binds to as an IDocument), and one could describe per-pane behaviour as "everything a pane does inside the window that owns the outer DockControl" — which reads as WorkspacePaneDocument's responsibility.
  • Under that proposal, WorkspacePaneDocument would receive the moved logic and WorkspacePaneViewModel would keep the lean tab-bag + aggregations + SaveCommand + Populated role.

Why WorkspacePaneViewModel was chosen instead: WorkspacePaneViewModel already owns Id, Tabs, SelectedTab, ContentLayout, and SaveCommand — the exact state that the moved logic operates on. Adding the per-pane tab→document registry and NavigateToDocumentTabAsync to the same type keeps state and behaviour co-located, avoids a second layer of forwarding through WorkspacePaneDocument, and makes the natural lifetime boundary (the pane VM's DisposeAsync) also be the registry's lifetime boundary — which is what makes #1340 mechanism (A) go away by construction. WorkspacePaneDocument stays a thin outer-dock wrapper (Document + TabHeaderViewModel + forwarded PropertyChanged), which is a better fit for what Dock expects an IDocument to be.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdiagnosedRoot cause identifiednext-up

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions