Skip to content

feat(grid): widget chassis — InfoTile, GRID_WIDGETS registry, layout v2, Tiles menu (ent#325) - #2042

Open
dolho wants to merge 1 commit into
feature/305-grid-org-overlayfrom
feature/ent325-grid-widget-chassis
Open

feat(grid): widget chassis — InfoTile, GRID_WIDGETS registry, layout v2, Tiles menu (ent#325)#2042
dolho wants to merge 1 commit into
feature/305-grid-org-overlayfrom
feature/ent325-grid-widget-chassis

Conversation

@dolho

@dolho dolho commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Related to abilityai/trinity-enterprise#325 · foundation for epic ent#94

Stacked on PR #1918 (ent#305). Base is feature/305-grid-org-overlay, not dev, per the issue's §E sequencing note — the overlay rewrites the same files and a parallel rewrite of FleetGrid.vue was the thing to avoid. Retarget to dev once #1918 merges; the diff below is this issue's work only.

What

The widget chassis every tile sub-issue on ent#94 (#95#101, #259) is blocked on. Until now none of them was claimable, because whoever picked one first had to build the whole chassis off-issue.

  • InfoTile.vue — the tile shell
  • GRID_WIDGETS registry + widget:* key namespace
  • layout v2 with a one-time v1 migration
  • the "Tiles ▾" show/hide menu
  • one reference tile, so the chassis is demonstrable

Info tiles are occupants of the same lattice, not a parallel system

Widgets join the same layout map under widget:* keys. : cannot appear in an agent name (sanitize_agent_name strips everything outside [A-Za-z0-9_-]), so collision is impossible and isWidgetKey is exact rather than a heuristic.

The payoff is that drag, swap-with-preview, keyboard reorder, viewport culling and the snap socket all work for tiles through the existing data-agent path — no second code path to keep in sync as those behaviours evolve. Keyboard came free: @keydown is on the world container and resolves through layout[name].

Deliberately absent from InfoTile: Run/Autonomy toggles (info tiles summarize, they don't operate — a fleet-scope operating control makes a misclick fleet-wide), and any org affordance. No connect port, so a tile cannot be a reporting-line endpoint; that is enforced by omitting the hover handlers AgentTile uses, not by a runtime check.

Org-overlay interlock — the load-bearing part

A. Tidy and Group-by-dept would have silently deleted every tile position. Both tidyByDept and arrangeByDept build a fresh map keyed only from agents, and applyLayout replaced the layout wholesale. Fixed at both ends as the issue asks: the helpers carry widget:* keys through (seeded first, so department blocks flow around them via nearestFreeCell), and applyLayout merges instead of blind-replacing. Either alone is a single point of failure, and one-guard-only is how this bug existed in the first place.

Adding nearestFreeCell to arrangeByDept is behaviour-preserving when no widgets are present — every requested cell is free, so placement is unchanged. There is a test pinning that equivalence, because it is the entire safety argument for touching that function.

B. Widgets stay out of zone hulls — left alone deliberately. computeZones iterates agents, so a widget:* key is excluded by construction. Not "fixed" by iterating the layout map; pinned by a test asserting a tile parked beside a department does not stretch its frame.

C. Gap budget. The peg overhangs 16px, inside GAP_X (40), so it cannot collide with ZONE_CHROME.

D. Seeding consults zoneAt and skips cells inside a department frame — rows −3…−1 are empty of tiles but not necessarily of hulls.

Layout v2

trinity-grid-layout-v2, migrated by a one-time copy from v1 — v1 is left in place, so downgrading is not a data-loss event.

normalizeLayout gains a widgetKeys argument. Without it every widget:* entry was dropped on the next reconcile; that is pinned as a test so the regression stays visible rather than becoming folklore. Agents reconcile first, so a collision evicts the tile — an info tile moving one cell is a smaller surprise than a constellation the user arranged shifting under them. The changed check now covers widgets too, or every reconcile would re-persist localStorage forever.

Default placement is the band above the fleet, which is what makes "default-on tiles appear without moving any saved agent position" true by construction: defaultLayout starts agents at r=0 and grows downward, so negative rows are unclaimed on every existing install.

Tiles menu

Per-user, persisted under its own key so it can never clobber the overlay's Zones/Lines prefs. Preferences are a sparse override map, not an allow-list — so a tile added in a later release appears for existing users instead of staying invisible behind a stale list, a user can keep a default-on tile off, and "Reset" is just an empty map. Placed in the bottom control cluster rather than as a fifth top-level header button, per the issue's responsive-ladder note.

Registry, and why the catalog lives elsewhere

utils/gridWidgets.js stays free of .vue imports so vitest's node environment can test it — and because gridOrg.js now imports isWidgetKey from it, a component import there would have broken #1918's existing gridOrg.spec.js. Catalog entries live in components/tiles/catalog.js, imported by FleetGrid for its side effect. Adding a tile is one registerWidget({...}) block plus the component.

cells is declared and ignored: v1 renders single-cell only, multi-cell occupancy is #94's stated v2. Declaring it now means catalog entries don't need rewriting when gridLayout.js learns occupancy.

One reference tile ships (Fleet summary): it reads the agents array the grid already holds and issues no request, so the chassis carries no data dependency of its own and could not be blocked behind the sibling ent#326 endpoint. This is not scope creep — the epic's ACs require a default-on tile to render, so an empty catalog would leave half of them unverifiable. Real tiles arrive one sub-issue at a time.

Tests

tests/unit/gridWidgets.spec.js — 28 tests: key namespace (incl. that no agent-shaped name reads as a widget), registry, an adminOnly tile never enabled for a non-admin even when prefs say on (prefs survive a role change; the catalog filter has to be what decides), override-map semantics incl. a newly-registered default-on tile reaching a user with existing prefs, seeding band + veto, normalize round-trip in both directions, the tidy/arrange preservation rule, and zone-hull exclusion. One guard asserts stray non-widget keys are not carried, so the carry-through can't drift into "preserve anything unknown" and resurrect a deleted agent forever.

tests/unit/gridWidgets.spec.js   28 passed   (new)
tests/unit/gridOrg.spec.js       21 passed   (#1918, unbroken)
tests/unit/gridLayout.spec.js     6 passed   (#1918, unbroken)
                                 55 passed

Verification note, stated honestly

npm run build and npm run test:unit cannot run against this machine's node_modules — it predates #1918 (no vitest) and is missing mermaid/qrcode. Both were run out-of-band instead: vitest standalone with an equivalent inline config (the 55 above), and a production vite build with the stale deps externalized, which transformed 638 modules clean — covering every file in this PR. CI runs both properly.

Not in scope

Multi-cell tiles (#94 v2), agent-declared custom tiles (#94 v2, unfiled), and the actual data tiles (#95#101, #259).

🤖 Generated with Claude Code

…v2, Tiles menu (ent#325)

The frontend half of epic ent#94's foundation. Until now every tile
sub-issue (#95-#101, #259) was unclaimable, because the first person to
pick one had to build the whole chassis off-issue.

Built ON TOP of the org overlay (ent#305, PR #1918) rather than beside
it, per the issue's sequencing note — this branch forks from
feature/305-grid-org-overlay, not dev.

## Info tiles are occupants of the same lattice

Widgets join the SAME layout map under `widget:*` keys rather than a
second map. `:` cannot appear in an agent name (sanitize_agent_name
strips it), so collision is impossible and `isWidgetKey` is exact. The
payoff: drag, swap-with-preview, keyboard reorder, viewport culling and
the snap socket all work for tiles through the existing `data-agent`
path, with no second code path to keep in sync as those behaviours
evolve.

## Org-overlay interlock (the load-bearing part)

A. `tidyByDept` and `arrangeByDept` rebuilt a fresh map keyed only from
   `agents`, and `applyLayout` replaced the layout wholesale — so one
   "Tidy up" or "Group by dept" deleted every tile position. Fixed at
   both ends per the issue: both helpers now carry `widget:*` keys
   through (seeded first, so department blocks flow around them via
   nearestFreeCell), AND applyLayout merges instead of blind-replacing.
   Adding nearestFreeCell to arrangeByDept is behaviour-preserving with
   no widgets present — every requested cell is free — which a test pins,
   since that equivalence is the whole safety argument.
B. `computeZones` iterates agents, so tiles are excluded from hulls by
   construction. Left alone deliberately; a test pins that a tile parked
   beside a department does not stretch its frame.
C. The peg overhang is 16px, inside the GAP_X budget of 40, so it cannot
   collide with zone chrome.
D. Seeding consults `zoneAt` and skips cells inside a department frame —
   rows -3..-1 are empty of tiles but not necessarily of hulls.

## Layout v2

`trinity-grid-layout-v2` with a one-time COPY from v1 (v1 left in place,
so a downgrade is not data loss). `normalizeLayout` gains a `widgetKeys`
argument; without it, widget entries were dropped on every reconcile —
pinned as a test so the regression stays visible. Agents are reconciled
first, so a collision evicts the tile rather than moving a constellation
the user arranged.

Default placement is the band above the fleet (rows -3..-1), which is
what makes "default-on tiles appear without moving any saved agent
position" true by construction: defaultLayout starts agents at r=0.

## Tiles menu

Per-user show/hide, persisted under its own key so it can never clobber
the overlay's Zones/Lines prefs. Preferences are a sparse OVERRIDE map,
not an allow-list — so a tile added in a later release appears for
existing users instead of staying invisible behind a stale list, and
Reset is just an empty map. Lives in the bottom control cluster rather
than as a fifth header button, per the issue's responsive-ladder note.

## Registry

`utils/gridWidgets.js` stays free of `.vue` imports so vitest's node
environment can test it (gridOrg imports isWidgetKey from it, so a
component import would have broken the existing gridOrg.spec.js too);
catalog entries live in `components/tiles/catalog.js`. `cells` is
declared and ignored — multi-cell occupancy is #94's stated v2.

Ships ONE reference tile (Fleet summary) reading the agents array the
grid already holds and issuing no request, so the chassis has no data
dependency of its own and could not be blocked behind ent#326. The epic's
own ACs require a default-on tile to render, so an empty catalog would
have made half of them unverifiable.

## Tests

`tests/unit/gridWidgets.spec.js` — 28 tests: key namespace, registry
(incl. adminOnly never enabled for a non-admin even if prefs say so),
override-map semantics, seeding band + veto, normalize round-trip (both
directions), the tidy/arrange preservation rule, and the zone-hull
exclusion. 55/55 pass across all three specs — #1918's 27 unbroken.

Related to Abilityai/trinity-enterprise#325

Co-Authored-By: Claude Opus 5 (1M context) <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