Skip to content

feat(lab): Embedded views — Frame surface live-bound to the lab, an A2UI release demo - #261

Merged
fts-forge[bot] merged 95 commits into
mainfrom
feat/sta-653-embedded-views
Nov 4, 2026
Merged

fts-forge[bot] merged 95 commits into
mainfrom
feat/sta-653-embedded-views

Conversation

@fts-forge

@fts-forge fts-forge Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

STA-653: the lab's Embedded views gallery. Following the Captain's change of focus, the work is in Frame and A2UI; ImGui is parked.

  • Recall: ⌘3, the palette's "Embedded views" row, or WORKSPACES_LOAD {name: "Embedded views"} over MCP/CLI. The layout is three columns: Dioxus over Luau ImGui, then Frame, then A2UI.
  • Dioxus: a small component in the lab's own document, unchanged.
  • Luau ImGui: the gallery's standalone Luau window script, unchanged. The ImGui demo-window commit is reverted (no imgui-demo extension, no CANVAS_IMGUI_DEMO). The metronome extension has no ImGui window, so there was nothing to open there; a metronome ImGui window can be a follow-up.
  • Frame: Frame's frame.surface, drawn by Frame's frame-surface process (feat(frame-view): the frame.surface view kind — a minimal provider (architect#224) frame#31).
    • Document: the surface comes from apps/lab/surfaces/drive.frame, a knob on surface/drive and a meter on the feed level. ARCHITECT_LAB_FRAME_DOCUMENT overrides it.
    • Hot reload: frame-surface watches the document and reloads it on save.
    • Live binding (extensions::frame_binding, 150 ms):
      • Loads the document (FRAME_SURFACE_LOAD) and pushes the drive (FRAME_SURFACE_PARAM_SET).
      • Adopts a dragged knob from FRAME_SURFACE_STATE and feeds the meter level = drive·66 − 60 dB (FRAME_SURFACE_FEED).
    • MCP / CLI: LAB_FRAME_SET {drive} moves the drive and LAB_FRAME_STATE reads drive, level, who set it, the document, its load count and any error. The FRAME_SURFACE_* actions are on the list too.
    • Without frame-surface: if it is missing, a stand-in draws the knob and meter. Architect still does not depend on Frame.
  • A2UI: LAB_A2UI_DEMO renders the release surface (apps/lab/ui/src/release.a2ui.json) as the agent demo-agent. The pane shows it on first open.
    • Form: version (TextField) and channel (ChoicePicker).
    • List: a task list of CheckBoxes.
    • Ship it: the button runs LAB_RELEASE_SHIP (Mutating), so the press waits for the person's yes in the confirmation sheet. The answer is then written back at /shipped.
    • Live patching: for example A2UI_PATCH {messages: [{updateDataModel: {surfaceId: "release", path: "/tasks/2", value: {title: "Notarize the build", done: false}}}]}.
  • architect-a2ui fix: a button's wantResponse and responsePath, as the catalog spells them, were silently ignored, because a2ui-base 0.3.1 reads only want_response and response_path. events::resolve now accepts the catalog's spelling.
  • Canvas fix (unchanged from before): a script canvas draws one more frame after a pointer release.
  • Guide scenario apps/lab/scenarios/embedded-views.styx, 9 steps:
    1. vault
    2. load
    3. dioxus
    4. luau
    5. frame: LAB_FRAME_SET {drive 0.9}, checking the level is −0.6 dB
    6. a2ui-render
    7. a2ui-patch
    8. a2ui-ship
    9. a2ui-confirm, checking v0.11.0 shipped to beta; 2 tasks still open.

Stack: merge FastTrackStudios/frame#31 first, then this PR. There is no build dependency between them.

Checks (local, once, at 41610ca):

  • cargo fmt --all: 0, no diff.
  • cargo clippy --workspace --all-targets: 0, no warnings.
  • cargo test -p architect-a2ui -p architect-lab-ui: all passed. The lab's 13 include the new embedded::tests::the_ship_button_ships_what_the_form_says, which renders the demo twice, resolves the button and runs LAB_RELEASE_SHIP through the list.
  • cargo run -p architect-lab-media -- embedded-views --test, with ARCHITECT_LAB_FRAME_SURFACE set to frame#31's frame-surface: ok embedded-views (9 steps), exit 0.
  • The GIF rendered locally: 238 frames, real Frame surface, release shipped after confirmation.
  • Skipped: full CI locally. The media branch GIF updates when just media-publish is run.

GIF: https://raw.githubusercontent.com/FastTrackStudios/architect/media/embedded-views.gif

🤖 Generated with Claude Code

fts-forge Bot and others added 2 commits November 2, 2026 02:12
…ace, A2UI

The lab's Embedded views screenset (⌘3, WORKSPACES_LOAD "Embedded views"):
one pane per kind, each naming it — a Dioxus component, a standalone Luau
ImGui script, Frame's frame.surface (frame-surface when found, a stand-in
otherwise; LAB_FRAME_SET / LAB_FRAME_STATE) and the A2UI agent surface
(LAB_A2UI_DATA reads a surface's data). Guide scenario embedded-views
uses each pane once. STA-653.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The Embedded views screenset's ImGui pane is now architect's
ShowDemoWindow: a lab extension's Luau script
(apps/lab/extensions/imgui-demo) with the sections of Dear ImGui's
imgui_demo.cpp (MIT; code and words our own), each drawing what
architect's ImGui can and naming in a disabled "not implemented" line
the API it lacks, looked up in the module as it runs. With no
scrolling, sections drill in. CANVAS_IMGUI_DEMO brings it up from
anywhere (Vault, screenset, pane focused) for the CLI and MCP.

The demo takes the left column's full height; Dioxus, Frame and A2UI
stack on the right. A script canvas now draws one more frame after a
release, so what a click changed shows without waiting for input.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The ImGui pane goes back to the Luau script's window; the effort is in
the Frame and A2UI panes.

- Frame: the pane loads apps/lab/surfaces/drive.frame into the real
  frame-surface extension, reloads it on save, and binds its knob to the
  lab's drive with the meter following (LAB_FRAME_SET, LAB_FRAME_STATE,
  over the extension's FRAME_SURFACE_* actions).
- A2UI: LAB_A2UI_DEMO renders a release surface as an agent: a form, a
  task list, and a Ship it button whose LAB_RELEASE_SHIP waits for the
  person's yes and writes its answer back. A2UI_PATCH patches it live.
- architect-a2ui: a button's wantResponse/responsePath, as the catalog
  spells them, are honoured (a2ui-base reads only the snake_case names).
- The embedded-views guide covers all of it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@fts-forge fts-forge Bot changed the title feat(lab): an Embedded views gallery — Dioxus, Luau ImGui, Frame surface, A2UI feat(lab): Embedded views — Frame surface live-bound to the lab, an A2UI release demo Nov 2, 2026
fts-forge Bot added 20 commits November 2, 2026 13:19
Use immutable integration checkpoints until the upstream dependencies ship as tags.
…el position

Work in progress, saved so it is not lost. Lab: Keyflow editor and chart panes,
the Chart screenset, New Chart command, Keyflow actions (state, focus, set source,
set cursor, chart view, chart theme), Embedded rail entry. Shell: full-width title
and status bars on the desk, page tabs in the title row. Workspace: ViewInput::Wheel
carries position and modifiers; View::headless hides a pane header.

Cargo.lock and local path overrides are not committed; the lab builds against
local checkouts through an uncommitted --config file.
…s only its colour

The lab's override was written for the caret-as-a-mark experiment
(editor f11406a, not taken): it forced an inline-block 2px box that fought
the editor's zero-width caret. The lab now sets --editor-caret only.
…bf4bc)

The IME commit (ba4ac02) sat on a base without v0.7.1's dioxus-vdom
suspended-boundary fix; 25bf4bc is the same change on the tag, so
dioxus-native, -dom and -vdom all come from v0.7.1's line.
…s branch

The Keyflow lab feature built only through local overrides: Keyflow
4605563 predates the wheel position in ViewInput::Wheel. keyflow#48 pins
this branch, editor#22 and the Blitz/vendor IME commits, so
`cargo check -p architect-desktop --features keyflow-lab` builds as is.
…es, THEME_* actions

A theme is a small palette plus overrides, resolved in OKLab into every
semantic token (architect-ui's, the shell's, the signals); a ThemeStore
composes the layers (builtin, app, org, person, device, workspace, pick,
preview) and broadcasts once per real change. Theme files are strict
JSON (comments allowed) with last-good reload, a notify watcher and the
shipped-upgrade rule. architect-ui's presets are now the built-in theme
files; ThemeStoreProvider follows a store.
…s follow a theme store

ShellTheme's colours are owned (Arc<str>) so they can change at run time;
ShellTheme::from_resolved builds them from a resolved theme, and AppShell
follows the app's store when given no theme. WorkspaceView's theme
defaults to the shell's. PaintPane gets the whole resolved theme as
ThemeTokens (From<&Resolved>); without a store, shell_tokens names the
accent trio brand/brand-ink/brand-soft. Icon.color is an Arc<str>.
Extensions may declare [[themes]]. The lab keeps one store: its shipped
themes in <app dir>/themes (watched), THEME_* on its Action List, a
⌘K picker, the OS's light/dark from the window.
…t, process views

Seen in the lab on a switch to a light theme:
- the workspace host kept its colours in a CopyValue, so its panes, tab
  bars and docks stayed on the theme they were first drawn with; it is a
  Signal now (set only when it changes);
- architect-ui's ThemeStoreProvider also writes its variables on :root (an
  app-level sheet), and hands them down as architect_theme::RootCss;
