Skip to content

feat(icons): taskbar/Start menu shell surfaces + File Explorer folders/drives (+ uninstall restore) - #12

Open
MelanieLezcano wants to merge 16 commits into
nicepkg:mainfrom
MelanieLezcano:feat/taskbar-startmenu-surfaces
Open

MelanieLezcano wants to merge 16 commits into
nicepkg:mainfrom
MelanieLezcano:feat/taskbar-startmenu-surfaces

Conversation

@MelanieLezcano

Copy link
Copy Markdown

Implements ADR-0024 (shell surfaces) + ADR-0004 D7 (Explorer module), carried on this fork per the 2026-07-21 community-proposal panel (the PR is expected to be deferred; write-surface reliability is the dominant ship risk, and the fork carries the feature until then).

Included (9 commits)

  • docs: propose shell surfaces — taskbar + Start menu styling (ADR-0024 + plan)
  • feat(icons): style taskbar pins + Start menu shortcuts (ADR-0024, fork draft)
  • feat(icons): shell surfaces participate by default (ADR-0024 owner amendment)
  • fix(icons): worker always-acks every message — dev-loop hardening
  • feat(icons): style Store/MSIX Start entries via owned appx wrappers (Task 11)
  • feat(icons): appx wrappers launch via host --activate-aumid fallback (Task 11 gate)
  • feat(icons): style File Explorer profile folders via desktop.ini (ADR-0004 D7)
  • feat(icons): style File Explorer fixed drives via the per-user registry (ADR-0004 D7)
  • feat(release): restore the desktop on uninstall (NSIS PREUNINSTALL hook)

Touched spec sections

ADR-0024 (new) · ADR-0004 D7 (amended) · specs 00 roadmap / 01 architecture / 06 icons.

Gates run (all green)

cargo test --workspace · bun test (670) · bun x tsc -b · bun run lint · bun run check:bindings · banned-colors.

Owner invariants

  1. Reversible / zero residue (the new uninstall hook restores every surface before files are removed).
  2. Progid-level file-type icons remain constitutionally banned (never built).
  3. The one-click hero is untouched (surfaces are a participation axis, not a new flow).
  4. No elevation beyond the disclosed, batched UAC (drives are per-user HKCU, in-host).
  5. Broader terminal only — never a generic registry RPC.

Verification status

On-box apply/restore cycles verified for the Explorer surfaces (profile folders + fixed-drive keys) and the uninstall restore; the full write-surface live pass remains owner-supervised (docs/verification/owner-supervised-live-runs.md). Records: docs/plans/2026-10-12-taskbar-startmenu-surfaces.md, docs/plans/2026-10-13-explorer-folder-drive-icons.md.

Melu added 9 commits October 12, 2026 05:07
…k draft)

Scan + wire three shell surfaces behind a second, OPT-IN participation axis (surfaces: {taskbar, startMenu}, all-OFF default — consent is never inherited; kindPolicy semantics inverted on purpose). The desktop stays always-on and kindPolicy-governed.

- dm-contracts: SurfaceDto + IconItemDto.{surface,parent} (serde defaults); bridge schema 10->11; bindings regenerated.
- dm-windows: taskbar pin root + both Start menu roots; surface-labeled walk (flat pin, recursive Start menu with parent grouping, depth cap 8, reparse skip); fs-fixture tests.
- dm-operations: recipe surfaces parsed in style_resolve + gated in effective_config (one participation authority); scope classify test for Common Start menu -> ProgramData; settings_store round-trip preservation test (G11).
- src-tauri: surface-aware icon_host assembly; live_slots stays desktop-only; non-desktop roots are best-effort (only the user desktop is mandatory/loud).
- Store: surfaces rides the recipe draft (4th knob; G11 whitelist on both bridge sides), surface gate inside effectiveTileConfig so disabling a surface auto-restores it on the next apply (RESTORE-FIRST), setSurface with one undo step per flip, history/preset/snapshot paths carry it.
- UI: SURFACES participation group with live counts, canvas surface chips (Desktop | Taskbar | Start menu), real pins rendered in the taskbar strip via the shared tile renderer, Start panel mock grouped by parent, per-surface consent copy; i18n en + zh-hans in lockstep.
- Resident/reconciler stays hard desktop-only (spec 07 scan filter unchanged).

