fix(shell): three VID-build-apps capture blockers — tile collision, truncated grants, icon 404s - #369
Merged
Merged
Conversation
…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
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>
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.
Unblocks the VID-build-apps shoot: three of the five polish rungs the capture dry-run filed (
POLISH-LAY-6,-7,-8in 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-2are 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 anICON_FOOTPRINT_W × ICON_FOOTPRINT_H(11×14 unit) box per icon — hence the captured fleetNotes@0,0 · Files@11,0 · Database@22,0 · … · Books@0,14.placeDashboardIconscanned a fictional 12-column grid of 1×1 cells, so cell1,0looked 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.tsnow owns the footprint constants, the overlap test and the first-free-slot scan (pure, no Electron/React — same posture asshared/locale-catalog.ts). The renderer'sdashboard/grid.tsand main'sdashboard/grid-placement.tsboth 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 from1fr auto autoto1fr 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 withoverflow-wrap: anywhereinstead 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:
file://to fetch (which surfaces asERR_UNEXPECTEDin the renderer — the trap the emoji handler already guards). Rules extracted from the 5k-linemain/index.tsinto a testablemain/apps/app-icon-resolver.ts.resolveAppIconSrchas always answerednullfor a known icon-less app — but the running strip, the window switcher, widget headers and marketplace cards each hand-built their ownbrainstorm://app-icon/...URL and so opted out of it. They all go through the shared resolver now (it grew an optionalepocharg so the widget header keeps its F-380 re-install retry and gains the suppression). The URL builder moved toshared/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 realDashboardStore: 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 carrytext-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) · fullbun run test✅ 1640 files / 16340 tests.Four-path evidence
aria-*, role or accessible name changed anywhere in the diff. Icon changes are onaria-hiddenartwork only.🤖 Generated with Claude Code