- a process extension's view is a Blitz sub-document that never sees the
  host's sheets: RemoteDocument::set_theme_css puts the host's :root sheet
  in its head, set when the view mounts and on every change, so its
  var(--…) follow the app.
…s, provenance, THEME_INSPECT

A domain app (Studio) can now put namespaced tokens on top of architect's
semantic ones and have them follow every palette:

- Token names are dot-separated `[a-z0-9-]` segments (`arrange.item.edge`)
  in theme files, layers, `Theme::from_tokens` and `THEME_TOKEN_SET`.
- Values may be expressions (`$name`, `mix(a, b, t)`, `alpha(a, t)`, nested;
  `features/theme/src/expr.rs`), evaluated after derivation and every
  layer, so a later literal wins and a reference sees the final value.
  An unknown name, a cycle, a non-colour or a t outside 0..1 is magenta
  plus a `Problem` naming the token (`ThemeStore::problems`,
  `THEME_PROBLEMS`).
- `Resolved::number`, `source` (`TokenSource`, `Origin`), `misses`,
  `problems`, `css_tokens`, `css_namespaced`; `Projected::revision`.
- `ThemeStore::revision` is an atomic; `insert_theme` / `remove_theme`
  with `Source::Memory` themes kept across a reload.
- `THEME_INSPECT {prefix?}`; `THEME_TOKEN_SET` / `_RESET` take `layer`.
- architect-ui's root CSS writes only undotted tokens;
  `ThemeStoreProvider { css_namespaces }` opts an app's in.

