The global:: prefixes existed because the test project's namespace
ICSharpCode.ILSpy.Tests used to shadow the app's old top-level ILSpy
namespace. With the UI code back under ICSharpCode.ILSpy there is
nothing left to shadow, so plain fully qualified names resolve fine.
Assisted-by: Claude:claude-fable-5:Claude Code
The Avalonia port had placed the UI app in an ILSpy.* namespace tree,
while the csproj RootNamespace and every prior release (through 10.1)
use ICSharpCode.ILSpy.*. Restoring the historical namespace reduces the
public API diff against release/10.1 for plugin authors and removes the
shadowing that forced global:: qualifiers in the test project. The
Images class and AccessOverlayIcon enum move back into the root
namespace (as in 10.1), since an ICSharpCode.ILSpy.Images namespace
would shadow the Images class for all code inside ICSharpCode.ILSpy.
Assisted-by: Claude:claude-fable-5:Claude Code
The freeze work pointed Dock's DocumentContextMenu (which it shows on a
document-tab right-click, overriding the standard ContextMenu) at a menu
holding only "Freeze tab". That hid the Close / Close all but this /
Close all entries defined in App.axaml, and on a frozen tab — where the
Freeze item is hidden — the whole menu came up empty.
Build the full menu (Close / Close all but this / Close all, plus the
preview-only Freeze entry) in PreviewTabContextMenuBehavior and drop the
now-superseded App.axaml ContextMenu setter, so there is a single source
for the document-tab menu.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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.
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