plan.md
c131 plan: Native App V1 (draft, before kick-off)
Gate: c130 G1 (D1–D10 ruled 2026-11-09; shape.md to accept) plus the repo (created 2026-11-09 at ../qnative, provisional directory name) and the name (c0 there). Phase 0 may run now beside the primer (Mark, 2026-11-09).
Reader: the executor who builds the app, then the engineers who receive it. Read c130's research/ and shape.md first once they exist; this plan assumes them.
Outcome assumed: B (we build V1; c130 D3). If G2 rules A, this plan is rewritten as "attach at the UI layer" and Phases 1–3 become a patterns-and-fixtures hand-off only.
Why
The sensor can't carry the web dashboard (c130 Why). A native app on the endpoint shows what qcontrol found and how it is doing, without a browser or a bundled web stack. Tyler wants the first shippable release to be this app plus the tray. Mark wants our V1 built first: a simple port of the pages we already have, every page running on synthetic data and ready for real data, the patterns shown by implementing them.
What exists to port
- Pages in
../../../qcontrol/crates/qdash/ui/app/pages:inventory/(agents, harnesses, mcp-servers, models, providers, skills, tools, workspaces),live.vue,activity/,overview/,organization/(endpoints, teams, users),integrations/,policies/,findings.vue,settings.vue. The native app takes the one-machine slice of these. - The data-interface bible (
ui/docs/data-interface.md):Transport(http(),local()), R13 types written from the backend handler, R15 primitives know transport not resources, R23 every number served / derived / staged, R25 staged state swaps transports in one line, R31 honest states (c108). These become Rust traits and rules. - The API (
../../../qcontrol/crates/qdash/src/web.rs): 38/api/*routes, among theminventory,harnesses,mcp-servers,skills,tools,sessions,workspaces,models,providers,stats,performance,events/stream. SQLite behind it (db.rs). - The tray (
../../../qcontrol/crates/qtray):tray.rs,menu.rs,model.rs,dashboard.rs;tray-icon+tao+ AppKit; linksqevents,qconfig,qtap. A passive viewer with click-to-tap. macOS only. - Fixture worlds: the Meridian seed (
scripts/qdash-seed/generate.py) and c113's baked demo worlds (R2 bucketqdash-demo-worlds). One of these becomes the synthetic store. - Self-update:
../../../qcontrol/crates/qupdate. - The framework: GPUI Kit 0.9.1 (
gpui-kit = "0.9.1"):application(),init(cx),open_window→Root; gpui-component's tables, lists, dock panels, charts; semantic theme tokens; headless tests (gpui_kit::test); AccessKit. Facts in c130plan.md§Facts.
The shape (from c130, to be confirmed by shape.md)
┌ tray ─────────────┐ ┌ app window ──────────────────────────────────────┐
│ ● qcontrol on │ │ Status Inventory Events Config Connect │
│ connected: HQ │ open │ ─────────────────────────────────────────────────│
│ 3 harnesses │ ─────▶ │ page │
│ taps ▸ │ │ │
│ Open app… │ │ footer: data: synthetic · local file · v0.1.0 │
└───────────────────┘ └──────────────────────────────────────────────────┘
Two states: local (reads the file system and SQLite; Config is editable) and connected (reads the control plane; Config is read-only; the app says "You're connected to HQ. The configuration is managed there. Here is what was found on this machine.").
Rules
- R1. Every number is served, derived or synthetic; synthetic flows through the real pipes. No view fabricates a value. The footer states the source (c108's honesty, bible R23).
- R2. Views know nothing about the source. A page asks a repository for
Harnesses; the repository asks theDataNozzle; the nozzle picks the transport among three sources,synthetic,localandplane(ruled 2026-11-09). One switch for the whole process, never a per-page flag. Amended 2026-11-11 (c138 ruling 3): on the app's Sensor page the switch has two positions, synthetic or the sensor (this machine or an address); the plane is a connection, not a data choice, and theplanetransport stays in the code. In developer mode the switch can be changed while the app runs. - R3. One type per served shape, written from the backend handler, in a
typescrate the app, the tray and (later) the control-plane client share. The TypeScripttypes/api/*are the existing contract; a check compares the two until one generates the other. - R4. Read-only when connected. Edits exist only in the local state, behind one guard.
- R5. Honest states. Loading, empty, error and "not on this machine" are drawn, never a blank. Carried from c108.
- R6. The demo is the test. Every page has a story per state (empty, minimal, typical, overflow, error) and a headless test that renders it. The stories crate is the app's design site.
- R7. The budget is measured. RSS idle, CPU idle, startup and binary size recorded per release against the numbers Tyler sets (c130 Q8), with strobe (anomalyco/strobe: an agent-native CLI and MCP server that measures any process on three OSes; Mark, 2026-11-09), so an agent can reproduce the figures.
- R8. Native before custom. A kit component before a drawn one; a drawn one only when the kit has nothing and the data would otherwise be shown wrong. Amended 2026-11-11 (D18): the kit for the shell; in the content area our layer is drawn on GPUI with our tokens, as c2's scorecard showed it had to be.
Decisions (defaults, for Mark at kick-off; Q-items for Tyler at G2)
| # | Question | Default |
|---|---|---|
| D1 | Repo and name | ../qnative, created 2026-11-09 as a Cargo workspace (provisional directory name; c0 "Call my name" there picks the real one with Tyler). Crates: app, nozzle, types, fixtures, stories, messages. |
| D2 | Kit layer | gpui-component (styled) with Qpoint semantic tokens; gpui-base only if a component is missing. |
| D3 | The switch | `--data synthetic |
| D4 | The synthetic store | One fixture world exported from the Meridian seed to JSON, bundled in the binary and overridable from a standard path ($XDG_DATA_HOME/qpoint/synthetic/ or the platform equivalent). The world is the same one the web demo uses, so the two apps agree. |
| D5 | The real sources in V1 | local: the local qdash API over its socket first (the 38 routes exist), SQLite directly only if Tyler prefers (c130 Q3). plane: a base URL plus a standardised credential (the connect token), designed with c134's identity decision and c136's grants; in the spike it points at a fixture plane (a small local server over the same JSON world), so the third source is real before the control plane exists. |
| D6 | Pages and order | Status → Inventory → Events → Config → Connect, one at a time with a review stop each. Mark's "Live view" is Events' live stream (/api/events/stream). Amended 2026-11-11 (c138 ruling 2): build both. Live is the raw firehose, in developer mode; Events is the curated layer of meaning on the same stream, in every build. Both are built in c138. |
| D7 | The tray | Absorbed into the app process (one binary, tray-only mode when the window is closed), porting qtray's model and menu. Falls back to "qtray stays and opens the app" if the event loops don't coexist (Phase 0 finds out). |
| D8 | Notifications and messages | The message channel's ports and bones ship in V1 (ruled 2026-11-09; c137 designs them): the schema in types, the inbox store, the OS notification plumbing with per-type controls, the tray count. Only Connect confirmations flow in V1; more is Tyler's at G2. The Root's notification host is used for in-app toasts. |
| D9 | qmap | A link to the control plane's qmap. No embed, no local secondary web app. |
| D10 | Accessibility | AccessKit names and roles on every control; keyboard reachability checked per page. |
| D11 | Hand-off form | The repo plus PATTERNS.md (how a page is built, the nozzle contract, the types rule, the stories), the fixture world, CI for three OSes, and a recorded walk. |
| D12 | Grants on the Status page | When a mechanic-access grant (c136) is active on the plane this endpoint reports to, Status shows one line; otherwise nothing. Read-only here. |
| D13 | The plane connection | Base URL, the standardised credential, how the app finds its plane (configuration, Connect, a deep link), what is read-only when connected. Specified with c134; the words come from c0. |
| D14 | Type | Inter for the UI and Geist Mono for code, bundled (SIL OFL), not the system font. Ruled 2026-11-09; Mark: note it for the degrees of expression the brand type gives us. c132's token bridge carries the type scale. |
| D15 | Synthetic worlds (ruled 2026-11-11) | The default world is a quiet laptop: meridian-day90 recut to nina-mbp (confirmed); priya-x1 at day 90 stays as pilot, the world the violation tour switches to (three steps: switch world, simulate a bad event, watch the local notices and tune the controls; a drill action on the synthetic source injecting the paste scare, the secret-file read or the incident, as one tour or three; c137's bones). The lab reads the same worlds (c132 Phase 0, D6 there). Detail: synthetic-data.md §Rulings. |
| D16 | Choosing a world (ruled 2026-11-11) | In the app's settings, shown while the source is synthetic; the flag and QPOINT_WORLD stay for captures. One re-open path: replace the RepoHandle global, bump a generation, pages observe and reload; Connect reuses it to move from local to plane (D13). |
| D17 | The six endpoint-only shapes (ruled 2026-11-11) | SensorState, Resources, EffectiveConfig, LogLine, ConfigFile, PlaneConnection become qdash routes (/api/sensor, /api/config, /api/plane), typed in TypeScript and generated into Rust like the other 38, under the strict drift test. Not hand-written, not imported from qcontrol's internal structs. The ask to qcontrol engineering: c134's API document, the plane engineer's list. |
| D18 | Brand on the kit (ruled 2026-11-11; amends c130 D5 and R8) | Our layer drawn on GPUI with our tokens; the kit keeps the shell (window chrome, title bar, sidebar frame, dialogs, menus). Ratifies qnative c2's scorecard (30 of 36 drawn, none composed from the kit). |
| D19 | c2 review stop 3 (ruled 2026-11-11) | No seed restart for side-by-sides: the measured specs stand. The extra stretch's four remaining items are done now (sticky header; keyboard and AccessKit; content-sized columns and height re-tracked; quiet nested rows and the new-row flash). The five page defaults accepted (git captured locally for workspaces; the real SKILL.md bytes shown locally; one provider per model; OAuth on an MCP server kept fleet-wide; skill runtime ledgers wait for data). The atlases are released when the window closes to the tray. |
| D20 | The Tour stretch (ruled 2026-11-11) | The launcher lives in a Getting started / Help section that gathers the tours as the list grows, not on the pages. The panel floats, locked to the window's right edge, never docked: resizing reveals what it covers. The guide's MCP endpoint is on, with a per-launch token. Keys while the panel has focus: arrows move, Esc closes, the mouse still works. The real-harness run is the c2 session's, once Mark refreshes the CLI login. |
| D21 | Status, Sensor, Activity (ruled 2026-11-11, from c132's Status brief S1–S6) | Status is the landing: the app opens on it, with the state line, the found-here set (sessions, agents, events, tokens and spend as micros; the harnesses here; the oldest event; the database size; the coverage counters) and the Inventory index's micros; Inventory keeps its own index. A Sensor page shows the five sensor facts in full (SensorState, Resources, EffectiveConfig, the log tail, PlaneConnection), each row honest "not served yet" until its D17 route lands; the log viewer itself is Developer Mode's (c138). An Activity page shows sessions over time as qdash's /monitor/activity does (the plane app's word). All three under a "This machine" group (working name until c0), then Inventory. Process states come from the source and the data, with a Dev switch for stories; the lab's knob is chrome. Built by qnative c5: Status, then Sensor, then Activity. |
| D22 | The two states, said and read (G1, 2026-11-11) | Local: "This machine runs on its own. The configuration lives here and you can change it. Here is what was found." Connected: "You're connected to HQ. The configuration is managed there. Here is what was found on this machine." (HQ is the plane's sender label). The nozzle's connected rule: the plane for what it owns (configuration, messages, grants, the fleet's view of this endpoint), this machine for what only it sees (the live stream, qcontrol's process figures); Status and Events read both, Config, Connect and the inbox read the plane. Budget: shape.md §6's V1 targets are the working targets until G2 (tray idle under 25 MB; window idle under 80 MB per display; after a full walk under 128 MB and back within a minute; idle CPU under 0.01%; first render under 400 ms warm; lean binary under 32 MB; nothing resident when quit). |
Phases
0. Spike (may start during c130 Phase 1, on Mark's word)
A hello app that answers the platform questions before anything is promised.
gpui-kit = "0.9.1", one window, a sidebar and a data table bound to a JSON inventory.- A tray icon beside GPUI's event loop (macOS first), with a menu that opens the window.
- Measured with strobe: RSS idle, CPU idle, startup, binary size, on Mark's Mac; the numbers written into
research/platform.mdand the repo's README. - A
planetransport stub against a fixture plane (a tiny local server over the same JSON world), so all three sources exist from the first commit. - Windows and Linux builds attempted in CI (GitHub Actions), results recorded even when they fail.
- Written back to c130
research/platform.md.
1. Skeleton
- The workspace (D1); the
typescrate written fromweb.rshandlers (R3) with the TS comparison check. nozzle: theDataNozzletrait,Synthetic(bundled world, D4),Local(qdash socket, D5) andPlane(base URL plus credential, D5, D13) transports, the switch (D3), repositories per resource with the same return protocol (bible R25).messages: c137's bones (the schema intypes), the inbox store, the OS notification plumbing and the per-type controls (D8).- The shell: sidebar, title, status footer (source, state, version), theme tokens (c132's bridge when it exists; the kit's defaults until then).
stories: the harness that renders a page in a state, as a headless test (R6).
2. Pages, one at a time (review stop after each)
| Page | Source today | One-machine slice | States to story |
|---|---|---|---|
| Status | overview, /api/stats, /api/performance, qcontrol logs and effective config | on / off, local / connected, CPU and RAM, workers, plugins, logs, effective config | off, starting, on-local, on-connected, degraded |
| Inventory | inventory/*, /api/harnesses etc. | harnesses, MCP servers, skills, tools, sessions and workspaces, models and providers seen here | empty (fresh install), minimal, typical, overflow (hundreds of sessions) |
| Events | live.vue, activity/, /api/events/stream | the live stream and the audit log, with the detail sidebar (JsonTree) | idle, streaming, paused, filtered to nothing, error |
| Config | settings.vue, qconfig | the file found, the effective result, edit locally | local editable, connected read-only, invalid file |
| Connect | new (Tyler's idea) | sign in, pick the control plane, confirm, connected | not connected, signing in, connected, failed |
3. The tray
Port qtray's model, menu and click-to-tap into the app (D7), on three OSes; the tray reads the same nozzle.
4. Packaging
Bundles for macOS (signed, notarized), Windows (signed) and Linux (formats per Tyler, c130 Q7); qupdate wired; CI builds and uploads artifacts; the budget recorded (R7).
5. Hand-off (G3)
PATTERNS.md, the fixture world, the stories, CI, a recorded walk, and the pitch deck's demo. Presented to the engineers as option B.
Clarifying questions
Name and repo (c130 Q1, Q2).Repo:../qnative(2026-11-09). Name: c0 there, with Tyler.- What the app reads locally: socket API, SQLite, or both (c130 Q3)?
- Does the tray move into the app, or stay as qtray (D7, c130 Q4)?
- Budget numbers (c130 Q8) and platform order (c130 Q9).
Fonts?Answered 2026-11-09: the brand type for now, Inter, bundled on three OSes (Geist Mono for code, per the brand's typography). Mark wants the decision noted because of all the degrees of expression the brand type gives us. Inter is under the SIL Open Font License, so bundling is allowed; how the kit loads a bundled font is a Phase 0 check.- Is Connect in V1 even without the control plane side, as a mocked flow on the synthetic transport?
The six endpoint-only shapes (SensorState, Resources, EffectiveConfig, LogLine, ConfigFile, PlaneConnection): new qdash routes typed in TypeScript and generated like the other 38, or hand-written inAnswered 2026-11-11: qdash routes (D17); synthetic-data.md §The six endpoint-only shapes.typesfor V1?
Patterns to consider
- Generate the types from the Rust handlers, then generate the TypeScript from the Rust, so the control plane, the web app and the native app share one contract (bible R13 extended).
- Tray-only mode as the default launch (login item), the window on demand; the budget then applies to the tray.
- Stories as the design site: a
storiesbinary that lists every page state, screenshotted bycapture.mts(c132 decides the medium). - The kit's wasm build for reviewing pages in the lab without a Rust toolchain on the reviewer's machine (verify in c130).
- Deep links (
qpoint://connect?…) for the connect flow from the web app.