Skip to content

fix(shell): three VID-build-apps capture blockers — tile collision, truncated grants, icon 404s - #369

Merged
th3-br41n merged 1 commit into
mainfrom
fix/vid-capture-dashboard-blockers
Jul 29, 2026
Merged

fix(shell): three VID-build-apps capture blockers — tile collision, truncated grants, icon 404s#369
th3-br41n merged 1 commit into
mainfrom
fix/vid-capture-dashboard-blockers

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

Unblocks the VID-build-apps shoot: three of the five polish rungs the capture dry-run filed (POLISH-LAY-6, -7, -8 in the harness plan). LAY-6 is the episode's payoff shot; LAY-8 was the only console-error source in the run. (POLISH-FN-1/FN-2 are a separate agent's app-side work; shell #367's demo-provider manifest fix is unrelated and untouched here.)

POLISH-LAY-6 — a newly installed app's tile landed ON TOP of Notes

Root cause — units vs cells. The renderer stores icon positions in 8px GRID_UNITs and paints an ICON_FOOTPRINT_W × ICON_FOOTPRINT_H (11×14 unit) box per icon — hence the captured fleet Notes@0,0 · Files@11,0 · Database@22,0 · … · Books@0,14. placeDashboardIcon scanned a fictional 12-column grid of 1×1 cells, so cell 1,0 looked free while Notes' box was sitting on it. Replaying the pre-fix scan against that exact fleet returns {col:1,row:0}, overlapping Notes — the reported symptom, reproduced before patching.

Fix — one placer, not two (shared helper). packages/shell/src/shared/dashboard-icon-grid.ts now owns the footprint constants, the overlap test and the first-free-slot scan (pure, no Electron/React — same posture as shared/locale-catalog.ts). The renderer's dashboard/grid.ts and main's dashboard/grid-placement.ts both build on it and re-export under their existing names, so no call site changed shape. Every install/pin path — marketplace, sideload, vault-source install, dev seeder, entity pin, Bin pin — computes its cell through it.

Same-family fix included: the Bin's shell-surface pin wrote {0,0} on the stale premise that "the dashboard's collision layer repositions onto the first free cell". Icon layout is free placement with no collision resolution, so pinning the Bin stacked it on whatever owned the origin. It now takes the first free slot like every other pin.

POLISH-LAY-7 — the grants popover ellipsised the scope

entities.read:brainstorm/Pro… — the panel that exists to answer "what can this app read?" truncated the only part that answers it. The row goes from 1fr auto auto to 1fr auto, stacking capability over "granted via" in one text column (the same shape as the app rows behind the popover — no new chrome), and the capability wraps with overflow-wrap: anywhere instead of ellipsising. It matters beyond the video: this is the surface that proves the sandbox claim.

POLISH-LAY-8 — brainstorm://app-icon/<id> 404s (×4)

Two sides, both root-cause:

  • Handler. "Installed, declares no icon" is a normal state — every icon surface already draws a deterministic initials tile for it — so it now answers 204 instead of 404. An id that resolves to nothing installed still 404s, because that is worth surfacing. A declared-but-missing icon file resolves as no-icon too, rather than handing the caller a missing file:// to fetch (which surfaces as ERR_UNEXPECTED in the renderer — the trap the emoji handler already guards). Rules extracted from the 5k-line main/index.ts into a testable main/apps/app-icon-resolver.ts.
  • Request side. resolveAppIconSrc has always answered null for a known icon-less app — but the running strip, the window switcher, widget headers and marketplace cards each hand-built their own brainstorm://app-icon/... URL and so opted out of it. They all go through the shared resolver now (it grew an optional epoch arg so the widget header keeps its F-380 re-install retry and gains the suppression). The URL builder moved to shared/app-icon-url.ts, shared by the preload bridge and the renderer — an icon URL is a string, not a privileged operation, so a renderer component no longer needs the whole preload bridge present to name one.

Tests

New / extended, all failing-first against the old behaviour where applicable:

  • main/dashboard/grid-placement.test.ts — footprint-stepped placement, the exact captured fleet (asserts no overlap with any existing icon), wrap to the next row, an off-lattice user-dragged icon, hole-filling after an uninstall. The LAY-6 case is called out by name.
  • main/dashboard/dashboard-store.test.ts — the install path end to end over the real DashboardStore: seeded fleet → install Client Pulse → install a second app, asserting no overlap with anything, plus the full-first-row wrap case.
  • main/apps/app-icon-resolver.test.ts (new) — declared icon, subdirectory icon, no icon, empty/non-string icon, declared-but-missing file, bundle escape, containment of an absolute-looking path, no active bundle, unreadable/unparseable manifest, and the 204/404 status split.
  • renderer/dashboard/app-icon-cache.test.ts — epoch appending and icon-less suppression through the epoch path.
  • renderer/settings/grants-panel-layout.test.ts (new) — a CSS ratchet: the capability line may not carry text-overflow: ellipsis / white-space: nowrap, must wrap, and the row must keep its two-column shape.

bun run verify ✅ · bun run lint ✅ (all ratchets clean) · bun run typecheck ✅ (packages + per-app) · full bun run test1640 files / 16340 tests.

