Tessera
Tessera is a native desktop UI host embedding Aster.
Agents submit ordinary Endor scripts through a local namespace API. Aster parses,
compiles and executes them; ui.page().render(root) selects a Tessera UI tree. Tessera extracts a
native tree, lays it out and draws it using the existing software renderer.
The initial page is empty.
Source and library integration
vendor/aster is an independent Git submodule pinned to a published Aster commit.
Cargo path dependencies directly use its frontend, compiler and core
(library name endor_core) packages, which bring in objects. The Aster workspace
and history remain independent. See vendor for initialization
and version updates, and src for the implementation map.
The runtime now uses Aster's Reader, Parser, AST, Compiler and VM. Tessera's former
experimental namespace / export scene / render parser has been removed.
Host UI integration lives in Tessera; Aster needs no Tessera-specific changes.
Lyra CLI and Hestia Server are independent hosts and are not application dependencies.
Native build and start
Use Rust 1.91 or newer. Initialize the source submodule following the dedicated codex-bot SSH identity instructions in vendor, then build:
git submodule update --init --recursive
cargo build --release --locked
./target/release/tessera
Cargo compiles the required Aster libraries together with Tessera. No separate Aster build, service or deployment is required. Local builds download dependencies from official crates.io by default. A temporary dependency mirror may be used when the official source is unavailable, without changing global configuration. Docker and automated tests are not used locally. The command above produces a native executable. To create a Finder-launchable development app on macOS, run:
sh scripts/build-macos-app.sh
Open target/release/Tessera.app. See packaging; this local
bundle does not include distribution signing, notarization or an installer.
The API listens only on 127.0.0.1:4737. Start integrations with its live guide:
curl http://127.0.0.1:4737/agent
Submit Endor
The active namespace starts as app.main:
GET /v1/namespaces/app.main/code
PUT /v1/namespaces/app.main/code
DELETE /v1/namespaces/app.main/code
Submit the complete example:
curl --request PUT --header 'Content-Type: text/plain; charset=utf-8' \
--data-binary @examples/page.endor \
http://127.0.0.1:4737/v1/namespaces/app.main/code
A minimal program is:
let heading = ui.text("Hello Endor", 2, "#242422")
let rectangle = ui.rect(240, 80, 12, "#DCE8FF")
let myui = ui.page()
myui.render(ui.column(24, 12, heading, rectangle))
The namespace comes from the request path. ui is a host global declared to
Aster's compiler and injected into the candidate VM. Top-level variables share
that VM's globals within this source submission. No main wrapper is generated.
Complete top-level inputs are separated by newlines; multiline calls and function
bodies are grouped before being handed to Aster's existing Interactive parser and
compiler. Expression continuations such as a member read on the following line
remain in the same input. Aster still owns every token, syntax rule, AST and
bytecode operation; the host only batches inputs and restores original AST offsets.
The older main-returning-View example and the richer
function example remain supported. A source consisting
entirely of function declarations uses Aster's Module entry and calls zero-argument
main. Mixed function declarations, bindings and expression statements use
Interactive inputs in source order. Type annotations remain syntactic in Aster;
Tessera checks actual UI values. The sample files are client-side material; the
API itself stores source by namespace.
UI host API
All arguments are positional. Defaults below describe omitted trailing arguments, not Endor keyword argument syntax.
| Member | Arguments and defaults |
|---|---|
ui.text |
String, integer size (default 1, range 1..8), color (default #242422) |
ui.rect |
Width, height, radius (default 0), color (default #DCE8FF) |
ui.column |
Padding, gap, followed by zero or more UI children |
ui.row |
Padding, gap, followed by zero or more UI children |
ui.spacer |
Size |
ui.page |
No arguments; creates a page with a bound render(root) member |
Colors are #RRGGBB or #RRGGBBAA. Dimensions are finite numbers in 0..1000000.
A namespace has one displayed page. If a script creates several pages or renders
more than once, its final successful render selects the visible root. Rendering
does not publish partial execution: a later error rejects the entire submission.
Aster keeps its exact numeric representation; conversion occurs only at the UI boundary. Text is shaped and antialiased with COSMIC Text and Swash, using installed system fonts and per-glyph fallback (including Chinese when a CJK font is installed). UI text uses a proportional sans-serif font; developer source uses a monospace font. The existing size argument remains a 1..8 multiplier: size 1 is 14 logical pixels with a 20-pixel line height; size 2 is 28/40. Row/Column layout uses measured text advances and multiline height. Device scale remains fractional, and glyphs are rasterized at the actual display resolution rather than enlarged. The application does not bundle proprietary OS fonts. Linux installations need fonts such as DejaVu Sans/Mono and Noto CJK; the remote test image installs these.
Candidate execution and publication
A dedicated worker performs source parsing, AST budget checks and compilation in
a fresh candidate VM. It executes each script input with shared globals (or module
initialization and zero-argument main for function-only source), checks the
selected UI root, extracts native data, builds and validates the frame, then
cleans up all VM roots. The window receives no VM handles. It relays out the owned
native tree during redraw and display scale changes without rerunning Endor.
Aster's scoped NativeContext allocates actual managed Block objects on demand. Blocks own managed Cells and data objects; child, history and forwarding edges are traced by the same VM collector. Native methods capture their owner through declared heap edges. No fixed node/page pool or separate mutable Arc graph remains. The desktop's tab registry holds open Pages. close releases that registry edge; referenced Pages remain usable and unreferenced cycles are collected between inputs. The bridge's diagnostic object index uses non-owning WeakObjectRef values.
Limits are 1 MiB source, 10000 visited AST nodes with a 64-level visitor depth, 4096 retained visual objects, 64 retained pages, 10000 expanded graph/layout visits and 16 KiB per text. Every execution has a 128-frame limit. Script inputs each receive an equal share of a total 1000000-instruction allowance; function-only module initialization and main execution each retain a 1000000-instruction budget. Repeated children count again during layout, preventing a small shared tree from expanding without bound. The build queue holds eight pending requests.
GET reads the last successful source while the worker executes. PUT waits for its result; a successful response includes submission and committed revision numbers. Parsing, compilation, execution, argument or layout errors return 422 with explanations and preserve the last successful document. Source diagnostics include line, column and byte ranges; VM tracebacks currently identify function starts and instruction offsets, not an exact source location for every instruction.
A newer accepted PUT invalidates older work. DELETE clears the document immediately and invalidates both queued and executing candidates. Superseded requests return 409 and cannot republish their page. A full or unavailable queue returns 503; rejected queue entries do not invalidate accepted work. Failed builds do not increment the committed revision. A worker-caught panic discards the entire candidate and returns 500, retaining the previous document.
This stage supports live mounted UI for both complete programs and Console inputs, with immutable window frames on the existing worker. Native mouse/keyboard activation and code-driven callbacks share onClick/click. Managed Mount objects expose each occurrence, and property dependencies and measurement caches avoid recomputing clean visual subtrees. Arrangement still traverses occurrences. Tab chrome/multiple windows, trait definitions and custom visual measurement/painting, window-size language inputs and process isolation are still being implemented. Worker threads, source budgets and VM instruction limits are not a complete untrusted-code sandbox.
Developer tools
Native visual protocols are under active implementation: Text/Rect/Row/Column,
container append/remove/replace/move, Page open/close/navigation, ui.present,
ui.state(value), ui.textOf(state, "value"), and button.onClick/click are available
in the persistent Console. Method bodies can call other methods without a trailing
return. Bound text tracks its source property; clean derived subtrees and unchanged measurements are reused. Buttons have a native background, focus ring and pointer/keyboard activation.
Page(), TextBlock(...), Row(...), Column(...) and Button(...) are callable managed type objects; Page.base is ContainerBlock and page.type is Page. isInstance(page, Block) and isSubtype(Page, Block) query nominal relations. Object and Type are explicitly bootstrapped in the same GC heap. Ordinary source class declarations, positional field construction, member assignment and inherited method dispatch are supported; source subclasses of concrete native visual types now reuse their real native allocator; abstract types remain unconstructible. Traits are still pending. Native visual methods now live on their nominal type tables and use the same receiver binding as source methods. See managed-types.endor, classes.endor and native-subclasses.endor.
Source native subclass construction takes native arguments first, followed by all inherited and own declared fields. For example, a TextBlock subclass with one field accepts CustomText("text", 2, "#242422", fieldValue); optional native arguments can be omitted before the required field suffix. Field storage belongs to the current shared Block content: replacement updates aliases and bindings and releases the previous fields through Aster. This is an implementation subset; field annotations are not yet runtime checked.
Events use block.on("click", callback) or block.on("click", callback, "capture"), returning a managed Subscription with disconnect() and isConnected(). A Block owns its connections; tokens refer to connections weakly and do not retain the target or callback. Dropping a token leaves the listener registered. Disconnection releases the callback and is safe to repeat.
block.dispatch(Event("custom")) returns the dispatched Event. block.click() creates a click Event and uses the same dispatcher as native activation. An optional Mount selects one occurrence: block.dispatch(event, mount) or block.click(mount). Without a Mount only the target is visited. With one, capture walks from Page through ancestors, then target capture, the object's onEvent(self, event) method and target listeners, then ancestors/Page in bubble order. The target method runs before non-capture target listeners. Event fields expose kind, target, mount, currentTarget, currentMount and phase (capture, target, bubble, then none). Source subclasses can override onEvent.
Event methods stopPropagation(), stopImmediatePropagation(), preventDefault() and isDefaultPrevented() control delivery or report cancellation; there is currently no additional host default action to cancel. stopPropagation permits remaining listeners on the current occurrence, while the immediate variant stops them too. Listener order is snapshotted at dispatch start; disconnection before a listener's turn skips it, new connections wait for the next dispatch. The existing onClick setter remains a single compatibility callback, called after other non-capture listeners on that Block. All handlers share the VM call budget, with an explicit 10000-delivery limit that reports failure instead of truncating propagation. Reentrant dispatch of the same active Event is rejected; a completed Event can be reused. See events.endor.
Visual objects also inherit onMount(self, mount) and onUnmount(self, mount) no-op methods, which source subclasses can override. Notifications occur once per actual occurrence transition: parents before children when attaching, children before parents when detaching. Updating text, color or geometry keeps an existing Mount and does not remount it. Shared content keeps its Block identity when replaced; hooks describe Mount attachment rather than each mutation of that content. Replacing a container child or switching Page produces the appropriate occurrence transitions, even when two Pages display the same root object. This currently covers the active presentation; native tab/window lifecycle is still pending.
Mount synchronization queues hooks after links and geometry are coherent. Hooks execute outside the session lock, under one VM budget per batch, after the source input or native event and before final publication. They may update objects or containers; resulting layout/notification work settles before a complete frame is published. A scalar Console result is evaluated before these deferred hooks. The rendering work budget explicitly fails on non-converging feedback. A hook failure after mount mutation stops that runtime and retains its previously displayed frame; failed PUT candidates preserve the old runtime. Hooks are never invoked by GC or by discarding an execution domain.
Saved detached Mounts expose mounted = false, cleared parent/child/sibling links and a weak Page query for cleanup; geometry, focus and hit-test operations reject them. Reopening creates new contexts and never revives an old saved Mount. layoutVersion is refreshed even when only paint inputs change. See mount-lifecycle.endor.
Native visual methods are now available on Block: measure(mount, Constraints(minWidth, minHeight, maxWidth, maxHeight)), arrange(mount, Bounds(x, y, width, height)), paint(mount, Bounds(...)) and hitTest(mount, Point(x, y)). The Mount must be live and belong to that exact Block occurrence. These query methods return managed MeasureResult, Arrangement and DisplayList objects, using the renderer's native geometry and painting functions. They do not publish geometry or pixels. Results are readable in Endor: result.width, arrangement.children.at(0).x, drawing.commands.at(0).color; indexed ObjectList results provide count, first and at(index), with traced references. Arrangement child bounds also carry their stable slot. Paint returns only that object's own commands; descendants are separate occurrences.
Native measurement finds intrinsic subtree size and clamps the returned size to the given constraints. Native Row/Column arrangement uses intrinsic child sizes, padding and gaps; it does not stretch children to fill the supplied bounds. Hit testing uses local coordinates against the current Mount dimensions. Coordinates/dimensions must be finite and remain within the native layout limit. Width/height and constraint ranges cannot be negative or inverted. The value constructors preserve fields as ordinary objects; methods validate their contents when used, and global annotation type checking remains pending.
invalidate("paint") requests a frame while retaining measurement caches; invalidate("layout") also clears affected intrinsic measurements; invalidate("content") also invalidates derived content and ancestor dependencies. The default reason is layout. Requests coalesce at the refresh boundary, cover all aliases/occurrences of the same current content and preserve Mount identities. Hidden objects do not force visible frames. See visual-protocols.endor. Source paint overrides now run during actual frame publication; measure/arrange/hitTest overrides during layout/input and contexts before first measurement remain future work, so this is not the complete custom visual protocol.
A source subclass can override paint(self, mount, geometry): DisplayList. The renderer invokes that method for each occurrence, after Mount synchronization and hooks, using current logical Bounds. Its commands replace that occurrence's native paint in preorder. Calling RectBlock.paint(self, mount, geometry) or another native base method obtains a fresh result that can be modified before returning. DisplayList(ObjectList(command, ...)) builds multiple commands; ObjectList accepts objects and has a 10000-member bound. PaintCommand fields use kind (roundedRect, strokeRoundedRect or text), x/y/color and the corresponding width/height/radius, strokeWidth, or text/size fields. Commands must pass the same native validation and a total 10000-command frame limit. See source-paint.endor, including a Button source method that updates the same tint field used by two occurrences.
Renderer-dispatched paint runs in a read-only Aster call: existing heap objects cannot be modified; fresh result graphs may be allocated and filled. Existing globals, model fields, containers, Mounts and types remain protected. invalidate is rejected in this phase. All overrides share one VM budget. Read dependencies are currently recorded per object, with versions used to detect changes without depending on bounded change-notification delivery. Native frames keep their existing dependency/cache behavior. Derived allocations do not produce application change notifications, preventing temporary results from causing redraw feedback. This protects managed heap writes; trusted Rust host implementations must also respect read-only calls for external state and I/O. Paint errors stop the affected runtime before publication; failed PUT candidates preserve the previously running heap and frame.
Toggle developer tools with Command-Option-I or F12 on macOS, Control-Shift-I or F12 on Windows/Linux. The compact Console, Sources, Network and Settings tabs share a draggable dock edge. The Sources tree/source boundary and Network's request/details boundary can also be dragged. Each content area has independent vertical and horizontal scrolling, draggable scrollbar thumbs, and trackpad support. Shift-wheel scrolls horizontally. Layout uses logical pixels and the actual display scale; resize clamps pane sizes to keep controls reachable.
Console is one live execution context: statements, function definitions and
expressions use the same VM and globals dictionary across submissions. There is no
Expression/Script selector. The current prompt, submitted commands and results
share one scrollable surface, with no separate input box, editor splitter or Run
toolbar. Typing appears at the prompt immediately; Enter executes it and the result
appears directly below. Shift-Enter inserts a newline. Each command and its result
are followed by a thin neutral horizontal separator. Input focus uses a text caret,
without a blue field border or hover fill. The prompt, history and caret share the
same scroll offset; typing and cursor navigation reveal the current insertion point.
Create objects with bare assignment or let, then mount them when ready:
title = ui.text("你好,Tessera", 2, "#243047")
At this point the object exists in Console memory and the page stays unchanged. Later inputs can refer to it:
note = ui.text("这是 Endor 渲染的页面", 1, "#637082")
card = ui.rect(320, 100, 16, "#DCE8FF")
page = ui.page()
page.render(ui.column(24, 16, title, note, card))
Ordinary values keep normal name assignment. UI references opt into Aster's
managed ObjectPayload::assignment_cell protocol: assigning another UI object updates the
existing shared reference and preserves its identity. Aliases and mounted parents
therefore observe the same replacement, including across function-local slots.
For example, after other = card and page.render(other), assigning
card = ui.rect(640, 200, 32) updates the page without another render. This is
shared-reference semantics, not variable-name tracking. Assigning a non-UI value
uses normal value assignment; it does not unmount the old UI reference.
page.render(root) mounts a live root and selects its page. Changes to mounted
references publish a new frame after a successful input; unrelated objects do not.
page.unmount() releases that page's mount and clears the frame if it was active.
Replacing a container updates its whole mounted position. Repeated aliases are
allowed; replacements that would create a cycle or exceed graph limits are rejected
before mutation. Scalar constructor arguments are evaluated once, as ordinary values.
Bare assignment creates/updates a name; let remains a declaration. Aster owns this
syntax, AST and bytecode; Tessera never inserts let or replays history. The actual
last value is also bound to _. Text editing supports cursor positioning, arrow
keys, Home/End, grapheme-safe deletion, select-all, macOS copy/cut/paste and IME.
Up/Down recall single-line history; Command/Control-Up/Down recall multiline history.
Console sends POST /v1/namespaces/app.main/repl as UTF-8 text/plain. Responses
include value, session/input IDs, bindings, persistent: true, rendered flag and
revision. Before the first mount, object creation never publishes a page or advances revision.
After mounting, changed shared UI references publish automatically. The last explicit
render in an input selects the active page, only after that input succeeds. Syntax and
compilation errors run no statements; runtime errors can retain earlier successful
statements from that input, while preserving the previously displayed page.
Reset session (DELETE .../repl) drops the VM and all bindings/objects, preserving
the displayed immutable frame. Clear only hides the transcript. Limits per session
are 256 executed inputs, 1 MiB transcript, 10000 cumulative AST nodes, 4096 UI nodes
and 64 retained pages. Unreachable objects and cycles are reclaimed between inputs; reset releases the whole session. Code and objects are never stored on disk.
Existing PUT .../code remains a complete program replacement with a fresh VM;
function-only source still invokes main. On success the new VM remains live for Console and GUI events; a failed replacement preserves the old VM. Accepted PUT/code DELETE invalidate older queued Console inputs. The compatibility POST .../evaluate
endpoint remains isolated for existing clients, but is not a Console mode. All use
the bounded eight-entry worker queue. GET /code returns the displayed source: exact
PUT source, or annotated Console transcript after a REPL render, distinguished by
X-Tessera-Source-Kind. The transcript shows actual executed input and can contain
failed statements; it is not guaranteed to replay as a standalone program.
External HTTP activity appears in the same transcript. Unsupported language operations (currently numeric arithmetic bytecode) show actual diagnostics. Transport failures appear in the Console footer and restore an otherwise empty draft.
Sources shows the full source snapshot or Console transcript with line numbers, Aster's
AST and selected source ranges. Selecting an AST node scrolls to its location while
keeping the entire file available. Cloud files has a separate file navigator and
source viewer, initially disconnected. The CloudSourceProvider boundary in
src/sources.rs supplies file identities/paths and full source on open; there is no
cloud backend, fake file listing or source persistence in this release.
Network observes only HTTP requests handled by this Tessera process on
127.0.0.1:4737, including pending requests, method, path, status, server duration,
request/response headers and bodies. Filter matches method, path or status. Clear
clears the Network view without changing the document or Console transcript. A bounded
ring retains 80 requests with explicitly marked 32 KiB body previews. Request
headers are allowlisted to Content-Type and Content-Length; credentials and cookies
are not captured. No external browser or operating-system traffic is represented.
Console and Network share the bounded capture ring and have independent clear
markers. Clicking a Console record opens its Network details and restores that view.
Settings applies six accent presets or a custom six-digit hex color immediately. Very bright custom colors are darkened to keep accents visible on white. Execution separators, selected tabs and focus indicators use the accent, while text keeps its readable neutral/error/success colors. Settings and source remain in memory only.
Management controls use the internal Tessera System UI widget library, independent of Endor-controlled page widgets. Buttons, tabs, toggles, rows, input fields, color swatches and drag handles share hover, pressed, selected, focus and disabled presentation. Buttons activate on release inside; dragging outside cancels. Tab/Shift-Tab navigate enabled controls and Enter/Space activate focused buttons. Plain Tab still indents at the Console prompt. Empty Clear actions are disabled. Splitters are painted and registered above all pane backgrounds, toolbars and status bars, so their entire length stays visible and draggable. macOS clipboard reads and writes explicitly use UTF-8 even when launched from Finder, and decoding errors are surfaced.
Validation
Local native builds and window runs are allowed; do not run local automated tests or Docker. Remote CI on ces-control runs the existing renderer checks and the new Aster script/module execution, page rendering, UI layout, source-range, lifetime, budget, failed-update and deletion-invalidation regressions, plus Clippy. The API regressions use actual HTTP requests, including DELETE while the worker is occupied and a queued PUT is superseded. Linux CI does not verify macOS window presentation. Aster has no standalone CI/CD; Lyra CI explicitly validates the pinned engine itself. CI uses official Cargo sources and standard HTTPS checkout without custom SSH secrets. Font checks cover proportional/monospace advances, fractional scaling, multiline height, Chinese fallback, grapheme-safe truncation, antialiasing and alpha blending.