Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,18 @@ Landed since:
DOM"). The pure `render(state) -> Frame` projection is unit-tested headlessly;
the hardware path is proven by `cargo run -p positron-wgpu --example counter_gpu`
(missing GPU is a loud `GpuError::NoAdapter`, never a software fallback).

Next (see `docs/ARCHITECTURE.md` § roadmap O4b–O6):
- `positron-lit` *(optional)* — Lit DOM renderer for a11y / text-reflow (O4b)
- `ContinuumHost` (in continuum) — session ↔ Commands/Events, first real `ViewState` (O5)
- `@positron/lit` — the Lit DOM renderer (O4b): `LitRenderer<S>`
(`Renderer<S, TemplateResult>`) + `LitHost`, the TS/DOM sibling of the wgpu
backend. This is also where the contract first **crosses the language boundary**
— the four Rust traits re-expressed as hand-authored TS interfaces in
`@positron/core` (typechecked and CI-gated by a `web` job), the same contract in
two languages beside the ts-rs-generated wire *data*. Same pure/impure seam as
the Rust outliers: `render(state) -> TemplateResult` is asserted headlessly on
the template's `strings`/`values`; the one DOM-touching call (`domCommit`) is
quarantined in `dom.ts`. Run it: `npm test` (workspace root).

Next (see `docs/ARCHITECTURE.md` § roadmap O5–O6):
- `ContinuumHost` (in continuum) — session ↔ Commands/Events, first real `ViewState` (O5). positron's portable contract and continuum's `sdk/typescript` **complement** (decided at O4b); they meet here, neither subsumes the other.
- persona `Observer` → RAG/tool bridge (O6)
- Theme pack (Loki / Matrix / Fallout / Tron) ported from the cyberpunk-cli experiment