Four-path evidence

  • Screenshot — the two visible changes (tile placement, grants row) are re-verified by the VID-build-apps capture re-run this PR unblocks; the placement is additionally pinned numerically by the fleet-overlap tests (a screenshot can't assert "clears every footprint", these do), and the grants layout by the CSS ratchet.
  • Keyboard — unchanged. No control was added, removed, reordered or re-parented: the grants row keeps exactly one focusable element (Revoke) in the same tab order; the Bin pin toggle, install flows and icon surfaces keep their existing bindings.
  • Screen reader — unchanged and slightly better: the grants row's capability text is now fully wrapped rather than visually truncated (SR text was always complete; sighted parity is what improves), and no aria-*, role or accessible name changed anywhere in the diff. Icon changes are on aria-hidden artwork only.
  • Discoverability — improves on two counts: a newly installed app's tile is now visible instead of hidden under an existing one, and an app's granted scopes are legible in the panel that exists to disclose them. Nothing became less reachable.

🤖 Generated with Claude Code

…runcated grants, icon 404s

POLISH-LAY-6 — a newly installed app's tile landed ON TOP of Notes.
Units-vs-cells mismatch: the renderer stores icon positions in 8px GRID_UNITs
and paints an ICON_FOOTPRINT_W × ICON_FOOTPRINT_H (11×14) box per icon, while
`placeDashboardIcon` scanned a fictional 12-column grid of 1×1 cells — so with
Notes at 0,0 the "free" cell 1,0 was inside Notes' box. Replaying the old scan
against the captured fleet (0,0 · 11,0 · 22,0 · 0,14) reproduces exactly the
reported 1,0.

Fixed by making one placer, not two: `shared/dashboard-icon-grid.ts` owns the
footprint constants, the overlap test and the first-free-slot scan; the
renderer's `dashboard/grid.ts` and main's `dashboard/grid-placement.ts` both
build on it, and every install/pin path (marketplace, sideload, vault-source,
dev seeder, entity pin, Bin pin) now computes its cell there. Also fixes the
Bin's shell-surface pin, which wrote {0,0} on the stale premise that "the
collision layer repositions it" — icon layout has been free-placement with no
collision resolution for some time, so it stacked on the origin tile.

POLISH-LAY-7 — the grants popover ellipsised the capability scope
(`entities.read:brainstorm/Pro…`), i.e. the panel that answers "what can this
app read?" truncated the only part that answers it. The row drops from three
columns to two, stacking capability over "granted via" in one text column —
the same shape as the app rows behind the popover — and the capability wraps
(`overflow-wrap: anywhere`) instead of ellipsising.

POLISH-LAY-8 — `brainstorm://app-icon/<id>` 404'd for icon-less sideloaded
apps, the only console errors in the dry-run. Two sides:
  * the handler now distinguishes "installed, declares no icon" (204 — not an
    error, the caller draws initials) from "resolves to nothing installed"
    (404, still worth surfacing). A declared-but-missing icon file resolves as
    no-icon too, instead of handing the caller a missing `file://` to fetch
    (ERR_UNEXPECTED in the renderer). Rules extracted to a testable
    `apps/app-icon-resolver.ts`.
  * the request side converges on the shared `resolveAppIconSrc`, which already
    answers `null` for a known icon-less app — the running strip, the window
    switcher, widget headers and marketplace cards each hand-built their own
    URL and so opted out of it. The URL builder itself moves to
    `shared/app-icon-url.ts` (preload + renderer share it).

Tests: pure placement + occupancy (incl. the exact captured fleet, the wrap
case and an off-lattice dragged icon), install-path placement over the real
DashboardStore, icon resolution + miss-status, icon-cache epoch/suppression,
and a CSS ratchet pinning the grants row's non-truncating layout.
`bun run verify`, `bun run lint`, `bun run typecheck`, full `bun run test`
(1640 files / 16340 tests) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@th3-br41n
th3-br41n merged commit 0a3b76b into main Jul 29, 2026
2 of 3 checks passed
@th3-br41n
th3-br41n deleted the fix/vid-capture-dashboard-blockers branch July 29, 2026 19:49
th3-br41n added a commit that referenced this pull request Jul 30, 2026
…#370)

`firstFreeIconCell` had no column bound — only a MAX_SLOT_SCAN — so the
install slot marched along one unbounded row. With the captured 20-app
fleet at `0,0 · 11,0 … 209,0`, the next install landed at `220,0` =
1760px on a 1440px stage: off the edge of `.dashboard-icons`, which does
not scroll horizontally, i.e. permanently unreachable. Books/Chat/Forms/
Agent were already past the fold for the same reason.

A column bound needs the icon surface's width, and only the renderer
knows it (`widget-host.ts` already documents the viewport as "which only
the renderer knows"). So placement moves:

- Main (`placeDashboardIcon`, `dashboard.pin`) writes
  `UNPLACED_ICON_POSITION` — a typed `-1,-1` sentinel, unambiguous
  because every real cell is ≥ 0, and not a magic `0,0` that would read
  as a deliberate top-left placement. `main/dashboard/grid-placement.ts`
  is gone: main no longer places anything.
- The icons layer resolves those against its measured viewport via
  `resolveIconPlacements` and persists the cell through the existing
  `dashboard.moveIcon` path. It resolves in the render memo, so a fresh
  install paints in its free on-screen slot the same frame the snapshot
  arrives — the live no-restart reveal is unchanged.
- The two renderer-side pin paths (app-grid pin, Bin pin) stop picking
  cells too; everything funnels through the one placer.

Wrapping derives from the shared footprint constants; a viewport too
narrow for one icon clamps to a single column. Pre-existing overflow is
re-flowed in the VIEW MODEL only (`IconPlacementReason.Offscreen`),
never persisted — mirroring `clampWidgetOrigin` (F-379): stranded icons
become reachable immediately, widening the window restores the user's
own arrangement, and dragging a rescued icon commits it normally.

The geometry stays in the one shared module (#369's point); what moved
is who calls it and with what bound.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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