Gates: bun test 670/0 · bun run lint · tsc -b · cargo test --workspace · bindings drift · banned-colors. Live [WINDOWS-VERIFY] pass pending (plan task 9). ADR-0024 status: Proposed — NOT owner-disposed.
…endment)

Owner decision 2026-10-12: the taskbar + Start menu legs now default TRUE
on both bridge sides (serde `impl Default for SurfacesJson` +
`DEFAULT_SURFACES`), the same participate-by-default rule as kindPolicy —
system-wide restyling is the app's promise. An explicit OFF leg still
gates that surface (RESTORE-FIRST: its items ride restoreIds on the next
apply) and the desktop stays never-gated. Round-trip/normalize/store/Rust
tests updated for the new default; ADR-0024 §4.2 amended.
…sk 9 point 2)

render-wasm.worker.ts posts `sourceReady ok:false` on init/decode failure
(main thread re-requests instead of waiting forever), exactly ONE
`baked png:null` on every failure path via a `posted` flag, releases
`arrowReady` in a finally, and guards drainRenders so a rejected
`rendererReady` can neither wedge `draining=true` nor surface as an
unhandled promise rejection. Drops the working-tree-only TEMP-DEBUG
tracing (command boundaries, UAC path, shell restart, stage beacon) and
adds vite's `**/target/**` watch ignore (EBUSY on the linker was crashing
the dev loop). Plan doc records the Task 9 field passes — CAS conflict on
a hand-edited pin, restore byte-identical 94/94 FileBytes — plus this
pass's findings (jpg attrs race, vanish-residue heal, 3 steady conflicts
open, WebView2 CDP policy workaround).
…task 11)

Start entries that are AppListEntries with no file-backed shortcut (7 of 97 on the dev box) become styleable through an OWNED wrapper .lnk in the user Programs root: target %SystemRoot%\explorer.exe + args shell:AppsFolder\<AUMID>, ownership marker DeskMakeover:appx-wrapper:v1, and System.AppUserModel.ID set on the ShellLink OBJECT (QI IPropertyStore -> SetValue VT_LPWSTR -> Commit) so the ONE IPersistFile::Save publishes link + property block atomically - Windows then merges the wrapper with the virtual entry (2 rows -> 1) and renders our icon. A by-path property write is impossible (the save target is a .tmp claim-temp with no shell property handler - verified E_FAIL on-box), and the icon-rewrite path reads the AUMID up front and re-attaches it to the loaded link before Save, so a re-style never un-merges the wrapper (fail closed if a known AUMID will not re-persist).

Scan classifies file-backed AppsFolder shortcuts from their arguments and synthesizes items for virtual-only apps via the new shell/appsfolder.rs (AppsFolder enumerate/resolve/shell-item/glyph; collisions with a walked stem or a duplicate name skip + warn). State reader splits the originals: a present wrapper reads IconRef like any .lnk; an absent one reads a dedicated AppxAbsent fingerprint sentinel and anchors RestoreAnchor::FileAbsent, so reset REMOVES the wrapper and the package icon returns. Extraction early-returns for AppxShortcut and renders the AppsFolder item's own glyph via IShellItemImageFactory::GetImage through the shared factory_icon_image tail - never the wrapper's IconLocation. Elevation never (user Programs root only); dm-operations rides the existing kind-agnostic driver, its fake world gains a virtual-item model, and the two orchestration tests live in the Mac-gated icons/tests.rs (verified on-box with the gate lifted: 272/272).

Docs: ADR-0024 records the sanctioned wrapper recipe (Decision 6); the plan fixes the SHCreateItemFromRelativeName parent phrasing (IShellItem parent, not FOLDERID), records the implemented atomic recipe, and keeps the on-box ship gates open (enumeration pin, synthesized scan count, click-to-launch end-to-end with the deskmakeover-desktop.exe --activate-aumid fallback if the explorer route still fails; then the HKLM WebView2 CDP dev value is deleted).
…(task 11 gate)