Expand Down
43 changes: 34 additions & 9 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,20 @@ the view from `ViewState` alone, the state type is incomplete — not the render
geometry, a theme *token* is arguably semantic and could ride in state, so this
is the recommended default rather than a primitive-enforced necessity; DESIGN.md
leans the same way.
- **`@positron/core` (npm) + `positron-lit` vs continuum `sdk/typescript`: decided
at O4b, not before.** Both generate types from Rust via ts-rs; whether the positron
DOM stack *subsumes* or *complements* the continuum SDK is resolved when the Lit
renderer actually lands. (O4 is `positron-wgpu`, the GPU surface — it has no bearing
on the npm/SDK reconcile, which is the DOM stack's concern.)
- **`@positron/core` + `@positron/lit` vs continuum `sdk/typescript`: they
COMPLEMENT — decided at O4b when the Lit renderer landed.** positron owns the
*portable* half: the widget contract (`ViewState`/`Renderer`/`Host`/`Observer`)
and the transport-neutral session frames (`ClientMessage`/`ServerMessage`),
generated/hand-authored once and reusable by any substrate. continuum's SDK owns
the *continuum-specific* half: its `Commands`/`Events`, connection/session
machinery, and domain payload types. A continuum web client composes both — it
renders `ViewState`s through `@positron/lit` and carries actions through the
continuum SDK. The two meet at `ContinuumHost` (O5), which lowers positron
session frames onto continuum `Commands`/`Events`; neither subsumes the other,
so there is no merge and no duplicated source of truth. (O4 `positron-wgpu`, the
GPU surface, had no bearing on this — the reconcile was always the DOM stack's
concern, and the answer is "different layers, composed," not "one replaces the
other.")

## Organizational task roadmap (one PR per unit)

Expand All @@ -181,11 +190,11 @@ the view from `ViewState` alone, the state type is incomplete — not the render
| **O2** | `examples/counter-cli` | renderer-agnostic **and** AI-perceives-same-state, in-process, zero transport (one `Counter` `ViewState`, ≥2 `Renderer`s, 1 `Observer`) | O1 |
| **O3** | `positron-ratatui` | terminal `Renderer` reference + `Host` event loop (**outlier A**: CPU cells, real stateful surface) | O2 |
| **O4** | `positron-wgpu` | the run-everywhere GPU `Renderer` (**outlier B**: native Metal/Vulkan/DX12 + web WebGPU/WASM from one codebase) — proves `Renderer<S>` carries no CPU-tree assumption; the load-bearing web+native surface | O2 |
| **O4b** | `positron-lit` *(optional)* | Lit DOM `Renderer` + regenerate `@positron/core`; a11y / text-reflow surface where the GPU path isn't ideal; reconcile with continuum `sdk/typescript` (subsume vs complement) | O4 |
| **O4b** | `@positron/lit` | Lit DOM `Renderer` (`Output = TemplateResult`) + `LitHost`, the TS/DOM outlier of the same contract wgpu implements for the GPU; the language-boundary proof (Rust traits → hand-authored TS interfaces in `@positron/core`) and the "web ≠ DOM as a peer paradigm" surface; reconcile with continuum `sdk/typescript` → **complement** | O4 |
| **O5** | `ContinuumHost` (in continuum) | positron session ↔ Commands/Events; first real `ViewState` (`ChatViewState`) flows to a positron renderer; reconciles positron's frame output with continuum's existing `RenderBackend`/`RgbaFrame` GPU seam; resolves the two-wire merge question | O3 or O4 |
| **O6** | persona `Observer` → RAG/tool bridge (in continuum) | perception into cognition + action as `CommandEnvelope` — closes "AI persona rag/tool integration" | O5 |

O1–O4 have landed: the boundary is pinned; `examples/counter-cli` proves "one
O1–O4b have landed: the boundary is pinned; `examples/counter-cli` proves "one
`ViewState`, many renderers, plus an observer perceiving the same state" in a
single process; `positron-ratatui` proves the first real stateful `Renderer` +
`Host` event loop against a genuinely different `type Output` (terminal cells,
Expand All @@ -200,7 +209,23 @@ while the hardware path (`Gpu::rasterize`) is proven by the `counter_gpu`
example (any real machine) and kept off the CI test surface (runners have no
adapter; a missing GPU is a loud `GpuError::NoAdapter`, never a software
fallback). `RgbaFrame` is deliberately continuum's avatar-frame shape.

O4b then crosses the contract into TypeScript. It is a **guaranteed-middle**
surface (the outlier pair already closed at O4), chosen deliberately for two
things a Rust renderer can't prove: the **language boundary** — the four Rust
traits re-expressed as hand-authored TS interfaces in `@positron/core` (a trait
has no wire form for ts-rs to project, so the contract lives twice, in sync by
hand), typechecked and CI-gated by a `web` job — and **"web ≠ DOM as a peer
paradigm"**: `@positron/lit` is a `Renderer<S, TemplateResult>` (`LitRenderer`) +
`LitHost`, the DOM sibling of the wgpu backend. It splits along the *same*
pure/impure seam as the two Rust outliers — the `render(state) -> TemplateResult`
projection is asserted headlessly on the template's `strings`/`values` (the DOM
analogue of wgpu's quad assertions), while the one DOM-touching call (`domCommit`,
Lit's `render` into a container) is quarantined in `dom.ts` and kept off the
headless test path.

**O5 is the next unit** — `ContinuumHost`: the first real `ViewState`
(`ChatViewState`) flowing to a positron renderer, the session↔Commands/Events
lowering, and the reconcile of positron's `RgbaFrame` with continuum's existing
`RenderBackend`/`RgbaFrame` GPU seam.
lowering (where positron's portable contract and continuum's SDK *complement*,
per the decision above), and the reconcile of positron's `RgbaFrame` with
continuum's existing `RenderBackend`/`RgbaFrame` GPU seam.
33 changes: 33 additions & 0 deletions npm/lit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@positron/lit",
"version": "0.1.1",
"description": "Positron Lit DOM renderer — LitRenderer<S> (Renderer<S, TemplateResult>) + LitHost over @positron/core. The web/DOM outlier of the same contract the wgpu backend implements for the GPU.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/CambrianTech/positron.git",
"directory": "npm/lit"
},
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "node --import tsx --test \"src/**/*.test.ts\""
},
"files": [
"src"
],
"dependencies": {
"@positron/core": "^0.1.1",
"lit": "^3.2.0"
},
"keywords": [
"positron",
"lit",
"dom",
"renderer",
"widgets",
"continuum"
]
}
37 changes: 37 additions & 0 deletions npm/lit/src/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* The one DOM-touching seam in `@positron/lit` — the analogue of the
* Rust wgpu outlier's `gpu.rs` (the file that talks to the device),
* kept separate from the pure `renderer.ts`/`host.ts` projection so the
* rest of the package stays headlessly testable.
*
* `domCommit` builds a {@link CommitSink} that commits a rendered
* `TemplateResult` into a live DOM container via Lit's `render`. This is
* the sink you pass to a {@link "./host".LitHost} in a browser; tests
* pass a capturing sink instead and never import this file.
*
* Importing this module is safe in Node (Lit's `render` reaches for
* `document` only when CALLED, not on import), but calling the returned
* sink requires a real DOM — which is exactly why it lives behind the
* seam and out of the headless test path.
*/
import { render } from "lit";
import type { TemplateResult } from "lit";

import type { CommitSink } from "./host";

