You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Inner document-dock tab headers render no icon/favicon/status — only Running+Notification templates are registered in the inner ItemsControl.DataTemplates #1324
Summary — REOPENED, new direction: centralized dock-TYPE management
Inner document-dock workspace tabs still render no header icons at all (no icon, no favicon, no status glyph, no agent-running / notification indicators — only title + close ✕). The outer workspace-pane tab row is unaffected.
The confirmed runtime cause has not changed: at runtime the inner tab strip falls through to the generic IDocumentDock fallback DataTemplate at Phantom.Workspaces/Templates/DockDataTemplates.axaml:165-167 (<DataTemplate DataType="dmc:IDocumentDock"><dock:DocumentDockControl/></DataTemplate>) which sets NO HeaderTemplate, so Dock.Avalonia's default headerless header renders. The reason the fallback is being selected is that the inner dock instance is NOT a WorkspaceContentDock at runtime — it is a base Dock.Model.Mvvm.Controls.DocumentDock re-hydrated by MainWindowViewModel.TryRestoreFromDockLayoutAsync from a pre-#1307 persisted layout.
The owner directive for the fix has changed: rather than stamping the correct HeaderTemplate on every IDocumentDock-matching template as a per-template patch, centralize template management via the workspace-specific dock TYPE. The workspace-specific document dock types already exist (WorkspaceContentDock, WorkspacesPaneDock). Make deserialization guarantee every document-dock region in the live tree IS one of those workspace-specific types, and then one authoritative HeaderTemplate on that type covers every dock region (inner panes, split docks, restored docks). The generic IDocumentDock fallback stays only as a belt-and-suspenders safety net.
Root cause (unchanged, kept for record)
The inner WorkspacePaneDocumentDockControl (DockDataTemplates.axaml:112, AutoCreateDataTemplates="False", no ancestor walk-up per #1130) resolves its child-dock template against its own DockControl.DataTemplates collection. Two templates can match an IDocumentDock:
DockDataTemplates.axaml:123 — concrete <DataTemplate DataType="vm:WorkspaceContentDock"> → <dock:DocumentControl> with an inline HeaderTemplate. Repaired by 3c9063db.
DockDataTemplates.axaml:165-167 — generic fallback <DataTemplate DataType="dmc:IDocumentDock"> → <dock:DocumentDockControl />. No HeaderTemplate. Renders exactly the plain title + close-✕ shape in the screenshot.
First-match-wins by declaration order: any IDocumentDock implementation that is NOT WorkspaceContentDock — including base Dock.Model.Mvvm.Controls.DocumentDock — falls through to line 165.
Runtime path that produces the base DocumentDock:MainWindowViewModel.TryRestoreFromDockLayoutAsync (MainWindowViewModel.cs:3353-3419) deserializes WorkspacePane.ContentLayout using DockSerializer + WorkspaceDockTypeInfoResolver. That resolver (WorkspaceDockTypeInfoResolver.cs:40-56) configures IDock polymorphism with UnknownDerivedTypeHandling.FallBackToBaseType and IgnoreUnrecognizedTypeDiscriminators = true. It honors whatever concrete $type the persisted JSON records. Layouts created and persisted before #1307 landed contain $type = "Dock.Model.Mvvm.Controls.DocumentDock" nodes (splits went through the un-overridden Factory.CreateDocumentDock() which returned base DocumentDock). Nothing migrates them on restore. MainWindowViewModel.cs:3411 (FindDocumentDock(layout) as WorkspaceContentDock) already implicitly acknowledges this — the as silently returns null when the primary dock is a base DocumentDock, and sibling split docks keep whatever concrete type they were persisted with. From that point the affected tab strips render through the generic fallback at line 165.
Why 3c9063db did not fix it
3c9063db (previous attempt) correctly centralized the per-item Icon/Favicon/Status/Running/Notification templates in TabHeaderItemTemplates.axaml and re-wired the concrete WorkspaceContentDock header path. Every one of its assertions is still true. It does not fix this bug because it only touched the WorkspaceContentDock template branch — nothing forces the runtime dock instance to be a WorkspaceContentDock after a persisted-layout restore.
Why the +291 tests passed
The MainWindowDockTemplateTests.cs suite exercises only the WorkspaceContentDock branch:
TabHeaderItemTemplates_HasKeyedResource_ForEveryTabHeaderItemViewModelSubtype (:835) — reflection over Application.Current.Resources, no dock scope.
TabHeaderTemplate_ItemsControlDataTemplates_ResolvesEveryTabHeaderItemViewModelSubtype (:858) and BuildTabHeaderItemsControl (:818) — resolve TabHeaderTemplate in isolation and call Build(...). Never instantiate the nested WorkspacePaneDocumentDockControl and never prove the ItemsControl materializes a child visual container per header-item VM.
Inner-render tests at :916/948/979/1010 construct a fresh MainWindowViewModel and open a tab via viewModel.OpenTabAsync(...), which routes through WorkspaceDockFactory.CreateWorkspaceContentLayout (WorkspaceDockFactory.cs:134-160) whose primary content dock is new WorkspaceContentDock(...). Line 123 always matches; line 165 is never touched.
The split-dock regression at :1041-1122 explicitly calls factory.CreateDocumentDock() (line 1070) which returns WorkspaceContentDock — again line 123 matches.
Test gap: no test deserializes a persisted layout whose $type is base Dock.Model.Mvvm.Controls.DocumentDock and asserts (a) the restored live dock is materialized as WorkspaceContentDock, or (b) the tab strip anchored to a restored/base dock renders per-item glyphs.
Primary site. Add deserialization-time type substitution so any persisted $type = "Dock.Model.Mvvm.Controls.DocumentDock" (or any other base IDocumentDock discriminator that is not the workspace-specific subclass) is materialized as WorkspaceContentDock. Same for the outer workspace-pane region → WorkspacesPaneDock.
TryRestoreFromDockLayoutAsync (:3353-3419, in particular the FindDocumentDock(layout) as WorkspaceContentDock at :3411). With type substitution in place, this cast becomes guaranteed. Alternative: a post-load migration walk here — see Design §Alternative.
Existing custom dock types (WorkspaceContentDock : DocumentDock at WorkspaceContentDock.cs:14; WorkspacesPaneDock : DocumentDock at WorkspacesPaneDock.cs:14). No structural change; they are the authoritative targets of the type-substitution and of the single centralized header template.
Already guarantees the workspace type for every non-restore creation path: CreateLayout (:80-108) creates the outer WorkspacesPaneDock at :82; CreateWorkspaceContentLayout (:134-160) creates WorkspaceContentDock at :136 for the inner region; CreateDocumentDock (:120-126, added by #1307) returns WorkspaceContentDock for splits. Restore is the only remaining hole.
With the dock-type guarantee, the existing vm:WorkspaceContentDock template at :123 and the outer-scope vm:WorkspacesPaneDock template are the single centralized header sites. Give the generic IDocumentDock fallback (:102-104 outer, :165-167 inner) the same centralized HeaderTemplate as a safety net so no dock region can ever render headerless even if an unforeseen base type slips in.
Home of the centralized keyed per-item templates from 3c9063db — kept as-is (the per-item Icon/Favicon/Status/Running/Notification templates are correctly centralized here). Optionally lift the shared HeaderTemplate body (ContentControl binding EffectiveTabHeader + type-selecting WebTabHeaderTemplate / TabHeaderTemplate) into one keyed WorkspaceDocumentHeaderTemplate here so both the WorkspaceContentDock template and the safety-net fallback reference the same keyed resource.
New tests locking (a) restore-time type substitution, (b) the restored-base-DocumentDock render path, (c) the safety-net fallback. See Expected Tests.
Design / Fix — centralized dock-TYPE management
Guiding principle (owner directive, new). Do not distribute the correct HeaderTemplate across every IDocumentDock-matching template. Instead: guarantee that every document-dock region in the live layout tree is the workspace-specific dock type (WorkspaceContentDock for content regions, WorkspacesPaneDock for the pane row). Once that invariant holds, a single centralized HeaderTemplate on the workspace dock type covers every region — inner panes, split docks, restored docks — and the generic IDocumentDock fallback becomes a pure safety net.
1. Primary: deserialization-time type substitution (preferred)
Make WorkspaceDockTypeInfoResolver (WorkspaceDockTypeInfoResolver.cs:141-154, CreateOptions) map the base Dock.Model.Mvvm.Controls.DocumentDock$type discriminator to WorkspaceContentDock (and any base pane-dock discriminator, if present in persisted JSON, to WorkspacesPaneDock) at the polymorphism layer, so the deserializer instantiates the workspace-specific subclass directly. Because WorkspaceContentDock : DocumentDock (no non-nullable required fields, same public surface plus shadowed Owner / StyleKey / ItemsSource / ItemContainerGenerator), a base-DocumentDock JSON payload deserializes into WorkspaceContentDock without loss.
Concrete sketch (inside CreateOptions for typeof(IDock), i.e. s_dockOptions):
// Register the workspace subclass under BOTH its own FullName AND the base// Dock.Model.Mvvm.Controls.DocumentDock FullName so persisted pre-#1307 layouts// materialize as WorkspaceContentDock.options.DerivedTypes.Add(newJsonDerivedType(typeof(WorkspaceContentDock),typeof(WorkspaceContentDock).FullName!));options.DerivedTypes.Add(newJsonDerivedType(typeof(WorkspaceContentDock),typeof(Dock.Model.Mvvm.Controls.DocumentDock).FullName!));// Same for WorkspacesPaneDock if the outer pane region is ever persisted as base DocumentDock.
Notes:
The current GetDerivedTypes(...) reflection scan (WorkspaceDockTypeInfoResolver.cs:178-199) auto-registers the base Dock.Model.Mvvm.Controls.DocumentDock under its own FullName. The substitution above must REPLACE that registration (skip the base type in GetDerivedTypes when a workspace-specific subclass is registered under the same discriminator), so JsonPolymorphismOptions.DerivedTypes does not contain two entries with the same discriminator string.
Same treatment for IDockable polymorphism (s_dockableOptions at :40-41) — the discriminator is registered at the interface level, so the swap must happen there too.
With IgnoreUnrecognizedTypeDiscriminators = true and FallBackToBaseType still on, unknown discriminators fall back to the interface base — but that path is no longer reached for the specific base-DocumentDock case that produces this bug.
Why this mechanism is preferred (owner directive): nothing downstream ever sees a base DocumentDock. MainWindowViewModel.cs:3411's FindDocumentDock(layout) as WorkspaceContentDock becomes a guaranteed non-null cast; the template selection at DockDataTemplates.axaml:123 always wins; the fallback at :165-167 is unreachable via restore. Behaviour is centralized in exactly one class (WorkspaceDockTypeInfoResolver) at exactly one lifecycle point (deserialization).
1. Alternative: post-load migration walk
If for any reason type substitution cannot be done at the polymorphism layer (e.g., a Dock.Serializer.SystemTextJson internal path re-materializes base DocumentDock regardless), do a migration walk in TryRestoreFromDockLayoutAsync immediately after serializer.Deserialize<IRootDock>(dockLayoutJson) at MainWindowViewModel.cs:3364 and before workspacePane.ContentLayout = layout; at :3403:
// After deserialization: replace any base DocumentDock in the tree with WorkspaceContentDock,// copying Id / Title / VisibleDockables / ActiveDockable / CanCreateDocument / IsCollapsable /// Proportion / etc. Re-parent VisibleDockables onto the new instance.MigrateBaseDocumentDocksToWorkspaceContentDock(layout);
The walk uses the same enumeration as EnumerateAllDocuments (see :3374), and the parent-relinking is straightforward: for each IDock parent, iterate VisibleDockables and substitute in place. This makes the invariant enforced at exactly one anchor (:3364-3403) but requires a bit more code than the resolver change.
The resolver change (§1) is preferred; the migration walk (§Alternative) is the fallback if the resolver approach cannot fully cover the internal Dock serializer paths. In either case, once the invariant holds, MainWindowViewModel.cs:3411's as WorkspaceContentDock is guaranteed non-null and can be simplified accordingly.
2. Confirm the factory already guarantees the type on the non-restore paths
Outer pane row: WorkspaceDockFactory.CreateLayout() (WorkspaceDockFactory.cs:80-108) constructs the outer document dock as new WorkspacesPaneDock { ... } at :82.
Inner content region: WorkspaceDockFactory.CreateWorkspaceContentLayout(...) (:134-160) constructs the primary content dock as new WorkspaceContentDock { ... } at :136.
So the ONLY way a base DocumentDock enters the live tree is deserialization of pre-#1307 persisted layouts. §1 closes that hole.
3. Centralized header template on the workspace dock type
With the type invariant in place, the existing DockDataTemplates.axaml:123<DataTemplate DataType="vm:WorkspaceContentDock"> template is authoritative for every content dock region — inner pane, split, restored — because there is no other kind of content dock in the tree. Same for the outer vm:WorkspacesPaneDock template. One centralized header template per workspace dock type; no per-region divergence.
Recommended cleanup: move the inline HeaderTemplate body currently at DockDataTemplates.axaml:125-145 into a keyed WorkspaceDocumentHeaderTemplate in TabHeaderItemTemplates.axaml, and reference it via HeaderTemplate="{StaticResource WorkspaceDocumentHeaderTemplate}" from the WorkspaceContentDock template (and from the safety-net fallback, see §4). This keeps the header body in the same dictionary as the per-item keyed templates that 3c9063db correctly centralized there.
4. Safety net — give the generic IDocumentDock fallback the same centralized HeaderTemplate
Belt-and-suspenders: even with §1 enforcing the type invariant, patch the generic fallback DataTemplates at DockDataTemplates.axaml:102-104 (outer) and :165-167 (inner) to use the same centralized HeaderTemplate reference:
This guarantees that if some future path (e.g. a Dock.Avalonia internal float / dock-manager operation, or a new deserialization case) ever produces a base DocumentDock, it still renders the correct header. This is a safety net, NOT the primary fix.
What survives from 3c9063db
Keep the keyed per-item templates in TabHeaderItemTemplates.axaml (AgentRunningIndicatorTabHeaderItemTemplate, NotificationIndicatorTabHeaderItemTemplate, IconTabHeaderItemTemplate, FaviconTabHeaderItemTemplate, StatusControlTabHeaderItemTemplate) and the shared TabHeaderTemplate / WebTabHeaderTemplate bodies. These are correct centralizations. The change here is orthogonal: it centralizes the outer template selection at the dock-TYPE layer.
Considered / Background (superseded)
Per-template HeaderTemplate stamping. Adding the correct HeaderTemplate to every IDocumentDock-matching DataTemplate (concrete WorkspaceContentDock/WorkspacesPaneDock plus outer and inner generic fallbacks) — surfaced as one candidate design. Superseded because it distributes the fix across every template site and does not address the deeper invariant (there should not be a base DocumentDock in a workspace layout tree at all). Retained only as the §4 safety net.
Keyed-only per-item template centralization (3c9063db). Correctly centralized the per-item templates and the two header bodies; correctly repaired the WorkspaceContentDock code path. Insufficient on its own because it never forces the runtime dock instance to BE a WorkspaceContentDock after restore. Kept as a precondition of the current fix.
The prior diagnosis ("per-item template provisioning is split across three files and diverges between the outer and inner DockControl scopes") was accurate but incomplete: it never asked what happens when the inner dock is not a WorkspaceContentDock. Recorded here for context; do not re-apply on its own.
Expected tests (Subject_Scenario_ExpectedOutcome)
CRITICAL new tests (these reproduce the runtime failure the previous suite missed):
Feed TryRestoreFromDockLayoutAsync a WorkspacePane.ContentLayout JSON whose sibling document dock is "$type": "Dock.Model.Mvvm.Controls.DocumentDock"; run restore; walk the resulting workspacePane.ContentLayout tree
Every IDocumentDock in the tree Is<WorkspaceContentDock>(); Id, Title, VisibleDockables count, ActiveDockable identity are preserved
Directly deserialize a minimal JSON with "$type": "Dock.Model.Mvvm.Controls.DocumentDock" under an IDock slot using new WorkspaceDockTypeInfoResolver()
Result is Assert.IsType<WorkspaceContentDock>(...) (locks the polymorphism-layer substitution)
Build a real MainWindow; restore a WorkspacePane.ContentLayout whose persisted JSON encodes a base DocumentDock hosting a WorkspaceDocument whose EffectiveTabHeader.Items carries all five per-item VMs (AgentRunning, Notification, Icon (🚀), Favicon (🌐 via WebTabHeaderViewModel), Status); anchor by reference to the restored dock instance and locate the tab-strip descendants
Non-null visual/container materializes for EACH per-item VM type (pulsating-brain ProgressBar, exclamation-indicator ProgressBar, "🚀" TextBlock, "🌐" TextBlock, StatusControl). Explicit ItemContainerGenerator container-count vs Items-count check on the header ItemsControl.
Reflect the IDataTemplate in DockDataTemplates whose .Match(new Dock.Model.Mvvm.Controls.DocumentDock()) succeeds via the generic IDocumentDock fallback (both outer and inner DockControl.DataTemplates scopes)
The template's built control is a DocumentControl whose HeaderTemplate is the same reference as the centralized keyed WorkspaceDocumentHeaderTemplate (or, if the keyed extraction is not adopted, has a non-null HeaderTemplate whose body binds EffectiveTabHeader)
After the base-DocumentDock restore scenario, call FindDocumentDock(workspacePane.ContentLayout) and cast to WorkspaceContentDock
Non-null (locks the MainWindowViewModel.cs:3411 invariant that the type-substitution is intended to guarantee)
Preserve existing tests (TabHeaderItemTemplates_HasKeyedResource_ForEveryTabHeaderItemViewModelSubtype, TabHeaderTemplate_ItemsControlDataTemplates_ResolvesEveryTabHeaderItemViewModelSubtype, WebTabHeaderTemplate_…, TabHeaderTemplateAndWebTabHeaderTemplate_ShareTheSameKeyedItemTemplates, WorkspaceDataTemplates_DoNotDeclareImplicitTabHeaderItemTemplates, the four inner-WorkspaceContentDock render tests at MainWindowDockTemplateTests.cs:916/948/979/1010, and MainWindowSplitDocumentTabStrip_AfterHorizontalSplit_RendersAllFiveHeaderItemTypes). They correctly lock the WorkspaceContentDock branch centralized by 3c9063db. They under-tested the restore/base-type path, which the new tests above fill.
880956e6 (split-dock regression test): correct as far as it goes; only covers WorkspaceContentDock produced by the factory. Extended by the new restore/base-type tests above.
Summary — REOPENED, new direction: centralized dock-TYPE management
Inner document-dock workspace tabs still render no header icons at all (no icon, no favicon, no status glyph, no agent-running / notification indicators — only title + close ✕). The outer workspace-pane tab row is unaffected.
The confirmed runtime cause has not changed: at runtime the inner tab strip falls through to the generic
IDocumentDockfallback DataTemplate atPhantom.Workspaces/Templates/DockDataTemplates.axaml:165-167(<DataTemplate DataType="dmc:IDocumentDock"><dock:DocumentDockControl/></DataTemplate>) which sets NOHeaderTemplate, so Dock.Avalonia's default headerless header renders. The reason the fallback is being selected is that the inner dock instance is NOT aWorkspaceContentDockat runtime — it is a baseDock.Model.Mvvm.Controls.DocumentDockre-hydrated byMainWindowViewModel.TryRestoreFromDockLayoutAsyncfrom a pre-#1307 persisted layout.The owner directive for the fix has changed: rather than stamping the correct
HeaderTemplateon everyIDocumentDock-matching template as a per-template patch, centralize template management via the workspace-specific dock TYPE. The workspace-specific document dock types already exist (WorkspaceContentDock,WorkspacesPaneDock). Make deserialization guarantee every document-dock region in the live tree IS one of those workspace-specific types, and then one authoritativeHeaderTemplateon that type covers every dock region (inner panes, split docks, restored docks). The genericIDocumentDockfallback stays only as a belt-and-suspenders safety net.Root cause (unchanged, kept for record)
The inner
WorkspacePaneDocumentDockControl(DockDataTemplates.axaml:112,AutoCreateDataTemplates="False", no ancestor walk-up per #1130) resolves its child-dock template against its ownDockControl.DataTemplatescollection. Two templates can match anIDocumentDock:DockDataTemplates.axaml:123— concrete<DataTemplate DataType="vm:WorkspaceContentDock">→<dock:DocumentControl>with an inlineHeaderTemplate. Repaired by3c9063db.DockDataTemplates.axaml:165-167— generic fallback<DataTemplate DataType="dmc:IDocumentDock">→<dock:DocumentDockControl />. NoHeaderTemplate. Renders exactly the plain title + close-✕ shape in the screenshot.First-match-wins by declaration order: any
IDocumentDockimplementation that is NOTWorkspaceContentDock— including baseDock.Model.Mvvm.Controls.DocumentDock— falls through to line 165.Runtime path that produces the base
DocumentDock:MainWindowViewModel.TryRestoreFromDockLayoutAsync(MainWindowViewModel.cs:3353-3419) deserializesWorkspacePane.ContentLayoutusingDockSerializer+WorkspaceDockTypeInfoResolver. That resolver (WorkspaceDockTypeInfoResolver.cs:40-56) configuresIDockpolymorphism withUnknownDerivedTypeHandling.FallBackToBaseTypeandIgnoreUnrecognizedTypeDiscriminators = true. It honors whatever concrete$typethe persisted JSON records. Layouts created and persisted before #1307 landed contain$type = "Dock.Model.Mvvm.Controls.DocumentDock"nodes (splits went through the un-overriddenFactory.CreateDocumentDock()which returned baseDocumentDock). Nothing migrates them on restore.MainWindowViewModel.cs:3411(FindDocumentDock(layout) as WorkspaceContentDock) already implicitly acknowledges this — theassilently returns null when the primary dock is a baseDocumentDock, and sibling split docks keep whatever concrete type they were persisted with. From that point the affected tab strips render through the generic fallback at line 165.Why
3c9063dbdid not fix it3c9063db(previous attempt) correctly centralized the per-item Icon/Favicon/Status/Running/Notification templates inTabHeaderItemTemplates.axamland re-wired the concreteWorkspaceContentDockheader path. Every one of its assertions is still true. It does not fix this bug because it only touched theWorkspaceContentDocktemplate branch — nothing forces the runtime dock instance to be aWorkspaceContentDockafter a persisted-layout restore.Why the +291 tests passed
The
MainWindowDockTemplateTests.cssuite exercises only theWorkspaceContentDockbranch:TabHeaderItemTemplates_HasKeyedResource_ForEveryTabHeaderItemViewModelSubtype(:835) — reflection overApplication.Current.Resources, no dock scope.TabHeaderTemplate_ItemsControlDataTemplates_ResolvesEveryTabHeaderItemViewModelSubtype(:858) andBuildTabHeaderItemsControl(:818) — resolveTabHeaderTemplatein isolation and callBuild(...). Never instantiate the nestedWorkspacePaneDocumentDockControland never prove the ItemsControl materializes a child visual container per header-item VM.:916/948/979/1010construct a freshMainWindowViewModeland open a tab viaviewModel.OpenTabAsync(...), which routes throughWorkspaceDockFactory.CreateWorkspaceContentLayout(WorkspaceDockFactory.cs:134-160) whose primary content dock isnew WorkspaceContentDock(...). Line 123 always matches; line 165 is never touched.:1041-1122explicitly callsfactory.CreateDocumentDock()(line 1070) which returnsWorkspaceContentDock— again line 123 matches.Test gap: no test deserializes a persisted layout whose
$typeis baseDock.Model.Mvvm.Controls.DocumentDockand asserts (a) the restored live dock is materialized asWorkspaceContentDock, or (b) the tab strip anchored to a restored/base dock renders per-item glyphs.Affected files
Phantom.Workspaces/ViewModels/WorkspaceDockTypeInfoResolver.cs$type = "Dock.Model.Mvvm.Controls.DocumentDock"(or any other baseIDocumentDockdiscriminator that is not the workspace-specific subclass) is materialized asWorkspaceContentDock. Same for the outer workspace-pane region →WorkspacesPaneDock.Phantom.Workspaces/ViewModels/MainWindowViewModel.csTryRestoreFromDockLayoutAsync(:3353-3419, in particular theFindDocumentDock(layout) as WorkspaceContentDockat:3411). With type substitution in place, this cast becomes guaranteed. Alternative: a post-load migration walk here — see Design §Alternative.Phantom.Workspaces/ViewModels/WorkspaceContentDock.cs,WorkspacesPaneDock.csWorkspaceContentDock : DocumentDockatWorkspaceContentDock.cs:14;WorkspacesPaneDock : DocumentDockatWorkspacesPaneDock.cs:14). No structural change; they are the authoritative targets of the type-substitution and of the single centralized header template.Phantom.Workspaces/ViewModels/WorkspaceDockFactory.csCreateLayout(:80-108) creates the outerWorkspacesPaneDockat:82;CreateWorkspaceContentLayout(:134-160) createsWorkspaceContentDockat:136for the inner region;CreateDocumentDock(:120-126, added by #1307) returnsWorkspaceContentDockfor splits. Restore is the only remaining hole.Phantom.Workspaces/Templates/DockDataTemplates.axamlvm:WorkspaceContentDocktemplate at:123and the outer-scopevm:WorkspacesPaneDocktemplate are the single centralized header sites. Give the genericIDocumentDockfallback (:102-104outer,:165-167inner) the same centralizedHeaderTemplateas a safety net so no dock region can ever render headerless even if an unforeseen base type slips in.Phantom.Workspaces/Templates/TabHeaderItemTemplates.axaml3c9063db— kept as-is (the per-item Icon/Favicon/Status/Running/Notification templates are correctly centralized here). Optionally lift the sharedHeaderTemplatebody (ContentControlbindingEffectiveTabHeader+ type-selectingWebTabHeaderTemplate/TabHeaderTemplate) into one keyedWorkspaceDocumentHeaderTemplatehere so both theWorkspaceContentDocktemplate and the safety-net fallback reference the same keyed resource.Phantom.Workspaces.Tests/MainWindowDockTemplateTests.csDocumentDockrender path, (c) the safety-net fallback. See Expected Tests.Design / Fix — centralized dock-TYPE management
Guiding principle (owner directive, new). Do not distribute the correct
HeaderTemplateacross everyIDocumentDock-matching template. Instead: guarantee that every document-dock region in the live layout tree is the workspace-specific dock type (WorkspaceContentDockfor content regions,WorkspacesPaneDockfor the pane row). Once that invariant holds, a single centralizedHeaderTemplateon the workspace dock type covers every region — inner panes, split docks, restored docks — and the genericIDocumentDockfallback becomes a pure safety net.1. Primary: deserialization-time type substitution (preferred)
Make
WorkspaceDockTypeInfoResolver(WorkspaceDockTypeInfoResolver.cs:141-154,CreateOptions) map the baseDock.Model.Mvvm.Controls.DocumentDock$typediscriminator toWorkspaceContentDock(and any base pane-dock discriminator, if present in persisted JSON, toWorkspacesPaneDock) at the polymorphism layer, so the deserializer instantiates the workspace-specific subclass directly. BecauseWorkspaceContentDock : DocumentDock(no non-nullable required fields, same public surface plus shadowedOwner/StyleKey/ItemsSource/ItemContainerGenerator), a base-DocumentDockJSON payload deserializes intoWorkspaceContentDockwithout loss.Concrete sketch (inside
CreateOptionsfortypeof(IDock), i.e.s_dockOptions):Notes:
GetDerivedTypes(...)reflection scan (WorkspaceDockTypeInfoResolver.cs:178-199) auto-registers the baseDock.Model.Mvvm.Controls.DocumentDockunder its own FullName. The substitution above must REPLACE that registration (skip the base type inGetDerivedTypeswhen a workspace-specific subclass is registered under the same discriminator), soJsonPolymorphismOptions.DerivedTypesdoes not contain two entries with the same discriminator string.IDockablepolymorphism (s_dockableOptionsat:40-41) — the discriminator is registered at the interface level, so the swap must happen there too.IgnoreUnrecognizedTypeDiscriminators = trueandFallBackToBaseTypestill on, unknown discriminators fall back to the interface base — but that path is no longer reached for the specific base-DocumentDockcase that produces this bug.Why this mechanism is preferred (owner directive): nothing downstream ever sees a base
DocumentDock.MainWindowViewModel.cs:3411'sFindDocumentDock(layout) as WorkspaceContentDockbecomes a guaranteed non-null cast; the template selection atDockDataTemplates.axaml:123always wins; the fallback at:165-167is unreachable via restore. Behaviour is centralized in exactly one class (WorkspaceDockTypeInfoResolver) at exactly one lifecycle point (deserialization).1. Alternative: post-load migration walk
If for any reason type substitution cannot be done at the polymorphism layer (e.g., a Dock.Serializer.SystemTextJson internal path re-materializes base
DocumentDockregardless), do a migration walk inTryRestoreFromDockLayoutAsyncimmediately afterserializer.Deserialize<IRootDock>(dockLayoutJson)atMainWindowViewModel.cs:3364and beforeworkspacePane.ContentLayout = layout;at:3403:The walk uses the same enumeration as
EnumerateAllDocuments(see:3374), and the parent-relinking is straightforward: for eachIDockparent, iterateVisibleDockablesand substitute in place. This makes the invariant enforced at exactly one anchor (:3364-3403) but requires a bit more code than the resolver change.The resolver change (§1) is preferred; the migration walk (§Alternative) is the fallback if the resolver approach cannot fully cover the internal Dock serializer paths. In either case, once the invariant holds,
MainWindowViewModel.cs:3411'sas WorkspaceContentDockis guaranteed non-null and can be simplified accordingly.2. Confirm the factory already guarantees the type on the non-restore paths
WorkspaceDockFactory.CreateLayout()(WorkspaceDockFactory.cs:80-108) constructs the outer document dock asnew WorkspacesPaneDock { ... }at:82.WorkspaceDockFactory.CreateWorkspaceContentLayout(...)(:134-160) constructs the primary content dock asnew WorkspaceContentDock { ... }at:136.WorkspaceDockFactory.CreateDocumentDock()(:120-126) returnsnew WorkspaceContentDock { Id = Guid.NewGuid().ToString() }for Dock'sNewHorizontalDocumentDock/NewVerticalDocumentDocksplit paths (commit2d330f54).So the ONLY way a base
DocumentDockenters the live tree is deserialization of pre-#1307 persisted layouts. §1 closes that hole.3. Centralized header template on the workspace dock type
With the type invariant in place, the existing
DockDataTemplates.axaml:123<DataTemplate DataType="vm:WorkspaceContentDock">template is authoritative for every content dock region — inner pane, split, restored — because there is no other kind of content dock in the tree. Same for the outervm:WorkspacesPaneDocktemplate. One centralized header template per workspace dock type; no per-region divergence.Recommended cleanup: move the inline
HeaderTemplatebody currently atDockDataTemplates.axaml:125-145into a keyedWorkspaceDocumentHeaderTemplateinTabHeaderItemTemplates.axaml, and reference it viaHeaderTemplate="{StaticResource WorkspaceDocumentHeaderTemplate}"from theWorkspaceContentDocktemplate (and from the safety-net fallback, see §4). This keeps the header body in the same dictionary as the per-item keyed templates that3c9063dbcorrectly centralized there.4. Safety net — give the generic
IDocumentDockfallback the same centralizedHeaderTemplateBelt-and-suspenders: even with §1 enforcing the type invariant, patch the generic fallback DataTemplates at
DockDataTemplates.axaml:102-104(outer) and:165-167(inner) to use the same centralizedHeaderTemplatereference:This guarantees that if some future path (e.g. a Dock.Avalonia internal float / dock-manager operation, or a new deserialization case) ever produces a base
DocumentDock, it still renders the correct header. This is a safety net, NOT the primary fix.What survives from
3c9063dbKeep the keyed per-item templates in
TabHeaderItemTemplates.axaml(AgentRunningIndicatorTabHeaderItemTemplate,NotificationIndicatorTabHeaderItemTemplate,IconTabHeaderItemTemplate,FaviconTabHeaderItemTemplate,StatusControlTabHeaderItemTemplate) and the sharedTabHeaderTemplate/WebTabHeaderTemplatebodies. These are correct centralizations. The change here is orthogonal: it centralizes the outer template selection at the dock-TYPE layer.Considered / Background (superseded)
HeaderTemplatestamping. Adding the correctHeaderTemplateto everyIDocumentDock-matching DataTemplate (concreteWorkspaceContentDock/WorkspacesPaneDockplus outer and inner generic fallbacks) — surfaced as one candidate design. Superseded because it distributes the fix across every template site and does not address the deeper invariant (there should not be a baseDocumentDockin a workspace layout tree at all). Retained only as the §4 safety net.3c9063db). Correctly centralized the per-item templates and the two header bodies; correctly repaired theWorkspaceContentDockcode path. Insufficient on its own because it never forces the runtime dock instance to BE aWorkspaceContentDockafter restore. Kept as a precondition of the current fix.DockControlscopes") was accurate but incomplete: it never asked what happens when the inner dock is not aWorkspaceContentDock. Recorded here for context; do not re-apply on its own.Expected tests (
Subject_Scenario_ExpectedOutcome)CRITICAL new tests (these reproduce the runtime failure the previous suite missed):
MainWindowViewModel_RestorePersistedLayoutWithBaseDocumentDock_MaterializesWorkspaceContentDockTryRestoreFromDockLayoutAsyncaWorkspacePane.ContentLayoutJSON whose sibling document dock is"$type": "Dock.Model.Mvvm.Controls.DocumentDock"; run restore; walk the resultingworkspacePane.ContentLayouttreeIDocumentDockin the treeIs<WorkspaceContentDock>();Id,Title,VisibleDockablescount,ActiveDockableidentity are preservedWorkspaceDockTypeInfoResolver_BaseDocumentDockDiscriminator_MapsToWorkspaceContentDock"$type": "Dock.Model.Mvvm.Controls.DocumentDock"under anIDockslot usingnew WorkspaceDockTypeInfoResolver()Assert.IsType<WorkspaceContentDock>(...)(locks the polymorphism-layer substitution)DockTemplates_RestoredBaseDocumentDockTab_RendersHeaderIconsForEachItemTypeMainWindow; restore aWorkspacePane.ContentLayoutwhose persisted JSON encodes a baseDocumentDockhosting aWorkspaceDocumentwhoseEffectiveTabHeader.Itemscarries all five per-item VMs (AgentRunning,Notification,Icon(🚀),Favicon(🌐 viaWebTabHeaderViewModel),Status); anchor by reference to the restored dock instance and locate the tab-strip descendantsProgressBar, exclamation-indicatorProgressBar, "🚀"TextBlock, "🌐"TextBlock,StatusControl). Explicit ItemContainerGenerator container-count vs Items-count check on the headerItemsControl.DockTemplates_GenericDocumentDockFallback_HasHeaderTemplateIDataTemplateinDockDataTemplateswhose.Match(new Dock.Model.Mvvm.Controls.DocumentDock())succeeds via the genericIDocumentDockfallback (both outer and innerDockControl.DataTemplatesscopes)DocumentControlwhoseHeaderTemplateis the same reference as the centralized keyedWorkspaceDocumentHeaderTemplate(or, if the keyed extraction is not adopted, has a non-nullHeaderTemplatewhose body bindsEffectiveTabHeader)MainWindowViewModel_FindDocumentDockAfterRestore_ReturnsNonNullWorkspaceContentDockDocumentDockrestore scenario, callFindDocumentDock(workspacePane.ContentLayout)and cast toWorkspaceContentDockMainWindowViewModel.cs:3411invariant that the type-substitution is intended to guarantee)Preserve existing tests (
TabHeaderItemTemplates_HasKeyedResource_ForEveryTabHeaderItemViewModelSubtype,TabHeaderTemplate_ItemsControlDataTemplates_ResolvesEveryTabHeaderItemViewModelSubtype,WebTabHeaderTemplate_…,TabHeaderTemplateAndWebTabHeaderTemplate_ShareTheSameKeyedItemTemplates,WorkspaceDataTemplates_DoNotDeclareImplicitTabHeaderItemTemplates, the four inner-WorkspaceContentDockrender tests atMainWindowDockTemplateTests.cs:916/948/979/1010, andMainWindowSplitDocumentTabStrip_AfterHorizontalSplit_RendersAllFiveHeaderItemTypes). They correctly lock theWorkspaceContentDockbranch centralized by3c9063db. They under-tested the restore/base-type path, which the new tests above fill.Relationship to prior work
3c9063db(previous Inner document-dock tab headers render no icon/favicon/status — only Running+Notification templates are registered in the inner ItemsControl.DataTemplates #1324 attempt): keyed per-item template centralization +WorkspaceContentDockheader wiring. Kept — precondition of the current fix.880956e6(split-dock regression test): correct as far as it goes; only coversWorkspaceContentDockproduced by the factory. Extended by the new restore/base-type tests above.2d330f54, split path returnsWorkspaceContentDock): guarantees the type on the split creation path. Restore is the remaining hole this issue closes.DockControlscope isolation): unchanged; centralized keyed templates are reached viaApplication.Current.Resources, which does not depend on inner-scope ancestor walk-up.