Two fixes covering the ~30 [Binding] errors that fire during MainWindow startup
and contribute to the 900 ms+ Window.Opened → AssemblyListPane ctor gap.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Two ordering fixes for startup so the user sees the assembly tree before any
decompiled content shows up in the document area, and so the lazy-load story
actually holds.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replace LoadedAssembly's eager Task.Run(LoadAsync) with a Lazy<Task<LoadResult>> so
construction is free and the load only starts on the first GetLoadResultAsync /
await / .Result access. Active assembly's load fires from the saved-path restore
and runs without competition; everything else stays cold until either the user
expands a tree node OR the cooldown sweep fires.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Saved tree-view path restoration on startup walked the tree synchronously,
calling AssemblyTreeNode.EnsureLazyChildren which itself blocks the UI thread
on assembly.GetLoadResultAsync().GetAwaiter().GetResult(). For users with a
deep saved path that pinned the UI thread until metadata finished loading.
Assisted-by: Claude:claude-opus-4-7:Claude Code
FindMemberNode's IMethod branch only checked direct MethodTreeNode children of
the type. Accessor methods (get_/set_/add_/remove_/invoke_) live as children of
their owning PropertyTreeNode / EventTreeNode, so the lookup always returned
null — and MMB on a metadata-grid accessor row silently no-opped because
OpenNodeInNewTab couldn't resolve the IMethod to a tree node.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Mirrors WPF's mechanism: ILSpyTreeNode.OnChildrenChanged calls ApplyFilterToChild
on every newly-added child while the parent IsVisible, writing the Filter result
into SharpTreeNode.IsHidden. Because SharpTreeNode.IsVisible defaults to true,
the cascade fires while a node's children are added in its constructor — so a
PropertyTreeNode's get_/set_ accessors are stamped IsHidden=true under the
default ShowApiLevel before the property even attaches to its type. The base
SharpTreeNode.ShowExpander then naturally returns false ("Children.Any(!isHidden)"
sees no visible children), no per-node override needed.
Assisted-by: Claude:claude-opus-4-7:Claude Code
PropertyTreeNode now eagerly adds Getter / Setter as MethodTreeNode children
(when present); EventTreeNode does the same for Add / Remove / Invoke
accessors. The ShowExpander => false overrides drop so the base default
(Children.Any) lights up the chevron when accessors exist.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The Tunnel-phase subscription on PointerPressed was silently never firing for
middle-clicks on metadata rows — ProDataGrid's row-level pointer handlers
appear to claim the event before our tunnel subscription gets a turn, so
MMB-on-a-row went nowhere despite the unit tests (which call RaiseRowActivated
directly) staying green. Switching to Bubble + handledEventsToo: true gives
us a guaranteed seat regardless of who marked the event handled along the way.
Both the assembly-tree and the metadata-grid MMB handlers move together for
consistency.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The 10x9 Path glyph was a fragile hover/click target. Wrap it in a transparent
Border with 8x4 padding so the hit area is comfortably bigger than the icon
itself, with the Path marked IsHitTestVisible=false so the Border owns hit
testing. Clicking the funnel opens the popup on flag columns or focuses the
TextBox on text columns — short-circuits the hover-then-click two-step.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Label and input now share the same Panel slot inside a horizontal DockPanel —
the funnel icon docks right and exactly one of {label, input} is visible at a
time. Hovering the header (or setting a filter, or opening the popup) swaps
the column name out for the input; otherwise the label stays. Two-row stack
is gone, so the data-rows no longer shift down when an input gets revealed.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each metadata column header now shows a small funnel icon to the right of the
column name. The TextBox / dropdown trigger collapses by default and only
becomes visible when the pointer is over the header, when a filter is set, or
while the [Flags] popup is open. Active filters tint the funnel SteelBlue so
the user can see at a glance which columns are constraining the view.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Each ContentTabPage now carries the SharpTreeNode it represents (SourceNode),
stamped at construction time so the activation event fires with it already
populated. DockWorkspace subscribes to factory.ActiveDockableChanged: when a
new tab becomes active — whether by MMB carve-out, the "Decompile to new tab"
context-menu entry, or the user clicking a different tab in the strip — the
assembly-tree selection is pulled across to that tab's SourceNode. A guard
prevents the change from cascading back into ShowSelectedNode and overwriting
MainTab's content. Existing single-tab reuse path stamps MainTab.SourceNode
on every selection so flipping back to it stays a no-op.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The new-tab path always built a DecompilerTabPageModel, so MMB / context-menu
on a metadata-table tree node forced the node into a decompiler tab and a second
metadata view couldn't coexist with the first. Move the carve-out into a single
DockWorkspace.OpenNodeInNewTab that asks node.CreateTab for custom content
(metadata tables, resource viewers, …) — wiring the same NavigateToCellRequested
/ RowActivated subscriptions AttachCustomContent applies on the reuse path —
and only falls back to spawning a fresh decompiler tab when the node has no
custom page-type. The assembly-tree MMB handler, the metadata-grid MMB handler,
and the "Decompile to new tab" context-menu entry now all delegate to this one
method.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Wires MMB on the assembly tree and the metadata grid to the existing "open in
new tab" path, matching the WPF gesture (DecompileInNewViewCommand carries
InputGestureText = "MMB"). Plain double-click reverts to reusing the active tab;
the unreliable Shift+double-click variant is dropped.
Assisted-by: Claude:claude-opus-4-7:Claude Code
PointerWheelChanged kept bubbling past the popup's ScrollViewer to the underlying
DataGrid, so spinning the wheel inside the dropdown also scrolled the grid behind.
Mark the event handled at the popup's root Border with handledEventsToo so the
inner ScrollViewer keeps its scroll behaviour but the event stops at the popup
boundary.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Two readability fixes for the metadata grid:
- Override DataGridColumnHeaderHoveredBackgroundBrush / Pressed to the toolbar accent
palette so the Simple theme's saturated dark fill stops burying header text while
sorting / resizing.
- Force StandardCursorType.Arrow on the filter popup's root Border. Without it the
EW-resize cursor set by the column header's drag-grip can leak into the popup if
the pointer enters from there before Avalonia recomputes the cursor.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The Simple theme's :checked default for ToggleButton paints a saturated dark blue
that buries the chip's label. Override with the toolbar's translucent hover accent
(#330078D7) on the inner ContentPresenter so active chips stay readable while still
reading as "selected"; pressed states keep the denser #660078D7 fill for a click cue.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Extends the double-click-to-decompile path with a new-tab variant. RaiseRowActivated
now carries an OpenInNewTab flag; DockWorkspace branches on it to either reuse the
active tab (existing tree-selection path) or spawn a fresh DecompilerTabPageModel.
The view's OnGridDoubleTapped reads KeyModifiers.Shift; a new
DecompileMetadataRowInNewTabCommand exports the same gesture as a context-menu entry.
Assisted-by: Claude:claude-opus-4-7:Claude Code
DataGrid.DoubleTapped raises a RowActivated event on the metadata
page model. The dock workspace subscribes during AttachCustomContent
and resolves the row's metadataFile + Token to an IEntity via the
type system, then selects the matching tree node — which triggers
ShowSelectedNode → CreateTab → decompiler view, the same path a
click in the assembly tree takes. Falls through silently for rows
whose token doesn't resolve to an IEntity (heap rows, AssemblyRef,
constants).
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replace the string-token flag predicate with a numeric bitmask on
ColumnFilter (FlagMask, default -1 meaning "all rows"). The dropdown
on a [Flags] column now drives the mask directly:
Assisted-by: Claude:claude-opus-4-7:Claude Code
[Flags] columns now get a small ▾ button next to the inline TextBox
that opens a popup with one CheckBox per defined flag value. Toggling
boxes rebuilds the filter as a comma-separated flag-name list (the
predicate's flags-aware path consumes it directly), so the user can
discover and combine flags without typing names from memory. The
TextBox stays for free-form input (regex, !-negation, mixed input);
opening the popup syncs the checkboxes from whatever's currently in
the filter.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Enum.ToString rejects multi-character format specs like "X8", so a
flags column raised FormatException out of Copy Row. Mirror what
HexFormatConverter does at bind-time: widen the value to its
underlying integer (Convert.ChangeType + GetTypeCode) before calling
IFormattable.ToString.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Sibling of "Copy" that emits the full row tab-separated, reusing the
same format-aware ReadColumnValue helper so hex columns serialise as
hex on the clipboard.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The DataGrid's built-in cell copy doesn't surface in the context menu
(only via Ctrl+C); removing the seed dropped Copy entirely from the
right-click menu. Add it back as a single-cell-value copy that mirrors
the column's display format (so "0x06000001" lands on the clipboard
rather than "100663297").
Assisted-by: Claude:claude-opus-4-7:Claude Code
Avalonia DataGrid surfaces a per-cell Copy in its default cell context
menu, so seeding our own at the top of the built menu produced a
duplicate "Copy" entry. Revert the seed; cancel our menu when no MEF
entries are visible (the built-in cell menu still shows).
Assisted-by: Claude:claude-opus-4-7:Claude Code
Right-click on a non-token cell was producing nothing because the
GoToToken entry hides itself there and ContextMenuProvider.Build then
returned null, cancelling the menu. Always seed a Copy entry (TSV row
dump matching DataGrid's built-in Ctrl+C behaviour) at the top of the
built menu; MEF entries follow after a separator when applicable.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replacing the default editor menu with the MEF-driven one stripped
the standard text-edit affordances. Always seed Copy and Select All
at the top of the built menu (Copy disabled when there's no
selection); MEF entries follow after a separator. Right-clicking
plain text now produces a usable menu instead of nothing.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Right-clicking an entity hyperlink in the decompiler view now offers
"Go to token" — when the click lands on a reference whose payload is
an IEntity backed by a real metadata module, the entry routes through
the dock workspace to the matching CLI metadata table at the entity's
row. The decompiler text view also gets a context-menu host so other
[ExportContextMenuEntry] contributors that target a TextView surface
become reachable here too.
Assisted-by: Claude:claude-opus-4-7:Claude Code
A column filter wrapped in `/.../` is parsed as a case-insensitive
regex; anything else stays on the existing substring path. Unparseable
patterns fall through to a literal substring match so a typo can't
take down the filter row. Compiled regexes are cached per filter
text.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The path-based restore (SessionSettings.ActiveTreeViewPath +
FindNodeByPath) already round-trips through the metadata sub-tree's
stable ToString() values — these tests pin that down: selecting
TypeDef and a debug-only Document table both save paths whose final
segments resolve back to the originating node, including the second
"Metadata: Debug Metadata (Embedded)" hop into the embedded PDB
sub-tree.
Assisted-by: Claude:claude-opus-4-7:Claude Code
SessionSettings now carries HideEmptyMetadataTables (default true,
matching prior behavior) and MetadataTablesTreeNode honors it: when
true, tables with zero rows are hidden; when false, every CLI
TableIndex is surfaced. The setting persists across launches via
ILSpy.xml. There's no options dialog yet to flip it, but the plumbing
is in place for a future settings UI to toggle.
Assisted-by: Claude:claude-opus-4-7:Claude Code
DebugDirectoryTreeNode is now a lazy folder. Expanding it walks the
debug directory and adds a nested MetadataTreeNode per
EmbeddedPortablePdb entry, so the user can browse the debug-only
metadata tables (Document, MethodDebugInformation, …) the same way
they browse the host module's tables. The grid view (entry list) is
preserved when the node itself is clicked.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The metadata page's KeyDown handler now mirrors the "Go to token"
context-menu entry: when Ctrl+G fires while focus is on a token-kind
DataGridCell (or, as a fallback, while one is hovered), dispatch the
same NavigateToCellRequested path as the hyperlink-style click. The
focus-resolution + dispatch logic lives behind an internal
TryNavigateToTokenInCell hook so tests can drive it without simulating
the keyboard event end-to-end.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Upgrade the integration filter test to drive the live DataGrid: locate
the TextBox the column builder embedded in the Name column's header,
set its Text, and assert on DataGridCollectionView.Count — the visible
row count the user actually sees. Drop the temporary verification
fixture; the regression coverage now lives alongside the predicate /
event-forwarding unit tests.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Object-initializer Filter assignment lands on the DataGridCollectionView
instance but is gone by the time the grid finishes wiring it up — the
live view ends up with Filter=null, so Refresh() returns the unfiltered
source. Construct the view first, attach to the grid, then assign Filter
so the predicate sticks. The verification oracle now asserts on the
view's Count (the actual visible-row count) rather than re-running the
predicate manually, which catches this exact regression.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Selecting a tree node was firing ShowSelectedNode twice — once via
SelectedItems.CollectionChanged and once via SelectedItem
PropertyChanged. Each call ran node.CreateTab(), producing a fresh
MetadataTablePageModel with a fresh ColumnFilters set. The DataGrid
ended up rendering the second page's columns, but the first page's
filters were what tests (and any code reading the model) saw — so
typing into a header TextBox updated a filter no one was reading.
Track the last shown nodes inside ShowSelectedNode and short-circuit
when they match.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Three unit tests pin down the bridge between the column-header TextBox
that MetadataColumnBuilder bakes into each header and the page model:
typing into the box updates ColumnFilter.Text, programmatic filter
changes push back to the box, and either direction raises the
page-level ColumnFilterChanged event the view subscribes to.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The PointerPressed/Released intercept was too aggressive — it ran
before the TextBox's own focus handler, so the user could no longer
click into the input. The TextProperty-observable change alone is the
fix that makes typing flow into ColumnFilter.Text.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Two fixes for the column-header filter inputs that landed visually but
did nothing on the running app: (1) intercept PointerPressed/Released
on the TextBox so DataGridColumnHeader's sort-click handler doesn't eat
the click before the box can take focus, (2) drive the filter from
TextBox.TextProperty's observable rather than the TextChanged event,
which doesn't fire reliably for a TextBox hosted inside a header.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Hosting an interactive TextBox inside a DataGridColumn.Header proved
unreliable: the column header's PointerPressed handler intercepts clicks
for sort, so the filter input never received focus or keypresses on the
running app. Move the filter row to a sibling StackPanel above the
DataGrid where each TextBox tracks the corresponding column's
ActualWidth via the grid's LayoutUpdated event, keeping per-column
alignment without competing with the header for input.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The page now subscribes to its own ColumnFilters' PropertyChanged via
CollectionChanged and re-raises ColumnFilterChanged. The view listens
to that single event instead of chasing per-filter listeners on every
schema swap, which was leaving the auto-refresh path silent on the
running app even though the predicate itself was correct.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replace the Avalonia.Data.Binding(Source = filter, Mode = TwoWay) on the
header TextBox with explicit TextChanged + PropertyChanged subscriptions.
The INPC plugin holds a WeakReference to the binding source and resolves
it lazily on write-back; for column-header-hosted controls the target
reliably comes back null, throwing "Non-static method requires a target"
from PropertyInfo.SetValue. The event-based wiring keeps strong refs and
avoids the weak-reference machinery entirely.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The CommunityToolkit.Mvvm source generator over a `value` field interacts
poorly with Avalonia's INPC binding plugin: typing into the per-column
filter TextBox throws TargetException("Non-static method requires a
target") because the cached accessor's target reference comes back null
on write. Renaming the backing field and property avoids the collision
with the contextual `value` setter identifier.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Replace the single text-bar above the grid with one filter input per
column, baked into each DataGridColumn's header. The page model carries
a ColumnFilters collection alongside Columns; the filter predicate ANDs
every non-empty column's case-insensitive substring match. Column
instances are built fresh per page so sibling tabs don't share filter
state, and the column name moves to Tag for cell-level lookups now that
Header is a Panel.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Right-click on a [Kind=Token] cell now exposes a "Go to token" menu item
that reuses the same NavigateToCellRequested path as the hyperlink-style
left-click. Visibility is gated on the click landing on a token-kind
column, so plain columns and non-metadata grids stay clean.
Assisted-by: Claude:claude-opus-4-7:Claude Code
MetadataCellTooltip resolves `{ColumnName}Tooltip` on the bound row by
reflection. The view subscribes to PointerMoved, walks up to the
DataGridCell under the pointer, and applies the resolved string via
ToolTip.SetTip whenever the cell changes.
Assisted-by: Claude:claude-opus-4-7:Claude Code