Repository navigation
feat(app): Docker tab grouped by project (logos + type badges + used-by association) - #42
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure parseSize/parseDate/parseDf/parseContainers/buildDockerItems functions turning `docker system df -v` and `docker ps -a` JSON output into DockerItem[]/DockerCategoryTotal[] with in-use/removable derivation. No exec, no daemon calls; fully unit-tested.
`docker rm` (no -f) refuses running, paused, restarting, and removing containers. Container inUse/removable derivation now matches that: inUse for running|paused, removable only for exited|created|dead. Also make parseDate robust to any numeric timezone offset (not just +0000) instead of silently returning 0.
Date.parse rejected the previous ISO-ish string because a space was left before the timezone offset, silently returning NaN -> 0 for every docker timestamp. Switch to an explicit regex extraction of date/time/offset and rebuild a proper ISO string. Add exact-epoch assertions (not just booleans) covering the standard "+0000 UTC" form, fractional seconds, and a non-UTC numeric offset.
Mirrors pnpm-store.ts's cache-and-refresh pattern for Docker: probes binary presence via findDocker, checks daemon reachability with `docker version`, then parses `system df` + `ps -a` into DockerInfo via docker-parse's buildDockerItems. Exposes __setExecForTests so the test suite needs no live daemon, plus run/CLI_TIMEOUT_MS/PRUNE_TIMEOUT_MS for reuse by the prune actions (Task 8).
getDockerInfo's inFlight coalescing ignored opts.binaryPath, so a concurrent call with a different binary path silently received the other path's pending result. Mirror pnpm-store.ts's inFlightKey pattern: only reuse the pending promise when its key matches. Add regression coverage: a "not installed" case and a concurrency test proving two different binaryPaths each run their own version probe instead of aliasing.
Adds useDocker (Phase-A subset: info/loading/refresh) and DockerView, grouping images/volumes/containers/build-cache into sections with CacheRow, filtered by the shared search query. Wires the 'docker' tab into LauncherApp: Segmented option, ⌘4 shortcut, ⌘R refresh, body switch. No renderer JSX test harness exists yet (no @testing-library/react, no prior .test.tsx files), so grouping/filtering/detail-line logic was extracted into DockerView.constants.ts and unit-tested with vitest; DockerView.tsx itself stays a thin consumer of that logic.
The Docker tab had no keydown guard, so arrow/Enter/⌘⌫ fell through to the projects-list handlers (doOpen, setConfirm) while browsing Docker resources. Add a tab === 'docker' early-return alongside the existing caches/packages blocks; Phase A has no row actions, so it just swallows the keys. DockerView also ignored its loading prop, rendering the "Docker not found" state during the initial fetch before info resolves. Render a neutral "Checking Docker…" state while loading && !info instead.
Adds a Settings toggle for settings.docker (default on) with an optional docker binary path override, and gates the Docker tab's Segmented option and its ⌘4 shortcut on the setting so disabling it hides the tab and can't be reached via keyboard.
Add removeDockerItem (kind→CLI mapping, buildcache refused, never -f) and pruneDocker (target→`docker <x> prune -f`) to docker.ts. Freed bytes come from a grand-total `docker system df` before/after diff, parsed as one JSON object per line via parseSize.
Exposes Task 8's removeDockerItem/pruneDocker over IPC behind the license gate, matching the prunePnpmStore/deleteNodeModules pattern: free tier sees everything but mutates nothing. Untrusted renderer payloads are validated with coerceRemoveArgs/coercePruneTarget before ever reaching the docker CLI (buildcache rejected for per-item removal; only the five known prune targets accepted). Successful mutations capture a clean_performed analytics event (kind: docker_<kind> / docker_prune). Wires removeDockerItem/ pruneDocker onto the preload window.clean bridge.
Turns the read-only Docker tab into an actionable cleanup surface: per-item remove and per-category prune (dangling/unused images, stopped containers, build cache, unused volumes), gated by a two-tier confirm — free users hit the paywall, Pro users get a single-confirm footer for image/container/build-cache actions and an additional typed-name gate for any volume removal (per-item or bulk) so an accidental Enter can never delete a volume. - useDocker gains remove(kind,id)/prune(target) + busyId, mirroring the pnpm-store hook's refresh-after-mutate pattern. - docker-confirm.ts is the TDD'd pure gate (needsTypedConfirm/ requiredConfirmText/confirmSatisfied) driving whether the footer's Remove/↵ is enabled. - DockerView adds per-category prune buttons and shows the CacheRow remove action only when item.removable; in-use/build-cache rows stay display-only. - LauncherApp wires both tiers into the existing confirm/unlock footer (no new modal) and threads Escape/Enter through the existing keyboard handler.
…rop unused ps call Docker remove/prune failures now surface an error toast instead of closing the footer silently on an irreversible-delete surface. Cmd+R on the Docker tab now also closes a stale pending confirm before refreshing. Dropped the dead `docker ps` round-trip in readDockerInfo since buildDockerItems never used its second argument.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add DockerItem.project/repository, DockerProject, and DockerInfo.projects to the shared docker types, plus pure parseContainerInspect/parseVolumeInspect for `docker container/volume inspect` JSON. Add projects: [] to the four early-return DockerInfo objects in docker.ts to keep typecheck green.
Pure engine that groups images/volumes/containers into their Compose project via compose labels and used-by container references. Items referenced by more than one project (or none) stay unaffiliated so totals are never double-counted. Also reformats docker-inspect.test.ts per biome (formatting only, no logic change).
The image association used a fallback (id, else name) rather than a union, so an image referenced by imageId in one compose project and by imageRef in a different project got wrongly assigned to the id-matched project instead of being flagged as used by 2 projects.
Wire the association engine (parseContainerInspect/parseVolumeInspect/ associateProjects from Task 1) into readDockerInfo: after the df fetch, run `docker ps -aq` + `container inspect` + `volume inspect`, associate items to compose projects, and attach a per-project logo via the scanner's detectKind/findProjectIcon on each project's working_dir. Fail-soft throughout: any inspect/daemon error leaves items unassociated (projects: []) but availability stays true. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace groupDockerItems/DockerGroup/GROUP_ORDER with
groupDockerForDisplay(info, { sortBy, typeFilter, query }), producing
project groups first, then repository/buildcache/unaffiliated groups
under "Other". Supports size/name/recent sort, type filtering (drops
non-matching items and resulting empty groups), and name/project query
matching. dockerItemDetail, PRUNE_TARGET_LABEL, and pruneEstimateBytes
are unchanged.
DockerView.tsx still imports the removed groupDockerItems and will not
typecheck/build until Task 4 updates it to consume DisplayGroup; unit
tests (pnpm test) are green.
Renders groupDockerForDisplay's DisplayGroup[] instead of the removed groupDockerItems: project groups get a logo/framework-icon header with total size + item count, "Other" groups (repository/buildcache/ unaffiliated) get a generic-icon header, and a "Not linked to a project" divider marks the start of the Other section. Bulk-prune buttons move off project headers (prune commands are global, so a per-project button would misleadingly imply a scoped prune) onto the Other headers only, keyed by the item kinds each group actually contains. Adds a one-prop TypeBadge chip (image/volume/container/ cache) rendered on every row via a new optional CacheRow `badge` slot. Per-row remove wiring (onRemove(item), canRemove gating) is unchanged. sortBy/typeFilter are optional on DockerViewProps with defaults so LauncherApp (wired in Task 5) still typechecks untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…scan + keyboard nav)
…t in TDD-only tasks) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w's verb Was gated on the pnpm store alone and hardcoded 'prune'; now shows whenever any live cache is present and labels Enter with the selected row's action (Prune/Delete). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…holder polish Final-review fixes: - Important: Enter on the Caches tab indexed the full liveCaches list while only the query-filtered rows were on screen, so Enter could trigger a hidden row's action. New tested selectedActionableCache() guard returns the selected cache only when it is visible and actionable; Enter no-ops otherwise. - Minor: footer hint now requires at least one non-disabled cache (was any cache, so a disabled-only pnpm row still showed the hint). - Minor: drop em dashes from the npm/yarn/bun placeholder copy (no-em-dash rule). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ons (M1h) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ng confirm) Whole-feature review (3 parallel reviewers) before merging the Docker stack to main: - Critical: drop em dashes from three user-facing strings (two remove/prune toasts + the Docker confirm footer 'Permanent, not sent to the Trash.') — the no-em-dash rule. - Important: dockerMaxBytes excluded build cache from the Docker rows' SizeViz bar scale; build cache left the Docker display but was still counted, compressing every visible bar. - Important: clear any pending Docker confirm when Docker is disabled, so disabling the feature mid-confirm (incl. a build-cache prune started from the Caches tab) can't leave a live destructive dialog floating over the wrong tab. - Refresh a stale doc comment; keep the buildcache remove-guards as defense-in-depth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The popover drops the reclaimable node_modules list and every destructive action, becoming a read-only dashboard: an aggregate "Tracked on disk" hero plus four click-through rows (Projects/Caches/Packages/Docker) that open their tab in the main window. Records the non-obvious calls: the hero counts the pnpm store once and is therefore smaller than the four rows added up (rows mirror their tabs so click-through never lies); "tracked" not "reclaimable", because the store's prunable share is unknowable before a prune; the panel never computes the package inventory; and the panel's removal takes all three paywall_shown triggers with it, leaving the launcher as the only paywall surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nine TDD tasks: widen the nav target, share DOCKER_STALE_MS, build the pure panelAreas brain (hero math then row gating), AreaBar + TrackedSummary, strip the panel, wire the dashboard, update STATUS. Also corrects D7 in the spec. It had put the pnpm store on the same >5 min staleness rule as Docker, but getPnpmStoreInfo has no TTL, so that would force a multi-second du every time the popover opened past the TTL. The launcher already solves this properly: it re-sizes the store on scan-completion only, and its stale rule refreshes Docker alone. The panel now matches that. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed once Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rounded literal Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…area Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tings load - LauncherApp: updated stale comment reflecting that nav now routes all targets (Settings + 4 tabs) via launcherNavState, not just Settings - PanelApp: gate the hero/empty region on settingsLoaded to prevent dashboard rendering with zeroed values for one frame while settings load Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… width) The panel's AreaBar puts the meter in a display:flex column, where PixelMeter's rootless-width div collapsed to zero (its cells are flex:1 with no basis), so the Projects/Caches/Docker bars showed only the GB number. Give the meter root width:100% so it fills its flex parent; a no-op in the block-context hero, where it already filled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rouping # Conflicts: # STATUS.html
…oard Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Regroups the Docker tab by project instead of by category, so you can see which Docker resources belong to which project — the node_modules-style view you asked for. Stacked on #40 (Docker cleanup); review/merge that first. Implements
docs/superpowers/specs/2026-07-08-docker-project-grouping-design.md, built subagent-driven with TDD.What it does
working_dir; generic Docker glyph when the folder is gone), name, and size/count. Rows are its containers, volumes, and images.Association (honest but aggressive)
Project identity only exists for Docker Compose projects (labels). On top of that, "used-by" association pulls in resources an existing container references — an image a project's container runs, a volume it mounts. An item used by more than one project stays unassociated, so the size sort never double-counts. Pure
docker runsetups with no compose fall to "Other" (a stated data-limitation ceiling, not a bug).Under the hood
readDockerInfoadds batcheddocker container inspect+docker volume inspectbehind the existing cache, all fail-soft (any inspect error → everything unassociated, tab still lists + cleans). PureassociateProjects+groupDockerForDisplaycarry the logic (heavily unit-tested, no daemon).Safety — unchanged
Presentation + enrichment only. Every row still flows its real
DockerItem.id/kind; the typed-name volume-confirmation gate is byte-identical; buildcache is never per-item removable; bulk-prune buttons appear only on "Other" headers (prune is a global op, never shown as project-scoped). A whole-branch Opus review verified no wrongful-delete path.Testing
257 tests green (typecheck/lint/build clean). The per-task + whole-branch review loop caught and fixed three real issues — the image id/ref match needing to be a union (not a fallback, else a two-project image mis-assigns), the "Dangling images" bulk prune being unreachable (danglings land in "Other"), and a fail-soft ordering gap that could silently hide items — each with a regression test. The docker CLI is mocked (no daemon in CI); a manual pass with a running compose project is in STATUS as a user action.
🤖 Generated with Claude Code