On-box click-to-launch verification: the documented primary recipe (explorer.exe + shell:AppsFolder\<AUMID>, what Windows' own Store pins use) failed a real-wrapper ShellExecute test, confirming the 3 earlier CLI attempts - pre-existing COM registration damage on this box (same family as the wallpaper 0x80040154). The sanctioned re-target is now implemented: create_appx_shortcut points the wrapper at the HOST binary (current_exe) with args '--activate-aumid <AUMID>', and main.rs gains a fast-path that calls IApplicationActivationManager::ActivateApplication (CLSID 45BA127D-10A8-46EA-8AB7-56EA9078943C, IID 2E941141-7F97-4756-BA1D-9DECDE894A3D) and exits without booting the GUI (0 activated / 2 missing arg / 3 failed). Host is asInvoker, so no UAC on wrapper click. The AUMID property still drives the Start-menu merge - independent of the launch mechanism.

parse_appx_aumid now recognizes BOTH wrapper recipes in a walked .lnk's arguments (launcher arm: first token after the flag is the AUMID, case-insensitive flag, anchored - same posture as the AppsFolder arm); without it a present wrapper would re-scan as an ordinary shortcut and re-style would compound. activate_app lives in shell/appsfolder.rs, owns its COM init, and both recipes are pinned by tests (args + target round-trip through an icon rewrite, launcher-arm parser cases, walk reclassification).

Evidence (plan status + checklist + ADR-0024 D6 updated): enumeration pin green; 42 synthesized AppxShortcut with zero walked collisions; apply created 42 wrappers verified field-by-field (target/args/marker/System.AppUserModel.ID/baked asset); click gate PASSED - direct CLI exit 0 with WhatsApp.Root up, then wrapper click -> WhatsApp.Root up; two full apply/restore cycles clean (all 42 removed via RestoreAnchor::FileAbsent, user-root + ProgramData + desktop originals byte-restored); steady-state scans show no duplicate-name warns (transient ones were the watcher rescan racing the deletions, first-wins). Gates: cargo test --workspace 0 failures (dm-windows 138), bun test 670 + banned-colors 146, tsc, lint, check:bindings all green. HKLM WebView2 AdditionalBrowserArguments CDP dev value + WebView2 key deleted after verification. Also lands the plan Task 11 doc corrections (SHCreateItemFromRelativeName IShellItem parent, factory_icon_image extraction tail, object-attached AUMID write recipe).
…-0004 D7)

Land the folder half of the `explorer` surface: enumerate the user
profile's browsable folders and style them through the existing
desktop.ini writer (apply/folder.rs), so folder icons follow a chosen
look across File Explorer.

- domain: `ItemSurface::Explorer` — one surface covers folders + drives,
  routing by `kind` (drives land in a follow-up).
- ops/contracts: `surfaces.explorer` toggle (default ON) + allows() arm.
- windows: `known_folders::profile_root()` (FOLDERID_Profile) +
  `walk_explorer_folders` — BFS (spends the shared icon-preview budget on
  shallow content first, never on deep tooling junk), depth cap 8,
  denylist + dot-prefix + Hidden/System + reparse exclusions, cap 1400;
  scan dedup drops profile folders the desktop surface already owns.
- frontend: participation toggle leg, consent count, en/zh i18n.

Verified on-box: profile folders carry our IconResource; AppData and
node_modules/target/android-sdk/.bun/... stay clean (0 writes). Gates:
cargo test --workspace, bun test (670), tsc, lint, check:bindings all green.
…ADR-0004 D7)

Phase B of the Explorer module (folder half landed in 82386c0): the fixed
drives the user browses are styled through the PER-USER icon key

  HKCU\Software\Classes\Applications\Explorer.exe\Drives\<L>\DefaultIcon