Breaking (pre-1.0): `Resolved::with_overrides` removed, `Composed` gains
`chosen_by` / `set_by`, `Source::Memory` added. Cargo.lock gains the
architect-extensions -> architect-theme edge that HEAD already had in its
manifest.
…pins it

A typed slice of the theme (ChartColours: paper, ink, staff from card,
card-foreground and muted-foreground) drives the chart's ChartTheme, so
the paint view repaints only when those colours change.
KEYFLOW_SET_CHART_THEME still chooses light or dark, and from then on the
chart keeps that choice.
fts-forge Bot added 4 commits November 3, 2026 14:38
…s, in and out as a diff

ThemeStore::contribute / withdraw / contribution keep each owner's
themes and token defaults beside the app's registry: a reload is a diff,
a broken file keeps the owner's last good theme and is a problem, an id
or token someone else has is left out and said. The tokens are a new
LayerKind::Extension, under App. THEME_LIST says extension:<id>
(Source::label); Rgba::packed gives ImGui's 0xRRGGBBAA.
…s theme store

Host::with_themes(store): an extension's [[themes]] files and its new
[tokens] table (its own namespace, Manifest::token_namespace, checked
like a theme's overrides) are contributed as it loads, diffed on a
reload (a dev copy's save shows at once), withdrawn as it unloads. A file
that does not read is a THEME_PROBLEMS entry, never a failed load
(host::theme_contribution).
…revision

architect_scripting::theme::ThemeModule over a ThemeSource (the app's
ThemeStore, or a ThemeCell a host sets): theme.color (0xRRGGBBAA),
value, number, scheme, id, revision, snapshot() (the same read-only
table until the revision moves) and subscribe(fn) for live scripts.
Module gains frame() (called as each live frame starts) and
live_fields() (a mutable global, so Luau never folds theme.scheme at
load).
… restyled on every switch

imgui::Style::from_theme maps each style colour (imgui::Col, Dear
ImGui's ImGuiCol_ values) to a theme token (imgui.button, …) or, when
the theme has none, a derivation from the shell's ($sheet, $pill,
mix($pill, $brand, 0.18), …). luau::Session::start_themed / set_theme
restyle the windows and move the script's theme (the scripting
ThemeModule over a ThemeCell). Scripts override one colour with
ImGui.PushStyleColor / PopStyleColor and ImGui.Col_*. use_script_canvas
follows the theme store above it: a theme change restyles and draws a
frame, so a Luau ImGui pane no longer keeps its first colours.
fts-forge Bot added 28 commits November 3, 2026 18:37
… own thread; busy cleared on unmount; an overdue event noticed as it is asked

Live::last_frame_time is the script's time on its thread, what
Session::frame_metered records (the host's own wait no longer counts as
the view's). A pane unmounted mid-frame clears its meter's busy state.
RemoteView looks again at the oldest unanswered event whenever one is
asked, so a view blocked by its newest event goes busy.
… a naughty view marked busy, a process view using use_work

heavy.luau counts primes below three million in its [[workers]] primes
worker with a live progress bar and a spinner that keeps turning;
naughty.luau spins 300 ms a frame on its own thread and is marked busy;
the process view counts primes with use_work and can block its own view
thread for two seconds. The lab provides one Meters (EXT_VIEWS, forwarded)
and logs slow or busy views through its ViewReport.
… runner, budgets and the busy mark; WASI's mapping; change files
The two compile errors of the merge: Luau's action copy carries its
WorkerGroup (the group is shared, so a stop cancels the action's workers
too), and a refused worker manifest names its field in the check. The
guide test names EXT_VIEWS, #274's meter action.
Doc paragraphs split (every word kept), a similar binding renamed, a
write! for a push of format!, asserts and indexing in the APP_SHOT test
helpers replaced by checked and get-based code, redundant clones removed.
No behaviour change, no new allow attributes.
…w edits, page files, generated architect.d.ts/.d.luau, TSX by oxc
…thread, frames as edits, reload on save keeping the last good page, a per-call interrupt, workers, the host in process
…cument, timed and marked busy; ViewPages so an override file or a view's page draws an extension's view live; [[views]] page
…read-only built-in under the lab's folder; overrides in <lab dir>/pages; PAGES_STATUS/CHECK/CLONE; the page types written at start
… clone-to-edit brings the page's files; a page in a shell slot has the bar's height
…he same edit path, the theme read through; the lab runs them
…ted types, the runners, safety and performance; boundaries rows; change file
The guide test names PAGES_STATUS, PAGES_CHECK and PAGES_CLONE, the pages
actions on the lab's list.
…one generated view API, live reload with last-good (#275)

Stacked on #274. Implements #273 (first phase): a page is an editable
file, re-rendered live on save, as in herdr's desktop app.

- **Fix (from #274):** a script's action calls run on the app's Tokio
runtime and the script awaits the answer over a channel
(\`features/scripting/src/vm.rs\` \`Dispatch\`); \`tests/runtime.rs\`
failed with "no reactor running" before.
- **One API, generated types:** \`features/pages/src/api.rs\`
\`PageHost\` (run, actions, settings, theme, shared state, notify,
subscriptions) and \`PageView\` (render, overlays, events) as
\`#[architect::rpc]\` services; \`architect.d.ts\` from vox's TS codegen
and \`architect.d.luau\` (\`features/pages/src/codegen.rs\`), checked in
under \`features/pages/types/\` with a drift test, written into the
app's folder at start.
- **QuickJS** (\`rquickjs\`): a runtime per page on its own thread; the
window only applies \`wire::Edit\` frames to the same \`RemoteDocument\`
process views use. Limits: 64 MB / 1 MB stack, 500 ms per
render/handler/timer (interrupt: "hand heavy work to a worker"), busy
mark after 16 ms. \`worker.spawn("job.js")\` in its own runtime. Imports
limited to the page's folder, no Node APIs, \`console.log\` to the
page's log, theme snapshot keyed by revision.
- **TSX by oxc:** TS strip and JSX → \`h()\` in about 1 ms with
file:line:col errors; save-to-screen about 150–300 ms (the 200 ms folder
watch dominates).
- **Luau element pages** on the same API.
- **File-backed pages:** \`[[views]] page = "pages/x.tsx"\`; a compiled
view is overridden live by \`<app dir>/pages/<view-id>.tsx|.luau\` and
restored when the file goes; built-in page files read-only with
clone-to-edit (the page's other files copied too). A broken save keeps
the last good page under a banner and writes \`<file>.error\`.
\`PAGES_STATUS\` / \`PAGES_CHECK\` / \`PAGES_CLONE\`.
- **Lab:** the Board's tab is \`apps/lab/board/pages/board.tsx\` (data
via \`useData("LAB_BOARD_STATE")\`, its own actions, theme tokens, a
primes worker).
- **Fix found live:** frames with no input behind them weren't drawn
until the next click — in \`PageView\` and the same gap in
\`RemoteView\`.

Tests: \`architect-pages\` diff (3), js (8: handlers, live reload with
broken-save fallback, runaway stopped, Tokio action, worker, imports/no
Node, theme, check), luau (1), pane (1: override then restore), types
(3), 6 unit; board page (1); scripting, extensions, board \`both_ways\`
pass. clippy clean on \`architect-pages\`; \`xtask docs\` 0; \`xtask
boundaries\` 16; \`just ci-changes\`; \`cargo check -p
architect-desktop\`. Live in a Loft test session: the TSX Board, pin,
worker result, a live edit appearing within 1 s with no input, the
broken-save banner and \`.error\`, a theme switch, a \`status-bar.tsx\`
overriding the compiled bar and the bar returning when deleted.

Not yet: A2UI JSON pages (kind recognised, not run); Luau pages without
workers/timers; runtime errors point at the transpiled JS (no source
maps); \`APP_STATUS\`/\`EXT_CHECK\` seams (\`Pages::problems\`,
\`Pages::check\`) not wired; the stream half of \`PageHost\`; the web
Web Worker runner; phase two (worker-side DOM); a first click with no
prior pointer move was missed once.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
#278)

## What

Makes `cargo clippy --workspace --all-targets` clean (default features,
exit 0, no warnings) and `cargo clippy --workspace --all-targets
--all-features` clean on the extension stack. No behaviour change.

- Cherry-picked the `main` fix (`crdt`: `use_here` doc paragraph split),
from #276.
- `features/workspace`: doc paragraphs split (`ctx`, `home`, `kinds`,
`paint/mod`); `const fn` on `HostData::shape`,
`PaintView::len`/`is_empty`/`invalidate`, and the private `with_wake`;
identical `Ime` commit and `Paste` arms merged; `mul_add` and
`Cell::get` path forms in tests (`W` and `H` are 8.0 and 6.0, so the
products are exact and the results do not change); `chunks_exact(4)` to
`as_chunks::<4>()`; `Default::default()` to `Insets::default()`; `;`
added to block-tail statements; test-only arithmetic
(`drawn.saturating_add(1)`, `checked_add(..).unwrap()`) for
`arithmetic_side_effects`.
- `features/extensions`: ten doc paragraphs split (`contrib`,
`contrib_actions`, `guest`, `interface`, `linked`, `manifest`, `wire`).
Words are kept; a few sentence breaks became paragraph breaks.
- `apps/lab/ui` (feature `keyflow-lab`, all-features only): `args["x"]`
indexing replaced by a `get` helper that returns `null` for a missing
field, as serde_json's index does; `apply` takes `&ChartCommand`; `ok()`
returns the bytes and the closures wrap it in `Ok`; two redundant clones
removed; a doc backtick. `paint.rs`: `stats` renamed to `timing`
(similar names). `vault.rs`: the `mut` is now only where the
`keyflow-lab` push needs it (the unused `mut` was a warning without the
feature).

## Counts

Each pass only shows what the one before let through:
`architect-workspace` failing stops its dependents from being checked.

| Build | Before | After |
| --- | --- | --- |
| default, `--all-targets`, pass 1 | 11 (`architect-workspace` lib) | |
| default, pass 2 | 27 (workspace tests, extensions docs) | |
| default, pass 3 | 12 (workspace tests, `architect-lab-ui`
`paint.rs`/`vault.rs`) | 0 |
| `--all-features`, `--all-targets` | 13 (`architect-lab-ui`
`keyflow.rs`, after default was clean) | 0 |

Totals across the passes: `too_long_first_doc_paragraph` 14 (4
workspace, 10 extensions), `missing_const_for_fn` 5, `match_same_arms`
1, `redundant closure` 1, `arithmetic_side_effects` 2,
`semicolon_if_nothing_returned` 3, `default_trait_access` 2,
`chunks_exact` to `as_chunks` 2, `suboptimal_flops` 7, `similar_names`
2, `unused_mut` 1, plus the `keyflow.rs` set: `redundant_clone` 2,
`doc_markdown` 1, `needless_pass_by_value` 1, `unnecessary_wraps` 1,
`indexing_slicing` 8.

No `#[allow]` added.

## Checks

- `cargo clippy --workspace --all-targets --keep-going` (default): exit
0, 0 diagnostics.
- `cargo clippy --workspace --all-targets --all-features --keep-going`:
exit 0, 0 diagnostics.
- `cargo fmt --all -- --check`: exit 0.
- `cargo xtask docs`: exit 0 (114 pages, 0 names that do not exist).
- `just ci-changes`: exit 0 (`.changes/fixed-clippy-lints-in-70b7.md`,
bump `none`).
- `cargo test -p architect-workspace -p architect-extensions -p
architect-lab-ui`: exit 0.
- `cargo test -p architect-lab-ui --features keyflow-lab`: exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ers, frame budgets with a busy state, guest spawn_work (#274)

Stacked on #271. The rule: **an extension's GUI never waits on its own
heavy work, and the app's UI never waits on any extension.**

- **Luau off the UI thread** (\`features/scripting\`): one-off runs
(actions, hooks) on a growing pool, dropping the future stops the
script; each live script (ImGui window, overlay) its own thread and Luau
state; the UI only awaits channels. The audit's one UI-path wait — Luau
futures polled inline on the UI executor — is gone. Theme snapshots work
across threads.
- **Workers:** \`[[workers]]\` in the manifest (\`script\` or
\`entry\`); Luau
\`worker.spawn/post/cancel/progress/on_message/on_done\`
(\`features/scripting/src/worker.rs\` \`Workers\`, \`WorkerGroup\`),
each worker its own Luau state with its own limits, no UI access;
cancelled with its owner or extension (under 1 s on unload). Process
guests: \`Guest::spawn_work\` / \`Work\` / \`WorkHandle\` (drop cancels)
/ \`use_work\` (\`features/extensions/src/work.rs\`).
- **Budgets and degradation:** \`ViewMeter\`/\`Meters\`
(\`features/workspace/src/meter.rs\`) time every extension view on its
own thread; past budget the pane keeps its last frame with a
theme-coloured \`BusyBadge\` and the rest of the app keeps answering;
\`EXT_VIEWS\` reports p50/p99/slow counts; \`Meters::with_report\` is
the seam for \`APP_STATUS\` (#270). Luau \`Limits\` gain \`frame\` (500
ms) and \`time\`.
- **Lab:** the clicker's primes job in a worker with a live progress
bar; a deliberately naughty view.
- **WASI mapping** (documented): a worker is another instance of the
component on a pool thread; on the web, a Web Worker.

Breaking (pre-1.0): \`Limits\` fields; process wire 5
(\`ANSWERED_WIRE\`): every event is answered with a frame.

Measured: with a 0.66 s job in a worker, 313 view frames at p50 20 µs /
p99 182 µs (the same job a quarter as long inline: one 97 ms frame);
while one view spent 250 ms per frame another drew 6,898 frames within
budget. Live in a Loft test session: busy mark on a naughty view and on
a blocked process pane while other panes update; the progress bar and
frame counter advancing during a job.

Tests: scripting \`tests/workers.rs\` (7) + \`vm.rs\` (3); canvas
\`a_naughty_view_goes_busy_and_the_other_panes_keep_updating\`,
\`a_window_with_a_worker_animates_within_budget_while_the_job_runs\`;
extensions \`tests/workers.rs\` (4, incl.
\`unloading_an_extension_cancels_its_workers\`), guest work (2),
manifest; clicker. fmt, \`xtask docs\`, \`xtask boundaries\` (16),
\`cargo check -p architect-desktop\`, \`just ci-changes\`.

Known: actions a script calls now run on its VM thread, so an action
needing a Tokio runtime context there fails (fix next); \`entry\`
workers not checked at hello; a self-sent process frame is attributed to
the oldest pending event; linked extensions not metered; per-view
budgets not in the manifest; Luau not yet on wasm.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ns manager (#277)

Stacked on #270. #178, #181 (widened into the Extensions manager), #239
X5.

- **Host service:** an extension sends one request (run an app action,
org, selection, open/read a document, read/write a file, its own setting
or data, log) and gets \`{ok}\`/\`{error}\`, the same from a process
(\`guest::host\`), Luau (\`host\`) and in-process (\`Handle::host\`); a
WASI runner maps one import onto it. Each call is checked against the
extension's manifest capabilities narrowed to what a person granted (new
\`capabilities.reads\`: org, selection, documents); a refusal names the
missing capability and is logged. Actions run as \`Principal::Extension
{ id, on_behalf_of }\` (**breaking** in \`architect-permissions\`);
\`perm.*\` on the span (wide-events behind an opt-in feature to keep the
UI build off tower-http).
- **Consent:** install asks first; grants per device or per org in
\`<app dir>/extension-grants.json\`, revocable (next call refused); an
agent asking to install or grant gets \`confirmation_required\` with a
ticket, never a hang; built-in and dev copies run with what they ask.
- **Actions:** \`EXT_PREVIEW\`, \`EXT_INFO\`, \`EXT_UPDATE\` (skips
unchanged), \`EXT_PIN\`, \`EXT_GRANT\`, \`EXT_REVOKE\`, \`EXT_GRANTS\`,
\`EXT_LOGS\`, \`EXT_HOST_CALL\`; \`EXT_INSTALL\` takes
\`grant\`/\`pin\`; \`MANAGER_KINDS\`/\`_LIST\`/\`_GET\`/\`_RUN\`.
- **Extensions manager** (\`features/extensions/src/manager/\`): one
place for everything that customizes an app, a registry of \`ItemKind\`s
(list, get, operations) shipping extensions, Luau scripts and hooks,
workers, themes, layouts and \`keys.json\`; an app adds kinds with
\`Manager::register\` (e.g. Studio's audio plugins). Itself a linked-in
extension (\`manager\`), overridable by a dev copy (#272). UI from
architect-ui and theme variables only: a settings section, a workspace
view, ⌘O \`ext:\`; a project's missing extensions use the same consent
prompt.

Tests (all pass):
\`the_metronome_runs_an_app_action_and_reads_its_setting_through_the_host\`,
\`a_call_outside_its_capabilities_is_refused_and_logged\`,
\`installed_it_runs_with_what_a_person_granted\`,
\`the_page_finds_extensions_by_name_kind_and_what_they_add\`,
\`the_manager_lists_and_acts_on_every_kind\`, the clicker asking over
its connection, a linked extension asking through its handle,
permissions' extension principal tests; 232 across touched crates + 29
action-list. No new clippy lints; \`xtask docs\` 0; \`xtask boundaries\`
16; \`architect-desktop\` builds. Live in a Loft test session: the
metronome's consent prompt and install (grant written), the Board's page
"linked into the app", Revoke and the next call refused at
\`reads.selection\`, \`EXT_HOST_CALL\` refused at \`capability:
actions\`, \`EXT_LOGS\`, \`APP_STATUS\` and \`MANAGER_LIST {kind:
layout}\` listing a broken layout file. Three bugs found live and fixed
(app actions looked up on the wrong list; clicks in a nested scroller; a
clipped overlay prompt).

Left: frame health (#274 \`EXT_VIEWS\`) is a placeholder; QuickJS (#275)
and WASI kinds not registered yet; the confirmation sheet for an agent's
ticket doesn't show the capabilities; per-org grants are a local file,
not synced; the manager view is cramped in a narrow pane (first 12
rows).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…_SHOT, EXT_CHECK, a generated guide, refused unknown args, clone-to-edit, an escape hatch (#270)

Stacked on #267 (alongside #269). #268 items H2, H3, H4, H5 and H7: the
tools herdr's desktop app gives a coding agent to change the running UI
and check its own work.

- **H2 Guide:** \`<app dir>/AGENTS.md\` and \`actions.schema.json\`,
written from the live Action List when the app settles and whenever an
action comes or goes (\`architect_control::guide::Guide\`: the app's own
opening plus architect's rules). A test fails if it misses an action.
- **H3 \`APP_SHOT {path?, pane?}\`:** the main window's Blitz document
painted again on the CPU (\`anyrender_vello_cpu\`) at its pixel size; a
pane crops to its measured rectangle. \`actions shot [file] [--pane
id]\`. Not pop-outs; GPU-only custom widget paint may be missing.
- **H4 \`APP_STATUS\`:** theme, workspace (revision, focus, screenset,
windows), pending confirmations and every problem with source, file,
field and message, from a \`Problems\` registry features report into
(\`crates/architect/src/action_list/app.rs\`); theme files and extension
loads report today.
- **H5 \`EXT_CHECK {folder}\`:** load an extension and render each view
once, or the exact error (\`features/extensions/src/check.rs\`
\`Checker\`); \`architect-desktop extensions check <folder>\` exits
nonzero.
- **H7 Safety:** an argument name an action doesn't take is refused
(\`bad_args\`) listing the valid names (**breaking**: it was ignored);
agents are told to ask, never left waiting; \`WORKSPACES_CLONE\` /
\`WORKSPACES_RESTORE_BUILTINS\`, and a save over a built-in from outside
the app answers \`builtin\` (**breaking**); \`EXT_CLONE\` (refused for
process extensions, whose program answers its old ids);
Ctrl+Alt+Shift+Escape restores the built-in layouts and can't be
rebound.
- Lab: \`LAB_GO {tab}\` so an agent can open a page.

Tests: \`unknown_argument_names_are_refused_with_the_valid_ones\`,
\`status_is_every_section_and_every_problem\`,
\`the_guide_names_every_action(_of_the_lab)\`,
\`the_escape_hatch_cannot_be_rebound\`,
\`a_built_in_set_is_cloned_to_change_it\`,
\`the_escape_hatch_restores_the_built_in_layouts\`,
\`a_shot_is_the_window_at_its_pixel_size\`,
\`the_metronome_checks_clean_and_a_broken_copy_says_exactly_why\`,
\`app_status_lists_a_broken_theme_and_a_broken_extension\`, and more;
existing MCP, theme, workspace, desktop and lab scenario suites pass.
\`xtask docs\`, \`xtask boundaries\` (16, unchanged), \`just
ci-changes\`, \`cargo check -p architect-desktop\`. Clippy clean on new
code (base lints in \`features/workspace\` and
\`extensions/manifest.rs\` remain).

Live check in a Loft test session: \`APP_SHOT\` matched the compositor's
screenshot; a broken theme file showed in \`APP_STATUS\` with file and
field; \`EXT_CHECK\` passed on the clicker and named \`effect\`, line
20, on a broken copy; a CLI save over a built-in was refused and
\`WORKSPACES_CLONE\` worked; the escape hatch put the layout back.

Left of #268: H1 (layouts and keymap as live files, \`.error\`), H6 (the
chrome as views), H8 (view parity) — the next workstream; Luau API type
files in the guide; pop-out shots.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…, in-process extensions, the chrome as slots, the interface as files (#271)

Stacked on #269. Workstream 3 of the extension plan: #179, #183, #239
(X1–X4, X6) and #268 H1 + H6 — extensions (and agents) change the app's
GUI at run time, and nothing in the default interface is special.

- **Contributions (#179):** \`[[openers]]\` by file extension,
\`DocKind\` and URL pattern (Ctrl/Cmd-click routes to a URL opener),
\`[[doc_kinds]]\`, \`[[tabs]]\`, view \`home\`, \`[[status]]\`,
\`[[menus]]\`, \`[[context]]\` (a target's context menu), \`[[bar]]\`
(phone action bar); also at run time from Luau (\`ext\`) and process
guests (wire 4). All withdrawn as a diff on unload (\`Contributions\`
registry).
- **Lifecycle (#239):** \`[[hooks]]\` on startup and app events (an
action or a script function); \`Invocation\` on \`ActionCaller\`
(palette, menu, context with its target, key, hook, agent); view
\`placement\` (tab, split, overlay, popup, zoomed, float) and
\`on_unload\`; status items and badges owned by their source,
time-limited, sequenced, withdrawn on unload.
- **In-process (#183):** \`features/extensions/src/linked.rs\`
\`Extension\` (manifest, files, \`build\`), registered with
\`Extensions::with_linked\` as a built-in, on/off at run time,
overridable by an installed or dev copy; the same value runs as a
process via \`Guest::extension(&ext).serve()\` (\`apps/lab/board\`, test
\`both_ways\`).
- **The chrome as slots (#268 H6):** \`Chrome\` above \`AppShell\`:
rail/ribbon, sidebars, tab bar, status bar and title row are slots a
built-in or extension view fills or replaces; the shell's own components
are the default fillers. A golden recording of every Dioxus mutation of
\`AppShell\` (desk and phone) from the base matches byte for byte.
- **The interface as files (#268 H1):** \`layouts/<name>.json\` (look
through the theme store's workspace layer, per-slot
\`view\`/\`hidden\`/\`tokens\`, optional arrangement) and \`keys.json\`,
watched and live; a broken file keeps the last good one and writes
\`<name>.error\`; hand changes go to \`.state/\`; built-in \`main\` is
read-only. \`HOME_PROBLEMS\` reports them (to be joined to #270's
\`APP_STATUS\`).
- **Lab:** \`apps/lab/board\`, an example contributing a tab, a status
item, a context-menu entry, \`.kf\` and GitHub-issue openers, a startup
hook and the status-bar slot.

Breaking (pre-1.0, in the change file): \`Manifest::openers\` →
\`Manifest::contributes.openers\`; runners get \`contributions\`;
process wire 4 (2–3 still run; an older host refuses wire 4).

Tests: architect lib 57; extensions lib 21 + contributions 2 + linked 2
+ existing 19; shell 22 incl. the golden; workspace 46+32+15 incl.
\`home::\`; commands 21+6; theme; \`architect-lab-board\` \`both_ways\`;
desktop 2. fmt, \`xtask docs\`, \`xtask boundaries\` (16), \`cargo check
-p architect-desktop\`. Live in a Loft test session: the Board's tab,
status slot, context-menu pin, \`.kf\` opener with the \`doc.opened\`
hook, the URL opener, a layout file applied and edited live, a broken
file kept with \`.error\`, \`EXT_DISABLE\` withdrawing everything, and
the Board as a process overriding the built-in.

Left: a unit test for the pane context menu; the left slot's look
doesn't recolour the sidebar ground; sizes/folds not yet in \`.state/\`;
the phone bar and contributed menu rows not seen on screen; a process
view isn't given its pane state.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ributed themes, Luau theme, ImGui styles, the theme as data for process guests (#269)

Stacked on #267. Workstream 1 of the extension plan: an extension and
every GUI it draws follow the app's theme live, and an extension can
bring themes and tokens of its own.

- **Contributed themes and tokens:** an extension's \`[[themes]]\` files
and \`[tokens]\` table (its own namespace, \`clicker.*\`, with \`$ref\`
defaults) go into the app's \`ThemeStore\` on load and leave on unload,
diffed (\`ThemeStore::contribute\` / \`withdraw\`,
\`LayerKind::Extension\`); \`THEME_LIST\` names the source
\`extension:<id>\`; a broken file is a \`THEME_PROBLEMS\` entry, not a
failed load; dev copies hot-reload. A theme file can restyle an
extension's tokens.
- **Luau:** \`theme.color/number/scheme/revision\`, a frozen snapshot
keyed by revision (herdr's \`__theme_revision\`), live fields so a
script sees a switch.
- **ImGui:** host style colours from tokens (\`imgui.<name>\` overrides,
else defaults over \`ink\`, \`sheet\`, \`pill\`, \`brand\`, \`rule\`,
\`chart-1\`), re-applied each revision; scripts can
\`ImGui.PushStyleColor\`.
- **Process guests get the theme as data:** wire 3 adds \`ThemeData\`
(id, scheme, revision, tokens), sent after hello and on every real
change; \`Guest::theme\`, \`on_theme\`, \`use_theme\`. Wire-2 processes
still load (no theme sent). A host older than this refuses a wire-3
process (exact version check).
- **Lab:** the clicker is a themed example — a process view styled by
\`var(--clicker-*)\` and a ramp computed from theme data, a Luau ImGui
window, its own token layer and a contributed "Clicker Neon" theme; the
Luau ImGui pane now follows \`THEME_SET\` (it stayed dark before).

Tests: theme contributions (6), extension themes (4), process theme (on
open and on switch), Luau snapshot (3), ImGui light/dark mapping and a
headless pixel check (3), clicker themed (2); existing suites of touched
crates pass. fmt, \`xtask docs\`, \`xtask boundaries\` (16, unchanged),
\`cargo check -p architect-desktop\`. Clippy: theme and scripting clean;
canvas, extensions, clicker clean but for the known \`kinds.rs\`
\`too_long_first_doc_paragraph\` in the base. Checked in a real lab
window (dark, Catppuccin light, Clicker Neon).

Left: a process extension can't yet ship a Luau window through its
manifest (lab glue runs it); extension tokens reach a process view's CSS
only where the app opts in (\`css_namespaces\`); no \`cargo hakari\` run
for the new dev-dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ew follows, app tokens, THEME_* actions (#267)

Stacked on #266. A runtime theming system modelled on herdr's desktop
app: a theme is a small palette plus overrides; everything else derives;
one revisioned store; every view follows a switch live.

**\`architect-theme\`** (\`features/theme\`, model wasm-clean):
- \`resolve\`: palette → every semantic token architect-ui and the shell
use (OKLab mixes, light/dark branches, ink contrast ≥ 4.5:1); magenta
for an unknown name.
- \`ThemeStore\`: layers builtin → app → org → person → device →
workspace → pick → preview; \`set\` is a no-op when nothing changes;
\`Send + Sync\`, lock-free \`revision()\`; \`insert_theme\` for
in-memory themes.
- App tokens: dotted names (\`arrange.item.edge\`), \`$name\` /
\`mix(a,b,t)\` / \`alpha(a,t)\` values resolved after the cascade
(cycles and unknowns: magenta + a \`Problem\`), \`number()\`, provenance
(\`source\`), \`misses\`.
- \`fs\`: strict JSON theme files, a watcher with last-good reload,
shipped-theme upgrades only over an unedited earlier version. The 20 old
presets are 40 theme files.
- \`actions\`:
\`THEME_LIST/GET/SET/PREVIEW/PREVIEW_END/TOKEN_SET/TOKEN_RESET/RELOAD/PROBLEMS/INSPECT\`.
- \`dioxus\`: \`use_theme_store\`, \`use_resolved\`,
\`use_themed::<T>()\` (a typed slice that changes only with its part).

**Wiring (breaking, see the change files):** \`ShellTheme\` is runtime
(\`Arc<str>\` fields); \`AppShell\`, \`WorkspaceView\` (its host theme
is a signal now) and architect-ui's \`ThemeStoreProvider\` (also on
\`:root\`) follow the store; paint panes get the resolved theme; a
process extension's view (a Blitz sub-document) gets the host's
\`:root\` sheet (\`RemoteDocument::set_theme_css\`). The lab: one store,
\`THEME_*\` on its Action List, a ⌘K picker, the OS scheme; its Keyflow
chart follows via a typed slice.

Checks: tests of theme, shell, ui, workspace (ui, paint-ui), extensions
(remote) — 217 passed, 0 failed; \`cargo xtask docs\` 0; \`cargo xtask
boundaries\` 16 violations (unchanged); \`cargo check --workspace
--all-targets\` and \`-p architect-desktop --features keyflow-lab\`
without overrides. In a real window: \`THEME_SET\` across dark and light
themes restyles the shell, panes, A2UI, the Keyflow chart and Frame's
process-drawn pane live.

Not yet: layers are not persisted; no Appearance settings page (#185);
pop-out windows take the theme when opened; extension \`[[themes]]\` are
declared but not loaded into the store.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…osition (#266)

Stacked on the paint view PR. Supersedes the WIP draft #264 (its work,
finished and pinned so it builds without local overrides).

- The lab's opt-in \`keyflow-lab\` feature: Keyflow's source editor and
chart (a paint view) in Embedded views and a Chart screenset;
\`KEYFLOW_*\` actions on the Action List (state, set source/cursor,
focus, chart view and theme).
- Shell bars, and \`ViewInput::Wheel\` carries the pointer position and
modifiers.
- The lab's Keyflow caret: the editor's own zero-width caret; the lab
sets only \`--editor-caret\`.
- Pins: Keyflow keyflow#48, vendor's IME commit on v0.7.1 (\`25bf4bc\`),
Blitz IME (\`db5b7ef\`).

Checks (no local overrides): \`cargo check --workspace --all-targets\`,
\`cargo check -p architect-desktop --features keyflow-lab\`. Run in a
real window: typing into the Keyflow editor, the chart via
\`KEYFLOW_*\`, the Frame surface pane driven by \`LAB_FRAME_SET\`.

Known: in the lab's real window the plain caret is not painted (it is in
every headless render, \`native_caret_probe\` in keyflow#48). Merge
after editor#21/#22 and keyflow#48; re-pin to their merges.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…demand, live theme, WebGL2 and web panes (#265)

Stacked on #261. The \`paint\` view kind (#221): an in-process view
drawn on the GPU in a pane.

- \`features/workspace/src/paint/mod.rs\`: \`PaintView\` (input / paint
/ hit), \`Painter::set_theme\`, \`Marks\`, \`FrameStats\`;
\`KindSpec::paint\`.
- Blitz host (\`paint/widget.rs\`, feature \`paint-ui\`): \`PaintPane\`,
\`PaintMount::wake_handle\` / \`PaintWake::wake\` for external changes,
live shell theme tokens.
- Web: \`paint/web.rs\` WebGL2 canvas (\`paint-web\`),
\`paint/web_ui.rs\` Dioxus web panes (\`paint-web-ui\`).

Tests: 37 paint-ui tests, Blitz CPU-raster pixel checks at 1× and 2×, a
browser smoke (hide/show, theme, coalesced wake, pointer, 2×). Used by
Keyflow's chart in the lab (next PR).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@fts-forge
fts-forge Bot merged commit cf81385 into main Nov 4, 2026
1 check passed
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.

0 participants