← Design · c131

phase2-plan.md

c131 Phase 2: Inventory in the Qpoint skin (plan, before kick-off)

Prompt: phase2-prompt.md (Mark, 2026-11-10). Written: 2026-11-10, from two surveys of the code (the qdash Inventory section field by field; the Tour, Data Machine and expand-row components) and a read of the kit's source at 0.9.1. Runs in: ../qnative as its cycle c2 (the spike was c1, the phase Mark calls the first). Recorded here as c131 Phase 2. It takes the parts of the plan's Phase 1 (skeleton) that Inventory needs. Mode: alchemy (how far the kit stretches) inside evolution (a port of pages that exist, on data that exists). Reader: Mark, then the executor in ../qnative.

Why

The spike showed that the kit draws a clean window cheaply: 52–80 MB, 264 ms to first render, nothing drawn by hand. It did not show whether the kit can carry our way of building a page. On the web, that way is the layer's composed vocabulary: Data Machines in aligned bands, ledgers with pills and logos, jump pills that pin, a skill file shown in an editor frame. Phase 2 is the test of that expressive range. We replicate one whole section, Inventory, in the Qpoint skin, on the same data the web pages show, cut to one machine. We record per component how close it came and what it cost.

What we are replicating

The Inventory submenu in qdash (AppSidebar.vue:137, lib/working-nav.ts:24-30) has seven listings, in nav order: Providers, Harnesses, Models, MCP Servers, Skills, Tools, Repos & Workspaces. Under them are nine detail pages:

  • the seven details;
  • Account detail (reached from a provider);
  • Harness install detail (/inventory/agents/detail). It is not in the nav, but nine ledgers link to it.

The section's landing is the Inventory section of Home: 20 DataMachineMicro cards, one per band machine (lib/index-micros.ts:96). Every Inventory breadcrumb leads there.

About 30 layer components carry it:

  • Overview and data devices: PageHeader, DataMachineGroup, DataMachine, DataMachineMicro, DataIndexSection, DataRankedBars, DataSegmentBar, DataDonutChart.
  • Tables and empty states: DataTable (with #details expand), DataListingBar (quick filter plus column picker), StateEmpty, FilterNoMatches.
  • Headers and navigation: NavJumpPills (sticky), ActorHeader, ActorHeaderCell.
  • People, agents and badges: AgentAvatar, AgentPawn, PeopleOwnerCard, UxPill, UxIcon, UxAvatar, ProjectedBadge.
  • Controls: UxSelectInline, UxTabGroup.
  • The skill file: UxEditorFrame, UxFileTree, UxCodeBlock.
  • Surface: DataSurfaceSection, DataSurfacePanel.

The three goals, read against the code

  • The Data Machine is on the main path, not beside it. It is the band on every listing and the tiles on every detail page; 15 of the Inventory components use it. The landing is 20 micros. It is in the core, and its full fidelity is a measured part of the core.
  • The expand row is on the main path too. Recent invocations on Tool detail (ToolCalls.vue:44) and Skill detail (SkillRuntime.vue:47) are the only two expandable tables in Inventory. A basic expand lands with those pages. The extra stretch is the full behaviour, below.
  • The Tour guide is the true stretch. It exists only in the design lab: app/components/tour/, the five c123 tours, and the qpoint-guide MCP relay on the dev server. No data-qp-id exists in qdash, and help mode ("Info View", after Ableton) was never built.

Fleet → personal

The web Inventory is a fleet bill of materials: what the organisation runs, across endpoints and people. The native app shows the same data from one chair, this machine and its person. Mark's instruction is to show the data those pages show. The work is in the structure: where fleet words stop meaning anything, and what a page about one machine needs that a fleet page hides.

The survey tagged every tile, column and section. The pattern behind the tags gives six rules:

  1. Activity carries as is. Sessions, tokens, spend, calls, denied, last used: on one machine, they are yours.
  2. "How many, where" becomes "here, at this path".
    • Install and host counts collapse to a statement: installed or not, monitored or not.
    • The statement is backed by what the scan already serves: the executable or app path, the config file, the fingerprint.
  3. People and places leave the rows. User, endpoint, team and the people counts are dropped from tables. The header says once whose machine this is and in which state (local or connected).
  4. Drift moves inward. Fleet drift is across installs. On one machine it becomes:
    • skill copies that disagree between your harnesses' configs;
    • an MCP server registered in one harness and called from another.
  5. The served-but-hidden fields come forward. At fleet scale the web hides each install's long tail. On one machine that tail is the most useful thing on the page:
    • binary path, fingerprint, config source;
    • permission, approval and sandbox modes, configured model, auth method, quotas;
    • MCP command, args, URL and env keys, with OAuth state.
  6. Nothing is blanked (R5). A fleet-only tile is replaced by its personal counterpart, or removed and the band rebalanced. No cell reads "1" just because the denominator collapsed.

What the rules do to each page (1M = one machine as is, CM = changes meaning, FO = fleet only; field-level tags in the survey):

Web pageNative pageCarriesTranslatesDrops
Home › Inventory (20 micros)Inventory landing, the same 20 microsproviders, tokens, spend, models, MCP, tools, workspacesHarness installs → harnesses here (monitored · shadow); Skill installs → skills per harness hereSkills by team → by harness
Harnesses listingHarnessesharness, providers, sessions, spend; plus version, default model, MCP and skills configured, last active (served, hidden on the web)installs → installed (path); coverage → monitoredendpoints column
Harness detail and Harness install detailone page: the harness as installed hereheader, providers, spend, surface (models, MCP, workspaces, tools); the install's long tail (rule 5); the Sessions ledger from the install pageVersions → the one version; Skills panel → configured here; the Installs ledger appears only when this machine has more than one install of the harnessUsers, Teams and Versions-drift panels; host and user columns
MCP Servers listingMCP Serversallharnesses and installs → which of your harnesses call it and which register itnone
MCP Server detailMCP Servertools ledger, denied, calls; plus config[] (where it is configured, how it starts)the gap sentence → "configured here · called here"; the Installs tile → the configs that register ithost and user columns
Models listing · detailModelsall but usersdefault_on → "the default in "users, endpoints
Providers listing · detailProvidersallaccounts → your accounts on itusers
Account detailAccount (smaller)header, harnesses, sessionsdual use ("you present a personal and a company account"), no identityUsers ledger, Shared, people count, endpoints
Skills listing · detailSkillsthe definition editor (the strongest carry-over), invocations, runtimeinstalls → the harnesses here that carry it; Definitions → copies that disagree across your configs; "canonical" → the copy most of your harnesses loadteam and people pickers; endpoint and user columns
Tools listing · detailToolsall, with the expandable invocationsnoneuser and endpoint columns
Repos & Workspaces listing · detailRepos & WorkspacesallUsers chips → youteams, hosts, the Users ledger, "shared"

A structural consequence: the web's orphan, Harness install detail (one harness for one user on one host), is the page that is already about one machine. On the endpoint it becomes the harness page. Nine ledger links stop pointing at a page hidden behind the footer toggle.

Seen from the plane: a connected app reads the plane, so the same cut must be served there. No Inventory handler takes ?host= today (web.rs: harnesses, mcp-servers, models, providers, skills, tools, workspaces). Only /api/endpoints?host= and /api/surface?host= take one. c134's Kamal prep already names this as candidate gap row 31; the survey confirms it. On a real endpoint, qcontrol's own local API is already one machine, and the backend has a single-host fallback (web.rs:1376, 5130, 5325).

How it is built

The web architecture in the bible is page → context component → layer component, with composables over a Transport. It maps one to one:

WebNative (../qnative)
ui/types/api/* (TS, written from web.rs)crates/types: every Inventory listing and detail shape, serde, the wire names
composables over Transport (http() · local())crates/nozzle: a repository per resource (harnesses(), harness(id), …) over the three transports
q-nuxt-layer componentscrates/layer: our components in Rust, with the web names (DataMachine, DataRankedBars, UxPill…), so the parity table reads one to one; the kit's styled components underneath
app/components/inventory/* (Band, Ledger, Header, Tiles)crates/app/src/inventory/*: the same context components
pages + routerpages + a route enum with history
design-site demos (/components/<name>)crates/stories: one story per component and per page state, rendered headless, captured to PNG

Steps

Each step ends in captures. ★ marks a review stop.

0. Kick-off

  • Write qnative cycles/c2.<name>/CYCLE.md.
  • Update c131's record.
  • Pin gpui-kit = "=0.9.1" for the whole phase; no upgrade mid-phase.

1. The world, cut to one machine (c131 synthetic-data.md made real for Inventory)

  • Cut the streams, not the database. Filter the seed's out/runs.ndjson and out/scan.ndjson by hostname. In the seed each user has one host and each install is keyed agent × host (generate.py:142-187, 436-441, 831-839).
  • Ingest through the real path (qdash ingest --from-file) into a fresh database, as generate.py does with a copied binary.
  • Serve and crawl. Serve that database and crawl every route the Inventory pages read: listings × the period presets, details × every id. Write the results to crates/fixtures/world/<name>/ with a manifest. This is c113's bake applied to one host. Nothing in qcontrol changes; the cut script lives in qnative.
  • The worlds:
    • meridian-day90: priya-x1. Claude, Codex and Cursor installed, plus an Ollama shadow install. This is the default.
    • first-week: chen-mba. One instrumented Claude install, one never-monitored Windsurf install. The minimal state.
    • fresh: empty.
    • Overflow waits for a power-user world.
  • Types for every shape, with a fixtures test that deserialises every crawled file (R3).
  • Repositories in the nozzle. The fixture plane serves the same directory.
  • Check the binary first. The seed binary dates from 2026-10-14. If web.rs has moved since, rebuild into a scratch CARGO_TARGET_DIR so qcontrol's shared target/ is untouched.

2. The skin ★

  • The token bridge (c132 deliverable 2, built here; c132 D2 holds: generated, never hand-edited).
    • Source: q-nuxt-layer/tokens/tokens.json plus semantic.mjs. That is the file tokens.css is generated from: 21 semantic roles, light and dark, documented in brand/design_tokens.md, and the roles qdash's pages actually use.
    • Generator: design scripts/tokens-to-kit.mjs. Output: qnative/crates/app/themes/qpoint.json, a ThemeSet with light and dark that maps our roles onto the kit's 134.
  • Beyond colour:
    • the card: radius 12, the two-layer shadow in light, a border in dark;
    • hairlines;
    • Inter's scale from 11 to 40 and its weights (D14);
    • density.
  • Dark mode follows the OS (Theme::sync_system_appearance).
  • A gallery story: the kit's components in the Qpoint skin, light and dark.
  • Captures without Screen Recording. GPUI 0.3.12 has VisualTestAppContext::capture_screenshot: a real macOS window rendered off-screen to an image. Stories become PNGs in the atlas with surface: native (c132 D3). This answers c132's open question of how medium B is captured; confirm the feature flags first.
  • ★ Review stop 1: the skin, kit components in Qpoint, light and dark, beside the web.

3. The layer in Rust (crates/layer)

One story per component, captured beside its design-site demo. Props become builder methods; slots become AnyElement children or closures. In build order:

  • a. Primitives.
    • UxPill and UxIcon (the kit's Lucide icons plus ours).
    • UxAvatar and AgentAvatar (logos as PNG).
    • AgentPawn: pixel art through img, never tinted (the pawn-components rule).
    • ProjectedBadge and ProjectedRing.
    • StateEmpty and FilterNoMatches, on the kit's Empty and Skeleton.
  • b. The Data Machine family.
    • DataMachine, DataMachineMicro, and DataMachineGroup with aligned rows. GPUI's layout engine has grid but not subgrid; try a first-frame measure that pins row heights across the band, and row slots sized from the type scale.
    • DataIndexSection: the auto-fill grid computed from the container's width.
    • The devices: DataRankedBars, DataSegmentBar, DataDonutChart (canvas paths), the spark.
    • The fade on fadeKey.
  • c. Page furniture.
    • PageHeader (breadcrumb, title, subtitle).
    • ActorHeader and ActorHeaderCell.
    • NavJumpPills. GPUI has no position: sticky, so the pills re-render pinned once the scroll offset passes them.
    • UxTabGroup on the kit's Tabs; UxSelectInline on the kit's Select.
  • d. The ledger.
    • DataTable as Qpoint draws it: logos, pills, links in cells. The kit's DataTable underneath for plain virtualised ledgers.
    • DataListingBar: quick filter and column picker on the kit's Popover and Checkbox.
    • The basic expand row: a full-width well under the row. It cannot live in the kit's DataTable, which is a uniform_list with one fixed row height (table/state.rs:2856). It is built on GPUI's variable-height list (splice, remeasure).
  • e. Surface. DataSurfaceSection and DataSurfacePanel, List view only (qmap stays a link, c130 D8).
  • f. The skill file.
    • UxEditorFrame.
    • UxFileTree, on the kit's Tree.
    • UxCodeBlock, on the kit's TextView, or its Editor with syntect. The syntect choice is a binary-size cost to measure (R7).

4. Shell and router

  • The sidebar: Inventory with its seven listings in nav order.
  • A route enum with history (back and forward, ⌘ and ⌘); breadcrumbs back to the landing.
  • The when: pill: the crawled presets, defaulting to 30 days (c128's shared default).
  • The footer names the source and the world (data: synthetic · meridian-day90 · priya-x1).

5. The pages

  1. The landing.
  2. Harnesses: the listing and the merged detail. ★ Review stop 2: the first listing and detail, beside the web.
  3. Providers, with Account.
  4. Models.
  5. MCP Servers.
  6. Tools, with the expand row.
  7. Skills, with the definition editor and the expand row.
  8. Repos & Workspaces.

Each page has stories for typical, minimal, empty and error, plus overflow on the ledgers once that world exists (R6).

6. How close we got ★

  • The scorecard, one row per component:
    • how it was built: kit as is · kit composed · drawn with GPUI primitives · not reached;
    • fidelity from 0 to 3, side by side with its design-site demo;
    • the HTML feature it lacked and what stood in for it;
    • effort and lines of code.
  • Per page: a side-by-side capture with the web page on the same one-host world.
    • For the capture run only, the :7882 seed is restarted on the one-host database, so the :3030 app shows the same rows without a second dev server. Then it is restored.
    • The budget before and after, with strobe (R7): RSS, startup, binary size.
  • ★ Review stop 3: the section, the scorecard, the budget.

Stretch: the Tour guide

The lab's tour leans on the DOM in three ways (survey):

  • it finds any element by attribute and measures it every frame;
  • it restyles the page from outside (fade, chrome dim, the docked padding);
  • it places callouts with Floating UI.

Native answers:

  • Targets. A .qp_id("harnesses.ledger") extension on any element, with optional row keys, records the element's bounds into a per-window registry during prepaint. The overlay reads the previous frame's bounds and requests a redraw while a tour runs.
  • Focus.
    • Ring: quads.
    • Dim: an even-odd path with rounded holes.
    • Fade: because every target goes through .qp_id, the helper applies the opacity itself. Restyling from outside becomes a context read.
  • The panel. Docked as a right column (the split layout is natural in a window), or floating, placed with anchored() plus our obstacle score.
  • Movement.
    • Steps cross pages through the router.
    • Scroll-into-view uses the page's ScrollHandle with the lab's glide easing, cancelled by wheel input.
  • Tours as data. The c123 Tour type is mirrored in serde. One new tour over native Inventory ("What's on this machine"), plus the mechanics of one lab tour.
  • Info View, native first. A footer pane that explains whatever is under the pointer, keyed by qp_id, with copy from app-map.ts and guide-knowledge.ts.
  • The MCP guide driving the native app (in scope, Mark 2026-11-10): the same qpoint-guide tools (list-tours, start-tour, read-step, find-in-app, open-page, point, highlight, read-page, explain, end-tour…).
    • An in-process loopback server replaces the dev server's SSE relay.
    • Each region supplies its own text, because there is no innerText.
    • As on the web, no tool presses a button for the person.
  • ★ Review stop.

Extra stretch: the expand row at full fidelity

The basic well lands in step 3d. The full behaviour is the rest of what the layer's ExpandRow and StretchBox do, plus what they don't:

  • Columns sized to content across every row. HTML table layout, done by hand.
  • Animated height.
    • The well animates to its measured content height. A custom element replaces StretchBox's clone-and-measure.
    • The height is re-tracked when nested rows open inside it.
  • Quiet nested rows, as in Findings.
  • The new-row flash.
  • A sticky header under the band.
  • Horizontal scroll only when the table is too wide.
  • Keyboard and accessibility. ↑ ↓ ⏎ Esc, and aria-expanded through AccessKit. The web has neither.
  • ★ Review stop.

Correlation with work in motion

CycleIn motionHow this phase meets it
c131stage 1; spike done; plan's Phase 1 skeleton, Phase 2 pages with Status first (D6); synthetic-data.md an outlineThis is Phase 2 for Inventory, with the Phase 1 parts it needs (types, repositories, stories, theme, shell). Inventory goes before Status (D6's order bent). synthetic-data.md is built for three worlds. The messages crate is not in this phase.
c130shape.md (G1) unwritten; D5 tokens-only brand; Tyler's session (G2)Mark's word starts the phase ahead of shape.md, as with the spike. The scorecard is evidence for shape.md and for D5.
c132not kicked off; a bake-off of A (Vue native skin) against B (stories crate) on Inventory; the token bridge; a native capture modeThis phase builds medium B on the bake-off's own page, the token bridge, and the native capture path. c132 keeps medium A, Mark's pick and process.md. Nothing is built twice.
c134Kamal prep: the endpoint scope missing (candidate row 31); the world as an artifact (§4.1)Row 31 confirmed: no Inventory handler takes ?host=; add it to inventory.md §6. The cut world is §4.1 made concrete for one host.
c135the layer audit, not startedThe scorecard audits about 30 components from the outside; near cousins found while porting are logged for it.
c125 · c123 · c127the tour layer in the lab; the MCP guide on the dev server; five toursThe stretch ports the engine; tours as data mirror c123's type. Info View is built native first. c127 is untouched.
c109 · c100the index of micros; the landingsThe native Inventory landing is c109's Inventory section.
c128a shared 30-day default windowThe when: pill defaults to 30 days.
c101the Surface as a qmap sceneList view only (c130 D8).
c113baked worldsThe same crawl and the same world names.
c136 · c137the grant line; the message bonesNot in this phase. The shell leaves room for the inbox.
c0the namesThe name constant only; no "qnative" in crate names or user-facing strings.

Rulings this phase bends

  • c131 D6 (page order): Inventory before Status.
  • c130 G1: starts before shape.md, on Mark's word.
  • c130 D5 and c131 R8 (tokens-only brand; a kit component before a drawn one): the experiment builds our vocabulary on top of the kit, and draws where the kit has nothing (Mark, question 1). Both rulings hold for V1 until he rules on the scorecard.
  • c131 D2 (gpui-component; gpui-base only when a component is missing): reaching down to gpui-base or raw GPUI is allowed where a styled component fights the look. Every reach is logged in the scorecard.

Decisions (defaults)

#QuestionDefault
P1Where it runs../qnative c2; recorded here as c131 Phase 2.
P2The dataThe one-host cut of the Meridian streams through the real ingest and handlers; meridian-day90 = priya-x1 by default (synthetic-data.md Q1).
P3Theme sourceThe layer's tokens.json through a generator in design scripts/; the kit theme is never hand-edited.
P4The Rust layercrates/layer, with the web components' names.
P5Fleet → personalThe six rules; Harness detail merges with Harness install detail.
P6The windowThe crawled presets; 30 days by default.
P7CapturesHeadless, through VisualTestAppContext; in the atlas with surface: native.
P8ReviewThree stops in the core, one after each stretch.
P9The kit=0.9.1 for the phase.
P10CommitsIn qnative per step, at each review stop on Mark's word (qnative has no remote; CLAUDE.md: commit only when asked).

Questions (answered by Mark, 2026-11-10)

  1. How deep does the Qpoint skin go? Our layer in Rust. crates/layer carries our component names on top of the kit and draws where the kit has nothing. c130 D5 and c131 R8 still hold for V1 until Mark rules on the scorecard. The phase is the evidence for that ruling.
  2. Fleet → personal? Translate. The six rules apply, and Harness detail merges with Harness install detail (P5).
  3. The Tour stretch? The engine, a new Inventory tour, Info View, and the MCP guide. The qpoint-guide MCP driving the native app over an in-process loopback server is in scope, not optional (see the stretch).
  4. Review? Three stops in the core (the skin, Harnesses, the section with the scorecard), plus one after each stretch (P8).

Risks

  • The HTML-shaped parts eat the phase: subgrid, table layout, sticky, animated height. Each is timeboxed (half a day) and logged as "not reached" rather than chased.
  • Kit churn: a release a week. The pin holds; a migration hour is expected at the phase's end.
  • The budget: images, syntect and the theme grow the binary. Measured before and after (R7).
  • Swapping :7882 for captures interrupts other sessions on :3030. The swap is announced and kept short.
  • Accessibility: drawn elements need AccessKit names (the spike's cell_label pattern), or the headless tests can't find them.

Qpoint Brand Style Guide