Frontend Development
Use this skill for UI/product work and for TypeScript state surfaces that feed
shared notebook UI. If the task is primarily Automerge protocol, daemon/kernel
execution, MCP session lifecycle, or release mechanics, use the more specific
repo skill for that subsystem first and return here only for app integration.
Quick Reference
| Task |
Command |
| Hot reload assets |
cargo xtask vite |
| Dev daemon |
cargo xtask dev-daemon |
| Human app launch |
cargo xtask notebook |
| Human attach to Vite |
cargo xtask notebook --attach |
| Full debug build |
cargo xtask build |
| Rust-only rebuild |
cargo xtask build --rust-only |
| Human bundled launch |
cargo xtask run |
| One-shot setup |
cargo xtask dev |
| Lint/format |
cargo xtask lint --fix |
| nteract-dev MCP server |
cargo xtask run-mcp |
| Regenerate TS bindings |
cargo test |
Design Exploration Mode
Use this mode when the user asks to iterate on UI, including notebook UI,
Elements docs, rail/sidebar surfaces, toolbar chrome, cell affordances, runtime
state language, or says the goal is to make the design feel fluid with natural
grouping.
Working Shape
- Start a task branch early and treat it as an exploratory design branch.
- Bring up the dev daemon and Vite unless the user only wants discussion.
- Prefer real fixture notebooks and existing Elements scenarios over synthetic empty states.
- If the production app is hard to drive, prototype the visual language in
apps/elements, then pull only the durable parts back into the main app.
- Add or update an Elements page when a new surface needs product/design review:
create the fixture component in
apps/elements/components/*-example.tsx,
document it in apps/elements/content/docs/*.mdx, and link it from
apps/elements/app/page.tsx plus apps/elements/content/docs/index.mdx.
- Treat Elements fixtures as stable review artifacts: keep data deterministic,
avoid live network dependencies, and cover dense, empty, error, and narrow
viewport states when the surface needs them.
- Commit each promising design state as a separate conventional commit so the user can
review, cherry-pick, or backtrack.
- Do not rush to PR until there is a tangible visual direction the user likes.
Design Bias
- Make UI feel like it belongs to the document, not like legacy app chrome bolted around it.
- Use color as subtle state and focus language, not decoration.
- Favor fluid separators, ribbons, inline controls, and calm state vocabulary over raised
pills, bubbles, boxed badges, or noisy status text.
- Use shared shadcn/nteract primitives from
src/components/ui where they fit;
add new primitives through the repo shadcn workflow instead of one-off
component copies.
- Avoid duplicating identity/mode/status labels when another nearby control already carries
that meaning.
- Keep desktop-local identity quiet; reserve explicit auth/account chrome for cloud surfaces.
- For cloud, separate app-level controls from notebook-level controls:
presence, connection, sharing, auth, and view/edit mode belong in cloud/app chrome;
execution, runtime/package language, and cell insertion belong in notebook chrome.
- For product-surface PRs and docs, describe nteract in concrete system terms:
live documents, explicit runtime state, workstation/compute attachment, and
collaboration mechanics. Avoid named comparisons to other products in PR
descriptions or shipped docs.
Visual Verification
- Check desktop and narrow widths before calling the design done.
- Use Playwright or Browser screenshots for at least one wide and one constrained viewport.
- Inspect common states: ready, queued, running, completed, failed, hidden input/output,
markdown-heavy notebooks, and package/outline rail panels.
- If motion is involved, verify fast-path behavior so short executions do not flicker.
Hosted Product Surface Work
Use this subsection for hosted notebook home, sharing, workstation, auth/session,
and public-notebook polish. These surfaces are app chrome around notebooks; they
should not fork cell, output, rail, toolbar, or execution UI.
Product Shape
- Prototype in
apps/elements first when the visual or interaction model is
still uncertain, then promote only stable pieces into apps/notebook-cloud.
- Keep
/n useful as a notebook home, not just a file list: continuation,
ownership/access state, created/open flows, and workstation/runtime readiness
should read together.
- Treat notebook titles as first-class product data. Untitled notebooks must
degrade cleanly, but a dashboard full of IDs is a signal that title capture,
rename, or cleanup tooling needs attention.
- Share previews and OG metadata must be safe by default. Private notebooks
should not leak content through server-rendered metadata; public notebooks
should use explicit published/revision-safe facts.
- Workstation state belongs to host/app chrome until it becomes the active
notebook runtime. Runtime controls stay in the shared notebook toolbar.
- Hosted compute is owner-only until an explicit execute capability exists.
Do not let
editor, edit mode, or a live runtime_peer alone surface run,
restart, or interrupt controls.
- R2 snapshot bundles and D1 catalog/ACL/revision rows are the durable hosted
truth. Treat Durable Object storage as live-room recovery/cache unless a new
ADR changes that boundary.
App-Shell Latency
- Avoid first-frame empty states when the route already owns the data. Prefer
server bootstrap or retained state for app-shell data such as
/n lists and
sharing ledgers.
- Do not solve app-shell data freshness by overusing Automerge when the source
of truth is an ACL/catalog/session resource. Use Automerge for notebook and
runtime documents; use host APIs for catalog, auth, sharing, and account data.
- If browser auth is localStorage-only, the Worker cannot server-render
authenticated HTML on first navigation. A first-party session/cookie layer can
bootstrap app-owned pages, but room WebSocket credentials should remain
explicit/ticketed and output frames must stay on the separate isolated origin.
Projection Discipline
- Derive dashboard/list/sidebar summaries in pure projection helpers outside
component render bodies. React should consume stable arrays/objects rather
than recreating ad hoc projections in JSX.
- For live notebook content, consume
CellChangeset and shared narrow
projection helpers when possible. Full live-cell rematerialization is a
bootstrap or fallback path, not the steady-state Cloud projection model.
- When adding new API facts for UI, keep them structured and host-owned. Avoid
parsing principal strings, display labels, or notebook IDs in React except as
compatibility fallback.
Reactive State and WASM Projection Work
Use this subsection when editing RxJS or shared-store code such as
packages/runtimed/src/sync-engine.ts,
packages/runtimed/src/observable-store.ts,
packages/runtimed/src/poll.ts,
packages/runtimed/src/*store*.ts,
src/components/notebook/state/*,
apps/notebook/src/lib/notebook-sync-store-bridge.ts,
apps/notebook-cloud/viewer/*store*.ts,
apps/notebook-cloud/viewer/*facts*.ts,
apps/notebook-cloud/viewer/use-cloud-*-store.ts, or
apps/notebook-cloud/viewer/browser-signals.ts.
State Boundary
- Start from the durable owner:
NotebookDoc, RuntimeStateDoc, CommsDoc,
CommentsDoc, or host-owned APIs/session facts. React state is for local UI
affordances, not another copy of document/runtime truth.
- Prefer WASM-emitted changesets and projections (
CellChangeset,
ExecutionViewChangeset, outputIdChanges$, commentsProjection$,
notebookSyncApplied$) over second-pass TypeScript diffs.
- Shared components should consume shared stores. Host code should inject source
facts and side-effect adapters, not duplicate projection logic.
- Keep host policy host-owned: D1/ACL/OIDC/session state, Tauri daemon
lifecycle, filesystem/package-manager access, and workstation source facts do
not belong in
runtimed-wasm or shared React stores. Project their results
only when shared UI needs them.
RxJS Shape
- Keep
Subject, BehaviorSubject, and ReplaySubject private. Expose
readonly Observable fields through .asObservable().
- Prefer one authoritative subject plus
select(project, equals) style
projections with distinctUntilChanged. Add structural comparators when the
projector allocates arrays or objects.
- Keep stream logic inside
.pipe() where it is composable. Manual
.subscribe(...) belongs at bridge edges that drive imperative sinks; collect
those subscriptions in a Subscription and tear them down.
- Choose flattening operators by semantics:
switchMap for latest-only work,
concatMap for ordered materialization or writes, mergeMap for independent
frame/event expansion, and exhaustMap for duplicate-submit suppression.
- Put
catchError inside the inner observable when the outer session stream
must keep running. Avoid letting one bad frame, blob fetch, or projection kill
a long-lived bridge.
- Use
shareReplay with an explicit lifetime. refCount: false is appropriate
for app/session-lifetime shared store projections that must keep one cache;
refCount: true is safer for cold sources whose subscription should end when
consumers detach.
React Binding
- Use
useSyncExternalStore for shared notebook store hooks. The observable
adapted by useRuntimeProjection-style helpers must emit synchronously on
subscribe, usually through BehaviorSubject, ReplaySubject(1), or a seeded
shareReplay projection.
- Subscribe to the narrowest projected fact the UI needs instead of the whole
runtime or notebook snapshot. This avoids re-rendering on unrelated daemon
ticks and keeps Desktop and Cloud behavior converged.
- For async materialization or blob/output resolution, preserve ordering with
concatMap or an explicit queue. After every await, check that the live
handle/session/auth/endpoint or activation epoch still matches before
writing stores.
- Reset paths must invalidate stale async writes, not just clear visible state.
Verification
- Add virtual-time tests (
rxjs/testing or VirtualTimeScheduler) for timers,
throttles, cancellation, and ordered stream behavior.
- Add store-level tests for synchronous snapshot seeding, deduped emissions,
loaded/default-state gates, and stale-write guards.
- If a change crosses the WASM projection boundary, regenerate or freshness-check
runtime WASM artifacts with the repo xtask flow before trusting TypeScript
results.
Module-Singleton Source Stores
For non-CRDT async sources (auth, catalog, access requests, workstations) held
in a module-level ObservableStore singleton, follow Decision 8
(docs/adr/frontend-sync-bridge.md). The store outlives every component, so
each async completion is a stale-write risk:
- Components consume named domain hooks. Components import
useCloudAuthState/useHostedCatalogAuth/useCloudWorkstationsRegistry, never
store.select(...) inline in a render body and never
observable-binding.ts directly. Desktop and cloud share one binding.
- Resolve store instances through context. The store stays a module
singleton for boot, drivers, and initial snapshot reads, but each domain hook
resolves its instance from
useCloudStores() (cloud-stores-context.ts),
whose default is the singleton bundle. Production mounts no provider and uses
those same instances; a test or Elements fixture mounts CloudStoresProvider
with its own instances and the subtree reads those. The provider overrides
consumption, never activation - its owner activates the instances it supplies.
A controller that dispatches actions on the same store reads it from the same
context too, so an override gets a coherent store.
- Check request identity after every await. Poll ticks and
imperative actions capture
{epoch, auth reference, endpoint} when the
request starts; after every await (success, error, and any follow-up
refetch), discard the result if the identity changed. Checking only the
first await leaves later completions able to write stale state.
- Invalidate pending work and clear its indicators.
dispose,
reset, and a signed-out closed gate bump the activation epoch so captured
requests become stale (a transient loading gate is recoverable and
keeps in-flight work alive); a dropped completion also clears any indicator
it wrote (by object-reference ownership, so it can never clobber a newer
identity's own state).
- Restart after-settle polls on completion. A self-scheduling
poll re-arms via
repeat() on completion, so a swallowed inner rejection
cannot kill the loop. Keep catchError on the inner fetch.
- Route fixed-rate triggers through one
exhaustMap. Triggers that must
not overlap (interval tick, visibility rise, manual wakeup) feed a single
exhaustMap; do not give each trigger its own guard. After-settle stores
instead serialize manual refresh and mutation refetches through a dedicated
concatMap action stream outside the poll loop, preserving action order
and polling cadence.
- Inject every clock.
scheduler, now, and the network operations are
activate(deps) arguments, so tests run entirely on virtual time and the
suite checks cadence, gates, aborts, and discarded stale results
deterministically.
- List every field in the comparator's manifest.
distinctUntilChanged
uses a named fooEquals(a, b) with a colocated
satisfies Record<keyof T, true> manifest. The manifest forces every key to
be listed when the type grows; it does not prove every key is compared -
treat a break as a prompt to revisit the comparator body, not proof of
correctness. A projection that allocates an array or object each tick needs
a structural comparator (length plus per-element identity/fields); a
reference check on a newly allocated value deduplicates nothing. A missing
manifest key surfaces as a tsc error (pnpm --dir apps/notebook-cloud typecheck); the
node test script alone will not catch it.
- Read
loaded$ to distinguish loading from loaded empty state. The state
subject emits its seeded default before the gate opens (state emits first,
then the gate); a consumer
that must tell "loading" from "loaded empty" reads loaded$, never infers
from an empty snapshot.
Hot Reload
Best for UI/React development. Start the dev daemon and Vite from agent terminals; let the human launch the Tauri GUI from their own terminal.
Agent terminals:
cargo xtask dev-daemon
cargo xtask vite
Human terminal:
cargo xtask notebook --attach
React changes hot-reload through the Vite dev server on port 5174.
Multi-Window Testing
Closing the first Tauri window kills Vite. Keep Vite alive independently:
# Agent terminal: standalone Vite (stays running)
cargo xtask vite
# Human terminal(s): attach Tauri to existing Vite
cargo xtask notebook --attach
Debug Build (cargo xtask build + run)
Bundles frontend assets into the binary. Emits JS source maps for native webview devtools.
cargo xtask build # Full build (frontend + Rust)
cargo xtask build --rust-only # Skip frontend rebuild (fast Rust iteration)
cargo xtask run # Run the bundled binary
cargo xtask run path/to/notebook.ipynb
Dev Daemon
Each worktree gets an isolated daemon in dev mode.
# Agent terminals
cargo xtask dev-daemon
cargo xtask vite
# Human terminal if they want the full setup flow
cargo xtask dev
cargo xtask dev --skip-install --skip-build # Fast repeat
cargo xtask dev-daemon, cargo xtask notebook, and cargo xtask run-mcp derive the worktree env automatically. Set RUNTIMED_DEV=1 and RUNTIMED_WORKSPACE_PATH="$(pwd)" only for raw ./target/debug/runt ... commands.
Useful Daemon Commands
./target/debug/runt daemon status # Check daemon state
./target/debug/runt daemon logs -f # Tail logs
./target/debug/runt ps # List running kernels
./target/debug/runt notebooks # List open notebooks
MCP Server Development
nteract-dev (recommended)
cargo xtask run-mcp # Start dev MCP server
cargo xtask run-mcp --print-config # Editor config output
Starts dev daemon, launches nteract-dev, spawns child runt mcp, proxies notebook tool calls, watches for file changes, and hot-reloads.
nteract-dev Tools
| Tool |
Purpose |
up |
Idempotent bring-up. Args: vite=true, rebuild=true, mode="debug"|"release" |
down |
Stop Vite. daemon=true also stops daemon |
status |
Read-only report of child, daemon, processes, build mode |
logs |
Tail daemon log |
vite_logs |
Tail Vite log |
Hot Reload Watches
python/nteract/src/, python/runtimed/src/, crates/runtimed-py/src/, crates/runtimed/src/:
- Python changes: Child restarts automatically
- Rust changes:
maturin develop runs first, then child restarts
Direct Mode (no proxy)
cargo xtask dev-daemon # Terminal 1
./target/debug/runt mcp # Terminal 2 (Rust-native, no Python)
Zed Integration
Codex app/CLI reads .codex/config.toml from the project. Keep the
project-scoped server named nteract-dev and pinned to the repo root:
[mcp_servers.nteract-dev]
command = "cargo"
args = ["xtask", "run-mcp"]
cwd = "."
startup_timeout_sec = 120
[mcp_servers.nteract-dev.env]
NTERACT_DEV_MODE = "attach"
RUNTIMED_DEV = "1"
SKIP_MATURIN = "1"
Installed Codex plugin servers such as nteract-notebook, nightly, or older
notebook aliases target release/plugin daemons. Do not use them for source
development against a local Browser/Vite worktree.
.zed/settings.json (gitignored):
{
"context_servers": {
"nteract-dev": {
"command": "cargo",
"args": ["run", "-p", "mcp-supervisor"],
"cwd": ".",
"env": { "NTERACT_DEV_MODE": "owner", "RUNTIMED_DEV": "1" }
}
}
}
TypeScript Bindings (ts-rs)
Types in src/bindings/ are auto-generated from Rust via ts-rs. Edit the Rust source, not the generated TypeScript.
How It Works
Annotate Rust types with #[derive(TS)] and #[ts(export)]:
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export)]
pub enum ThemeMode {
System,
Light,
Dark,
}
Generates src/bindings/ThemeMode.ts:
export type ThemeMode = "system" | "light" | "dark";
Adding New Bindings
Add ts-rs to crate's Cargo.toml:
[dependencies]
ts-rs = { version = "12", features = ["serde-compat"] }
Annotate type with #[derive(TS)] and #[ts(export)]
Run cargo test to generate the TypeScript file
Import from src/bindings/index.ts:
import type { MyNewType } from "@/bindings";
Configuration
Export directory set in .cargo/config.toml:
[env]
TS_RS_EXPORT_DIR = { value = "src/bindings", relative = true }
Zed Editor Tasks
Pre-configured in .zed/tasks.json (cmd-shift-t):
| Task |
Command |
| Dev Daemon |
cargo xtask dev-daemon |
| Human Dev App |
cargo xtask notebook |
| Daemon Status |
./target/debug/runt daemon status |
| Daemon Logs |
./target/debug/runt daemon logs -f |
| Format |
cargo xtask lint --fix |
| Setup |
pnpm install && cargo xtask build |
Common Gotchas
Daemon code changes not taking effect: Restart cargo xtask dev-daemon in dev mode. In production: reinstall the .app or run ./scripts/install-nightly.
App says "Dev daemon not running": Start cargo xtask dev-daemon in another terminal.
Port conflicts with Vite: Default 5174 may conflict across worktrees. Use cargo xtask build + run to avoid Vite, or use CONDUCTOR_PORT for automatic assignment.
Frontend changes not showing: With cargo xtask notebook they hot-reload. With cargo xtask run you need cargo xtask build first. With --rust-only frontend is intentionally skipped.
1---2name: frontend-dev3description: Frontend development for nteract UI/product surfaces, Elements fixtures, hot reload, dev daemon setup, MCP server workflow, TypeScript bindings via ts-rs, and reactive state work that touches RxJS streams, useSyncExternalStore stores, or WASM-backed notebook/runtime projections.4---56# Frontend Development78Use this skill for UI/product work and for TypeScript state surfaces that feed9shared notebook UI. If the task is primarily Automerge protocol, daemon/kernel10execution, MCP session lifecycle, or release mechanics, use the more specific11repo skill for that subsystem first and return here only for app integration.1213## Quick Reference1415| Task | Command |16|------|---------|17| Hot reload assets | `cargo xtask vite` |18| Dev daemon | `cargo xtask dev-daemon` |19| Human app launch | `cargo xtask notebook` |20| Human attach to Vite | `cargo xtask notebook --attach` |21| Full debug build | `cargo xtask build` |22| Rust-only rebuild | `cargo xtask build --rust-only` |23| Human bundled launch | `cargo xtask run` |24| One-shot setup | `cargo xtask dev` |25| Lint/format | `cargo xtask lint --fix` |26| nteract-dev MCP server | `cargo xtask run-mcp` |27| Regenerate TS bindings | `cargo test` |2829## Design Exploration Mode3031Use this mode when the user asks to iterate on UI, including notebook UI,32Elements docs, rail/sidebar surfaces, toolbar chrome, cell affordances, runtime33state language, or says the goal is to make the design feel fluid with natural34grouping.3536### Working Shape3738- Start a task branch early and treat it as an exploratory design branch.39- Bring up the dev daemon and Vite unless the user only wants discussion.40- Prefer real fixture notebooks and existing Elements scenarios over synthetic empty states.41- If the production app is hard to drive, prototype the visual language in42 `apps/elements`, then pull only the durable parts back into the main app.43- Add or update an Elements page when a new surface needs product/design review:44 create the fixture component in `apps/elements/components/*-example.tsx`,45 document it in `apps/elements/content/docs/*.mdx`, and link it from46 `apps/elements/app/page.tsx` plus `apps/elements/content/docs/index.mdx`.47- Treat Elements fixtures as stable review artifacts: keep data deterministic,48 avoid live network dependencies, and cover dense, empty, error, and narrow49 viewport states when the surface needs them.50- Commit each promising design state as a separate conventional commit so the user can51 review, cherry-pick, or backtrack.52- Do not rush to PR until there is a tangible visual direction the user likes.5354### Design Bias5556- Make UI feel like it belongs to the document, not like legacy app chrome bolted around it.57- Use color as subtle state and focus language, not decoration.58- Favor fluid separators, ribbons, inline controls, and calm state vocabulary over raised59 pills, bubbles, boxed badges, or noisy status text.60- Use shared shadcn/nteract primitives from `src/components/ui` where they fit;61 add new primitives through the repo shadcn workflow instead of one-off62 component copies.63- Avoid duplicating identity/mode/status labels when another nearby control already carries64 that meaning.65- Keep desktop-local identity quiet; reserve explicit auth/account chrome for cloud surfaces.66- For cloud, separate app-level controls from notebook-level controls:67 presence, connection, sharing, auth, and view/edit mode belong in cloud/app chrome;68 execution, runtime/package language, and cell insertion belong in notebook chrome.69- For product-surface PRs and docs, describe nteract in concrete system terms:70 live documents, explicit runtime state, workstation/compute attachment, and71 collaboration mechanics. Avoid named comparisons to other products in PR72 descriptions or shipped docs.7374### Visual Verification7576- Check desktop and narrow widths before calling the design done.77- Use Playwright or Browser screenshots for at least one wide and one constrained viewport.78- Inspect common states: ready, queued, running, completed, failed, hidden input/output,79 markdown-heavy notebooks, and package/outline rail panels.80- If motion is involved, verify fast-path behavior so short executions do not flicker.8182## Hosted Product Surface Work8384Use this subsection for hosted notebook home, sharing, workstation, auth/session,85and public-notebook polish. These surfaces are app chrome around notebooks; they86should not fork cell, output, rail, toolbar, or execution UI.8788### Product Shape8990- Prototype in `apps/elements` first when the visual or interaction model is91 still uncertain, then promote only stable pieces into `apps/notebook-cloud`.92- Keep `/n` useful as a notebook home, not just a file list: continuation,93 ownership/access state, created/open flows, and workstation/runtime readiness94 should read together.95- Treat notebook titles as first-class product data. Untitled notebooks must96 degrade cleanly, but a dashboard full of IDs is a signal that title capture,97 rename, or cleanup tooling needs attention.98- Share previews and OG metadata must be safe by default. Private notebooks99 should not leak content through server-rendered metadata; public notebooks100 should use explicit published/revision-safe facts.101- Workstation state belongs to host/app chrome until it becomes the active102 notebook runtime. Runtime controls stay in the shared notebook toolbar.103- Hosted compute is owner-only until an explicit execute capability exists.104 Do not let `editor`, edit mode, or a live `runtime_peer` alone surface run,105 restart, or interrupt controls.106- R2 snapshot bundles and D1 catalog/ACL/revision rows are the durable hosted107 truth. Treat Durable Object storage as live-room recovery/cache unless a new108 ADR changes that boundary.109110### App-Shell Latency111112- Avoid first-frame empty states when the route already owns the data. Prefer113 server bootstrap or retained state for app-shell data such as `/n` lists and114 sharing ledgers.115- Do not solve app-shell data freshness by overusing Automerge when the source116 of truth is an ACL/catalog/session resource. Use Automerge for notebook and117 runtime documents; use host APIs for catalog, auth, sharing, and account data.118- If browser auth is localStorage-only, the Worker cannot server-render119 authenticated HTML on first navigation. A first-party session/cookie layer can120 bootstrap app-owned pages, but room WebSocket credentials should remain121 explicit/ticketed and output frames must stay on the separate isolated origin.122123### Projection Discipline124125- Derive dashboard/list/sidebar summaries in pure projection helpers outside126 component render bodies. React should consume stable arrays/objects rather127 than recreating ad hoc projections in JSX.128- For live notebook content, consume `CellChangeset` and shared narrow129 projection helpers when possible. Full live-cell rematerialization is a130 bootstrap or fallback path, not the steady-state Cloud projection model.131- When adding new API facts for UI, keep them structured and host-owned. Avoid132 parsing principal strings, display labels, or notebook IDs in React except as133 compatibility fallback.134135## Reactive State and WASM Projection Work136137Use this subsection when editing RxJS or shared-store code such as138`packages/runtimed/src/sync-engine.ts`,139`packages/runtimed/src/observable-store.ts`,140`packages/runtimed/src/poll.ts`,141`packages/runtimed/src/*store*.ts`,142`src/components/notebook/state/*`,143`apps/notebook/src/lib/notebook-sync-store-bridge.ts`,144`apps/notebook-cloud/viewer/*store*.ts`,145`apps/notebook-cloud/viewer/*facts*.ts`,146`apps/notebook-cloud/viewer/use-cloud-*-store.ts`, or147`apps/notebook-cloud/viewer/browser-signals.ts`.148149### State Boundary150151- Start from the durable owner: `NotebookDoc`, `RuntimeStateDoc`, `CommsDoc`,152 `CommentsDoc`, or host-owned APIs/session facts. React state is for local UI153 affordances, not another copy of document/runtime truth.154- Prefer WASM-emitted changesets and projections (`CellChangeset`,155 `ExecutionViewChangeset`, `outputIdChanges$`, `commentsProjection$`,156 `notebookSyncApplied$`) over second-pass TypeScript diffs.157- Shared components should consume shared stores. Host code should inject source158 facts and side-effect adapters, not duplicate projection logic.159- Keep host policy host-owned: D1/ACL/OIDC/session state, Tauri daemon160 lifecycle, filesystem/package-manager access, and workstation source facts do161 not belong in `runtimed-wasm` or shared React stores. Project their results162 only when shared UI needs them.163164### RxJS Shape165166- Keep `Subject`, `BehaviorSubject`, and `ReplaySubject` private. Expose167 readonly `Observable` fields through `.asObservable()`.168- Prefer one authoritative subject plus `select(project, equals)` style169 projections with `distinctUntilChanged`. Add structural comparators when the170 projector allocates arrays or objects.171- Keep stream logic inside `.pipe()` where it is composable. Manual172 `.subscribe(...)` belongs at bridge edges that drive imperative sinks; collect173 those subscriptions in a `Subscription` and tear them down.174- Choose flattening operators by semantics: `switchMap` for latest-only work,175 `concatMap` for ordered materialization or writes, `mergeMap` for independent176 frame/event expansion, and `exhaustMap` for duplicate-submit suppression.177- Put `catchError` inside the inner observable when the outer session stream178 must keep running. Avoid letting one bad frame, blob fetch, or projection kill179 a long-lived bridge.180- Use `shareReplay` with an explicit lifetime. `refCount: false` is appropriate181 for app/session-lifetime shared store projections that must keep one cache;182 `refCount: true` is safer for cold sources whose subscription should end when183 consumers detach.184185### React Binding186187- Use `useSyncExternalStore` for shared notebook store hooks. The observable188 adapted by `useRuntimeProjection`-style helpers must emit synchronously on189 subscribe, usually through `BehaviorSubject`, `ReplaySubject(1)`, or a seeded190 `shareReplay` projection.191- Subscribe to the narrowest projected fact the UI needs instead of the whole192 runtime or notebook snapshot. This avoids re-rendering on unrelated daemon193 ticks and keeps Desktop and Cloud behavior converged.194- For async materialization or blob/output resolution, preserve ordering with195 `concatMap` or an explicit queue. After every `await`, check that the live196 handle/session/auth/endpoint or activation epoch still matches before197 writing stores.198- Reset paths must invalidate stale async writes, not just clear visible state.199200### Verification201202- Add virtual-time tests (`rxjs/testing` or `VirtualTimeScheduler`) for timers,203 throttles, cancellation, and ordered stream behavior.204- Add store-level tests for synchronous snapshot seeding, deduped emissions,205 loaded/default-state gates, and stale-write guards.206- If a change crosses the WASM projection boundary, regenerate or freshness-check207 runtime WASM artifacts with the repo xtask flow before trusting TypeScript208 results.209210### Module-Singleton Source Stores211212For non-CRDT async sources (auth, catalog, access requests, workstations) held213in a module-level `ObservableStore` singleton, follow Decision 8214(`docs/adr/frontend-sync-bridge.md`). The store outlives every component, so215each async completion is a stale-write risk:216217- **Components consume named domain hooks.** Components import218 `useCloudAuthState`/`useHostedCatalogAuth`/`useCloudWorkstationsRegistry`, never219 `store.select(...)` inline in a render body and never220 `observable-binding.ts` directly. Desktop and cloud share one binding.221- **Resolve store instances through context.** The store stays a module222 singleton for boot, drivers, and initial snapshot reads, but each domain hook223 resolves its instance from `useCloudStores()` (`cloud-stores-context.ts`),224 whose default is the singleton bundle. Production mounts no provider and uses225 those same instances; a test or Elements fixture mounts `CloudStoresProvider`226 with its own instances and the subtree reads those. The provider overrides227 consumption, never activation - its owner activates the instances it supplies.228 A controller that dispatches actions on the same store reads it from the same229 context too, so an override gets a coherent store.230- **Check request identity after every await.** Poll ticks and231 imperative actions capture `{epoch, auth reference, endpoint}` when the232 request starts; after every `await` (success, error, and any follow-up233 refetch), discard the result if the identity changed. Checking only the234 first await leaves later completions able to write stale state.235- **Invalidate pending work and clear its indicators.** `dispose`,236 `reset`, and a signed-out closed gate bump the activation epoch so captured237 requests become stale (a transient `loading` gate is recoverable and238 keeps in-flight work alive); a dropped completion also clears any indicator239 it wrote (by object-reference ownership, so it can never clobber a newer240 identity's own state).241- **Restart after-settle polls on completion.** A self-scheduling242 poll re-arms via `repeat()` on completion, so a swallowed inner rejection243 cannot kill the loop. Keep `catchError` on the inner fetch.244- **Route fixed-rate triggers through one `exhaustMap`.** Triggers that must245 not overlap (interval tick, visibility rise, manual wakeup) feed a single246 `exhaustMap`; do not give each trigger its own guard. After-settle stores247 instead serialize manual refresh and mutation refetches through a dedicated248 `concatMap` action stream outside the poll loop, preserving action order249 and polling cadence.250- **Inject every clock.** `scheduler`, `now`, and the network operations are251 `activate(deps)` arguments, so tests run entirely on virtual time and the252 suite checks cadence, gates, aborts, and discarded stale results253 deterministically.254- **List every field in the comparator's manifest.** `distinctUntilChanged`255 uses a named `fooEquals(a, b)` with a colocated256 `satisfies Record<keyof T, true>` manifest. The manifest forces every key to257 be *listed* when the type grows; it does not prove every key is *compared* -258 treat a break as a prompt to revisit the comparator body, not proof of259 correctness. A projection that allocates an array or object each tick needs260 a structural comparator (length plus per-element identity/fields); a261 reference check on a newly allocated value deduplicates nothing. A missing262 manifest key surfaces as a tsc error (`pnpm --dir apps/notebook-cloud typecheck`); the263 node test script alone will not catch it.264- **Read `loaded$` to distinguish loading from loaded empty state.** The state265 subject emits its seeded default before the gate opens (state emits first,266 then the gate); a consumer267 that must tell "loading" from "loaded empty" reads `loaded$`, never infers268 from an empty snapshot.269270## Hot Reload271272Best for UI/React development. Start the dev daemon and Vite from agent terminals; let the human launch the Tauri GUI from their own terminal.273274Agent terminals:275276```bash277cargo xtask dev-daemon278cargo xtask vite279```280281Human terminal:282283```bash284cargo xtask notebook --attach285```286287React changes hot-reload through the Vite dev server on port 5174.288289### Multi-Window Testing290291Closing the first Tauri window kills Vite. Keep Vite alive independently:292293```bash294# Agent terminal: standalone Vite (stays running)295cargo xtask vite296297# Human terminal(s): attach Tauri to existing Vite298cargo xtask notebook --attach299```300301## Debug Build (`cargo xtask build` + `run`)302303Bundles frontend assets into the binary. Emits JS source maps for native webview devtools.304305```bash306cargo xtask build # Full build (frontend + Rust)307cargo xtask build --rust-only # Skip frontend rebuild (fast Rust iteration)308cargo xtask run # Run the bundled binary309cargo xtask run path/to/notebook.ipynb310```311312## Dev Daemon313314Each worktree gets an isolated daemon in dev mode.315316```bash317# Agent terminals318cargo xtask dev-daemon319cargo xtask vite320321# Human terminal if they want the full setup flow322cargo xtask dev323cargo xtask dev --skip-install --skip-build # Fast repeat324```325326`cargo xtask dev-daemon`, `cargo xtask notebook`, and `cargo xtask run-mcp` derive the worktree env automatically. Set `RUNTIMED_DEV=1` and `RUNTIMED_WORKSPACE_PATH="$(pwd)"` only for raw `./target/debug/runt ...` commands.327328### Useful Daemon Commands329330```bash331./target/debug/runt daemon status # Check daemon state332./target/debug/runt daemon logs -f # Tail logs333./target/debug/runt ps # List running kernels334./target/debug/runt notebooks # List open notebooks335```336337## MCP Server Development338339### nteract-dev (recommended)340341```bash342cargo xtask run-mcp # Start dev MCP server343cargo xtask run-mcp --print-config # Editor config output344```345346Starts dev daemon, launches `nteract-dev`, spawns child `runt mcp`, proxies notebook tool calls, watches for file changes, and hot-reloads.347348### nteract-dev Tools349350| Tool | Purpose |351|------|---------|352| `up` | Idempotent bring-up. Args: `vite=true`, `rebuild=true`, `mode="debug"\|"release"` |353| `down` | Stop Vite. `daemon=true` also stops daemon |354| `status` | Read-only report of child, daemon, processes, build mode |355| `logs` | Tail daemon log |356| `vite_logs` | Tail Vite log |357358### Hot Reload Watches359360`python/nteract/src/`, `python/runtimed/src/`, `crates/runtimed-py/src/`, `crates/runtimed/src/`:361- **Python changes:** Child restarts automatically362- **Rust changes:** `maturin develop` runs first, then child restarts363364### Direct Mode (no proxy)365366```bash367cargo xtask dev-daemon # Terminal 1368./target/debug/runt mcp # Terminal 2 (Rust-native, no Python)369```370371### Zed Integration372373Codex app/CLI reads `.codex/config.toml` from the project. Keep the374project-scoped server named `nteract-dev` and pinned to the repo root:375376```toml377[mcp_servers.nteract-dev]378command = "cargo"379args = ["xtask", "run-mcp"]380cwd = "."381startup_timeout_sec = 120382383[mcp_servers.nteract-dev.env]384NTERACT_DEV_MODE = "attach"385RUNTIMED_DEV = "1"386SKIP_MATURIN = "1"387```388389Installed Codex plugin servers such as `nteract-notebook`, `nightly`, or older390`notebook` aliases target release/plugin daemons. Do not use them for source391development against a local Browser/Vite worktree.392393`.zed/settings.json` (gitignored):394395```json396{397 "context_servers": {398 "nteract-dev": {399 "command": "cargo",400 "args": ["run", "-p", "mcp-supervisor"],401 "cwd": ".",402 "env": { "NTERACT_DEV_MODE": "owner", "RUNTIMED_DEV": "1" }403 }404 }405}406```407408## TypeScript Bindings (ts-rs)409410Types in `src/bindings/` are auto-generated from Rust via `ts-rs`. Edit the Rust source, not the generated TypeScript.411412### How It Works413414Annotate Rust types with `#[derive(TS)]` and `#[ts(export)]`:415416```rust417use ts_rs::TS;418419#[derive(Debug, Clone, Serialize, Deserialize, TS)]420#[serde(rename_all = "lowercase")]421#[ts(export)]422pub enum ThemeMode {423 System,424 Light,425 Dark,426}427```428429Generates `src/bindings/ThemeMode.ts`:430```typescript431export type ThemeMode = "system" | "light" | "dark";432```433434### Adding New Bindings4354361. Add `ts-rs` to crate's `Cargo.toml`:437 ```toml438 [dependencies]439 ts-rs = { version = "12", features = ["serde-compat"] }440 ```4414422. Annotate type with `#[derive(TS)]` and `#[ts(export)]`4434443. Run `cargo test` to generate the TypeScript file4454464. Import from `src/bindings/index.ts`:447 ```typescript448 import type { MyNewType } from "@/bindings";449 ```450451### Configuration452453Export directory set in `.cargo/config.toml`:454```toml455[env]456TS_RS_EXPORT_DIR = { value = "src/bindings", relative = true }457```458459## Zed Editor Tasks460461Pre-configured in `.zed/tasks.json` (cmd-shift-t):462463| Task | Command |464|------|---------|465| Dev Daemon | `cargo xtask dev-daemon` |466| Human Dev App | `cargo xtask notebook` |467| Daemon Status | `./target/debug/runt daemon status` |468| Daemon Logs | `./target/debug/runt daemon logs -f` |469| Format | `cargo xtask lint --fix` |470| Setup | `pnpm install && cargo xtask build` |471472## Common Gotchas473474**Daemon code changes not taking effect:** Restart `cargo xtask dev-daemon` in dev mode. In production: reinstall the .app or run `./scripts/install-nightly`.475476**App says "Dev daemon not running":** Start `cargo xtask dev-daemon` in another terminal.477478**Port conflicts with Vite:** Default 5174 may conflict across worktrees. Use `cargo xtask build` + `run` to avoid Vite, or use `CONDUCTOR_PORT` for automatic assignment.479480**Frontend changes not showing:** With `cargo xtask notebook` they hot-reload. With `cargo xtask run` you need `cargo xtask build` first. With `--rust-only` frontend is intentionally skipped.