Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
feec6b0
docs: design spec — Docker images/volumes/containers/build-cache cleanup
AhmedFr Jul 7, 2026
d0d5e1c
docs: implementation plan — Docker cleanup
AhmedFr Jul 7, 2026
1c26e5f
feat(app): Docker shared model, IPC channels, settings keys
AhmedFr Jul 7, 2026
ae27b20
feat(app): find-docker binary resolution
AhmedFr Jul 7, 2026
ad45ac9
feat(app): docker df/ps parsers
AhmedFr Jul 7, 2026
c2abca6
fix(app): docker container removable states (exited/created/dead only)
AhmedFr Jul 7, 2026
9e34897
fix(app): parseDate handles docker timestamp format correctly
AhmedFr Jul 7, 2026
aa59cb3
feat(app): getDockerInfo with availability probe + cache
AhmedFr Jul 7, 2026
16a3b29
fix(app): key-aware getDockerInfo coalescing + tests
AhmedFr Jul 7, 2026
a763cef
feat(app): docker:get IPC + preload
AhmedFr Jul 7, 2026
9d591d4
feat(app): Docker tab (read-only listing)
AhmedFr Jul 7, 2026
47bcbb2
fix(app): docker tab keyboard guard + cold-load state
AhmedFr Jul 7, 2026
94569b8
feat(app): Docker settings toggle + binary override
AhmedFr Jul 7, 2026
8ef5140
feat(app): docker remove + prune with freed-bytes diff
AhmedFr Jul 8, 2026
0ba4460
fix(app): docker freed-bytes measurement failure handling + safety tests
AhmedFr Jul 8, 2026
efe9f82
feat(app): gated docker remove/prune IPC + validation
AhmedFr Jul 8, 2026
95ae792
test(app): license-gate + payload-rejection coverage for docker IPC h…
AhmedFr Jul 8, 2026
ceb86f8
feat(app): docker cleanup actions + tiered confirmation
AhmedFr Jul 8, 2026
5bea7ab
fix(app): docker remove button copy + volume gate hardening + build-c…
AhmedFr Jul 8, 2026
4ba976f
fix(app): docker removal failure toast + close-confirm-on-refresh + d…
AhmedFr Jul 8, 2026
1c37c9a
docs: STATUS — Docker cleanup built (pending PR review)
AhmedFr Jul 8, 2026
ec7c2a9
chore(app): sort docker imports after rebase onto main
AhmedFr Jul 8, 2026
9df5292
docs: design spec — Docker project-grouped display + used-by association
AhmedFr Jul 8, 2026
0ed55b8
docs: implementation plan — Docker project-grouping
AhmedFr Jul 8, 2026
c3a408a
feat(app): docker inspect parsers (compose labels + refs)
AhmedFr Jul 8, 2026
79b5c48
feat(app): associateProjects (compose + used-by) with types
AhmedFr Jul 8, 2026
acff559
fix(app): union imageId/imageRef project matches (no misassignment)
AhmedFr Jul 8, 2026
bcd4e9d
feat(app): docker project enrichment (inspect + logos) in readDockerInfo
AhmedFr Jul 8, 2026
c5957e3
feat(app): project-aware docker grouping (groupDockerForDisplay)
AhmedFr Jul 8, 2026
d3e3b7e
feat(app): DockerView project-grouped rendering + type badges
AhmedFr Jul 8, 2026
c9fe2d3
fix(app): dangling-image bulk prune + favicon onError fallback
AhmedFr Jul 8, 2026
a3effa2
feat(app): docker sort tabs + type-filter chips
AhmedFr Jul 8, 2026
963d014
fix(app): make readDockerInfo enrichment fail-soft atomic (no silent-…
AhmedFr Jul 8, 2026
28864c0
docs: STATUS — Docker tab regrouped by project (M1e, pending review)
AhmedFr Jul 8, 2026
4ef579f
fix(app): guard docker info.projects against stale/older cache shape
AhmedFr Jul 8, 2026
3344c38
fix(app): make docker enrichment resilient so repository grouping alw…
AhmedFr Jul 9, 2026
e207593
feat(app): standardize Docker rows on Row primitives (hover trash, Si…
AhmedFr Jul 9, 2026
f41374b
docs: STATUS — Docker grouping refinements from real-daemon feedback
AhmedFr Jul 9, 2026
a244e28
fix(app): robust volume labels via 'volume ls'; in-use green dot + no…
AhmedFr Jul 9, 2026
0c6a0e3
docs: STATUS — real-daemon diagnosis (volume ls fix, 3 projects found)
AhmedFr Jul 9, 2026
7a975a3
fix(app): docker rows use the node_modules sliding highlight (.cc-hl)…
AhmedFr Jul 9, 2026
707384a
docs: spec for per-tab headline data-viz + docker auto-scan
AhmedFr Jul 9, 2026
85470af
docs: plan for per-tab headline data-viz + docker auto-scan
AhmedFr Jul 9, 2026
3071c1e
feat(settings): add cacheThresholdGB and dockerThresholdGB limits
AhmedFr Jul 9, 2026
7b2dac0
feat(app): severityCounts helper for the packages headline
AhmedFr Jul 9, 2026
9d3750a
feat(app): SeverityMeter — compact packages severity headline
AhmedFr Jul 9, 2026
0b8ac31
feat(app): tabSummary — per-tab footer line
AhmedFr Jul 9, 2026
78a796e
style: tabSummary.test.ts — biome formatting
AhmedFr Jul 9, 2026
e175307
feat(app): TabHeadline — per-tab header data-viz switcher
AhmedFr Jul 9, 2026
dbd706f
feat(app): per-tab header data-viz and footer summary in LauncherApp
AhmedFr Jul 9, 2026
6869d76
feat(app): auto-scan Docker on tab open when data is stale
AhmedFr Jul 9, 2026
ae37edc
feat(settings): cache and docker limit inputs in Settings
AhmedFr Jul 9, 2026
23225ce
fix(app): packages headline shows nothing (not "all clear") when advi…
AhmedFr Jul 9, 2026
688cffd
docs: STATUS — per-tab headline data-viz + docker auto-scan; fix pre-…
AhmedFr Jul 9, 2026
4275491
docs: spec for Docker collapsible project rows (shared Accordion prim…
AhmedFr Jul 9, 2026
76a1a60
feat(app): shared Accordion primitive (open-card wrapper)
AhmedFr Jul 9, 2026
46b2b5b
refactor(app): Packages tab uses the shared Accordion primitive
AhmedFr Jul 9, 2026
870d952
feat(app): dockerGroupActive + projectRowExpanded helpers
AhmedFr Jul 9, 2026
d15ac15
feat(app): collapsible project rows in the Docker tab via Accordion
AhmedFr Jul 9, 2026
57efea6
fix(app): drop em dash from Docker remove label (no-em-dash rule)
AhmedFr Jul 9, 2026
1ebe569
docs: STATUS — Docker collapsible project rows (shared Accordion)
AhmedFr Jul 9, 2026
eabdf75
docs: spec for Docker build cache → Caches tab + clearer deletion but…
AhmedFr Jul 14, 2026
47d7691
docs: plan for Docker build cache → Caches tab + clearer delete buttons
AhmedFr Jul 14, 2026
6385636
refactor(app): drop Docker build-cache group; add delete-verb labels …
AhmedFr Jul 14, 2026
e5dfe2e
docs(app): refresh stale build-cache comments in Docker constants
AhmedFr Jul 14, 2026
36abe2e
feat(app): Docker prune buttons read as deletes (verb + trash icon + …
AhmedFr Jul 14, 2026
681bb48
feat(app): CacheRow supports a danger action button with an optional …
AhmedFr Jul 14, 2026
1bf13ea
feat(app): Caches tab renders a live-cache list (pnpm + future caches)
AhmedFr Jul 14, 2026
6e9dfe6
feat(app): Docker build cache lives in the Caches tab (delete + auto-…
AhmedFr Jul 14, 2026
653d129
style(app): biome format fixes in Docker/Caches constants (missed lin…
AhmedFr Jul 14, 2026
c75b258
fix(app): Caches footer hint tracks the live-cache list + selected ro…
AhmedFr Jul 14, 2026
916e497
fix(app): Caches Enter never fires a query-hidden row; footer + place…
AhmedFr Jul 14, 2026
9575a5e
docs: STATUS — Docker build cache to Caches tab + clearer delete butt…
AhmedFr Jul 14, 2026
2c223f9
fix(app): pre-merge review fixes (em dashes, docker bar scale, dangli…
AhmedFr Jul 14, 2026
282ed39
docs: spec for the visualization-only menu bar panel
AhmedFr Jul 15, 2026
8ae0089
docs: plan for the visualization-only menu bar panel
AhmedFr Jul 15, 2026
d05a7ae
feat(app): launcher can be told to open on any tab, not just settings
AhmedFr Jul 15, 2026
d5e806e
refactor(app): share DOCKER_STALE_MS so the panel can reuse the stale…
AhmedFr Jul 15, 2026
49a6bc4
feat(app): panelAreas computes the panel hero totals, each byte count…
AhmedFr Jul 15, 2026
795dd5c
feat(app): panelAreas resolves the four panel rows with honest gating
AhmedFr Jul 16, 2026
7707dc7
fix(app): assert the projects row against its exact summation, not a …
AhmedFr Jul 16, 2026
b24fae8
feat(app): AreaBar renders one click-through area row for the panel
AhmedFr Jul 16, 2026
c089c17
feat(app): TrackedSummary generalizes the panel hero beyond node_modules
AhmedFr Jul 16, 2026
338ca6a
refactor(app): strip the panel to read-only (no list, CTA, store prun…
AhmedFr Jul 16, 2026
62cc3a2
feat(app): the menu bar panel is a read-only dashboard of every disk …
AhmedFr Jul 16, 2026
38ec0f2
docs: STATUS — the menu bar panel is now a read-only dashboard
AhmedFr Jul 16, 2026
b3dede7
fix(app): accurate nav comment + no zeroed-dashboard flash before set…
AhmedFr Jul 16, 2026
963efb6
fix(app): per-row PixelMeter bars render in the panel (fill container…
AhmedFr Jul 16, 2026
1465461
Merge remote-tracking branch 'origin/main' into feat/docker-project-g…
AhmedFr Jul 16, 2026
68ca47c
chore(release): v1.1.0 — Docker cleanup stack + read-only panel dashb…
AhmedFr Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions STATUS.html

Large diffs are not rendered by default.

1,069 changes: 1,069 additions & 0 deletions docs/superpowers/plans/2026-07-07-docker-cleanup.md

Large diffs are not rendered by default.

547 changes: 547 additions & 0 deletions docs/superpowers/plans/2026-07-08-docker-project-grouping.md

Large diffs are not rendered by default.

1,040 changes: 1,040 additions & 0 deletions docs/superpowers/plans/2026-07-09-per-tab-headline-dataviz.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1,396 changes: 1,396 additions & 0 deletions docs/superpowers/plans/2026-07-15-panel-visualization-only.md

Large diffs are not rendered by default.

186 changes: 186 additions & 0 deletions docs/superpowers/specs/2026-07-07-docker-cleanup-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Docker images/volumes/containers/build-cache cleanup (launcher "Docker" tab)

Date: 2026-07-07
Status: awaiting user review
Branch (implementation): feat/docker-cleanup

## Goal

Add a fourth cleanup surface to the TidyDisk launcher: a "Docker" tab that lists
Docker's disk usage (images, volumes, stopped containers, build cache) with size,
created date, and in-use status, sortable like projects, and lets the user
reclaim space via the `docker` CLI. This is a large disk-cleaning win (Docker
images and build cache are frequently the single heaviest thing on a dev Mac)
and a natural extension of the free-scan / paid-clean model. Because Docker
removals are PERMANENT (no Trash), the safety model is the crux of the design.

## Decisions made with the user (2026-07-07)

- Safety model: **tiered by recoverability**. Images / build cache / stopped
containers get a single confirm (like today's project delete); volumes get an
EXTRA typed-name confirmation because the data is unrecoverable. Every Docker
action is labeled "permanent, not sent to the Trash".
- Scope: **all four** `docker system df` categories (images, volumes, build
cache, stopped containers).
- Row semantics: **created date + in-use flag** (honest; Docker exposes no true
per-item "last used", so we do not fake one). Sort by size, created, unused-first.
- Cleanup: **per-item removal + bulk prune**.
- In-use items are **read-only** (no `-f`/force escape hatch): an image or volume
a container holds cannot be removed through the GUI. This is Docker's own rule
and a strong safety property.
- Listing/scanning is cache-and-refresh (never blocks the UI), mirroring the pnpm
store.

## Architecture

Follows the pnpm-store / Caches feature verbatim (it is the closest analog).

### Shared — `src/shared/docker.types.ts`
```ts
export type DockerItemKind = 'image' | 'volume' | 'container' | 'buildcache';

export interface DockerItem {
id: string; // image ID / volume name / container ID / cache ID
kind: DockerItemKind;
name: string; // repo:tag, volume name, container name, cache short-id
sizeBytes: number;
createdAt: number; // ms epoch; 0 when docker does not report it
inUse: boolean; // referenced by an existing container / mounted
removable: boolean; // false when inUse (GUI never force-removes)
}

export interface DockerCategoryTotal {
kind: DockerItemKind;
sizeBytes: number;
reclaimableBytes: number;
count: number;
}

export interface DockerInfo {
available: boolean; // docker CLI found AND daemon reachable
reason?: string; // when unavailable: "not installed" | "daemon not running"
checkedAt: number;
totals: DockerCategoryTotal[];
items: DockerItem[];
}

export interface DockerActionResult { ok: boolean; freedBytes: number }
```

### Main — `src/main/docker/`
- `find-docker.ts` — mirrors `find-pnpm.ts`: resolves the `docker` binary from an
optional `settings.dockerBinaryPath` override, then the runtime bin dirs
(`versionManagerBinDirs`), then known locations (`/usr/local/bin/docker`,
`/opt/homebrew/bin/docker`, Docker Desktop's `/Applications/Docker.app/...`).
Reuses the `pnpmExecEnv`-style spawn env so Finder-launched builds get a real
PATH. Cached per run.
- `docker.ts` — CLI wrappers via `promisify(execFile)` (NO shell; args as arrays
so item names/ids can never inject). Functions:
- `getDockerInfo(force?)` — availability + full listing. In-memory + on-disk
cache (`docker-cache.json` in userData) so the tab renders last-known state
instantly; concurrent recomputes coalesced via an `inFlight` promise. Exactly
the `getPnpmStoreInfo` shape.
- `removeDockerItem(kind, id)` — per-item removal for images (`docker rmi <id>`),
volumes (`docker volume rm <name>`), and stopped containers (`docker rm <id>`).
Build cache has no clean single-record removal in the docker CLI, so it is
listed for size awareness but reclaimed via bulk prune only (no per-item
action on build-cache rows).
- `pruneDocker(target)` where `target ∈ { danglingImages, unusedImages,
stoppedContainers, buildCache, unusedVolumes }` — the corresponding
`docker <x> prune -f` (the `-f` skips docker's own prompt; the GUI supplies
the confirmation). Freed bytes come from a `docker system df` before/after
diff, mirroring `prunePnpmStore` (`freedBytes = max(0, before - after)`).
- Availability probe: `docker version --format '{{.Server.Version}}'` — a
nonzero exit or missing binary distinguishes "not installed" from "daemon not
running", surfaced as `reason` (renders an explanatory empty state like the
pnpm store's `reason`, not an error).
- Data source: `docker system df -v --format '{{json .}}'` is the richest single
call (images, volumes with size + mount `Links`, containers, build cache).
Enriched with `docker ps -a --format json` (container→image in-use map, states)
and `docker image ls --all --format json` (tags, created, dangling). `inUse`:
image referenced by any container; volume with `Links > 0`; container in
`running` state (only stopped containers are removable).

### IPC — `src/shared/ipc.constants.ts` + `src/main/ipc/register-ipc.ts`
- Channels: `docker:get` (read-only, ungated), `docker:remove`, `docker:prune`.
- `docker:remove` / `docker:prune` handlers each OPEN with the license gate,
identical to prune/delete:
```ts
if (!ctx.license.get().pro) return { ok: false, freedBytes: 0 }
```
- Payload validation: `kind` checked against the union, `id` non-empty string,
`prune target` checked against the allowed set — same discipline as
`coerceSetting` / the analytics whitelist. Anything else is rejected.
- Preload: expose `getDocker`, `removeDockerItem`, `pruneDocker` on `window.clean`
(`src/preload/index.ts` + `api.types.ts`).

### Renderer
- `src/renderer/src/launcher/views/DockerView.tsx` — clones `CachesView`:
category sections (Images / Volumes / Containers / Build cache), each a list of
rows filtered by the shared header `query`, plus per-category "Prune" actions
and a totals/reclaimable summary. Unavailable → explanatory state from `reason`.
- Rows reuse `CacheRow` (icon, name, detail = created date + in-use badge, size,
action). In-use rows render the badge and NO action button (`removable: false`);
build-cache rows likewise have no per-item action (bulk prune only).
- `useDocker` hook clones `usePnpmStore`: `{ info, loading, busyId, remove,
prune, refresh }`; mutations re-fetch on completion.
- Formatting reuses `formatSizeStr` and `relativeTime` (created date).

### Confirmation UX (extends the existing footer)
- Single-confirm items (image / container / build cache): reuse the current
two-step footer bar (`confirm` state in `LauncherApp.tsx`), worded
"Remove <name>? Frees N. This is permanent (not sent to the Trash)."
- Volume removals (per-item and the unused-volume prune): a SECOND step — the
footer shows a small text input; the action stays disabled until the user
types the exact volume name (per-item) or the word `prune` (bulk). This is the
only new confirmation primitive.
- Free users hit the existing `UnlockPrompt` paywall (fires `paywall_shown` with
a `docker` trigger), same as delete/prune.

### Launcher wiring — `src/renderer/src/launcher/LauncherApp/`
- `LauncherTab` union gains `'docker'`; Segmented gains `{ value:'docker',
label:'Docker' }`; `⌘4` added to the shortcut branch; body switch renders
`DockerView`; per-tab footer hints extended.

### Settings — `src/shared/settings.types.ts` + validation
- `docker?: boolean` (default true; when docker is absent the tab shows the
explanatory empty state rather than hiding, so users learn the feature exists)
and `dockerBinaryPath?: string` override. Add `DEFAULT_SETTINGS.docker = true`,
a `coerceSetting` case for each (boolean / path string), and a Settings toggle
+ path override row mirroring the pnpm block.

### Analytics
- Extend `clean_performed`'s `kind` values with `docker_image | docker_volume |
docker_container | docker_buildcache | docker_prune`; captured main-side in the
`docker:remove` / `docker:prune` handlers with `freed_gb`, exactly like the
existing prune/delete instrumentation. No new renderer-originated events beyond
the shared `paywall_shown`.

## Error handling / edge cases
- Docker CLI missing → `available:false, reason:"not installed"`.
- CLI present, daemon down → `available:false, reason:"daemon not running"` with a
hint to start Docker Desktop.
- A removal that Docker refuses (e.g. an image gained a container between listing
and action) → `{ ok:false }`; the view refreshes and re-reads state (no crash).
- Slow `docker system df -v` on large setups → cache-and-refresh keeps the UI
responsive; a lightweight spinner/pill during recompute (mirrors the pnpm
store's footer pill).
- All CLI calls use `execFileAsync` with a timeout (prune can be slow; reuse the
10-minute ceiling pattern) and array args (injection-safe).

## Testing
- Unit (main): `find-docker` resolution order; `docker system df -v` JSON parsing
→ `DockerItem[]`/totals (fixture-driven, no live daemon); `inUse`/`removable`
derivation; freed-bytes diff math; payload validation rejects bad kind/id/target.
- Unit (renderer): `DockerView` category grouping + query filtering; the volume
typed-name confirmation gating (action disabled until the name matches).
- Existing suites stay green; typecheck/lint/build green; the packaged app's
Docker tab is a manual (user) verification since it needs a real daemon.

## Out of scope (later)
- Pricing bump to €29 that this unlocks (already in the business model / GTM doc).
- Menu-bar panel surfacing — Docker lives in the launcher only, to keep the tiny
panel focused.
- Xcode / DerivedData / simulators and other non-Docker caches.
- Podman/nerdctl support (docker CLI only for v1).
- Any `-f`/force removal of in-use items.
158 changes: 158 additions & 0 deletions docs/superpowers/specs/2026-07-08-docker-project-grouping-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Docker tab: project-grouped display with type badges + used-by association

Date: 2026-07-08
Status: awaiting user review
Branch (implementation): feat/docker-project-grouping (stacked on feat/docker-cleanup / PR #40)

## Goal

Replace the Docker tab's category-grouped display (Images / Volumes / Containers /
Build cache sections) with a **project-grouped** list, so a developer can see at a
glance which Docker resources belong to which project. Each resource keeps a
**type badge**, there's a **type-filter chip row** and **Size / Name / Recent**
sort tabs mirroring the node_modules Projects tab, and resources are associated to
projects as aggressively as the data honestly allows (Compose labels + used-by
references), with the genuinely project-less remainder in an "Other" section.

## Decisions made with the user (2026-07-08)

- Presentation: **one project-grouped list + a type-filter chip row** (All /
Images / Volumes / Containers / Cache). Not category tabs (that is the type
axis, which fights the project-awareness goal).
- Association: **Compose labels + used-by references** (maximize coverage).
- Missing working_dir → generic Docker icon + Compose project name (group still
works for cleanup).
- Sort: **Size / Name / Recent** tabs that reorder the GROUPS (Recent = newest
resource, since Docker has no true "last used"); items within a group sort by
size.
- Keep containers as a badged type.

## Honest ceiling (stated plainly)

Project *identity* only exists for **Docker Compose** projects (name +
`working_dir` from `com.docker.compose.project*` labels). A pure `docker run`
setup with no compose has no project structure, so everything falls to "Other".
The "used-by" logic extends coverage of images/volumes to the compose projects
that do exist; it does not invent projects for non-compose resources. This is a
data limitation, not a design shortcut.

## Association model (main process)

Project identity is always a Compose project. For each resource:

- **Containers**: `com.docker.compose.project` label → that project;
`com.docker.compose.project.working_dir` → working_dir (for the logo). No
compose label → Unaffiliated ("Other").
- **Images**: (a) a compose-built image carrying `com.docker.compose.project` →
that project; else (b) **used-by**: if exactly ONE compose project's containers
reference this image (by ImageID or repo:tag), group it under that project.
Used by multiple compose projects, or unused/dangling → "Other" (repo-grouped).
Rationale for single-project-only: an item lives in exactly one group so the
size sort never double-counts.
- **Volumes**: (a) `com.docker.compose.project` label → that project; else (b)
**used-by**: the single compose project whose container mounts it; else → "Other".
- **Build cache**: never associated → its own "Build cache" group in "Other".

## Data sources (behind the existing cache-and-refresh)

Current: `docker version`, `docker system df -v` (inventory), `docker system df`
(freed-bytes). This adds two batched reads:

- `docker ps -aq` then `docker container inspect <ids…>` — one call returning
each container's `.Config.Labels` (compose project + working_dir), `.Image` /
`.Image` id (image reference for used-by), and `.Mounts[].Name` (named-volume
mounts for used-by). This is the enrichment backbone.
- `docker volume inspect <names…>` — one call for volume `.Labels` (compose
project). (df -v already gives volume size + link count; inspect adds labels.)

Both fail-soft: if inspect fails or the daemon hiccups, association degrades to
"everything in Other" rather than erroring (the tab still lists + cleans). Cached
in `docker-cache.json` like today; recomputed on refresh/⌘R.

Per distinct project working_dir, reuse the scanner's `detectKind(workingDir)` +
`findProjectIcon(workingDir)` (from `src/main/scanner/`) to compute the project
logo. Guard when the dir no longer exists on disk → `kind`/`iconDataUrl`
undefined → the renderer falls back to a generic Docker icon + the compose name.

## Data model changes (`src/shared/docker.types.ts`)

```ts
export interface DockerItem {
// …existing: id, kind, name, sizeBytes, createdAt, inUse, removable
/** Compose project this resource is grouped under; undefined = unaffiliated. */
project?: string
/** Image repository (name before the last ':'), for repo sub-grouping in Other. */
repository?: string
}

/** A Compose project surfaced as a group header, with a detected logo. */
export interface DockerProject {
name: string
workingDir?: string
/** Reused from the scanner; undefined → renderer shows a generic Docker icon. */
kind?: FrameworkKind
iconDataUrl?: string
}

export interface DockerInfo {
// …existing: available, reason?, checkedAt, totals, items
projects: DockerProject[]
}
```

`buildDockerItems` stays pure; a new pure `associateProjects(items, containers,
volumeLabels)` computes `project`/`repository` and the `DockerProject[]` from the
inspect data — fully unit-testable with fixtures, no daemon.

## Renderer

- Pure `groupDockerForDisplay(info, { sortBy, typeFilter, query })` in
`DockerView.constants.ts` → an ordered list of groups. A group is one of:
`{ kind: 'project', project: DockerProject, items }` |
`{ kind: 'repository', repository, items }` |
`{ kind: 'buildcache', items }` | `{ kind: 'unaffiliated', items }`.
Ordering: project groups first (sorted by the active tab), then a
"Not linked to a project" divider, then repository/buildcache/unaffiliated
groups (sorted). `typeFilter` drops non-matching items (and now-empty groups);
`query` filters by item name or project name. Group sort keys: Size = total
group bytes; Name = project/repo name; Recent = max item `createdAt`. Items
within a group sort by size desc. Unit-tested.
- `DockerView.tsx` reworked: group headers render the project logo (reuse
`ProjectIcon`-style rendering driven by `DockerProject.kind` + `iconDataUrl`,
generic Docker icon fallback) or a repo/cache/orphan header; rows render via
the existing row with a new `TypeBadge` (IMAGE / VOLUME / CONTAINER / CACHE).
Per-group prune actions stay (dangling images, build cache, unused volumes,
stopped containers) where they map cleanly.
- `LauncherApp.tsx`: add `dockerSortBy` state + a type-filter chip row, cloning
the Projects/Packages sort-tab pattern. The Docker keyboard guard and body
branch already exist.

## Safety (unchanged — regression-guarded)

This is presentation + data-enrichment only. Every row still carries its
`DockerItem.id` and `kind`, and cleanup is still driven by
`requestDockerRemove(item)` / `requestDockerPrune(target)`. The typed-name
volume-confirmation gate (`dockerConfirm`, `dockerConfirmBlocked`,
`commitDockerConfirm`, `docker-confirm.ts`) is untouched. A test asserts the
confirm-flow wiring is unchanged, and the existing docker + confirm tests stay
green.

## Testing

- Pure unit tests (vitest, no daemon): `associateProjects` (compose-label,
used-by single project, used-by multi-project → Other, orphan, image
repository extraction), the working_dir → generic-icon fallback path, and
`groupDockerForDisplay` (group ordering, each type-filter chip, each sort key,
query filter, empty-group omission).
- Container/volume inspect JSON parsing tested with fixtures.
- Existing `docker-parse`, `docker`, `validate-docker-arg`, `docker-confirm`, and
`register-ipc` docker tests remain green; typecheck / lint / build green.
- Manual (user, needs a real daemon + a compose project): the group with the
right logo, images appearing under the project whose container uses them, the
filter chips, the sort tabs, and the volume typed-name confirm still firing.

## Out of scope

- Docker networks (no meaningful disk footprint).
- Any change to what the cleanup actions do (remove/prune commands unchanged).
- Inventing project identity for non-compose (`docker run`) resources.
Loading
Loading