The document tabs had two parallel hierarchies: the ContentTabPage dockable wrapper, and four unrelated content viewmodels in its object? Content slot -- three TabPageModel subclasses plus OptionsPageModel, which derived from a bare ObservableObject. The wrapper bridged the gap by reflection (reading Title off the runtime type) and the router duck-typed IsStaticContent across two unrelated classes.
Collapse the content side under one abstract ContentPageModel base (named to avoid a clash with Avalonia.Controls.ContentPage). OptionsPageModel joins it, dropping its duplicate Title/IsStaticContent. ContentTabPage.Content becomes ContentPageModel?, so title/language-switching read directly and IsWritablePreview collapses from two runtime type-tests to one IsStaticContent check. CreateTab and the dock-router helpers are typed through. Morph-in-place is unchanged -- the One still swaps its Content in place; this only strengthens the types behind it.
MoveDockable freezes the One on in-strip and cross-dock drags, but the tear-off-to-floating-window gesture never reaches it: Dock funnels that through CreateWindowFrom (FloatDockable -> SplitToWindow -> CreateWindowFrom), where the torn-off document is still the One before it gets wrapped in a fresh dock. Overriding it makes a float-out one more way to keep the tab, consistent with every other drag.
Pinning the One immovably at index 0 made a drag on it a dead no-op, which reads as a broken tab. Dragging now freezes it instead: pulling the tab out is itself the gesture to keep it, mirroring the snowflake and right-click Freeze, and the next tree selection forges a fresh preview at index 0. Index-0 protection and the self-healing re-assert now apply only while the One is still the preview.
Ctrl+W closes the active document; closing the One drops the cached decompiler viewmodel so the next selection rebuilds it. Long type signatures no longer stretch a tab without bound -- the on-tab title is capped and ellipsised with the full title in a tooltip, and the close button names its shortcut.
The One preview tab's visuals had grown as ad-hoc IsPreview converter bindings
layered on shared selectors, each forced to also answer "what about a non-preview
tab?". The trick that handled the negative case -- a converter returning
UnsetValue so a DynamicResource FallbackValue could win -- misfired and stripped
the blue hover from regular tabs entirely.
Drive the distinction with a previewTab style class instead, toggled on each
DocumentTabStripItem from IsPreview by PreviewTabClassBehavior. The One's purple
fill, hover, accent line and italic title now hang off `.previewTab` selectors
that match ONLY the One, so frozen tabs are never touched and keep their theme
states intact -- the regression becomes structurally impossible. Every color is a
named token declared in both the light and dark dictionaries, and the focused
tool-pane chrome is neutralised (a subtle header tint plus a readable title in
place of the saturated system accent and its white-on-white text).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The One must stay leftmost: a frozen tab dragged before it, or the One dragged
out of slot 0, would break the 'preview is always first' rule. Dock's in-strip
reorder commits through FactoryBase.MoveDockable (the tab strip ignores
IDockable.CanDrag), so override it: refuse moving the One, and clamp any tab
targeting the One's slot to position 1 instead. Override the cross-dock variant
too, and add an idempotent OnDockableMoved guard that re-asserts the One at
index 0 against any drag path.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The preview tab only got reused when it was the active document; selecting a
node while a frozen tab was active spawned a fresh preview beside it, so
previews piled up. Make 'the One' an invariant: tree selections always
find-or-create the single preview tracked by factory.MainTab and reuse it
wherever it sits (activating it), only forging a new one when it was frozen
away or closed. The fresh One is inserted at documents-dock index 0 (leftmost)
rather than appended, so the preview always sits at the front.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The freeze affordance was a pushpin, and the preview-tab accent stripe was the
same blue as the selection highlight -- so a selected preview tab read as just
'selected'. Swap the glyph for a snowflake (matches the Freeze verb) and the
accent to purple (#9B59B6), keeping the One visually distinct from a
blue-highlighted selected tab. The snowflake is a stroked vector Path with its
Stroke bound to the tab Foreground (theme-inherited), six spokes + tip barbs,
no rotation.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Pure rename, no behavior change. 'Pin' conventionally means stuck-at-front-of-
strip + excluded-from-close-all, which is a different (future) feature; the
gesture that makes the preview tab stop following the tree selection is now
called Freeze throughout: FreezeCurrentTab / FreezeCurrentMainTab, the
PreviewTabFreezeButtonBehavior, the 'Freeze tab' menu entry/tooltip, and the
Window_FreezeCurrentTab command + resource. The pushpin glyph and accent colour
are unchanged here -- they move in the following commit.
Assisted-by: Claude:claude-opus-4-8:Claude Code
To audit what each UI test actually exercises, every step now snapshots the
live window to <TestFixtureName>/<TestName>_<NN>_<ShortDescription>.png: a
booted frame (emitted automatically by TestHarness.BootAsync), one after each
state-changing action, and one before each assertion. Flip ILSPY_TESTS_VISIBLE=1
to render the filmstrip; it lands under %TEMP%/ilspy-test-captures (overridable
via ILSPY_TEST_CAPTURES).
The step number and fixture/test name are derived automatically so inserting a
breakpoint never renumbers the rest. The identity is recorded up front from the
real ITest in an ITestAction hook rather than read live: NUnit's
TestContext.CurrentContext does not flow onto async continuations, so a capture
after an await would otherwise collide under the ad-hoc context. And when
rendering is off the whole call is a true no-op -- not even a dispatcher pump --
so instrumenting a test can never perturb the navigation/tab timing it asserts
on. Full headless suite stays green.
Nearly every headless UI test opened with the same four-line prologue
(resolve the shared MainWindow, show it, cast its DataContext, wait for the
assembly list), then repeated the corelib lookup, the EnsureLazyChildren +
Children.OfType<T>().Single() drill, the registry single-by-header lookups,
and the open-an-assembly-and-wait dance. The duplication made the intent of
each test hard to see and every signature tweak a suite-wide edit.
Collapse those into TestHarness (BootAsync, OpenAssemblyAsync, GetCommand,
GetEntry) and TreeNavigation extensions (FindCoreLib, GetChild<T>, Expand<T>),
then apply them across the suite. Net ~865 lines of boilerplate removed with
no change in behaviour; the full headless suite stays green.
Closing and reopening Options or About built a fresh ContentTabPage (and a
fresh owned view) each time, losing the selected options page and
re-rendering the About output. Retain these static-content tabs by key on
DockWorkspace and re-add the same instance on reopen, so the dockable --
and the view it owns via IDockableViewOwner -- persists for the session.
Embedded About resource pages (license, third-party notices) are
singletons too.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Pin the DockableViewRecycling contract: one view per dockable identity,
distinct views for distinct ContentTabPages, owners bypass the global
fallback cache, an already-parented view re-parents without throwing, and
the active document tab renders its own view (the slot-sharing guard).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The MainMenu UserControl previously built a regular Avalonia Menu of
MenuItems, which on macOS would render inline in the window instead
of in the system menu bar -- not what Mac users expect. Avalonia's
NativeMenu + NativeMenuBar is the cross-platform abstraction: on
macOS the menu is projected into the system bar, on Windows / Linux
NativeMenuBar's presenter renders the same items inline. The MEF
registry, theme submenu, tool-pane toggles, and dynamic tab list all
flow through unchanged; only the leaf widget type swaps from MenuItem
to NativeMenuItem. KeyModifiers.Control is translated to Meta on
macOS so the system menu bar shows Cmd glyphs instead of Ctrl.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The four `WaitForDecompiledTextAsync` calls in PreviewTabPromotionTests
asserted only on dock structure -- SourceNode, IsPreview, tab count,
Content reference -- never on decompiled Text. Those values are set
synchronously by ShowSelectedNode before DecompileAsync's first await
returns, so the ~25s wait was pure dead weight. Worse, the fire-and-forget
DecompileAsync kept chewing CPU after the test returned and contended on
the ThreadPool with the next test's decompile, which is what pushed the
same workload from 12-16s in isolation to 24-28s in full-suite -- right
up against the 30s budget. One slow GC pause and PinCurrentTab_Flips
timed out at 30.997s exactly.
The InitMainMenu build path already inserts Separators between
MenuCategory groups (Open / Save / Remove / Exit in File, View /
Navigation / Options in View, etc.). The Simple-theme default style
renders them as a 1px line at very low contrast — barely visible
against the menu background, which is why "no groups in the menus"
was a reasonable read.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Previously pin used an explicit 22x22 size, custom padding, and a
Button.preview-pin class with hand-rolled #33000000/#55000000 hover
tints. The close button (its sibling in the tab strip) is a plain
Avalonia.Controls.Button with a ControlTheme applied by Dock's tab
template — no classes, no local sizing. Visual mismatch was noticeable.
Assisted-by: Claude:claude-opus-4-7:Claude Code
VS-style preview-tab semantics, refined per user spec: pinning a tab
should freeze its contents in place but NOT immediately spawn a fresh
preview tab beside it. The new preview tab opens lazily, on the next
tree-node selection that finds the active tab frozen — at which point
the new content lands in a brand-new tab and the pinned one is left
untouched.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Preview tab semantics. ContentTabPage gains IsPreview; the persistent MainTab
starts preview (tree-node clicks replace its Content in place). The user pins
via Window menu, right-click context menu, or inline pin icon — the
just-pinned tab keeps its content/identity and a fresh preview MainTab spawns.
Carve-out tabs (Open in new tab, Options) are born pinned and survive tree
selections.
Assisted-by: Claude:claude-opus-4-7:Claude Code
User-reported: opening multiple document tabs and restarting brings them
all back broken except the first one.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Sweep-up commit. The dotnet-format pre-commit hook keeps re-ordering
these usings on every other commit; landing them once stops the hook
from grumbling at unrelated diffs going forward.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Persists the live dock layout to ILSpy.Layout.json next to ILSpy.xml on
MainWindow.OnClosing; loads it on DockWorkspace ctor; falls back to
factory.CreateLayout when the file is absent or fails to deserialize.
WPF stays XML in ILSpy.xml — this is Avalonia-side only.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Brings the WPF decompiler-pipeline-stepper feature across. Available only
in Debug builds — the entire feature set is gated behind `#if DEBUG`, so
Release users see neither the pane nor the ILAst languages in the picker.
Replaces the temporary 3-column Grid placeholder in MainWindow with a
Dock.Avalonia DockControl driven by an ILSpyDockFactory and orchestrated
by a [Shared] DockWorkspace exported via MEF.
Assisted-by: Claude:claude-opus-4-7:Claude Code