Skip to content

feat(app): switch legend kinds with an only pill, and keep tooltips off the pointer - #124

Closed
huntharo wants to merge 108 commits into
tobi:mainfrom
pwrdrvr:app/legend-only-chip
Closed

huntharo wants to merge 108 commits into
tobi:mainfrom
pwrdrvr:app/legend-only-chip

Conversation

@huntharo

@huntharo huntharo commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

What

Only pill. Once a legend set (kinds, or ages) filters, hovering another chip of it shows an only pill over the chip's right end. Clicking it makes that chip the set's one only chip and clears the rest of the set, hidden ones included:

  • Code set → hover Cache → only → just Cache. Before, this took two clicks on Code (only → hidden → shown) and one on Cache.
  • Code, Cache, Media set → only on Media → just Media.

A click on the chip itself still adds or cycles, so building "Code, Cache" still works. No pill when nothing is set (a click already means only), on a chip that is already the set's one only chip, or on Suggested. o on a focused chip does the same. The pill sits over the chip's size rather than beside it, so it never moves the chips after it out from under the pointer. Modelled on PwrSnap's sidebar facets (Library.tsx, psl__facet-only).

Tooltips off the pointer. GPUI places a tooltip 1px from the pointer's tip, so it sat under the cursor and over the next chip and the pill. Every app tooltip now goes through widgets::with_tooltip, which waits 700 ms instead of Omarchy's 400 ms and draws the surface size::TOOLTIP_CLEARANCE (26px, past the arrow cursor) below the pointer. Near the window's bottom GPUI flips it above the pointer, which is clear too.

Mockup: Legend Only Pill and Tooltips in the PwrDisk design project.

Tests

  • legend::only_switches_a_set_to_one_chip_in_one_step: when the pill is offered, and that it clears only its own set.
  • the_only_pill_switches_kinds_and_the_tooltip_stays_clear (window harness): no pill with nothing set; with Code set, hovering Cache shows the pill, the tooltip waits for the delay and its surface starts below the cursor and off the pill, and clicking the pill leaves just Cache.
  • Two existing tooltip tests now wait for the new delay and find the new surface.

make ci is green locally (macOS). Not eyeballed in the running app.

* Rename the app to PwrDisk, a maintained fork of disktree

Only what users see changes: the binary is pwrdisk, the macOS bundle is
PwrDisk.app with the identifier com.pwrdrvr.pwrdisk, and the desktop
entry, icon name, window class, titles, menus, exports and release
archives follow. Crate, module and type names and the disktree-scan SSH
helper keep their upstream names so merging tobi/disktree stays clean.

The bundle identifier has to change: releases are signed with our
Developer ID, and the old one belongs to upstream.

The Power Efficiency setting moves to a pwrdisk folder; a missing one
falls back to disktree's, so switching keeps the choice.

LICENSE keeps Tobi Lütke's notice and adds ours; the README credits
disktree up front.

* Name PwrDisk in the messages the rename missed

The Full Disk Access notice told Mac users to grant access to disktree,
an app System Settings no longer lists. The missing-scanner error, the
remote helper's install hint and disktree-scan's help also named the old
command.
…nts (#3)

* Sign releases through the apple-signing and windows-signing environments

Releases now fail rather than ship unsigned. macOS signs with the
Developer ID from apple-signing, notarizes, staples and checks the app
with spctl; the identity is read from the certificate. The secrets reach
only the signing steps, after the tests and the build.

Windows build jobs hand their unsigned executables to windows-sign, the
only job with the Azure credentials, which neither checks out nor builds
source. It signs both with Azure Artifact Signing, checks each signature
is valid, timestamped and PwrDrvr's, and packages the zips.

Both environments accept only v* tags and wait for approval.
scripts/release/upload-csc-link-from-1password.sh, from PwrRoute, fills
apple-signing; docs/releasing.md lists every value.

* Upload the Windows signing settings from 1Password

The PwrDisk counterpart of PwrRoute's upload-azure-signing-from-1password.sh
(pwrdrvr/PwrRoute#127): reads the 'PwrDisk - Windows Azure Trusted
Signing' item and sets the four WIN_AZURE_SIGN_* variables and three
AZURE_* secrets on windows-signing.

* Harden the release workflow's signing jobs

- A rebuild dispatched from a branch could never pass the v*-only signing
  environments; the first job now requires running from the tag.
- The Windows publisher check compares the certificate's name whole
  instead of matching a substring of its subject.
- README and LICENSE for the Windows zips are fetched in bash; a
  PowerShell redirect re-encoded them and mangled the copyright notice.
- Every action is pinned to a commit, since the signing jobs run them
  before their credentials arrive.
Like PwrRoute's. Build is a debug build of the app; Test and Lint run
the xtask gate. Start runs a release build, as make run does, but through
cargo so it also works on Windows, which has no make. On macOS the
unbundled run still gets its Dock icon.
…pipe (#17)

* feat(windows): link the window to its MCP agent over a named pipe

* fix(windows): cancel pipe I/O before releasing link handles

* test(windows): accept the named pipe disconnected error

* fix(windows): authenticate the window pipe owner before use

* fix(mcp): describe Windows window handover in agent guidance
#89)

* feat(app): keep the removal list on screen and check it off as it goes

Removing and Done now draw every target of the run from the first frame:
a drawn checkbox that goes from grey to green (red on failure), the row
dimmed until it is reached, the name over its folder with the middle of
the path shortened to fit, what the scan classified it as, and its size.
The list follows the item in hand until it is scrolled by hand, and Done
keeps it instead of dropping it.

A sidebar beside it shows progress and the reported, private and
observed figures as running totals, the disk meter and what the mode
means for them; on Done it shows the result, with the ledger under the
list.

* feat(app): follow the item in hand like the live log, and give Review the same rows

The removal list keeps the item in hand in view while it is on screen.
Scrolling it out of view pauses that and shows a pill at the foot of the
list, "Follow the removal", which follows it again, as does scrolling
back until it shows.

Review's rows now read as Removing's do: the kind's colour, the name over
its folder shortened from the middle on one line, and a chip saying why
it is listed, with the icon where Removing puts its checkbox.

* perf(app): draw only the removal rows in view, and cache what Review's marks are

Removing redrew up to 1,200 rows on every 60 ms poll and every free-space
sample. The list is now a uniform list that builds only the rows in view,
so a run of any length scrolls end to end with no block that moves under
the reader, and the item in hand is placed from the rows' one height
rather than from bounds of rows that may not be drawn. The poll redraws
only when a report arrived, the run ended or following changed.

Review resolved every mark on the page on every frame, which indexes each
directory the paths pass through; what each mark is now lives in a cache
kept until the tree changes. The pill's click also clears a wheel turn
the poll has not read yet, so it cannot pause following again.

* fix(app): stretch each removal row across the list

A uniform_list row is only as wide as its content, so the size and
status lanes moved with each row's kind chip. Found in the screenshots;
the follow test now checks that rows span the list.
* refactor(app): keep one record of a removal run

The run's list was the third copy of each worker report, beside
run_summary and run_log, and ledger::Start held a fourth copy of every
target. Each item now owns its Target, the summary counts come from
RunList::totals, and what went is a filter over the list in plan order.

A failure reported against no listed target, which is how the worker
says it could not start, still counts as failed. The worker's Done
tally is kept only to log a warning if it ever disagrees with the list.

* fix(app): keep unlisted failures out of the run's per-item figures

A failure the worker reports against no listed target was counted in
Totals::failed, which the screens subtract from the item count. Count
it apart, and give the agent and the ledger both through failed_all.
The cohort also reads its figures from one totals pass.
…st the size limit (#97)

* fix(removal): never offer home folders for removal, and gate marks past the limit

Mark for removal was a filled, highlighted button on /Users: the guards
only refused it later, in Review, and another account's folder under
/Users was not refused at all on macOS.

- Guards: /Users and every folder directly in it are refused on macOS, as
  C:\Users and each profile already are on Windows (and now C:\Users
  itself). `removal::refusal` judges one path as `plan` does.
- Inspector: a refused path shows Protected with the reason and a
  disabled "Can't remove"; Space marks nothing and says why.
- Size limit at mark time: a mark that takes the marked total past the
  limit shows "Over 5% limit" with a chevron whose menu holds Mark
  anyway. Space refuses and points there.
- Review: past the limit, the commit stays off until the person types
  "delete 20% of my disk" (or "trash ..."), and the worker is held to the
  share typed, so growth since is refused.
- Settings > Safety: limit 1/5/10/25/50% or Off; over-limit confirmation
  Type a phrase (default), Tick a box, or No second step; read-only
  Always protected and Agents rows.
- Agents: a mark onto a protected path or past the limit is refused; with
  the window's limit Off, agents keep 5%.

* fix(app): make Mark anyway and Safety settings real buttons

* fix(app): draw the Mark chevron at heading size

* test(app): lift the size limit where a fixture outgrows a small disk

* fix(app): tighten the over-limit gate after review

- Clear the typed phrase and the tick once a removal starts, so the next
  removal past the limit asks again.
- Read a saved 100% limit as Off, so agents keep the 5% default.
- Measure capacity when the ticker has no reading yet, so an early mark
  is still judged.
- Leave Home, End and the arrows to Review's phrase field while it is
  being edited.
- Judge a new mark afresh rather than from the inspector's cached answer.
- commit() plans once and asks removal_limit directly.
- Skills: a window checks marks made before it was linked.
* fix(removal): refuse other users' home folders on Linux

/home and every directory directly in it are refused, as /Users is on
macOS and the profiles folder on Windows. Where /home is a link (Fedora's
atomic editions point it at /var/home) both names are refused, so a scan
from either spelling is judged alike. The folders come from the layout,
not /etc/passwd, which lists service homes like / and /var/lib/*, misses
LDAP and SSSD accounts, and forgets a removed account's leftover files.

* fix(removal): build the linked-home helper only where it is used

Its test runs on Unix only, so a Windows test build saw the helper as
dead code and the lint gate failed.

* fix(removal): name /home in Settings and test refusal by both names

Settings › Safety now lists /home among what is always protected on
Linux. The profiles check moves into profile_refusal, so the linked-home
test runs the refusal itself against both names of the folder.
#103)

* fix(removal): hold each disk to its own confirmed share past the limit

Review's over-limit gate counted every marked byte against the scanned
volume and handed the worker that one percent, which Limit::check then
applied to each volume separately. A mark on a disk mounted inside the
scan could therefore pass at the boot disk's confirmed share without
anyone confirming it for that disk.

Core gains Ceilings, a base Limit with per-volume raised limits, and
usage(), the grouping Limit::check already used; Limit::check delegates
to Ceilings so there is one implementation. The worker takes Ceilings.

Review now groups the plan by volume the same way, lists each disk past
the limit with its own meter, and asks for a phrase naming every share
("delete 30% and 10% of my disks"). The worker holds each listed disk to
its confirmed share and every other disk to the window's limit. Agents
still get a plain Limit and can never pass it.

* fix(removal): name a marked link's disk by where it sits, and reread disks on every way into Review

Review's disk cache was cleared only by open_review; the agent's review
request and the side panel's Review button now clear it too, so a disk
remounted since the last visit is read again.

disk_of took its key from the path the worker's check reads (a link's
directory) but its name from the link itself, which statfs and
canonicalize follow. Both now come from removal_limit::volume_entry.

Also corrects the mark-time comment, which claimed counting against the
scanned volume only over-counts, and lets rustfmt keep
Ceilings::check_with within 80 columns.
* fix(removal): hold an agent's removal worker to the agent limit

An MCP removal's preview is checked at the agent limit, which stays 5%
when the window's limit is Off. agent_remove then started the run through
begin_removal, which held the worker to the window's limit: 100% with
Off. A selection that grew past 5% between the preview and the start of
the removal was removed.

agent_remove now starts the run through begin_agent_removal, which hands
the worker Ceilings::from(policy.agent_limit()) and ignores Review's
over-limit gate, so nothing the person confirmed raises an agent's
removal. The person's path through begin_removal is unchanged.

* test(app): share the agent link setup between the two link tests
…93)

* feat(core): distinguish artifact detections from cleanup suggestions

* fix(core): preserve ownership in narrow artifact scans

* fix(app): scroll artifact guidance above pinned free space

* fix(app): give artifact assessments their own hierarchy

The inspector drew the status, artifact, evidence and guidance as three
grey caption lines of equal weight, so Detected and Suggested for review
looked alike, and the filled Mark for removal stayed the strongest
element under guidance to manage a VM in its own app.

Draw the assessment as one block: a status chip beside the artifact,
the evidence as a sentence, and the guidance in body ink, ruled in the
kind's colour. A detection that is not a suggestion leaves Mark as an
outline button. Review repeats a caution chip on marked VM data.

The hatch filter's chip and totals line say Suggested instead of
Reclaimable, and an empty Worth a look no longer implies that its rows
are disposable. The guide follows.
…indows (#106)

An empty Marked or Worth a look is now its header alone, with "Space to
mark" or "none" where a full section shows its count or total.

Under 48 rem tall the panel is compact: the selection's four figures
fold into one summary line that opens the grid again, the selection's
label goes, the spacing tightens, and the idle "Connect an AI agent"
line waits until an agent connects (Help still offers it). At 1200x600
with an assessment selected, Marked now starts in sight. The name, the
size, the assessment and Mark never fold.
#108)

* feat(app): size the legend's chips and fold empty kinds into a +N menu

PR #93 grew the legend to eleven kinds plus Suggested, and the row
wrapped onto a second line on narrower windows, showing kinds that hold
nothing on this disk.

Each chip now carries what it keeps of the whole scan, worked out once
per tree off the UI thread by `filter::shares`, which judges files by
the same facts as `Rule::passes`, so a chip's size is the "Showing"
figure clicking it gives. The row is one line: chips with no data or
under a thousandth of the scan, then the smallest when the measured
labels still do not fit, go into a "+N" menu at the row's end. A chip
that is set always stays on the row; setting one from the menu moves it
there.

* fix(app): hang the legend's +N menu at the row's right end

* test(app): pin the legend's set chips across drilling down; say sizes are scan-wide

* fix(app): close a stranded legend menu, wrap set chips, open the compact figures by keyboard

The +N menu closes when nothing draws it, so Escape is not swallowed and
it does not reopen by itself; Escape closes it only on Explore. Set
chips, which never fold into the menu, wrap rather than clip on a narrow
row. Chip widths are shaped once per frame, not once per pass. The
compact panel's summary line is focusable and opens with Enter or Space.
* feat(app): route cache cleanup through supported owners

* fix(app): refine owner cleanup previews and results

* fix(core): detect active npm launchers and pip3

* fix(app): make cleanup previews explicit and refuse installing shims

* fix(core): reject npm download proxies before discovery

* fix(core): preserve exact owner cache paths from probes

* fix(core): refuse ambiguous cache-root line endings

* fix(removal): preserve owner cleanup boundaries

* test(app): use native separators in cache export fixture
#109)

* feat(app): decide cache cleanup per path, with deliberate Delete files

Cache cleanup is now best effort and decided item by item. Check owners
returns one proposal per marked path: the exact owner command (executable,
version, arguments, environment, root, actual scope, effects, limitations,
recoverability) or why there is none, in one problem vocabulary. A missing
CLI, unsupported version or failed probe is that path's status, not the
whole review's.

For each path the person chooses Skip (the default), the owner's exact
command, or Delete files: a separate destructive intent for exactly the
selected path, with its own disclosure (owner bypassed, caches inside,
unknown owners never called disposable), Trash or permanent, and its own
tick in Review or its own confirmation text over MCP. Owner approval never
approves deleting files, and a failed or cancelled owner command never
falls back to it. Guards, the capacity limit and an in-use check apply.

The worker runs each step on its own and reports every path: succeeded,
failed with a reason, cancelled, skipped or not reached. MCP previews take
per-path decisions and name each intent in the confirmation.

* test(core): gate Unix-only cleanup test helpers

owner_error and owners are only called from Unix tests, so Windows'
-D warnings lint rejected them as dead code.

* fix(app): name disabled owner commands with Run, and read the cleanup confirmation plainly

A proposal whose owner is known but whose command did not resolve drew
its disabled choice as "pip cache purge" instead of "Run pip cache
purge". The confirmation listed sizes in raw bytes and the untouched
paths as one bracketed list; it now uses human sizes and one path per
line.

* fix(app): say "1 command" in Review's cache cleanup header

* fix(app): keep cleanup decisions, offer Delete files only where checkable

From a review of #101 and #109:

- The window's owner check expired after five minutes. The person's
  decisions vanished at the next redraw, and an agent's unexpired preview
  failed at pwrdisk_remove with "Check owners first". The worker already
  rechecks the executable, version, root, scope, guards, limit and in-use
  state before each step, so the check no longer expires.
- Delete files was offered on Windows, where the in-use check is
  unavailable, so every such step failed as "in use". It is no longer
  offered there; docs and the Windows reference say so.
- Review rebuilt the approved steps (cloning the whole check and planning
  against the mount table twice) several times per frame. They are now
  kept in the review and rebuilt only when a decision changes.
- Two expressions were over 80 columns where rustfmt had given up; they
  are now a helper and a constant it formats.
* fix(core): recognize pip3.12 and uvx as running owners

A running pip3.12 (as python.org and Homebrew install it) or uvx (uv's
tool runner) writes the same cache as pip or uv, but the in-use check
compared only pip, pip3 and uv, so cleanup could start while they ran.

* test(core): property and oracle tests for cache cleanup

Seeded, generated cases for the rules earlier fixes restored: the cache
root is exactly what the owner printed; a cache's sign protects every
path under it; probing never runs a launcher that could install; a
running owner is recognized in every launcher form; approval keeps each
decision and Skip means nothing happens. Each failure names its seed,
and PWRDISK_SEED replays it.

* test(app): check Review's cache decisions against a model of them

After any sequence of decisions, Trash or permanent choices and ticks,
the window's kept approval equals a fresh one, a change to what is
deleted clears the consent tick, and the export lists the marks.

* test(core): replayable seeds and poison-tolerant fixture locks

Each Review model seed starts from the same state, so the seed named on
failure replays alone; seed ranges wrap instead of overflowing; a failed
test holding the execution lock no longer fails the others with a
poisoned-lock error; the test-only owner_argument wrapper is gone now
that tests call owner_named directly.
…ff the pointer

Once a legend set filters, hovering another chip of it shows an "only"
pill over the chip's right end. Clicking it makes that chip the set's one
only chip and clears the rest of the set, hidden chips included, so going
from Code to just Cache, or from Code, Cache and Media to one of them, is
one click. A click on the chip itself still adds or cycles. `o` does the
same from the keyboard. Modelled on PwrSnap's sidebar facets.

GPUI puts a tooltip one pixel from the pointer's tip, so it sat under the
cursor and over the next chip. Every app tooltip now goes through
`widgets::with_tooltip`, which waits 700 ms instead of 400 ms and draws
the surface below the cursor's height.
@huntharo

huntharo commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Opened here by mistake; this belongs in the fork. Sorry for the noise.

@huntharo huntharo closed this Nov 4, 2026
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