written IN-HOST with NO elevation. An on-box spike proved the shell honours
this key (SHGetFileInfo("C:\") changed and reverted), so drives stay inside the
Warm tier; the machine HKLM ...\Explorer\DriveIcons tree is read ONLY as the
original-icon fallback and is never written. autorun.inf is never touched, and
only DRIVE_FIXED letters are keyed (removable/optical/network letters excluded).

- domain: ItemKind::Drive; DriveAnchor + RestoreAnchor::Drive (dm-domain).
- apply/drive.rs: read_current (per-user -> machine fallback), apply, restore,
  strict letter_from_path (X: / X:\ only). Restore removes just the (Default)
  value we created — recycle-bin posture, never delete_subkey_all over a key
  another program may share.
- state_reader + fingerprint_surface: Drive surface state, read fingerprint,
  expected-after-apply.
- source.rs: original_drive() recovers the original from the CAPTURED anchor
  (never a live read post-apply — SHGetFileInfo would answer with our own asset
  and compound Style(Style(orig))); an absent value falls back to the STATIC
  stock glyph shell32.dll,8.
- shell: known_folders::visible_fixed_drives() (GetDriveTypeW, DRIVE_FIXED only);
  scan emits one Drive item per fixed root, display name from the shell
  ("SYSTEM (C:)"), fallback "(C:)".
- bucket: Drive rides the Folder bucket (same policy switch + explorer consent);
  IconKindDto/TS union/bindings gain "Drive".
- tests: letter_from_path, visible_fixed_drives (on-box smoke), drive-item
  emission, bucket_of coverage, serde/TS literals, domain kind coverage.

Docs: ADR-0004 amendment (per-user key, in-host, no elevation), roadmap tick,
plan status (two clean on-box apply/restore cycles verified).
Closes the "uninstall residue" live-pass check (taskbar/startmenu plan Task 9).
The per-user NSIS uninstaller reverted nothing, so uninstalling while styled left
the applied icons, wallpaper, arrow overlay, per-user drive keys and app data
behind — a gap against the module's "one-click reversible with zero residue"
promise.

- lib.rs::run_uninstall_restore(): headless full restore reusing the same
  composition roots as setup (startup recovery -> settings store -> icon host
  restore + restore_overlay -> wallpaper restore per monitor -> calm/tweaks
  restore); resolves the app-data dir as %APPDATA%\com.xiaominglab.deskmakeover
  (app.path().app_data_dir() without a running Tauri app).
- main.rs: handle --uninstall-restore before booting the GUI, exit with its code.
- src-tauri/installer-hooks.nsh + bundle.windows.nsis.installerHooks: the
  NSIS_HOOK_PREUNINSTALL runs the restore BEFORE the uninstaller removes our
  files, then drops the per-user data dirs + C:\ProgramData\DeskMakeover. The
  machine-wide HKLM overlay restore raises one UAC (the uninstaller is per-user).

Tested on-box against a live applied state: exit 0; HKLM Shell Icons\29 removed;
the three HKCU Drives values cleared; 417/423 profile desktop.ini removed. Gates
green (cargo workspace, bun 670, tsc, lint, check:bindings). The full
install->apply->uninstall->verify loop rides the owner-supervised live pass.
Melu added 7 commits October 16, 2026 06:57
…ADR-0004 D7)

Explorer phase 2, per the owner rulings of 2026-10-16:

- folders: one breadth-first walk over every fixed root (protected-tree and
  Users gates; depth 10 / 5000-folder caps), with extensions collected along
  the way. Sources are content-addressed `dmicon://` keys WITHOUT the item id,
  so N folders showing the same stock glyph cost ONE preview-budget entry.
- file types: new `ItemKind::FileType` + `FileTypeAnchor`, a per-user
  `HKCU\Software\Classes\<ProgId>\DefaultIcon` writer/reader/restore that
  refuses poisoned ProgIds, and a two-pass scan enumeration with the image and
  `%1`/`%L`/`%l` gates (one item per ProgId, never one per extension).
- bucket "File" as a second, independent kill switch; DTO + generated
  bindings; file types sorted LAST so `.ext` labels cannot starve the preview
  budget of desktop/taskbar/folder items.

Two scale defects the first full-size apply (~5000 items) forced ? the owner
box died at ~3 GB twice ? are fixed here because they are entangled with the
same identity change: `package_masters` now bakes identical masters once and
shares one `Arc<IcoAsset>` across every item that uses it, and
`commit_apply` / `version_switch` build each `ApplyRequest` inside its own
per-item transaction instead of materialising every item's ICO bytes up front
(~1.5 GB per stage before). Regression:
`identical_masters_bake_once_and_share_one_allocation`.
The worker registered one 256x256 RGBA buffer per item id, so 5042 explorer
folders showing the same stock folder glyph held 5042 copies of the same
pixels (~1.3 GB) and the WebView renderer died mid-apply.

Decode identity is now the CONTENT (the source URL): `ready` and `seeds` are
keyed by URL, an `itemSource` alias map records item -> URL, and render/bake
worker messages carry `skey` (the content key) for the pixel lookup while the
coalescing lane, the tile cache and the reply stay per item. Items sharing a
URL now await ONE decode instead of each paying a round trip.

Regression: tests/compositor-source-alias.test.ts (one dispatch per content,
alias-resolved seedOf/getTile/bake, failed decode clears its claim).
- ADR-0004 D7 amendment: folder styling broadened to every fixed drive; the
  file-type ban lifted by owner ruling with explicit mitigations (ProgId-level
  image gate, %1/%L/%l refusal, poisoned-ProgId refusal); images carved out.
- ADR-0024 amendment: the owner ask was re-checked item by item ? pinned
  taskbar icons are already styled; running-window exe resources stay a
  platform floor (an item-by-item re-check, no decision changed).
- new plan doc 2026-10-16 (all drives + file types) with the final gates, the
  on-box verification numbers and the two scale defects plus their fixes.
- roadmap and the two prior plans annotate the follow-on.
Three owner rulings after seeing the applied desktop (2026-10-16b):

- Loose images on the desktop were wrapped in a styled companion `.lnk` with the
  original hidden ? visible as "styled" images, including inside the Desktop
  folder in Explorer. `NEVER_RESTYLE_EXTENSIONS` (images + `.3mf`) now marks them
  `PreviewOnly` with `Icons_Reason_KeptOriginal`, so they render original AND ride
  the RESTORE-FIRST set: the next apply removes the wrappers. The static image
  list gained the formats with no `PerceivedType` (`jxl`, `jp2`, `j2k`, `dds`,
  `exr`, `hdr`, `qoi`, `psb`, `svgz`); `PerceivedType = image` stays the systemic
  gate, and `.3mf` joined the list explicitly.
- Folders no longer wear the plate card: the `Folder` shape is a plate-filled
  silhouette that reads as a WHITE card under the Mono + white-fallback look
  ("la parte blanca no deberia estar"). The Folder bucket's ladder now uses the
  passthrough `None` shape (tinted glyph alone), the built-in preset ladders
  follow, and the recipe migration chain gained v1 -> v2, rewriting a stored
  `Folder: { shape: "Folder" }` override to `None` so an already-applied recipe
  self-heals on the next apply (a user's other choices are untouched).

Tests: never-restyle list coverage + the desktop loose-file gate; the v1 -> v2
migration (only the Folder card is rewritten; other buckets and shapes survive).
An all-drives apply (~5200 masters, ~50 KiB each) tripped the session's
cumulative-base64 cap (`MAX_SESSION_B64_BYTES = 256 MiB`, sized for a few hundred
icons) and EVERY apply failed silently in ~8 s ? the desktop stayed native behind
a generic "couldn't apply" toast. The truly DISTINCT data is a few MiB: every
folder shares the stock glyph.

`IconApplySession::push` now stores identical payloads ONCE (shared `Arc`) and
counts only DISTINCT bytes, so the cap bounds the memory the session actually
holds (the audit-F4 guard stays meaningful) instead of re-counting the same
master once per item. `package_masters` already dedups the post-commit side.

Verified on-box: 5200 items applied in one pass, peak RSS ~205 MB.
…fect

Plan doc gains the follow-up section (images / `.3mf` kept original, folders
without the plate card, the v1 -> v2 recipe migration) and the third scale defect
? the apply session's byte budget ? with its fix and the on-box verification
numbers (5200 items, ~205 MB peak RSS).
The owner's desktop came back with NO taskbar and a black screen after an apply:
explorer.exe was alive but had created neither `Shell_TrayWnd` nor the desktop
layer, and `restart_shell`'s success criterion was the PROCESS only ? a
shell-less Explorer passed verification and the app reported success over a
desktop with no shell. (The wallpaper was untouched; the black screen was the
missing desktop layer, not a wallpaper change.)

The restart now requires BOTH halves ? `shell_ready` (pure, unit-tested):
process alive AND `Shell_TrayWnd` present. A shell-less Explorer is cleared
before relaunching (spawning on top of it never helps), and the failure error
names the taskbar so the caller can surface an actionable message.

Plan doc records the defect, the on-box repair (restart Explorer) and the
verification.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant