← Design · c131

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 them inventory, 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; links qevents, 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 bucket qdash-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 c130 plan.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 the DataNozzle; the nozzle picks the transport among three sources, synthetic, local and plane (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 the plane transport 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 types crate the app, the tray and (later) the control-plane client share. The TypeScript types/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)

#QuestionDefault
D1Repo 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.
D2Kit layergpui-component (styled) with Qpoint semantic tokens; gpui-base only if a component is missing.
D3The switch`--data synthetic
D4The synthetic storeOne 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.
D5The real sources in V1local: 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.
D6Pages and orderStatus → 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.
D7The trayAbsorbed 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).
D8Notifications and messagesThe 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.
D9qmapA link to the control plane's qmap. No embed, no local secondary web app.
D10AccessibilityAccessKit names and roles on every control; keyboard reachability checked per page.
D11Hand-off formThe 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.
D12Grants on the Status pageWhen a mechanic-access grant (c136) is active on the plane this endpoint reports to, Status shows one line; otherwise nothing. Read-only here.
D13The plane connectionBase 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.
D14TypeInter 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.
D15Synthetic 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.
D16Choosing 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).
D17The 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.
D18Brand 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).
D19c2 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.
D20The 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.
D21Status, 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.
D22The 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.md and the repo's README.
  • A plane transport 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 types crate written from web.rs handlers (R3) with the TS comparison check.
  • nozzle: the DataNozzle trait, Synthetic (bundled world, D4), Local (qdash socket, D5) and Plane (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 in types), 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)

PageSource todayOne-machine sliceStates to story
Statusoverview, /api/stats, /api/performance, qcontrol logs and effective configon / off, local / connected, CPU and RAM, workers, plugins, logs, effective configoff, starting, on-local, on-connected, degraded
Inventoryinventory/*, /api/harnesses etc.harnesses, MCP servers, skills, tools, sessions and workspaces, models and providers seen hereempty (fresh install), minimal, typical, overflow (hundreds of sessions)
Eventslive.vue, activity/, /api/events/streamthe live stream and the audit log, with the detail sidebar (JsonTree)idle, streaming, paused, filtered to nothing, error
Configsettings.vue, qconfigthe file found, the effective result, edit locallylocal editable, connected read-only, invalid file
Connectnew (Tyler's idea)sign in, pick the control plane, confirm, connectednot 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

  1. Name and repo (c130 Q1, Q2). Repo: ../qnative (2026-11-09). Name: c0 there, with Tyler.
  2. What the app reads locally: socket API, SQLite, or both (c130 Q3)?
  3. Does the tray move into the app, or stay as qtray (D7, c130 Q4)?
  4. Budget numbers (c130 Q8) and platform order (c130 Q9).
  5. 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.
  6. Is Connect in V1 even without the control plane side, as a mocked flow on the synthetic transport?
  7. 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 in types for V1? Answered 2026-11-11: qdash routes (D17); synthetic-data.md §The six endpoint-only shapes.

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 stories binary that lists every page state, screenshotted by capture.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.

Qpoint Brand Style Guide