Proves the TreeKeyboardController extraction: pressing Right expands a node in the analyzer tree the same way it does in the assembly tree, via the shared controller wired onto both.
Extract the standard tree keyboard gestures -- Left/Right expand-collapse + parent/child nav, numpad +/-/*, and new type-ahead incremental search -- out of AssemblyListPane into a reusable TreeKeyboardController that drives any hierarchical DataGrid. The model implements ITreeKeyboardTarget (PrimarySelectedNode + SelectNode) so each tree keeps its own select-and-reveal behaviour; expansion goes through the grid's IHierarchicalModel. Both the assembly tree and the analyzer tree now attach one, so they share consistent keyboard behaviour. Delete (unload) and Ctrl+R (analyze) stay assembly-specific in the pane.
Numpad + expands the focused node, - collapses it, and * expands it recursively. The recursive walk only descends into children whose SharpTreeNode.CanExpandRecursively is true, which is false for lazy-loading nodes -- so * stays bounded and won't try to materialise an entire assembly. Mirrors WPF SharpTreeView.
Standard tree keyboard navigation in the assembly list: Left collapses an expanded node or moves to the parent when already collapsed; Right expands a closed node or steps into its first child. Operates on the primary selection via the hierarchical model (FindNode/Expand/Collapse) and SelectNode; the parent step is gated on the parent being a visible row so it never selects the hidden root.
Pressing Delete unloaded the assembly but left the grid showing a row selected while model.SelectedItems went empty -- so a second Delete read an empty selection and no-op'd ("spamming Delete breaks"). Record the deleted node's flattened index and, once the visible tree rebuilds, re-select the node that now occupies that slot (clamped to the new end) via SelectNode, which re-syncs grid and model. Selects nothing when the list empties.
Unload / Clear / ReloadAssembly / HotReplaceAssembly all called LoadedAssembly.Dispose() on the assembly they removed, which disposes its MetadataFile and unmaps the underlying file. But open document tabs and tree nodes can still hold that MetadataFile, and the list has no safe point at which to know those references are gone -- so disposing risked unmapping a file out from under a live reader (use-after-dispose). Drop the removed assembly and let the GC reclaim it once nothing references it instead. The DockWorkspace cancel-on-remove is now a courtesy, not a guard against an unmap race.
Search, Analyzer and Debug Steps cluttered the default layout. They now opt out via ExportToolPane.IsVisibleByDefault = false (which BuildToolDock finally honours), so a fresh launch shows just the assembly tree. Each pane keeps its home alignment and is materialised there on demand by ShowToolPane, so opening Search / Analyze surfaces it in the same place as before.
ShowToolPane only activated panes still in the layout, so once the user closed the Analyzer panel (Dock removes the pane and collapses its now-empty dock) Analyze became a silent no-op -- the entity was added but the panel never reappeared. ShowToolPane now finds-or-rebuilds the pane's home ToolDock at its alignment, splicing it back into the layout where CreateLayout would have placed it, then adds and activates the pane. This same materialize-on-demand path is what lets panes be hidden by default.
The Display-options "Expand using declarations / member definitions after decompilation" checkboxes had no effect: the live decompile cloned DecompilerSettings and baked in the language version but never copied these two flags from DisplaySettings. TextTokenWriter reads settings.ExpandUsingDeclarations / ExpandMemberDefinitions to decide each fold's DefaultClosed, so without the bridge both defaulted false and every using / member fold came back collapsed regardless of the setting.
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.
DecompilerSettingsGroupViewModel carries the full tri-state AreAllItemsChecked plumbing to bulk-toggle every setting in a category (the C# language-version groups), but the panel's Expander header had been reduced to plain category text, so each group rendered without its checkbox. Restore the header CheckBox bound to AreAllItemsChecked, matching the original.
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
Switching to a document tab is supposed to pull the tree selection over to
what that tab shows, but it didn't reliably -- the gap was masked until the
right-click change stopped moving the selection on its own. Two problems:
The SelectedItem setter replaced the collection with Clear()+Add(), and the
transient empty step made the grid sync defer its completion flag, which then
suppressed the sync for the real new value -- so the tree visual stopped
following tab activation. Route every selection replacement through a single
batched SelectNodes() that fires the selection-changed fan-out once, with the
final set, so consumers never observe the transient empty (or a transient
multi, which would break metadata-tab reuse).
And a tab decompiled from several nodes carries no single SourceNode, so
activating it restored nothing. Restore the tab's full node set from
CurrentNodes (one or many), and mirror a multi-selection into the grid so every
restored node is highlighted, not just the primary.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Right-clicking a tree row to reach its context menu used to move the real
selection there first, because ProDataGrid selects the row on press. With the
preview document bound to the selection, that meant 'Decompile to new tab' on
node B (while viewing A) jumped the preview to B before the command ran, so you
ended up with B twice instead of the intended A + B. Middle-click avoided it,
but not every mouse has a usable one.
Capture the right-clicked row in ContextRequested (which fires even when a
previous menu's light-dismiss popup swallows the press) and swallow the
right-press so the grid never reselects: the menu now acts on the clicked row
as a Thunderbird-style context target while the selection -- and the document
-- stay put. The targeted row gets a faint focus-box highlight, cleared when
the menu closes (guarded so a stale menu's close can't wipe a newer target).
Also adds TestHarness.ClickItem to collapse the repeated
Items.OfType<MenuItem>().Single(...).RaiseEvent(...) menu-click dance.
Assisted-by: Claude:claude-opus-4-8:Claude Code
This fixture was missed by both the helper-extraction and visual-breakpoint
sweeps, so it still carried 16 copies of the boot prologue and no captures.
Route it through TestHarness.BootAsync / TreeNavigation and add a Step after
each decompile, matching every other headless fixture.
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 command resolves each selected assembly's references through the
per-assembly resolver, which adds the targets to the live list as
auto-loaded entries. It then finished with a full list Refresh (F5),
whose LoadList rebuilds the list from persisted state -- which never
contains on-demand auto-loaded assemblies -- so the just-resolved
dependencies were discarded the instant they were added and the command
looked like a no-op. Re-decompile the active view instead (the original
behaviour) without reloading the list. F5 still drops auto-loaded
assemblies by design; the two refresh paths must stay distinct.
Two selection-sync defects in the assembly tree. Ctrl+A only selected
the last row on the first press: SelectedItem is the last entry of
SelectedItems and every collection change re-raised it, so the grid->model
sync bounced back through SyncSelectionFromModel and assigned the singular
SelectedItem, collapsing the just-made multi-selection. The syncingSelection
guard now also covers that notification.
Clicking one row of a multi-selection left every row selected: ProDataGrid
keeps the selection on press so a row-drag can move all of them (multi-row
reorder is a real feature), but 12.0.0 has no release-side collapse for a
plain click. Mirror the usual behaviour by collapsing to the clicked row on
release when the pointer did not move far enough to be a drag.
The menu builder hard-set each NativeMenuItem's IsEnabled from static
export metadata (always true), overriding the state that assigning Command
would otherwise derive from the command's CanExecute. The inline
NativeMenuBar on Windows/Linux re-derives from the command and greys out
OS-gated items (e.g. Open from GAC, which is Windows-only), but the macOS
native menu reads NativeMenuItem.IsEnabled directly -- so those items
stayed enabled there even though invoking them is a no-op. Drop the
explicit IsEnabled so every command-backed item follows its CanExecute on
all platforms.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The banner used one amber colour for every state. Show green when ILSpy
is up to date and amber when an update is available, driven by a new
UpdatePanelViewModel.UpdateAvailable bound to a style class on the border.
The message text also inherited the dark theme's near-white foreground,
which was unreadable on the always-light banner; pin it to a dark colour
since the banner stays light in both themes.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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
Capture the editor's caret, scroll offset, and expanded foldings on
demand when a navigation record is written -- the model exposes a
CaptureViewState delegate the view sets and DockWorkspace invokes --
instead of pushing every caret/scroll event onto the view-model. The
push mistook AvaloniaEdit's programmatic caret-to-end on text replace for
a user move and recorded it, poisoning the captured position. Back /
Forward restores through a single PendingViewState channel the editor
consumes once the document Text lands.
AvaloniaEdit 12.0.0's ScrollToVerticalOffset is a no-op, so the editor's
ScrollViewer.Offset is set directly (TODO: drop once AvaloniaEdit #594
ships and the package is bumped).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two first-run behaviours that the previous version had and the port had
lost. When a launch has nothing to restore (no saved tree path, no
command-line target), greet the user with the About page in the main
tab instead of an empty "(no selection)" view; the greeting is fired
after the tree is ready and only takes the main tab when it's still
empty and active, so it can't steal activation from a tab the user
opened meanwhile (e.g. Compare...). And seed the default list with the
.NET framework ILSpy itself runs on - every managed assembly in the
running runtime's shared-framework directory. Headless tests opt out of
that seed (re-opening ~150 assemblies per test is ~10x slower) and fall
back to the minimal trio, leaving their expectations unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The dialog lost the entry point for building a preconfigured list when
the view-model wasn't ported; the backing AssemblyListManager.
CreateDefaultList was already present and cross-platform. Re-add the
button and a flyout of the generated lists. The three GAC-based
framework lists resolve nothing without a GAC, so they're only offered
when a GAC directory exists (i.e. on Windows); the per-installed-runtime
entries work everywhere.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The PE/metadata nodes had collapsed onto the generic MetadataTable glyph
during the port. Bring back the per-node icons the previous version
shipped: a Metadata glyph for the root, a Header glyph for the DOS/COFF/
Optional headers, a Heap glyph for the heaps, a table-group glyph for
"Tables", and the folder glyphs for the Data Directories container. The
four missing SVG assets are ported from history.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The +/- expander was a 13x13 toggle whose visible glyph (a 9x9 box) was
also the only hit-testable surface, so the real tap target was barely
9px. Grow the toggle to 16x16 but keep its laid-out width at 13 via a
negative right margin, because TreeLines hardcodes a 13px expander
column with the glyph centred at +8.5 and would otherwise misalign. A
transparent wrapper fills the 16x16 so the whole area receives input;
the visible glyph is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Compare opens a structural side-by-side diff of two assemblies --
it's an analytical operation, not navigation between existing
documents. With Compare in Navigate it sat next to "Decompile to
new tab", which is genuinely navigational; users reading the menu
top-to-bottom had to mentally split the group.
Move it into Analyze (matching the resource string already used by
"Search Microsoft Docs") and place it at Order 220 so it sits
immediately above Search in the rendered menu. The two Analyze
entries now form a single coherent group of "tell me more about
this code" actions, and the Navigate group is left to actions that
actually navigate.
Assisted-by: Claude:claude-opus-4-7:Claude Code
WPF's CSharpLanguage doesn't override DecompileNamespace -- clicking
a namespace falls through to the base Language.DecompileNamespace,
which writes just the namespace name as a `// Foo.Bar` comment.
The avalonia port's override decompiled every top-level type in the
namespace, which on a large assembly turned a single click into a
multi-second blocking decompile of hundreds of types.
Remove the override so clicking a namespace lands the same cheap
comment-only output the WPF app shows. Users who do want the full
namespace decompiled can still get it via right-click > Save Code,
which routes through a separate path that constructs the project
explicitly.
ILLanguage's override is intentionally preserved -- IL mode in WPF
does disassemble the namespace; we match that.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Matches the WPF behaviour at master:ILSpy/AssemblyTree/AssemblyTreeModel.cs:440
where DEBUG builds get "ILSpy {DecompilerVersionInfo.FullVersion}" so the
developer can tell from a glance which build the running window came from
(especially handy when the avalonia-preview tag floats forward and several
windows are open at once). Release builds keep the bare "ILSpy" title.
The title is bound from MainWindowViewModel.Title (axaml: Title="{Binding
Title}"), so threading the version through the VM is the natural seam --
no axaml change and the bound value can later be made notifying if we
want to mix in the active assembly-list name.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Both menu builders previously ignored the MenuIcon/Icon metadata on
ExportMainMenuCommand and ExportContextMenuEntry: the strings were
preserved through the WPF -> Avalonia port (~12 main-menu commands
declare MenuIcon, plus a handful of context-menu entries), but the
NativeMenuItem and MenuItem instances they fed into had no icon
assignment. Avalonia's toolbar already had a private path-to-image
reflection resolver; this lifts it into Images.cs as a shared helper
so the two menu sites can consume it too.
NativeMenuItem.Icon is Bitmap-typed (macOS NSImage is the platform
target and has no vector form), so the main-menu path rasterises
SVG icons through RenderTargetBitmap at attach-time -- one-time cost
per menu item at boot. MenuItem.Icon accepts any Control, so the
context-menu path keeps the IImage live inside an <Image> control
and preserves vector rendering.
SaveCodeContextMenuEntry and AssemblyTreeNode.ReloadAssembly were
missing Icon metadata even though their main-menu equivalents
declared MenuIcon. Adding Icon = "Images/Save" and "Images/Refresh"
respectively, matching the toolbar/menu sibling. Other context-menu
entries without Icon metadata are intentionally textless: most are
node-type-specific actions (Remove, Copy results, Search MSDN) where
no main-menu sibling exists to inherit an icon mapping from.
The App.axaml separator style selector "MenuItem Separator" only
matched separators nested inside a templated MenuItem (the main-menu
submenu case). ContextMenuProvider adds Separator directly to
ContextMenu.Items between Category groups, where there's no MenuItem
ancestor for the selector to match -- so context-menu separators
rendered with the Simple theme's default near-invisible style.
Extending to "MenuItem Separator, ContextMenu Separator" covers both
surfaces; toolbar and docking-chrome separators stay scoped out.
MenuIconWiringProbe walks the materialised NativeMenu tree post-build
and asserts File > Open has its Icon populated -- regression net for
the metadata-flow path.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The trailing HeaderedContentControl in the Display panel (Other -> Sort
results by fitness) sat 15 px below the ScrollViewer.Extent's reported
height: the outer StackPanel's measure under-counted that last child's
inner content, so MaxYOffset never grew enough to scroll the checkbox
into view. The Reset-to-defaults border below the ScrollViewer covered
the gap visually, making the checkbox look obscured.
ScrollViewer.Padding alone can't compensate because Avalonia 12's
Simple-theme ScrollContentPresenter collapses Padding on the axis the
scrollbar lives on -- the horizontal 6 px stuck, the vertical 6 px was
dropped. Wrapping the content in a Border with explicit Padding makes
the padding part of the StackPanel's measured extent, and the last
child becomes reachable. Applied to both panels that use a top-level
StackPanel under a ScrollViewer; Misc is unaffected (no ScrollViewer,
two checkboxes never overflow).
The new OptionsPageScrollReachTests.Last_Item_In_Display_Panel_Is_-
Reachable_At_Max_Scroll opens the Options tab, scrolls the Display
panel to ScrollViewer.Extent.Height, and asserts the "Sort results by
fitness" CheckBox's rendered bottom sits at or above the Reset border's
top in window-coordinate space.
Assisted-by: Claude:claude-opus-4-7: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.
Every_ExportToolbarCommand_Resolves_An_Icon used GetField against
ILSpy.Avalonia.Images.Images to verify every [ExportToolbarCommand]'s
ToolbarIcon resolves to a live IImage. Images.Images currently
declares its icons as static readonly fields, but a future refactor
could lazify them into properties (we explored that path earlier in
this session before reverting). Accept either FieldInfo or
PropertyInfo so the test keeps holding under that refactor.
Assisted-by: Claude:claude-opus-4-7:Claude Code
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
The ShowIdentical toggle was wired model-side
(CompareTabPageModel.OnShowIdenticalChanged → RootEntry.EnsureChildrenFiltered)
and view-side (Rebind on PropertyChanged), and the cascade was
correctly setting IsHidden via ComparisonEntryTreeNode.Filter. But the
HierarchicalModel's ChildrenSelector and SetRoots passed node.Children
through unfiltered — so IsHidden flags had no effect on what the
DataGrid actually rendered. Toggling the checkbox looked like a no-op.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The Language and Language-Version pickers in the main toolbar have no
effect when the active tab renders PE-header / metadata-table fields
straight from the metadata, or shows a structural compare diff —
language choice doesn't change what's drawn. Mirrors WPF's per-tab
TabPageModel.SupportsLanguageSwitching flag.
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
DisplaySettings.SortResults was persisted but unused — RunningSearch hardcoded
ComparerByFitness. Now SearchPaneModel.RestartSearch reads the setting at
start-of-search and passes either ComparerByFitness (default) or ComparerByName
into RunningSearch. Capturing at start matches WPF — mid-run toggles only
affect the next search.
Assisted-by: Claude:claude-opus-4-7:Claude Code