The document tab strip's multi-row layout and the mouse-wheel gesture
that toggles it were only reachable by the wheel itself, with no
discoverable UI and no way to opt out of the gesture. Surface both as
checkboxes under a new "Tab options" group on the Display page, and add
a MouseWheelTogglesTabStripRows setting (default on) so the wheel toggle
can be turned off while keeping the persisted multi-row choice.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Clone / Rename / Delete / Select act on the selected list but were always
enabled, so clicking them with nothing selected silently no-op'd (the
handlers already guarded on SelectedListName). Bind their IsEnabled to the
list's selection via ObjectConverters.IsNotNull so they disable until a list
is picked; New / Reset / Add-preconfigured / Close stay enabled.
Assisted-by: Claude:claude-opus-4-8:Claude Code
SharpTreeView is a ListBox with AutoScrollToSelectedItem left at its default
(true), so it chased the selected row whenever its index shifted: expanding an
unrelated node pushed the selection off-screen and the ListBox yanked the
viewport back to it, fighting the expand's own reveal -- the "weird scrolling".
The rule is that a user mutating a control directly should not have the app
mutate the view too; only navigation from a *different* control (search
results, code/metadata links, analyzer nodes, Back/forward) may sync the tree.
The app already does that sync explicitly through the model
(TreeSelectionBinder -> CenterNodeInView), so the ListBox's own auto-scroll is
redundant and wrong for in-tree actions. Disable it.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The search panel rendered hits in a bare ListBox with no headers, so the only
ordering was the fixed fitness/name streaming order chosen at search time --
the user could not re-sort by Name, Location or Assembly. Replace the ListBox
with the app's standard sortable Avalonia DataGrid (mirroring OpenFromGacDialog
/ MetadataTablePage): three template columns that keep the per-field icons,
each with a SortMemberPath so clicking a header sorts by Name / Location /
Assembly. No initial column sort is applied, so the default fitness ranking is
preserved until the user clicks a header.
The code-behind is unchanged: ListBox and DataGrid share the
SelectingItemsControl surface, so the existing selection / activation /
keyboard / middle-click handlers port as-is (verified by the existing search
niceties tests now running against the DataGrid).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Export Project/Solution, Save Code and Compare tests decompiled whatever
the default assembly list seeds -- System.Linq, CoreLib, the Uri assembly --
so each spent ~10s purely on type count (Solution/CreateSolution/SaveCode/
DecompileAssembly ~10s, CompareView ~4.5s).
Add FixtureAssembly, which emits a minimal two-type assembly via
PersistedAssemblyBuilder, and point these tests at it. They still exercise the
full project/solution writer, save-to-file and compare-tree pipelines, but
each now runs well under a second (e.g. Solution_Mode 10.8s -> 0.3s,
CreateSolution 10.8s -> 0.2s, ShowIdentical 4.5s -> 0.3s).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Pdb2Xml command (ILSpy) and the PDB round-trip tests (Decompiler.Tests)
reference Microsoft.DiaSymReader*, previously gated on the build host being
Windows. That made dotnet restore resolve a different graph on Windows than
on Linux -- the packages (and their transitive tail: DiaSymReader.PortablePdb,
the legacy NETCore.Platforms/Win32 packages) appear only on Windows, and
DiaSymReader.Native flips between Direct and CentralTransitive. So a checkout
could not be developed across OSes without the committed packages.lock.json
churning on every Windows restore.
Drop the OS gate (keep Debug-only) so the restored graph, and the committed
lock, are identical on every OS. The consuming code is still gated by DEBUG
and WINDOWS, so on non-Windows the packages are restored but never compiled
in; the native asset only resolves for win-* RIDs.
The "Verify package contents" step (which checks the committed *.filelist
snapshots still match the built VSIX/MSI contents) had been excluding
packages.lock.json from its git diff to tolerate that per-OS churn. With the
locks now identical across OSes the carve-out is unnecessary, so it goes back
to a plain git diff --exit-code.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ICSharpCode.Decompiler.Tests builds for the host RID (so the native
Microsoft.DiaSymReader.Native assets the Windows PDB tests need are copied),
which made its packages.lock.json host-specific (linux-x64 vs win-x64) and
drift between Linux and Windows restores. Add an explicit <RuntimeIdentifiers>
list next to the single <RuntimeIdentifier>: the lock then records every RID
host-independently while the build still targets the host RID. Apply the same
list to the TestRunner it pulls in.
TestRunner's lock is now fully portable. The test project's lock still carries
the OS-conditional DiaSymReader.Native difference in its base graph, which the
*.filelist-scoped "Verify package contents" step already tolerates.
Assisted-by: Claude:claude-opus-4-8:Claude Code
create-filelists.ps1 filtered to bin\Debug with `-contains` (collection
membership, always false for a substring test), so it skipped every artifact;
drop the broken filter. Point the installer ProductIcon at the relocated
ILSpy.ico (Images -> Assets) and regenerate the committed .filelist snapshots.
Assisted-by: Claude:claude-opus-4-8:Claude Code
publish.ps1 now takes a -Configuration parameter (the workflow passes the
GNU-style --configuration <Debug|Release>, which pwsh binds to it) and uses
it for both the -c flag and the output paths. The win-x64 framework-dependent
bundle is produced for every configuration; the win-arm64 framework-dependent
and win-x64 self-contained bundles stay Release-only, matching what the zip
steps consume.
The build workflow publishes per-configuration before zipping, and the
framework-dependent binaries zip now reads straight from the publish output
(...\win-x64\publish\fwdependent) for the active configuration instead of
globbing the build output.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Replace the Windows-only .bat helpers (clean / debugbuild / releasebuild /
restore / updatedeps and BuildTools/format) with cross-platform pwsh
scripts at the repo root: restore.ps1, build.ps1 (-Configuration), clean.ps1,
updatedeps.ps1 and BuildTools/format.ps1, alongside the existing publish.ps1.
Enable a packages.lock.json for every project by hoisting
RestorePackagesWithLockFile into the root Directory.Build.props (the four
core libraries set it individually before) and commit the generated locks,
so restores are repeatable and CI can cache packages off them.
Cache the NuGet packages folder in the three setup-dotnet workflows
(build-ilspy, build-frontends, codeql-analysis), keyed on the lock files
per the setup-dotnet caching guidance.
Scope the Debug "Verify package contents" check to the *.filelist outputs
it actually generates. A project's packages.lock.json is keyed only by
(framework, RID), with no host-OS axis, so a lock produced on Linux
legitimately differs from one produced on Windows whenever an OS-conditional
PackageReference applies (Debug+Windows pulls Microsoft.DiaSymReader*). The
Windows restore then rewrites those locks; that churn must not fail a step
whose job is to police the VSIX/MSI file lists.
Also drop the dead ILSpy.BamlDecompiler publish line from
publishlocaldev.ps1, mirroring the earlier publish.ps1 fix.
Assisted-by: Claude:claude-opus-4-8:Claude Code
publish.ps1 is a WPF-era script that was never updated for the current
tree, and the "Publish x64/arm64" CI step aborted on two stale things:
- It published ./ILSpy.BamlDecompiler/ILSpy.BamlDecompiler.csproj, which
does not exist here (the BAML library is ICSharpCode.BamlDecompiler and
already ships transitively via the ILSpy dependency graph) -> MSB1009.
- It publishes the ReadyToRun plugin RID-specifically without restoring
again, but the plugin declared no RuntimeIdentifiers, so the RID-less
solution restore produced no win-x64/win-arm64 assets -> NETSDK1047.
Drop the dead BamlDecompiler lines and give the plugin RuntimeIdentifiers
mirroring ILSpy.csproj, so it restores and publishes for every platform
the host supports (the plugin is pure managed and RID-agnostic).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Keep the headless UI tests from timing out on the slow Windows runner:
decompile a small CoreLib type (System.Object) in the folding and
token-history tests instead of a large one, and raise the shared
decompile-wait timeout so a cold first decompile (JIT plus building a
CoreLib-scale type system) finishes in time.
Assisted-by: Claude:claude-opus-4-8:Claude Code
XmlDocLoader's modern-.NET fallback built the ref-pack path from the EXACT runtime
patch version (shared/Microsoft.NETCore.App/10.0.8 -> packs/Microsoft.NETCore.App.Ref/
10.0.8/ref), but the targeting (ref) pack version usually differs from the installed
runtime patch -- e.g. pack 10.0.0 -- and a relocated CI install (DOTNET_INSTALL_DIR)
may carry only a different feature band. The exact-version lookup then missed and the
provider came back null, leaving CoreLib hovers undocumented. Broaden the search:
prefer an exact match, then the newest pack sharing the runtime's major.minor, then
the newest pack present.
Assisted-by: Claude:claude-opus-4-8:Claude Code
NavigationHistory.Record's 0.5s debounce collapsed ANY two selections inside the
window -- including two DIFFERENT nodes -- so a navigation whose decompile finished
quickly (cheap targets, or a fast machine) never pushed the previous node onto the
back stack, silently losing history. Its real purpose is only to swallow the
double-fire a single click produces (SelectedItems.CollectionChanged + SelectedItem
PropertyChanged, same node) and tree-refresh re-selects, so gate the collapse on the
rapid entry being equal to the current one; a distinct node now records normally.
The view-state/navigation tests navigate between two cheap namespace nodes (a C#
namespace decompiles to just its "// Some.Name.Space" line) instead of decompiling
CoreLib types in full -- fast enough for the headless 15s budget on slow CI runners,
and a direct regression test for the collapse bug above.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Three comments described the old WPF host: the NoWarn rationale and the
DiaSymReader-gating note in ILSpy.csproj, and the no-[Shared] note in
DecompilerSettingsViewModel. Reword them to describe the current
System.Composition / build behavior directly. No code or suppression changes;
the TomsToolbox.Composition.Analyzer (MEF002/MEF004 source) stays.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Avalonia app targets net10.0, so dotnet build emits ILSpy/bin/<config>/net10.0/,
but the framework-dependent zip still globbed a net10.0-windows folder that does not
exist, so it captured nothing. Point that zip at net10.0. The self-contained and arm64
zips keep net10.0-windows because that is the explicit -o path publish.ps1 (and the
installer/VSIX) write the published output to.
Assisted-by: Claude:claude-opus-4-8:Claude Code
.gitattributes marked *.cs as `text` with no `eol=`, so the checked-out line
ending was left to each machine's git config. On the CI runner the .cs come out
LF, but .editorconfig sets end_of_line=crlf, so dotnet-format's --verify-no-changes
flagged every line of every file and the Format check step failed repo-wide
(independent of code formatting). Pinning eol=crlf makes the checkout match the
editorconfig on every platform. Blobs stay LF; only the working-tree EOL is fixed.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Make explicit what was only implied: the pre-commit hook IS the formatter, so
always let it run and never commit .cs with --no-verify (bypassing it is what
lets unformatted code accumulate). Scope the "new files must be CRLF" rule to
Windows -- on Linux/macOS that fights the repo's text=auto LF storage and makes
the whole tree look phantom-modified. Update the test example to the
Microsoft.Testing.Platform syntax the repo now pins. The parity-tracker line is
dropped (that tracker is on its way out).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Windows build workflow had several run steps on one very long line --
the framework-dependent 7z zip with six glob paths, the restore/build/test
invocations, and the nuget push calls -- which were hard to scan and review.
Reformat them: single-command steps use a YAML folded scalar (run: >) with
one argument group per line, and the long lines inside the nuget-push |
blocks use PowerShell backtick continuation. The folded scalars rejoin to
byte-identical command strings, so runtime behavior is unchanged. No step
ordering, conditions, or the net10.0-windows publish paths are touched.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Restores the result-navigation gestures the pane shipped with before the
port: Ctrl+T/M/S jump the picker to Type/Member/Constant, Down/Up arrow
hand focus between the search box and the result list, and Ctrl+Enter or a
middle-click open a result in a new document tab instead of reusing the
active one. New-tab activation routes through the existing
OpenNodeInNewTab so it matches the tree's open-in-new-tab behaviour.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The metadata-grid hover infrastructure shipped but no table entry fed it,
so every cell tooltip the previous version had was silently gone: the
heap-offset/value hints on string and blob columns, the entity description
on token columns, and the per-bit breakdown on flag columns.
Reinstate all of them. FlagsTooltip renders the rich per-bit view (a
checkbox per flag plus the selected member of each mutually exclusive
sub-range) instead of a flat string; GenerateTooltip on the table base
describes what a token column points at; the remaining columns expose
their heap-offset and value hints. MetadataCellTooltip now hands a
FlagsTooltip back as a built control and stringifies everything else.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The list-root node had no Decompile override, so selecting it produced
empty output. Restore the behaviour of emitting a "// List:" header and
each contained assembly under a comment rule.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The previous version's About page carried a Check-for-updates / Download
button and an "automatically check for updates every week" toggle. Embed
both back into the About output: the button reuses the toolbar banner's
shared UpdatePanelViewModel (same check, same state), and the toggle is
two-way bound to UpdateSettings.AutomaticUpdateCheckEnabled.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The previous version let you pick the theme from a dropdown on the Display
options page; the Avalonia port only exposed theme switching via the menu.
Bind a ComboBox to SessionSettings.Theme -- the same property the menu's
SetThemeCommand sets and ThemeManager applies live -- so the page switches
the theme too, and resetting Display defaults restores the default theme.
Assisted-by: Claude:claude-opus-4-8:Claude Code
AvaloniaEdit's copy puts only plain text on the clipboard, so pasting into
an HTML-aware target (a document, mail, chat) lost the colours the previous
version preserved. Build a coloured HTML fragment from the selection that
merges BOTH the xshd syntax highlighter and ILSpy's semantic RichTextModel
(the decompiler's reference / theme colours the xshd alone doesn't carry),
and place it alongside the text under the native HTML format -- text/html on
Linux/macOS, CF_HTML on Windows. Wired to Ctrl+C (tunnel, so it pre-empts
the plain-text copy) and the right-click Copy entry.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Port the mixed IL/C# disassembler language that the previous version
shipped: it disassembles IL with the decompiled C# source interleaved
above each instruction (mapped via sequence points) as gray comments. The
language was missing entirely from the Avalonia tree, though it is a
Release-visible entry in the language dropdown.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A member that decompiled to a very large amount of text could hang or
exhaust memory on the UI thread with no escape. Cap display output at five
million characters and, on overflow, show a message offering to display
anyway (at an extended limit) or save to disk.
Also bridge three Display options that were persisted but never reached the
decompiler -- brace folding, debug-symbol info, and the indentation string
-- so toggling them once again affects the generated source.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The search already stopped accepting results at 1000 and cancelled the
producer, but the user was never told the list had been truncated. Append
the "Search aborted, more than 1000 results found" row (a no-op to click).
Assisted-by: Claude:claude-opus-4-8:Claude Code
An on-demand resolved dependency had no way to be promoted into the
assembly list, so it was discarded on the next list reload. Restore the
context entry that clears IsAutoLoaded and re-saves the list.
Assisted-by: Claude:claude-opus-4-8:Claude Code
FindTreeNode only handled entity references, so activating an assembly,
resource, or namespace search result -- and clicking a resource or
namespace link -- silently did nothing. Restore the non-entity cases
(LoadedAssembly / MetadataFile / Resource / namespace) the previous
version handled.
Assisted-by: Claude:claude-opus-4-8:Claude Code
TestPlugin was still WPF (net10.0-windows, UseWpf, System.Windows,
TomsToolbox, the old ICSharpCode.ILSpy namespace), so it could not load
into the cross-platform app. Re-target it to net10.0 and map each
extension point onto the port's contracts: the custom Language, About-page
addition, context-menu entry, main-menu/toolbar command, and an Avalonia
options page whose view the ViewLocator resolves by naming convention. The
command needs [ImportingConstructor] for System.Composition to satisfy its
service dependency. Reference it build-only from the tests (no DLL copied
into the test output) so a composition test can load it without the
headless app auto-loading the plugin into every other test.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A plugin whose assembly won't load, or an exported command whose
constructor can't be satisfied, previously either blanked the app into the
startup error window or threw mid-build and took the whole menu/toolbar
down with it. Collect such failures in a CompositionErrors sink instead,
skip the offending part, keep running, and show the errors in a document
tab via an ITextOutput once the window is up. Add a Composition log
category for opt-in file logging.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Rename Avalonia.Xaml.Behaviors to its successor Xaml.Behaviors.Avalonia
(12.0.0.1), and bump Dock (12.0.0.2), Svg.Controls.Skia.Avalonia,
AvaloniaUI.DiagnosticsSupport, CliWrap, coverlet.MTP, and NUnit to their
latest releases.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The document tab strip can now flow tabs onto multiple rows or stay a
single scrolling row; the mode is toggled by the mouse wheel over the
strip and persisted in SessionSettings. In single-line mode an overflow
dropdown at the strip's trailing edge lists every open document for quick
switching. Popups render in the window overlay layer (OverlayPopups) so
the dropdown's menu doesn't open as a native X11 child window that loses
focus and self-dismisses before it becomes visible.
Assisted-by: Claude:claude-opus-4-8:Claude Code
#3753: when many documents are open, the tab strip scrolls a single row,
hiding most tabs behind the overflow scroller.
Dock's DocumentTabStrip hardcodes a horizontal StackPanel inside a
PART_ScrollViewer in its theme template and ignores an ItemsPanel set via
a Style, so a MultiRowTabStripBehavior sets the WrapPanel ItemsPanel
directly and disables the scroll-viewer's horizontal scrolling, giving
the WrapPanel a bounded width so tabs wrap onto new rows. Enabled on the
document strip (not tool tabs) from App.axaml.
Drag-reorder across rows is not yet validated (Dock's drop-position logic
assumes a single horizontal strip), so this stays an opt-in prototype for
now rather than a closing fix for the issue.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Project/solution export, PDB generation, Create Diagram and Extract
Package all ran via RunWithCancellation on the active preview tab --
whose cancellation token tree-node navigation cancels. So selecting any
node while one of these ran killed it: you couldn't browse while an
export was in progress.
Add DockWorkspace.RunInNewTabAsync, which opens a new frozen document
tab (its own decompile/cancellation scope, never the navigation target),
runs the work there, and shows the report in it. Route the long-running
command sites through it. A frozen tab is immune to the selection-driven
re-decompile, so navigation can no longer cancel the work -- and the
report opens in its own tab instead of replacing the current view.
The single-node Save Code path intentionally stays on the active tab
(matches the prior version and is quick).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Right-click Save Code on a node without a Save() override (type,
namespace, member) did nothing: the context-menu entry only called
node.Save() and stopped when it returned false, unlike Ctrl+S which
falls through to a generic decompile-to-file save. The prior version
always had this fallback in both paths; the port dropped it from the
context entry.
Extract the shared "Save() else decompile the node to a single file"
flow into SaveCodeHelper and route both Ctrl+S and the context-menu
entry through it, targeting the clicked node. As before, the fallback
runs in the active decompiler tab with its progress/cancel overlay and
then shows a "decompilation complete" breadcrumb with an Open-folder
button; the default file name derives from the node's text.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Window > Debug Steps did nothing. Two bugs in ToolPaneMenuItem:
the getter treated a pane with a null Owner (one hidden by default and
never placed in the layout) as visible, so the menu showed it checked
and toggling tried to hide it; and the show branch used
factory.RestoreDockable, which only un-hides a previously-shown pane and
is a no-op for one that was never in the layout.
Report visibility from real layout membership, and show via
ShowToolPane, which materialises the pane and (re)creates its home dock.
Assisted-by: Claude:claude-opus-4-8: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
Selecting a list in the dialog only set SessionSettings.ActiveAssemblyList
directly, which merely persists the choice for the next launch — it never
reloaded the tree, so the active list appeared unchanged. Route selection
through AssemblyTreeModel.ActiveListName instead, whose setter reloads the
list (OnActiveListNameChanged -> ShowAssemblyList) and persists the choice.
Also make double-clicking an entry a shortcut for the Select button (both
go through the same activation path).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The metadata grid's double-tap handler activated grid.SelectedItem, so
rapidly clicking a column-header sort button registered as a double
click and navigated to the selected row. Resolve the row from the
double-tapped element instead and activate only when the double-click
actually landed on a data row.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The command always built a decompiler tab and force-decompiled the
node, so a metadata-table (or resource) node opened an empty code tab
instead of its grid. Route a single node through OpenNodeInNewTab, which
honours the node's CreateTab() custom content and falls back to a
decompiler tab for ordinary nodes; multi-node selections keep the
decompile-the-union behaviour.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The tree node's text leads with the table's token-kind byte (e.g.
"02 TypeDef (1234)"), but the opened tab's title dropped it and showed
just "TypeDef (1234)". Lead the tab title with the same byte so the two
match.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Jumping to a token (the "Go to token" entry and token-cell clicks) set
suppressHistoryRecording while selecting the target table, so the jump
left no back-stack entry and Back skipped over it. Stop suppressing so
the destination is recorded, and carry the target row on the entry
(TreeNodeEntry.MetadataRow) so Back/Forward restores the exact token
rather than the top of the table.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Commands and context-menu entries kick off async work as `_ = SomeAsync()`,
discarding the task. When one faulted, the failure only surfaced via
TaskScheduler.UnobservedTaskException after the GC finalized the dropped
task -- late, on the finalizer thread, and disconnected from the gesture
that caused it (a click would appear to do nothing, then a mysterious
crash report arrived seconds later).
Add a HandleExceptions() extension that observes such a task on the UI
context and routes any fault to the existing GlobalExceptionHandler at
the point of failure (treating cancellation as a normal outcome), and
route the user-invoked command/entry sites through it. The global
unhandled-exception net already existed; this just makes these failures
prompt and attributable instead of deferred to GC.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The quick Save Code path exports a project (one assembly) or a solution
(several) straight through a file picker, with no way to tune the
output. Add a dedicated, discoverable "Export Project/Solution..." entry
(File menu + assembly context menu, alongside Save Code) that opens a
configuration dialog: pick the output folder, preview the projects that
will be written (with invalid / duplicate-name / PDB-eligible badges),
toggle project-format and decompiler options, optionally sign with a
strong-name key, and optionally emit a portable PDB per assembly --
defaulting source-embedding off since the project's .cs are on disk.
The export runs on a clone of the live settings (never the persisted
instance) behind the tab's cancellable progress UI and reports into the
active decompiler tab. StrongNameKeyFile now flows through
DecompilationOptions into the project writer, which had no reachable
setter before. The engine (ProjectExporter) and the preview computation
are split out from the window so they are headless-testable.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The portable-PDB writer carried its knobs (no-logo, pdb id, progress,
progress title) as a growing list of optional WritePdb parameters.
Turn the type into a configured instance whose options are properties,
and add EmbedSourceFiles (default true): when a PDB is generated next
to a project export whose .cs are already on disk, embedding the source
again is redundant, so the caller can turn it off. The per-document
checksum/hash is computed either way, so documents still resolve.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The single-assembly project save was already wired, but selecting
multiple assemblies and choosing Save Code only ever saved one of
them: SolutionWriter had never been ported, so there was no path that
emitted a .sln with one decompiled project per assembly.
Port SolutionWriter UI-agnostically and route both Save Code surfaces
(the context-menu entry and the Ctrl+S command) through a shared
SolutionExport helper that recognises a multi-assembly selection,
prompts for the .sln path, runs the export behind the tab's cancellable
progress UI, and reports the result into the active decompiler tab
(matching how the other long-running commands surface their output).
Unlike the prior app, the project-export path writes the .csproj to
disk itself and only returns a breadcrumb through the text output, so
each project's on-disk file is located after decompiling rather than
captured from the output stream.
Assisted-by: Claude:claude-opus-4-8:Claude Code
These PackageVersion entries had no PackageReference anywhere in the tree -- WPF-era leftovers (TomsToolbox.Wpf.*, Microsoft.Xaml.Behaviors.Wpf, AvalonEdit, Dirkster.AvalonDock, DataGridExtensions), unused theme/diagnostics variants (Avalonia/Dock Fluent themes, Avalonia.Diagnostics, Fonts.Inter), the VSTest SDK (the suite runs on Microsoft.Testing.Platform), and bare System.* versions that pin nothing under central package management. Restore leaves every packages.lock.json unchanged, confirming none were in use.
Assisted-by: Claude:claude-opus-4-8:Claude Code