Format: one section per decision. Lightweight ADR.
Date: 2026-04-21 Status: Accepted
.NET 10 is LTS (released Nov 11 2025). C# 14 adds field keyword, extension members, ref-readonly improvements. No reason to use .NET 8 or earlier.
Consequences: users must install .NET 10 SDK to build. Runtime deployment uses win-x64 single-file publish with PublishReadyToRun=true.
Date: 2026-04-21 Status: Accepted
WPF has the lowest memory floor, the most stable HWND hosting story (critical for EasyWindowsTerminalControl), and 20 years of ecosystem stability. WinUI 3 and MAUI still have HWND-interop quirks. Avalonia would force cross-platform abstractions we don't need — Windows-only is the product.
Consequences: no Fluent-by-default controls — we layer Wpf.Ui on top. No XAML Islands / WinUI.
Date: 2026-04-21 Status: Accepted
EasyWindowsTerminalControl wraps Windows Terminal's official renderer (DirectX atlas engine). It is native WPF, HWND-composable, and matches the product's "native Windows" positioning. The alternative (xterm.js in a WebView2) would add 100–150 MB of Chromium per window and cross a JS interop boundary for every keystroke.
Caveat: package targets net6.0. Forward-compatible with net10.0-windows. If build fails or rendering is broken on .NET 10, plan B is fork to /vendor/EasyWindowsTerminalControl and retarget.
Date: 2026-04-21 Status: Accepted
Provides Fluent styles, dark mode, Mica backdrop, and Win11-native title bar on WPF. Actively maintained. Package targets net8.0 — forward-compatible with net10.0-windows.
Consequences: accept the package's style surface; theme tweaks go through its ApplicationThemeManager API.
Date: 2026-04-21 Status: Accepted
git on PATH is guaranteed on the target developer machine. LibGit2Sharp lags behind modern git features (worktree UX, partial clones, commit graph) and bundles a native DLL we'd rather not ship. All git operations are IO-bound anyway — Process.Start overhead is negligible.
Date: 2026-04-21 Status: Accepted
Built-in, zero-dep, fast, source-gen friendly. Newtonsoft only wins on exotic scenarios we don't have.
Date: 2026-04-21 Status: Provisional — revisit in Fase 2
Simpler state model and no surprises when two windows race to write projects.json. Can be lifted when multi-window support lands.
Date: 2026-04-21 Status: Accepted
Team-default combo. No UI tests in Fase 1 (would require a heavy harness for WPF + HWND-hosted terminal control).
Date: 2026-04-21 Status: Accepted
projects.json always writes a version field. Load path checks version, runs migrations in order, writes back upgraded file. Unknown fields are preserved on roundtrip so downgrades don't lose data.
Date: 2026-04-21 Status: Accepted
Dark-first aesthetic consistent with the rest of the IDE-class tools our users already live in (VS Code, Rider). Users can switch later through Wpf.Ui's theme manager (wired in Phase 2 settings).
Date: 2026-04-21 Status: Accepted — design overhaul tracked as Fase 7
A project-wide docs/DESIGN.md was installed via npx getdesign@latest add figma --out ./docs/DESIGN.md. This is a curated DESIGN.md derived from Figma's marketing / product UI language (variable fonts, pill/circle geometry, black-and-white chrome with colorful product content).
Why: CodeScope's current Fluent/Mica look is functional but generic. Picking an opinionated style guide up front gives any future UI work a concrete reference — colors, spacing, type weights, component geometry — so agent-authored UI changes converge on one aesthetic instead of drifting. The Figma system in particular emphasizes typography-driven hierarchy, which plays well with a dev-tool density.
Consequences:
- Any Fase 7 UI pass should read
docs/DESIGN.mdfirst and mirror its color tokens and geometry rules in the Wpf.Ui theme. - Not every element of the Figma system is portable (custom variable font license, web-specific effects); Fase 7 scope must include a "what we're keeping vs adapting vs dropping" table.
- DESIGN.md is a reference, not a contract — if the system conflicts with Windows platform conventions (e.g. keyboard shortcuts, Mica backdrop), Windows convention wins.
Date: 2026-04-21 Status: Accepted
Per-monitor v2 is required for the app to look correct on mixed-DPI setups. Declared via app.manifest.
Consequences: CodeScope.App.csproj references an app.manifest; all child windows inherit.
Date: 2026-04-22 Status: Accepted
ViewModels backed by ObservableObject split into <Name>.cs (state + constructor),
<Name>.Commands.cs ([RelayCommand] surface), and <Name>.StoreSync.cs (event-projection
into observable collections) when the single file crosses ~400 LOC or mixes three distinct
responsibilities. Below that threshold, keep the single file.
Applied to: SidebarViewModel (was 640 LOC). Not yet applied to: MainViewModel (964 LOC),
which is scheduled to follow the same split once the layout-persistence code moves out of
the VM into LayoutStore directly. Tracking under the mid_level_elegance review cluster.
Consequences:
- Fresh contributors grep for
<Name>.Commands.csto find a context-menu action; this is the convention. - Reviewers should not flag a single-file VM under the threshold as "inconsistent with the split" — size matters.
Date: 2026-04-26 Status: Accepted
MainViewModel.NewSessionAsync previously matched any soft-closed session for
(worktree, agent) and restored it transparently in place of minting a fresh
session. The implicit behaviour shipped because there was no UI to see closed
sessions. Side-effect: a second closed conversation on the same worktree was
shadowed by FirstOrDefault.
Decision: Drop the auto-resume block. New session always mints fresh. Reopening a closed session is an explicit action driven by the per-worktree history surface (sidebar disclosure under each worktree + a "Reopen most recent closed session" item in the worktree context menu).
Consequences:
- Returning users learn one new affordance (history disclosure) and the one-keystroke worktree-context-menu shortcut for the most-recent-closed case.
- The
TryRestoreSessionAsynchelper stays as the implementation of explicit reopen. - The drag-between-groups path (
MoveTabToGroup) is unaffected — it never routed through the auto-resume block. projects.jsonnow accumulatesSessionentries for closed shells too; pre-this-change, shells were always hard-removed on close soprojects.jsonnever carried shell rows.
Date: 2026-04-26 Status: Accepted
Dragging a session tab between editor groups used to respawn the underlying
ConPTY child. Root cause was a WPF lifecycle interaction, not a terminal-control
limitation: each EditorGroupView bound an ItemsControl to
EditorGroupViewModel.Tabs with a DataTemplate for SessionTabViewModel. A
cross-group VM move destroyed the source ContentPresenter, which unloaded
SessionTabView, which destroyed the inner EasyTerminalControl HwndHost,
which killed pwsh / claude / codex along with the scrollback.
Decision: Introduce ISessionViewHostPool (UI-singleton) that owns one
SessionTabView per session id. Each EditorGroupView is a single
ContentControl whose Content is resolved from the pool keyed by
SelectedTab.Descriptor.Id. Reparenting the same SessionTabView between
two ContentControls preserves the inner HwndHost (WPF documents this:
"removed HwndHost is not destroyed, just reparented under a non-visible
window"). The pool calls a renamed SessionTabView.Teardown() on Release
(close / restart / cascade) — we removed the Unloaded teardown hook because
it fires on every reparent, which is the exact bug we were fixing.
Decision considered and rejected:
- TermPTY-only transfer (
DisconnectConPTYTerm→ assign to a freshEasyTerminalControl'sConPTYTermproperty): preserves the process and agent state but the renderer's scrollback lives in theMicrosoft.Terminal.WpfHWND, not in TermPTY. The new HWND boots empty. A replay buffer would be approximate at best for full-screen TUIs. - Win32
SetParenton the inner ConPTY HWND: HwndHost has no supported way to swap its child HWND between two host instances. Workarounds are fragile acrossMicrosoft.Terminal.Wpfversions.
Consequences:
MoveTabToGrouplost ~25 lines of agent-resume / telemetry-rebind plumbing that existed to mask the respawn — no respawn now, no need to mask.SessionTabViewis no longer free to manage its own ConPTY lifecycle; the pool is the single owner. Any new code path that closes a session must callpool.Release(id)(otherwise pwsh lingers and locks its cwd, breakinggit worktree remove).- Pool is UI-only (lives in
CodeScope.Ui.Services);Coreknows nothing about it. Dispatcher-affined, no locking. - Spec:
docs/superpowers/specs/2026-04-26-cross-group-terminal-drag-design.md - Plan:
docs/superpowers/plans/2026-04-26-cross-group-terminal-drag.md
Date: 2026-05-01 Status: Accepted Issue: #33
Soft-closed sessions accumulate in projects.json and in the sidebar History
disclosure for the lifetime of an install. Without a bound, long-running
installs grew hundreds of closed sessions per worktree, slowing config load
and growing the sidebar tree. ADR-0013 introduced the explicit history surface
but deliberately deferred the retention policy.
Decision: Two complementary cuts, applied per-worktree on every prune sweep:
- TTL — sessions whose
ClosedAtis older than 90 days are dropped. - Cap — if the per-worktree closed count is still above 100, the oldest entries beyond the cap are dropped (newest-first ordering preserved).
Pruning runs on LoadAsync (one-time migration of pre-policy state) and on
every SoftCloseSessionAsync (so the cap stays enforced as new closes arrive).
Each pruned row emits SessionStoreChange.SessionRemoved so VM History rows
disappear in lockstep. Live sessions (ClosedAt is null) are never touched.
Decision considered and rejected:
- Manual purge only (per-worktree "Clear history" action): accumulates silently between user actions and never bounds the long tail. Useful as a complementary affordance but not a sufficient policy on its own.
- Per-project retention instead of per-worktree: a project with one busy worktree would crowd out the closed-session history of its quieter siblings.
Consequences:
- Constants live in
SessionRetentionPolicy(MaxPerWorktree = 100,MaxAge = 90 days). Bumping them requires a code change + new ADR amendment; not user-configurable for now. - Behaviour change: on first launch after this lands, any pre-existing closed session older than 90 days OR beyond the cap-100 of its worktree is irrecoverably dropped. The migration is logged as a warning if persistence fails so the user can re-run.
- Live sessions, sessions with
ClosedAt = null, and orphan sessions (WorktreeId = null) are still bounded — orphans collapse into a single "no-worktree" bucket so the cap still bounds them. - A future "Clear history" UX (mentioned as a complementary action in #33) doesn't conflict with this policy; it'd just be a manual variant of the automatic sweep.
Date: 2026-05-01 Status: Accepted
The bottom-docked diff panel (Ctrl+D) was never used in practice — every
real review surface (status bar git stats, sidebar dirty markers, in-terminal
git diff, the agent CLI's own diff view) covers the same ground better. The
panel had a known memory issue (#32: triples large patches in memory + bursts
duplicate work on every selection change) that we'd otherwise have had to fix.
Cheaper to drop than to maintain.
Removed:
DiffPanelViewModel,DiffPanelView(.xaml+.xaml.cs)IGitService.GetDiffAsync+ thegit diff --no-color HEADrunner inGitService(no other consumers)- DI registration in
App.xaml.cs, theMainViewModel.Diffproperty +AttachDiffPanel+ToggleDiffPanelcommand MainWindow.xaml:Ctrl+DKeyBinding, theGridSplitter+ panel mount, the now-unusedBoolVisiblewindow-resource converter (other Views redeclare it locally), and theWorkspaceLayerGrid's row definitions (single child, no rows needed)SidebarViewModel.WorktreeSelectedevent (only consumer was the diff-panel bridge)- Command palette entry "Toggle diff panel"
README.mdCtrl+Drowdocs/design/html/CodeScope - Diff Panel.html
Decision considered and rejected:
- Keep the panel and fix #32: investment without a user; the panel was never integrated into anyone's actual review flow.
- Replace with an inline diff-on-hover affordance: out of scope; no current demand and would re-introduce the same per-selection work.
Consequences:
- Closes #37 (this ADR) and supersedes #32 (the perf fix is moot once the feature is gone).
- One fewer
IGitServicesurface for tests/mocks to track. Ctrl+Dis now free for future bindings.
Date: 2026-05-04 Status: Accepted
A fresh-install user reported a black workspace and no terminals ever
initialising. Root cause: EasyWindowsTerminalControl →
Microsoft.Terminal.Wpf.dll → Microsoft.Terminal.Control.dll (in
runtimes/win-x64/native/) is a native binary built with MSVC. It depends on
vcruntime140.dll, vcruntime140_1.dll, and msvcp140.dll. On a Windows
box without the Visual C++ 2015–2022 Redistributable installed, the native
DLL silently fails to load — the HwndHost stays empty and every terminal
tab is a black rectangle. Velopack does not bundle VCRedist.
Decision: ship the three DLLs app-local, beside CodeScope.exe.
App-local DLL resolution wins over System32, so the bundle works whether
or not the user has VCRedist installed, and never interferes with the
system-wide install for other apps.
Implementation:
- DLLs committed under
src/CodeScope.App/native/vcredist/(~709 KB total —msvcp140.dll541 KB,vcruntime140.dll121 KB,vcruntime140_1.dll47 KB). Source: System32 on a developer box where the latest VCRedist is installed (bit-identical to the redist payload). CodeScope.App.csprojContent glob copies them to output + publish, in the same style as the existingconpty.dllglob (ADR notes / PR #27).tools/refresh-vcruntime.ps1re-pulls from%SystemRoot%\System32and prints fresh versions + SHA-256s forNOTICE.md.VcRuntimeBundleTests(4 tests) guards both the source DLLs in the repo and the copied DLLs in the build output — silent regression if anyone removes the Content glob is now a build break.NOTICE.mdnext to the DLLs documents version, SHA-256, source, and the Microsoft redistribution license terms.
Decisions considered and rejected:
- Startup-probe + friendly download dialog: shifts the work to the user, doesn't help offline installs, and a black workspace is a brutal first impression even with a dialog. Kept on the shelf as a possible additional safety net but not needed once the runtime is bundled.
- Velopack prerequisite stage that installs VCRedist: requires admin, re-runs Microsoft's installer, ~25 MB download per fresh install, and ties our installer flow to a third-party MSI. The bundled DLLs are ~3% of that size, run with no admin, work offline.
- Pull DLLs from a NuGet package at build time: no official Microsoft package ships these for app-local redistribution; community packages exist but are unmaintained. Committing the binaries is more honest about what we depend on.
Consequences:
- Repo grows by ~709 KB of binary content. Acceptable — same order of magnitude as the icon set, smaller than several screenshot assets.
- The DLL versions need a manual refresh when Microsoft ships an important security update to the VC runtime. The refresh script + NOTICE table make this a 30-second operation.
- Velopack installer payload grows by ~709 KB.
Date: 2026-05-11 Status: Accepted
The C# build configures the equivalent fields via hand-edited settings.json
(plus a handful of App.config entries); there is no in-product Settings UI on
that side. The Rust port adds one — Ctrl+, opens a centered modal that
surfaces the existing [codescope_core::Settings] fields (theme, default agent,
font family / size / line-height, scrollback, cursor shape + blink) through
form controls. This is a one-way deviation from the C# build: nothing about
the on-disk schema changes, the dialog merely renders the existing struct.
Why deviate: hand-editing JSON is a poor first-run experience and the
Rust port has no separate App.config surface to fall back on. The visual
idiom mirrors the in-app modals (new_project_dialog.rs,
new_worktree_dialog.rs) so the chrome stays consistent.
Consequences:
- The Rust port ships
src/settings_dialog.rswith no C# counterpart. - Saves go through [
Settings::save] — the existing settings-file watcher inapp.rspicks up the change on its next tick, andapply_settingsis also called inline so the swap is instant. - The dialog can preview the theme live as the user clicks through the
list; Cancel explicitly reapplies the snapshot of
Settingscaptured when the dialog opened, reverting any in-flight preview (the file-watch poller cannot do this on its own becausesettings.jsonis untouched during a preview). Font + scrollback edits only take effect for newly-spawned tabs (running terminals keep their baked-in palette / scrollback); the dialog surfaces a small "Applies to new tabs only." hint to make this explicit. - No new schema fields — anything not yet representable in
Settings(e.g. per-theme overrides for an external theme bundle) stays out of reach of the dialog until the underlying struct grows the capability.
Date: 2026-05-12 Status: Accepted (PR #154)
The Rust port (codescope-rs/) now ships through a separate
rs--release.yml workflow built on cargo-dist +
velopack-rs, running alongside the existing C# release.yml.
Both pipelines target the same GitHub repository and the same product
brand — the split is purely build-system.
Why deviate from the single-pipeline default?
- Different toolchains: the C#
release.ymlshells out tovpk(the .NET CLI) which orchestrates MSBuild, Velopack'sdotnet publishwrapper, and the Velopack uploader in one pass. Trying to run cargo-dist inside that workflow would mean pulling Rust + cargo-dist + MSI tooling into the .NET pipeline just to ship an artifact that's independent of the C# build. - Independent release cadence: the Rust port is still pre-feature- parity in places (cross-platform, code signing, full Velopack channels). A combined pipeline would force both ports to rev tags together, which doesn't make sense yet.
- Tag-namespace separation (
rs-prefix vs.vprefix). Both pipelines rungit describe --tags --match v*so the C# build's informational version (and the Rust build'sCODESCOPE_VERSION_DISPLAY) only ever see product-version tags, never the Rust-pipeline-internalrs-vX.Y.Ztags. Without that--match,git describewould pick whichever tag is closest in commit-distance regardless of prefix — exactly the failure mode Copilot caught on PR #162. SeeDirectory.Build.targetsandcodescope-rs/build.rs.
Consequences:
- Two release-pipeline files live in
.github/workflows/. The C#release.ymlis unchanged; the Rust file isrs--release.yml. - Two parallel release flows for the user:
git tag v0.2.6 && git push --tags→ C# release (existing).git tag rs-v0.0.2 && git push --tags→ Rust release (new).
- The Rust pipeline ships multiplatform artifacts as of the
multiplatform-release-pipeline PR:
x86_64-pc-windows-msvc(MSI + zip),aarch64-apple-darwin+x86_64-apple-darwin(.tar.xz), andx86_64-unknown-linux-gnu(.tar.xz). The original cross-compile-from-Windows blocker (ring's build.rs needingcc/clang/zig) is sidestepped by running native builds on the GH Actions matrix (windows-2022,macos-14,macos-15-intel,ubuntu-22.04). Seedocs/HANDOFF.md"Cross-platform status" for the runtime test matrix and known per-platform gaps (code signing / SmartScreen / Gatekeeper remain follow-ups). velopackcrate replaces a hand-rolled apply path. Thecodescope_core::update_checkmodule (which polls the GitHub releases endpoint and emits anUpdateStatus::Available) keeps its existing role; the bin-sidevelopack_bridge.rsconsumes that signal and either auto-applies (Velopack-install path) or surfaces the URL (cargo-dist MSI /cargo runpath).dist-workspace.tomlcarriesallow-dirty = ["ci"]so the hand-edited "tag-push only" trigger inrs--release.ymlsurvives futuredist generateruns.
Date: 2026-05-13 Status: Accepted
The C# EmptyStateView renders the "Clone from Git URL" button as
IsEnabled="False" with a tooltip "Clone from Git URL (coming soon)" —
the WPF clone flow was never wired up there.
The Rust New Project dialog already implements the full
git clone <url> <parent>/<name> flow via DialogMode::Clone (see
codescope-rs/src/new_project_dialog.rs). Leaving the empty-state
ghost disabled in the Rust port would be a strictly worse UX than the
underlying capability already supports.
Decision: the Rust empty-state Clone CTA is interactive. Clicking
it opens the New Project dialog already switched to the Clone tab via
AppShell::open_new_project_dialog_clone. Same git clone flow the
sidebar's "+ New Project → Clone from URL" tab uses today.
Consequences:
- The Rust empty-state hero diverges visually from the C# spec: where C# shows a faded disabled-looking ghost, Rust shows an enabled ghost button with hover state.
- One additional entry point (
open_new_project_dialog_cloneonAppShell) routes through the existingSidebar::set_new_project_modeswitch, so the divergence is isolated to one helper — no fork of the dialog state machine. - If/when the C# build wires its clone flow, both ports converge on the same affordance.
Date: 2026-05-14 Status: Accepted
Until this ADR the Rust port had two sources of truth for "which
sessions are open across a restart": projects.json carried the live
Session rows (with closed_at = None), and layout.json carried a
parallel open_tabs: Vec<RestoreTab> that the cold-start path
(AppShell::rehydrate_or_cold_start) actually read. Any divergence
between the two — a crash before save_layout fired after a spawn, a
manual file edit, a partially-restored backup — left "live" rows in
projects.json that were invisible to the user because they weren't
in layout.json.
The C# build doesn't have this problem: LayoutStore.Layout carries
only GroupCount, FocusedGroupIndex, Dictionary<string, int> SessionToGroup and GroupWidths. MainViewModel.HydrateFromLoaded
iterates every Session with ClosedAt is null and uses
SessionToGroup[s.Id] purely to decide where the tab lands, not
whether it lands at all.
Decision: mirror the C# split. LayoutState.session_placements: Vec<SessionPlacement> replaces open_tabs as the placement record;
each entry binds a Session.id to its group index and an
active_in_group flag. The legacy open_tabs field stays
deserialise-only and is migrated in place on first load, then dropped
on next save. rehydrate_or_cold_start walks self.projects.projects[].sessions
where closed_at.is_none() and falls back to group 0 / non-active for
sessions without a placement.
Consequences:
- A session opened via
Ctrl+Tis durable from the momentSessionManager::openwritesprojects.json— even a process kill before the nextsave_layoutkeeps the tab on next launch (it just comes back in group 0 if the placement wasn't captured yet). Tab.auto_typeis no longer persisted as separate state; the resume command line is rebuilt at rehydrate time fromSession.agent_id+Session.agent_session_idviabuild_resume_auto_type, matching the C# hydrate path.- Free-floating tabs (no project context at spawn time → no row in
projects.json) no longer survive a restart. This matches C#'s invariant that every session belongs to a project (the "unsorted" bucket is the catch-all;AppShellalready routes there via the Add Project flow). - One-shot migration: pre-ADR
layout.jsonfiles keep their tab groups via theopen_tabs → session_placementsconversion inLayoutState::migrate; entries withoutsession_idare dropped (those predate resume-by-id and had noprojects.jsonlink anyway).
Date: 2026-05-14 Status: Accepted
The Rust port (codescope-rs/) has matched the C# build's feature set
since rs-v0.3.0-rc.5 (session 41) and has been the sole daily driver
since rc.4. The last twenty merged PRs are all (rs)-namespaced; the
C# tree under src/CodeScope.{App,Core,Ui}/ has been untouched for
weeks. Carrying both implementations costs: duplicated release
pipelines, divergent build settings, a parity audit doc that goes
stale within days, and a "where is the canonical answer for X"
question on every change.
The Rust port is also platform-portable in a way WPF is not. Velopack feeds now ship for Windows, macOS arm64/x64, and Linux x64 from one pipeline (ADR-0019, PRs #201 / #202).
Earlier ADRs in this file describe the C# stack as canonical
(ADR-0001 .NET 10, ADR-0002 WPF, ADR-0003 EasyWindowsTerminalControl,
ADR-0004 Wpf.Ui, ADR-0006 CommunityToolkit.Mvvm, etc.). Those decisions
remain historically accurate for the v0.2.X line and are kept as
written. From this ADR onward, the Rust + GPUI stack is the active
record:
- GPUI for the application shell (was: WPF +
Wpf.Ui). codescope-terminal(GPUI-native ConPTY/PTY) for terminal hosting (was:EasyWindowsTerminalControl).- Cargo workspace at
codescope-rs/Cargo.tomlfor builds (was:CodeScope.sln+Directory.Build.{props,targets}). velopack-rsfor auto-update (was: Velopack .NET); pack id stayscodescope-rsthrough cutover-2 and is renamed tocodescopein cutover-3.serde_jsonforprojects.json/layout.json(was:System.Text.Json). Same JSON shape — see docs/MIGRATION-csharp-to-rust.md.
Consequences:
- The "mirror C# implementation 1:1" workflow rule (introduced in session 33 after PR #56 invented a non-existent UX) is retired together with the C# code. Future deviations from the v0.2.6 behavior are normal product work, not parity violations.
docs/PARITY-AUDIT.mdis closed-stamped historical.- The last commit where the C# source tree builds is tagged
legacy/v0.2.6-finalon the cutover-1 merge commit. The C# code is fully removed in cutover-2; the workspace is flattened and the Velopack pack id is renamed tocodescopein cutover-3. - Historical
v0.2.XGitHub releases stay published; theupdate_checkfloor filter (see ADR-0020 and PR #206) keeps them out of the live update flow. - Solo dogfooding: no preflight migration release is shipped. Users
(= the developer) reinstall once between the
codescope-rsandcodescopeVelopack pack ids;%APPDATA%\CodeScope\projects.jsoncarries across unchanged.