Summary
Alt+N / Alt+Shift+N keyboard tab indexing and the numbered tab badges are computed from our internal flat ObservableCollection projections (WorkspacePaneViewModel.Tabs for content tabs, MainWindowViewModel.WorkspacePanes for workspace tabs) instead of from the Avalonia.Dock structure (the IDock.VisibleDockables of the real visual tab strips). The internal Tabs list is a flat, single-strip projection that cannot represent split/floated strips and only stays in visual order because a fragile back-sync (SyncPaneTabsOrderFromDock) copies the Dock order into it after certain reorder events. Consulting that projection is the wrong source of truth: it diverges from the visual order under splits and under the #1065 insert-to-the-right move whenever the back-sync does not fire (e.g. the drag path emits Remove+insert, not Move).
The fix is to compute both numberings and both sets of badges directly from the Dock VisibleDockables, per visual strip and per active workspace, keeping #1043's active-workspace scoping:
- Alt+N (content tabs) → the active workspace's content
DocumentDock.VisibleDockables (WorkspaceContentDock, reached via pane.ContentLayout), in visual order.
- Alt+Shift+N (workspace tabs) → the workspace-tab host dock's
VisibleDockables (the root WorkspacesPaneDock, Id="WorkspacesDock"), in visual order.
A dockable is mapped back to its WorkspaceTabViewModel via WorkspaceDocument.TabViewModel (and pane dockables via GetPaneDocument) for activation and label assignment.
Maintainer directive: "Consulting the internal tabs list for Alt+N and badge labels is incorrect, and should be addressed in #1067. Rewrite #1067 correctly using Avalonia.Dock primitives."
Root Cause / Current State
All indexing/badge logic is in Phantom.Workspaces/ViewModels/MainWindowViewModel.cs.
Alt+N content-tab order is read from the internal Tabs list
OnGoToTabAtIndex resolves the target through ComputeActiveWorkspaceTabOrder():
// MainWindowViewModel.cs:1759-1761
private void OnGoToTabAtIndex(int index)
{
var match = FindTabByAltShortcutIndex(this.ComputeActiveWorkspaceTabOrder(), index);
ComputeActiveWorkspaceTabOrder() delegates to ComputeGlobalTabOrder([pane], …), which walks pane.Tabs — the internal ObservableCollection<WorkspaceTabViewModel> — not the Dock:
// MainWindowViewModel.cs:2779-2789
private List<(WorkspacePaneViewModel Pane, WorkspaceDocument Document)> ComputeActiveWorkspaceTabOrder()
{
var pane = this.selectedWorkspacePane;
if (pane?.ContentLayout is null || pane.Id.StartsWith("loading-workspace:", …)) return new();
return ComputeGlobalTabOrder([pane], this.dockFactory.GetDocumentForTab);
}
// MainWindowViewModel.cs:2754-2771
internal static List<…> ComputeGlobalTabOrder(IEnumerable<WorkspacePaneViewModel> panes, Func<string, WorkspaceDocument?> getDocumentForTab)
{
var order = new List<…>();
foreach (var pane in panes)
foreach (var tab in pane.Tabs) // <-- internal flat list, NOT VisibleDockables
if (getDocumentForTab(tab.Id) is WorkspaceDocument doc)
order.Add((pane, doc));
return order;
}
Alt+N badge labels are assigned by iterating the internal Tabs list
// MainWindowViewModel.cs:2691-2708
private void RefreshActiveWorkspaceAltShortcutLabels()
{
foreach (var pane in this.WorkspacePanes)
foreach (var tab in pane.Tabs) // <-- internal flat list
if (this.dockFactory.GetDocumentForTab(tab.Id) is WorkspaceDocument doc)
doc.EffectiveTabHeader.AltShortcutLabel = null;
AssignGlobalAltShortcutLabels(this.ComputeActiveWorkspaceTabOrder()); // also Tabs-derived
}
Alt+N badge visibility iterates the internal Tabs list
// MainWindowViewModel.cs:2791-2804
private void PropagateBadgeVisibility(bool isAltHeld, bool isShiftHeld)
{
var contentBadge = isAltHeld && !isShiftHeld;
foreach (var pane in this.WorkspacePanes)
{
var isActive = ReferenceEquals(pane, this.selectedWorkspacePane);
foreach (var tab in pane.Tabs) // <-- internal flat list
if (this.dockFactory.GetDocumentForTab(tab.Id) is WorkspaceDocument doc)
doc.EffectiveTabHeader.IsShortcutBadgeVisible = isActive && contentBadge;
}
// workspace-pane badges below (Alt+Shift): iterate this.WorkspacePanes …
}
Alt+Shift+N workspace-tab order is read from the internal WorkspacePanes list
// MainWindowViewModel.cs:2674-2689
internal static void RefreshWorkspacePaneAltShortcutLabels(
IReadOnlyList<WorkspacePaneViewModel> workspacePanes,
Func<string, WorkspacePaneDocument?> getPaneDocument)
{
for (var i = 0; i < workspacePanes.Count; i++) // <-- internal WorkspacePanes, NOT WorkspacesDock.VisibleDockables
if (getPaneDocument(workspacePanes[i].Id) is { } paneDoc)
paneDoc.EffectiveTabHeader.AltShortcutLabel = AltShortcutLabelForIndex(i);
}
// MainWindowViewModel.cs:1791-1798
private void OnGoToWorkspacePaneAtIndex(int index)
{
if (index < 0 || index >= this.WorkspacePanes.Count) return;
this.SelectedWorkspacePane = this.WorkspacePanes[index]; // <-- indexed off internal list
…
}
Why the internal list is the wrong source of truth
WorkspacePaneViewModel.Tabs (WorkspacePaneViewModel.cs:88) is our own ObservableCollection<WorkspaceTabViewModel> bound as the ItemsSource of a single WorkspaceContentDock (WorkspaceDockFactory.cs:115-128). It is a flat, single-strip projection:
- It maps 1:1 to one strip only. Once a user splits/floats documents,
pane.ContentLayout holds multiple DocumentDock strips, and the flat Tabs order no longer corresponds to any single visual strip's order.
- Its order only tracks the visual order because
SyncPaneTabsOrderFromDock (MainWindowViewModel.cs:2626-2647) copies VisibleDockables order back into Tabs — but only when SyncPaneTabsFromDockChange (:2603-2619) decides to, i.e. only on Move/Reset:
// MainWindowViewModel.cs:2603-2619
if (e.Action == NotifyCollectionChangedAction.Remove) { /* close/float only */ }
else if (e.Action is NotifyCollectionChangedAction.Move or NotifyCollectionChangedAction.Reset)
SyncPaneTabsOrderFromDock(workspacePane, documentDock);
// Add + the Remove+insert that Dock.Avalonia's drag path emits are NOT re-synced.
So after a live drag-reorder (Remove+insert, not Move) or an #1065 insert-to-the-right move, RefreshActiveWorkspaceAltShortcutLabels re-reads a stale pane.Tabs and re-labels tabs in the old order — the originally reported "badge 2 retained after reordering" symptom. The Dock's VisibleDockables is always in correct visual order; the projection is not. The correct fix is to read the Dock directly and stop depending on the projection for indexing/badges.
Affected Files
| File |
Contribution |
Phantom.Workspaces/ViewModels/MainWindowViewModel.cs |
OnGoToTabAtIndex (:1759), ComputeActiveWorkspaceTabOrder/ComputeGlobalTabOrder (:2779, :2754) read pane.Tabs; RefreshActiveWorkspaceAltShortcutLabels (:2691), PropagateBadgeVisibility (:2791) iterate pane.Tabs; RefreshWorkspacePaneAltShortcutLabels (:2674) / OnGoToWorkspacePaneAtIndex (:1791) index this.WorkspacePanes; FindDocumentDock (:2816) and EnumerateAllDocuments (:2843) are the Dock-traversal building blocks; SyncPaneTabsFromDockChange/SyncPaneTabsOrderFromDock (:2603, :2626) is the back-sync that indexing/badges will no longer depend on |
Phantom.Workspaces/ViewModels/WorkspaceDockFactory.cs |
Root WorkspacesPaneDock (Id="WorkspacesDock", ItemsSource=WorkspacePanes, :79-107) = Alt+Shift+N host; per-pane WorkspaceContentDock (Id="WorkspaceContent_{paneId}", ItemsSource=workspacePane.Tabs, :115-141) = Alt+N scope; GetDocumentForTab (:44), GetPaneDocument (:71) |
Phantom.Workspaces/ViewModels/WorkspaceContentDock.cs |
The DocumentDock strip type (VisibleDockables, Owner) hosting content documents |
Phantom.Workspaces/ViewModels/WorkspaceDocument.cs |
WorkspaceDocument is the content IDockable; TabViewModel (:165, base.Context as WorkspaceTabViewModel) maps a dockable back to its tab VM; EffectiveTabHeader (:109) carries AltShortcutLabel/IsShortcutBadgeVisible |
Phantom.Workspaces/ViewModels/WorkspacePaneDocument.cs |
The workspace-tab IDockable hosted by WorkspacesPaneDock; its EffectiveTabHeader carries the Alt+Shift+N badge |
Phantom.Workspaces/ViewModels/WorkspacePaneViewModel.cs |
Tabs (:88) remains the content ItemsSource + membership source, but is no longer the indexing/badge source of truth |
Design / Fix (Dock primitives)
Compute all four things — Alt+N order, Alt+N badges, Alt+Shift+N order, Alt+Shift+N badges — from IDock.VisibleDockables, mapping each dockable back to its VM.
1. Enumerate the active workspace's content dockables in visual order (Alt+N)
pane.ContentLayout is an IRootDock; its content strip(s) are WorkspaceContentDock : DocumentDock. Walk the tree and take WorkspaceDocuments in visual order. EnumerateAllDocuments (:2843) already recurses VisibleDockables depth-first and handles split content docks:
private List<(WorkspacePaneViewModel Pane, WorkspaceDocument Document)> ComputeActiveWorkspaceTabOrder()
{
var pane = this.selectedWorkspacePane;
if (pane?.ContentLayout is null || pane.Id.StartsWith("loading-workspace:", StringComparison.Ordinal))
return new();
// Source of truth = the Dock tree's VisibleDockables order, NOT pane.Tabs.
return EnumerateAllDocuments(pane.ContentLayout)
.Select(doc => (pane, doc))
.ToList();
}
EnumerateAllDocuments yields documents in VisibleDockables order across every DocumentDock strip in the workspace, so a split layout numbers strip-by-strip in visual order. (If per-strip restart is desired later, iterate each WorkspaceContentDock.VisibleDockables separately — but for Alt+N the active workspace's documents form one numbering 1..N, consistent with #1043.)
2. Assign Alt+N badges from the same Dock-derived order
RefreshActiveWorkspaceAltShortcutLabels clears every document's label, then assigns from the Dock-derived order. Clearing should also iterate the Dock, not pane.Tabs:
private void RefreshActiveWorkspaceAltShortcutLabels()
{
foreach (var pane in this.WorkspacePanes)
if (pane.ContentLayout is { } layout)
foreach (var doc in EnumerateAllDocuments(layout))
doc.EffectiveTabHeader.AltShortcutLabel = null;
AssignGlobalAltShortcutLabels(this.ComputeActiveWorkspaceTabOrder()); // now Dock-derived
}
3. Alt+N badge visibility from the Dock
private void PropagateBadgeVisibility(bool isAltHeld, bool isShiftHeld)
{
var contentBadge = isAltHeld && !isShiftHeld;
foreach (var pane in this.WorkspacePanes)
{
var isActive = ReferenceEquals(pane, this.selectedWorkspacePane);
if (pane.ContentLayout is not { } layout) continue;
foreach (var doc in EnumerateAllDocuments(layout))
doc.EffectiveTabHeader.IsShortcutBadgeVisible = isActive && contentBadge;
}
// Alt+Shift+N workspace-pane badges from the workspace-tab host's VisibleDockables (see 4).
var paneBadge = isAltHeld && isShiftHeld;
var host = this.FindDocumentDock(this.Layout); // the WorkspacesPaneDock
if (host?.VisibleDockables is { } dockables)
foreach (var paneDoc in dockables.OfType<WorkspacePaneDocument>())
paneDoc.EffectiveTabHeader.IsShortcutBadgeVisible = paneBadge;
}
4. Alt+Shift+N order and activation from the workspace-tab host dock
The workspace-tab host is the root WorkspacesPaneDock (Id="WorkspacesDock"), reachable via FindDocumentDock(this.Layout). Its VisibleDockables are WorkspacePaneDocuments in visual order. Number and resolve off that:
private IReadOnlyList<WorkspacePaneDocument> WorkspaceTabHostOrder()
=> this.FindDocumentDock(this.Layout)?.VisibleDockables?.OfType<WorkspacePaneDocument>().ToList()
?? [];
private void RefreshWorkspacePaneAltShortcutLabels()
{
var order = this.WorkspaceTabHostOrder();
for (var i = 0; i < order.Count; i++)
order[i].EffectiveTabHeader.AltShortcutLabel = AltShortcutLabelForIndex(i);
}
private void OnGoToWorkspacePaneAtIndex(int index)
{
var order = this.WorkspaceTabHostOrder();
if (index < 0 || index >= order.Count) return;
var paneDoc = order[index];
// map dockable -> pane VM (paneDoc.Context / registry) and activate:
this.dockFactory.SetActiveDockable(paneDoc);
…
}
5. Dockable → VM mapping for activation
- Content:
WorkspaceDocument.TabViewModel (WorkspaceDocument.cs:165) gives the WorkspaceTabViewModel; the document itself is the dockable, so SetActiveDockable(doc) / SetFocusedDockable(dock, doc) activate it. The owning strip for focus is doc.Owner as IDock (or FindDocumentDock(pane.ContentLayout)).
- Workspace:
WorkspacePaneDocument maps back to its WorkspacePaneViewModel; GetPaneDocument(paneId) remains the id→dockable registry.
Why this stays correct after the #1065 insert-to-the-right move
#1065 makes the Dock the ordering authority: a new tab is moved to sourceIndex + 1 inside the source document's DocumentDock.VisibleDockables. Because indexing/badges now read VisibleDockables directly, the numbering reflects the new visual order immediately, without waiting for SyncPaneTabsOrderFromDock to copy the order back into pane.Tabs. This also fixes the original drag-reorder symptom: the drag path's Remove+insert changes VisibleDockables, and reading it directly always yields the current visual order.
Relationship to #1043 and #1065
Considered / Background (rejected: wrong layer)
The current implementation treats WorkspacePaneViewModel.Tabs (and MainWindowViewModel.WorkspacePanes) as the ordering source of truth and keeps it aligned to the Dock with a back-sync (SyncPaneTabsOrderFromDock). Rejected per the maintainer: the internal flat list is a single-strip projection that cannot represent split/floated strips and only appears correct when the back-sync fires. Depending on it for Alt+N and badge labels is incorrect; the Dock VisibleDockables tree is the real visual structure and must be the source.
Remaining legitimate consumers of the internal Tabs list (keep — do NOT remove in this bug)
Tabs is not removed by #1067; it stays for uses unrelated to visual ordering of indexing/badges:
ItemsSource generation — WorkspaceContentDock.ItemsSource = workspacePane.Tabs (WorkspaceDockFactory.cs:124) is what creates the WorkspaceDocument dockables via WorkspaceDocumentGenerator. Removing Tabs would remove the documents themselves.
- Membership / lookup / open / close / replace — many sites resolve or mutate tabs by id via
pane.Tabs (e.g. MainWindowViewModel.cs:264, 1725, 1930, 1949, 2178, 2223-2236, 2255-2267, 2327-2340). These use Tabs for membership, which is legitimate.
AnyTabIsRunning status aggregation — WorkspacePaneViewModel.RecomputeAnyTabIsRunning (WorkspacePaneViewModel.cs:113, 191-195) and notification aggregation iterate Tabs.
- Persistence write-back —
AppendWorkspaceTabRelationshipChanges / WriteBackWorkspaceTabs (MainWindowViewModel.cs:2500) derive saved workspace-tab relationships from Tabs. (This consumes membership; if saved order must match the visual order, the existing SyncPaneTabsOrderFromDock back-sync can remain for persistence only — indexing/badges no longer depend on it.)
Scope: #1067 switches only Alt+N/Alt+Shift+N indexing and badge computation to Dock primitives. Fully retiring the Tabs projection everywhere is a larger, higher-risk refactor and is explicitly out of scope here.
Expected Tests
Match the existing style in Phantom.Workspaces.Tests/MainWindowIntegrationTests.cs (e.g. GoToTabAtIndexCommand_WithThreeTabs_ActivatesCorrectTab at :1275, badge assertions reading documentDock.VisibleDockables … EffectiveTabHeader.AltShortcutLabel around :4117). Existing tests that assert badges/indexing off pane.Tabs should be updated to assert off VisibleDockables.
| Test Name |
Class |
What It Verifies |
AltN_IndexesFromActiveWorkspaceDockVisibleDockables |
MainWindowIntegrationTests |
Alt+N order is computed from the active workspace's content DocumentDock.VisibleDockables (documents in visual order), not from pane.Tabs |
AltShiftN_IndexesFromWorkspaceTabHostVisibleDockables |
MainWindowIntegrationTests |
Alt+Shift+N order is computed from the WorkspacesPaneDock.VisibleDockables (WorkspacePaneDocuments in visual order), not from this.WorkspacePanes |
BadgeLabels_DeriveFromDockVisibleDockablesOrder |
MainWindowIntegrationTests |
Content-tab AltShortcutLabels are assigned 1..N following VisibleDockables order per strip |
AltN_AfterInsertToRight_ReflectsVisualOrder |
MainWindowIntegrationTests |
After the #1065 insert-to-the-right move (Dock VisibleDockables reordered), Alt+N numbering and badges reflect the new visual order without depending on SyncPaneTabsOrderFromDock |
AltN_AfterDragReorderViaRemoveAndInsert_ReflectsVisualOrder |
MainWindowIntegrationTests |
Reordering by removing a dockable from VisibleDockables and re-inserting it (the live drag path, which fires Remove+insert not Move) updates badges/indexing to the new visual order |
Indexing_DoesNotConsultInternalTabsList |
MainWindowIntegrationTests |
When pane.Tabs order is deliberately out of sync with VisibleDockables, Alt+N resolves the tab at the visual position (proving the Dock, not Tabs, is the source of truth) |
AltShortcut_AfterReorder_ActivatesDockableAtNewPosition |
MainWindowIntegrationTests |
Pressing Alt+N after a reorder activates the dockable now displaying badge N (badge remains the single source of truth, mapped via WorkspaceDocument.TabViewModel) |
SplitWorkspace_ContentBadges_FollowVisibleDockablesAcrossStrips |
MainWindowIntegrationTests |
With the active workspace's content split into two DocumentDock strips, content badges number 1..N across strips in VisibleDockables order |
Related
Summary
Alt+N / Alt+Shift+N keyboard tab indexing and the numbered tab badges are computed from our internal flat
ObservableCollectionprojections (WorkspacePaneViewModel.Tabsfor content tabs,MainWindowViewModel.WorkspacePanesfor workspace tabs) instead of from the Avalonia.Dock structure (theIDock.VisibleDockablesof the real visual tab strips). The internalTabslist is a flat, single-strip projection that cannot represent split/floated strips and only stays in visual order because a fragile back-sync (SyncPaneTabsOrderFromDock) copies the Dock order into it after certain reorder events. Consulting that projection is the wrong source of truth: it diverges from the visual order under splits and under the #1065 insert-to-the-right move whenever the back-sync does not fire (e.g. the drag path emitsRemove+insert, notMove).The fix is to compute both numberings and both sets of badges directly from the Dock
VisibleDockables, per visual strip and per active workspace, keeping #1043's active-workspace scoping:DocumentDock.VisibleDockables(WorkspaceContentDock, reached viapane.ContentLayout), in visual order.VisibleDockables(the rootWorkspacesPaneDock,Id="WorkspacesDock"), in visual order.A dockable is mapped back to its
WorkspaceTabViewModelviaWorkspaceDocument.TabViewModel(and pane dockables viaGetPaneDocument) for activation and label assignment.Root Cause / Current State
All indexing/badge logic is in
Phantom.Workspaces/ViewModels/MainWindowViewModel.cs.Alt+N content-tab order is read from the internal
TabslistOnGoToTabAtIndexresolves the target throughComputeActiveWorkspaceTabOrder():ComputeActiveWorkspaceTabOrder()delegates toComputeGlobalTabOrder([pane], …), which walkspane.Tabs— the internalObservableCollection<WorkspaceTabViewModel>— not the Dock:Alt+N badge labels are assigned by iterating the internal
TabslistAlt+N badge visibility iterates the internal
TabslistAlt+Shift+N workspace-tab order is read from the internal
WorkspacePaneslistWhy the internal list is the wrong source of truth
WorkspacePaneViewModel.Tabs(WorkspacePaneViewModel.cs:88) is our ownObservableCollection<WorkspaceTabViewModel>bound as theItemsSourceof a singleWorkspaceContentDock(WorkspaceDockFactory.cs:115-128). It is a flat, single-strip projection:pane.ContentLayoutholds multipleDocumentDockstrips, and the flatTabsorder no longer corresponds to any single visual strip's order.SyncPaneTabsOrderFromDock(MainWindowViewModel.cs:2626-2647) copiesVisibleDockablesorder back intoTabs— but only whenSyncPaneTabsFromDockChange(:2603-2619) decides to, i.e. only onMove/Reset:So after a live drag-reorder (Remove+insert, not
Move) or an #1065 insert-to-the-right move,RefreshActiveWorkspaceAltShortcutLabelsre-reads a stalepane.Tabsand re-labels tabs in the old order — the originally reported "badge2retained after reordering" symptom. The Dock'sVisibleDockablesis always in correct visual order; the projection is not. The correct fix is to read the Dock directly and stop depending on the projection for indexing/badges.Affected Files
Phantom.Workspaces/ViewModels/MainWindowViewModel.csOnGoToTabAtIndex(:1759),ComputeActiveWorkspaceTabOrder/ComputeGlobalTabOrder(:2779,:2754) readpane.Tabs;RefreshActiveWorkspaceAltShortcutLabels(:2691),PropagateBadgeVisibility(:2791) iteratepane.Tabs;RefreshWorkspacePaneAltShortcutLabels(:2674) /OnGoToWorkspacePaneAtIndex(:1791) indexthis.WorkspacePanes;FindDocumentDock(:2816) andEnumerateAllDocuments(:2843) are the Dock-traversal building blocks;SyncPaneTabsFromDockChange/SyncPaneTabsOrderFromDock(:2603,:2626) is the back-sync that indexing/badges will no longer depend onPhantom.Workspaces/ViewModels/WorkspaceDockFactory.csWorkspacesPaneDock(Id="WorkspacesDock",ItemsSource=WorkspacePanes,:79-107) = Alt+Shift+N host; per-paneWorkspaceContentDock(Id="WorkspaceContent_{paneId}",ItemsSource=workspacePane.Tabs,:115-141) = Alt+N scope;GetDocumentForTab(:44),GetPaneDocument(:71)Phantom.Workspaces/ViewModels/WorkspaceContentDock.csDocumentDockstrip type (VisibleDockables,Owner) hosting content documentsPhantom.Workspaces/ViewModels/WorkspaceDocument.csWorkspaceDocumentis the contentIDockable;TabViewModel(:165,base.Context as WorkspaceTabViewModel) maps a dockable back to its tab VM;EffectiveTabHeader(:109) carriesAltShortcutLabel/IsShortcutBadgeVisiblePhantom.Workspaces/ViewModels/WorkspacePaneDocument.csIDockablehosted byWorkspacesPaneDock; itsEffectiveTabHeadercarries the Alt+Shift+N badgePhantom.Workspaces/ViewModels/WorkspacePaneViewModel.csTabs(:88) remains the contentItemsSource+ membership source, but is no longer the indexing/badge source of truthDesign / Fix (Dock primitives)
Compute all four things — Alt+N order, Alt+N badges, Alt+Shift+N order, Alt+Shift+N badges — from
IDock.VisibleDockables, mapping each dockable back to its VM.1. Enumerate the active workspace's content dockables in visual order (Alt+N)
pane.ContentLayoutis anIRootDock; its content strip(s) areWorkspaceContentDock : DocumentDock. Walk the tree and takeWorkspaceDocuments in visual order.EnumerateAllDocuments(:2843) already recursesVisibleDockablesdepth-first and handles split content docks:EnumerateAllDocumentsyields documents inVisibleDockablesorder across everyDocumentDockstrip in the workspace, so a split layout numbers strip-by-strip in visual order. (If per-strip restart is desired later, iterate eachWorkspaceContentDock.VisibleDockablesseparately — but for Alt+N the active workspace's documents form one numbering1..N, consistent with #1043.)2. Assign Alt+N badges from the same Dock-derived order
RefreshActiveWorkspaceAltShortcutLabelsclears every document's label, then assigns from the Dock-derived order. Clearing should also iterate the Dock, notpane.Tabs:3. Alt+N badge visibility from the Dock
4. Alt+Shift+N order and activation from the workspace-tab host dock
The workspace-tab host is the root
WorkspacesPaneDock(Id="WorkspacesDock"), reachable viaFindDocumentDock(this.Layout). ItsVisibleDockablesareWorkspacePaneDocuments in visual order. Number and resolve off that:5. Dockable → VM mapping for activation
WorkspaceDocument.TabViewModel(WorkspaceDocument.cs:165) gives theWorkspaceTabViewModel; the document itself is the dockable, soSetActiveDockable(doc)/SetFocusedDockable(dock, doc)activate it. The owning strip for focus isdoc.Owner as IDock(orFindDocumentDock(pane.ContentLayout)).WorkspacePaneDocumentmaps back to itsWorkspacePaneViewModel;GetPaneDocument(paneId)remains the id→dockable registry.Why this stays correct after the #1065 insert-to-the-right move
#1065 makes the Dock the ordering authority: a new tab is moved to
sourceIndex + 1inside the source document'sDocumentDock.VisibleDockables. Because indexing/badges now readVisibleDockablesdirectly, the numbering reflects the new visual order immediately, without waiting forSyncPaneTabsOrderFromDockto copy the order back intopane.Tabs. This also fixes the original drag-reorder symptom: the drag path'sRemove+insert changesVisibleDockables, and reading it directly always yields the current visual order.Relationship to #1043 and #1065
Tabs/WorkspacePaneslists to the DockVisibleDockables. It therefore supersedes Alt+N tab indices computed globally across workspaces instead of scoped to the active workspace's content dock #1043's implementation choice of readingpane.TabsinsideComputeActiveWorkspaceTabOrder/ComputeGlobalTabOrder, while keeping Alt+N tab indices computed globally across workspaces instead of scoped to the active workspace's content dock #1043's behavioural contract (each workspace numbers1..N; badges only on the active workspace; Alt+Shift+N independent).DocumentDock/VisibleDockablesas the ordering authority for inserts. Alt+N/Alt+Shift+N indexing and tab badges must be computed from Avalonia.Dock VisibleDockables, not the internal flat Tabs list #1067 aligns indexing/badges with that same authority, removing the reliance on theTabsback-sync so the two cannot diverge.Considered / Background (rejected: wrong layer)
The current implementation treats
WorkspacePaneViewModel.Tabs(andMainWindowViewModel.WorkspacePanes) as the ordering source of truth and keeps it aligned to the Dock with a back-sync (SyncPaneTabsOrderFromDock). Rejected per the maintainer: the internal flat list is a single-strip projection that cannot represent split/floated strips and only appears correct when the back-sync fires. Depending on it for Alt+N and badge labels is incorrect; the DockVisibleDockablestree is the real visual structure and must be the source.Remaining legitimate consumers of the internal
Tabslist (keep — do NOT remove in this bug)Tabsis not removed by #1067; it stays for uses unrelated to visual ordering of indexing/badges:ItemsSourcegeneration —WorkspaceContentDock.ItemsSource = workspacePane.Tabs(WorkspaceDockFactory.cs:124) is what creates theWorkspaceDocumentdockables viaWorkspaceDocumentGenerator. RemovingTabswould remove the documents themselves.pane.Tabs(e.g.MainWindowViewModel.cs:264, 1725, 1930, 1949, 2178, 2223-2236, 2255-2267, 2327-2340). These useTabsfor membership, which is legitimate.AnyTabIsRunningstatus aggregation —WorkspacePaneViewModel.RecomputeAnyTabIsRunning(WorkspacePaneViewModel.cs:113, 191-195) and notification aggregation iterateTabs.AppendWorkspaceTabRelationshipChanges/WriteBackWorkspaceTabs(MainWindowViewModel.cs:2500) derive saved workspace-tab relationships fromTabs. (This consumes membership; if saved order must match the visual order, the existingSyncPaneTabsOrderFromDockback-sync can remain for persistence only — indexing/badges no longer depend on it.)Scope: #1067 switches only Alt+N/Alt+Shift+N indexing and badge computation to Dock primitives. Fully retiring the
Tabsprojection everywhere is a larger, higher-risk refactor and is explicitly out of scope here.Expected Tests
Match the existing style in
Phantom.Workspaces.Tests/MainWindowIntegrationTests.cs(e.g.GoToTabAtIndexCommand_WithThreeTabs_ActivatesCorrectTabat:1275, badge assertions readingdocumentDock.VisibleDockables … EffectiveTabHeader.AltShortcutLabelaround:4117). Existing tests that assert badges/indexing offpane.Tabsshould be updated to assert offVisibleDockables.AltN_IndexesFromActiveWorkspaceDockVisibleDockablesMainWindowIntegrationTestsDocumentDock.VisibleDockables(documents in visual order), not frompane.TabsAltShiftN_IndexesFromWorkspaceTabHostVisibleDockablesMainWindowIntegrationTestsWorkspacesPaneDock.VisibleDockables(WorkspacePaneDocuments in visual order), not fromthis.WorkspacePanesBadgeLabels_DeriveFromDockVisibleDockablesOrderMainWindowIntegrationTestsAltShortcutLabels are assigned1..NfollowingVisibleDockablesorder per stripAltN_AfterInsertToRight_ReflectsVisualOrderMainWindowIntegrationTestsVisibleDockablesreordered), Alt+N numbering and badges reflect the new visual order without depending onSyncPaneTabsOrderFromDockAltN_AfterDragReorderViaRemoveAndInsert_ReflectsVisualOrderMainWindowIntegrationTestsVisibleDockablesand re-inserting it (the live drag path, which firesRemove+insert notMove) updates badges/indexing to the new visual orderIndexing_DoesNotConsultInternalTabsListMainWindowIntegrationTestspane.Tabsorder is deliberately out of sync withVisibleDockables, Alt+N resolves the tab at the visual position (proving the Dock, notTabs, is the source of truth)AltShortcut_AfterReorder_ActivatesDockableAtNewPositionMainWindowIntegrationTestsWorkspaceDocument.TabViewModel)SplitWorkspace_ContentBadges_FollowVisibleDockablesAcrossStripsMainWindowIntegrationTestsDocumentDockstrips, content badges number1..Nacross strips inVisibleDockablesorderRelated
VisibleDockablesas the ordering authority for inserts.