/**
* A DOM-backed {@link CommitSink}: each committed template is rendered
* into `container`. Lit tracks the root part on the container itself, so
* repeat commits diff against the previous render — no manual part
* bookkeeping.
*
* @param container the live DOM node the widget owns — an `HTMLElement`,
* shadow root, or `DocumentFragment`.
*/
export function domCommit(
container: HTMLElement | DocumentFragment,
): CommitSink {
return (result: TemplateResult): void => {
render(result, container);
};
}
97 changes: 97 additions & 0 deletions npm/lit/src/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* `LitHost` — the DOM surface's {@link Host} implementation, the glue
* between the positron substrate (producing {@link ViewState} updates and
* consuming commands) and a {@link LitRenderer}.
*
* State flows down: `onState` renders the state to a `TemplateResult` and
* hands it to a **commit sink** — the seam that actually touches the DOM.
* Events flow up: `onEvent` maps a surface event to an optional command
* via an injected mapper (the app owns which DOM interaction means which
* command; the host stays app-agnostic).
*
* ## Why the commit sink is injected
*
* Committing a `TemplateResult` into live DOM is Lit's `render(result,
* container)` — it needs a real `Element`. Keeping that call BEHIND an
* injected sink is the same pure/impure split as the Rust wgpu outlier
* (`frame.rs` pure, `gpu.rs` touches the device): `LitHost` itself is
* DOM-free and headlessly testable (a test injects a capturing sink),
* while the real DOM commit lives in `./dom` (`domCommit`). The host
* never imports a DOM value — only the `TemplateResult` type.
*
* This mirrors positron-ratatui's Host, which owns the render→backend
* commit and the event→command translation for the terminal surface;
* `LitHost` is the DOM analogue.
*/
import type { Host, ViewState } from "@positron/core";
import type { TemplateResult } from "lit";

import type { LitRenderer } from "./renderer";

/**
* Commits a rendered `TemplateResult` to a surface. The production sink
* ({@link "./dom".domCommit}) calls Lit's `render` into a container; a
* test sink captures the result. This is the one seam that touches (or
* stands in for) the DOM.
*/
export type CommitSink = (result: TemplateResult) => void;

/**
* Translates a surface event into an optional command. `undefined` (the
* Rust `Option::None`) means "this event produces no command". The app
* supplies this; the host does not hardcode DOM-event semantics.
*/
export type EventToCommand<Event, Command> = (
event: Event,
) => Command | undefined;

/** Construction inputs for a {@link LitHost}. */
export interface LitHostOptions<S extends ViewState, Command, Event> {
/** The renderer this host drives (one per widget kind). */
readonly renderer: LitRenderer<S>;
/** Where rendered templates go — DOM in production, capture in tests. */
readonly commit: CommitSink;
/** How surface events become commands for the substrate. */
readonly toCommand: EventToCommand<Event, Command>;
}

/**
* The DOM {@link Host}: renders {@link ViewState} to Lit templates and
* commits them through an injected sink; maps surface events to commands
* through an injected mapper.
*/
export class LitHost<S extends ViewState, Command, Event>
implements Host<S, Command, Event>
{
readonly #renderer: LitRenderer<S>;
readonly #commit: CommitSink;
readonly #toCommand: EventToCommand<Event, Command>;
#lastRendered: TemplateResult | undefined;

constructor(options: LitHostOptions<S, Command, Event>) {
this.#renderer = options.renderer;
this.#commit = options.commit;
this.#toCommand = options.toCommand;
}

/** New state from the substrate — render and commit to the surface. */
onState(state: S): void {
const tree = this.#renderer.render(state);
this.#lastRendered = tree;
this.#commit(tree);
}

/** Surface interaction — map to a command for the substrate, or none. */
onEvent(event: Event): Command | undefined {
return this.#toCommand(event);
}

/**
* The most recently committed template, or `undefined` before the
* first `onState`. Lets a caller (or a test) inspect what was last
* rendered without re-running the renderer.
*/
get lastRendered(): TemplateResult | undefined {
return this.#lastRendered;
}
}
29 changes: 29 additions & 0 deletions npm/lit/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @positron/lit — the Lit DOM renderer for positron.
*
* The "web ≠ DOM as a peer paradigm" surface: a {@link LitRenderer}
* projects a `@positron/core` `ViewState` to a Lit `TemplateResult`, and
* a {@link LitHost} drives that renderer and commits the result into
* live DOM. It is the TS/DOM outlier of the same `Renderer`/`Host`
* contract the Rust wgpu backend implements for the GPU — one contract,
* many surfaces.
*
* Two halves, split by whether they touch the DOM (the same pure/impure
* split as the Rust wgpu outlier's `frame.rs` vs `gpu.rs`):
*
* - **Pure, headlessly testable:** `renderer.ts` (`LitRenderer` — the
* `Renderer<S, TemplateResult>` specialization) and `host.ts`
* (`LitHost` — glue with an injected commit sink and event→command
* mapper). A `TemplateResult` is a plain value; tests assert on its
* `strings`/`values` with no live DOM.
* - **DOM-touching seam:** `dom.ts` (`domCommit` — the sink that calls
* Lit's `render` into a container). Only browser code imports it.
*/
export type { LitRenderer } from "./renderer";
export type {
CommitSink,
EventToCommand,
LitHostOptions,
} from "./host";
export { LitHost } from "./host";
export { domCommit } from "./dom";
Loading
Loading