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 complete visual type/effect contracts 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/insert/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(...), ScrollBlock(...) 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. Explicit host-registered capabilities are available; formal trait syntax and full type/effect checks remain 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. Mount lifecycle follows the active presentation in each native window.

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.

Outside a renderer layout call, native measurement queries find intrinsic native subtree size and clamp it to the given constraints. During layout, native base methods use the staged measured children of that occurrence. Native Row/Column arrangement uses 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 with declared-field type checks; methods also validate their geometry when used. Whole-program static typing remains pending.

Visual, EventTarget, Children and Presentable are explicit managed Trait definitions. Block declares Visual (measure/arrange/paint/hitTest) and EventTarget (on/dispatch/onEvent); ContainerBlock declares Children (append/insert/remove/replace/move/children), inherited by Page. ui.supports(value, capability) queries an instance; ui.implements(type, capability) queries a type. ui.implement(type, capability) checks required methods and establishes an explicit relation. Matching member names alone never declare a capability. Aster checks source positional parameter counts, including self, when registering, creating subclasses or replacing methods; declaration and ancestor updates also validate existing affected descendants. Source and native positional intervals are checked against the canonical call count, including self; native methods without metadata cannot declare a capability; source argument/return annotations are checked at invocation as described below. Typed requirements also compare parameter/return types and known native effects. Runtime calls check actual results across delegation. Conservative bytecode and current call-graph proofs recognize ReadOnly source functions; general effect inference and formal trait syntax remain pending.

A plain source object can declare Presentable by implementing present(self, window: Window): Block and calling ui.implement(MyType, Presentable). ui.present(value) invokes this method with the calling window and checks the returned actual Block; ui.open(value) also opens the result in a Page. Block values pass through directly. The existing summary viewer remains the fallback for unregistered values, and merely viewing a function does not execute it. Presentation runs through the same bounded VM call sequence; there is no recursive host evaluator. A source adapter can keep the original object through fields/callbacks/bindings; presentable.endor shows a source-preserving view and GUI calls to the original object's method. Presentation methods are ordinary calls; visual measure/arrange/paint/hitTest still run read-only and check their nominal results at runtime. Source implementations of EventTarget.on/dispatch must provide the contract's canonical 3/2 arguments after self; native methods retain their existing optional convenience arguments.

ScrollBlock(width, height, child) is a ContainerBlock with a bounded viewport. Its axis attribute defaults to "both", preserving unconstrained content measurement. "vertical" wraps/restricts content to the actual occurrence viewport width minus padding, while "horizontal" restricts its height. The disabled axis has a zero offset/limit, including after a live axis change. Source-arranged viewport dimensions participate in remeasurement. Child stacking remains Column-like; use a Row child for horizontal composition. See scroll-axis.endor. It also supports inherited child operations and padding/gap. Its width/height are managed attributes and can be bound or assigned. Each attached Mount owns its scroll offset; two occurrences of the same ScrollBlock scroll independently. Moving/remounting content creates a new Mount and starts at zero. Changes to content or viewport clamp offsets to the new limits.

mount.scrollTo(x,y) and scrollBy(dx,dy) update that occurrence; multiple relative requests in one input accumulate. scrollPosition() returns a snapshot with x/y/maxX/maxY from the last committed layout. mount.scrollIntoView() minimally adjusts all scrollable ancestors, and requestFocus() uses it too. Requests settle at the same successful input/event boundary as other UI changes. Detached mounts and mutations during read-only rendering are rejected. Wheel input selects an eligible scrolling ancestor of the foremost visible occurrence, with outer scrolling when the inner range is exhausted; Shift+wheel selects the horizontal axis. A diagonal gesture currently goes to one eligible container rather than splitting axes across ancestors. Scrollbars/thumb dragging, touch inertia and PageUp/PageDown are not yet implemented.

Native/source painting, focus outlines, pointer selection and Mount.hitTest share the final translated geometry and intersected ancestor clip rectangles. A clipped descendant keeps its identity and state but cannot receive pointer clicks outside the viewport. This does not remove it from the managed object graph. Paint x/y may be negative within the existing coordinate budget after projection; dimensions and radii remain nonnegative. See scroll.endor for a shared object tree in two independent native windows.

block.bind(property, compute) binds an existing managed field to a zero-argument function and returns a Subscription with disconnect()/isConnected(). Targets are Block fields, including source-defined fields; arbitrary non-Block objects are not binding targets yet. Native visual fields are now ordinary attributes: TextBlock.text/size/color, RectBlock.width/height/radius/color, Spacer.size and container padding/gap. Direct assignments and native setters feed the same visual snapshot. Text size remains 1..8, geometry must be finite/nonnegative and within the existing limits, and colors use the existing color parser. Legacy textOf still owns its derived text until setText or a text binding replaces it.

The target's managed content owns its bindings; each binding traces the actual compute function and its captures. Tokens and the host registry hold weak references. Rebinding a field disconnects its previous binding; disconnecting leaves the last value in place. Hidden but still-referenced Blocks update at successful input/event/viewport stabilization boundaries. Functions run read-only against existing objects; they may construct result objects. Dependencies are recorded from actual VM reads, with object-level versions and conservative global-dictionary invalidation. This replaces the earlier candidate API's explicit source list. Binding results are settled before frame publication, including across windows and after lifecycle changes. Values read earlier in the same input precede this batch boundary. Explicit dependencies and property-level dependency precision remain future work.

One Block can have up to 128 active bindings. Each computation has 100000 instructions/128 frames, and a refresh rejects after 10000 computations if dependencies do not stabilize; it reports an error and preserves the published picture instead of silently truncating propagation. A live binding-computation failure stops that runtime. General type/effect checking remains pending. See property-bindings.endor for actual Endor source.

Declared fields now use the same runtime type-check switch as source functions, enabled in Tessera. Ordinary and native subclass constructors validate field arguments before allocating/opening native objects. Subsequent source assignments, native set_attr calls and binding writes check the actual declaring field before changing its storage; an error preserves that field's old value. Declared fields cannot be deleted while checks are enabled. Inherited fields use their declaring ancestor and environment. The declaring class name means that actual definition, so REPL redefinition preserves old self-type fields; other names resolve dynamically from the declaration's globals/builtins. Shared Block forwarding selects the current content's type and storage. See field-contracts.endor.

These are checks at construction/write boundaries. Enabling does not scan old objects, and changing a referenced object's shared content does not revalidate every field that refers to it. Trusted hosts still own low-level payload/storage/attach operations. Complete initialization, local/captured type-name resolution, override variance, static and effect typing remain 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 measure/arrange overrides now participate in actual layout with structural Mount contexts available before first measurement. Source paint and hitTest also run in the frame/input pipeline. Full parameter/return type and effect contracts, more precise context dependencies and additional container allocation policies remain future work.

A source subclass can override measure(self, mount, constraints): MeasureResult and arrange(self, mount, geometry): Arrangement. The persistent renderer creates stable Mount identities and parent/child/sibling links before measuring. Measurement proceeds bottom-up with the current logical viewport as the Page constraints (headless callers default to 0..1000000); native parents and explicit native base calls use the current occurrence's measured child sizes, including source overrides. Arrangement proceeds top-down; each child Bounds must be assigned exactly once using its stable slot, with nominal result types and finite geometry checked. Source arrangements can resize or move children; omission, duplicate/foreign slots and invalid geometry reject the frame. See source-layout.endor.

Source layout now repeats bottom-up measurement and top-down arrangement inside one read-only VM call until input constraints, desired sizes and all allocated bounds repeat unchanged. Source-arranged dimensions that differ from a child's measured size become exact constraints for that axis during this solve. Native parents then account for the newly measured size. A source container measurement that differs from native measurement also constrains its content on the relevant policy axis. Selected limits remain until this solve finishes; the next input/resize starts fresh. Ordinary native intrinsic-size policies are retained. See layout-negotiation.endor.

The solver detects a previously visited nonconsecutive state as a cycle. It has no fixed propagation/pass cutoff: the whole solve shares the existing 1000000-instruction/128-frame VM budget, and exhaustion is an explicit failure. It reuses one bytecode driver frame rather than nesting one frame per pass. All measurement phases clear provisional geometry from every Mount, and only the complete settled result is committed. Failed candidates preserve the active program; live layout failures stop the domain and preserve its published picture. This is not a resumable layout scheduler or an optimal/general flex solver.

ui.stats().layoutPasses reports passes for the latest layout (native-only fast path uses one). Measured/arranged/cache counters include the work of all solve passes. Source annotation validation is distinguished from actual content reads: merely checking a Mount parameter's type no longer disables measurement caching, while program reads of Mount layoutVersion/geometry still bypass it conservatively. Other type/validation dependencies remain version-checked. Runtime type checks stay enabled.

Mount.bounds() rejects reads during measurement, including old geometry on an existing Mount. The current occurrence's allocated Bounds become available while its arrange method runs. Structural contexts are prepared before layout, geometry is committed after successful layout, and onMount/onUnmount notifications then run before paint/publication. Both layout phases share one read-only VM call and instruction budget; temporary results can be modified, existing business state cannot. Read versions are captured before hooks, so a hook changing a measured dependency schedules another pass before publication. Dependencies are currently tracked per object. Source measurement results are cached by Mount identity, method, child sizes, constraints and actual object reads, including decoding an existing returned MeasureResult. Cache entries contain borrowed identities and versions, adding no GC roots. Mount-reading measurements currently bypass the cache conservatively because object-level tracking cannot distinguish layoutVersion/geometry from stable structure. Global dictionary writes or changes to another field on a read object can also cause conservative misses. Native measurements keep their shape cache, and arrangement still visits each active occurrence. ui.stats() exposes sourceMeasureHits/sourceMeasureMisses; diagnostic statistics are rejected inside read-only rendering because allocation/count observations are not stable layout inputs.

Window: Object represents a native window resource. Window() creates another window in the same Aster VM, up to 16 open windows per runtime. ui.window() returns the current event/render window, with the main window as the top-level Console default. page.window() reports its actual owner or nil. page.open(window) and page.moveToWindow(window) move the same Page, including navigation and business references, to that window. Omitting the open target keeps an existing owner; an unowned Page uses the calling view. page.detachToWindow() creates a new Window, moves the Page there and returns the resource. Dragging a Tab away from its strip invokes that same Page method. Moving a Page unmounts the old occurrence and creates destination Mounts. window.pages() returns a traced ObjectList in open order. See windows.endor.

container.insert(position, block) inserts at a zero-based position from 0 through the current child count and returns a new stable slot. Existing slots and attached Mount identities survive insertion. Invalid positions and cyclic child graphs fail before replacing the child list. page.moveTab(position) moves the same Page to a final zero-based position in its owning window; a code reorder does not implicitly activate it.

Native dragging starts after 12 logical pixels and shows a blue insertion marker. Same-window drops call the actual Page moveTab and activate; cross-window drops first call moveToWindow. Source overrides participate. An override that declines or redirects the window move prevents the remaining destination reorder. Page identity and Page-owned tasks survive movement, while a cross-window move replaces Mounts and cancels old Mount tasks. Invalid source/target generations, layout versions or unpublished tab orders are rejected before callbacks. Callback failure stops the domain; earlier writes do not roll back. Escape cancels a drag. Overflow drops use visible gaps; drag auto-scroll remains pending. See tab-placement.endor.

window.moveTo(x, y) queues a native outer-window origin in signed physical desktop pixels, within +/-1000000 per axis. A detached tab uses the release point and saved grab offset to place its new window. The window manager may adjust placement. Global coordinates are required for cross-window drops; unsupported native placement reports a diagnostic. This host tracks its own focus order when resolving overlapping windows; external-app occlusion and mixed-DPI dragging have not been fully verified.

window.close() and a secondary native close button call the same managed method. Closing first removes Page ownership, then commits unmounts and removes that context from the runtime registry; the native resource disappears after successful publication. Retained Pages can be opened in another live Window; otherwise Aster reclaims them normally. Window resource objects may remain referenced after closing, but reject resource operations; id/pages remain queryable. A retained leaf Mount uses a weak context link and cannot keep a closed window alive through its query methods. The primary shell still defines the application's lifetime: closing its OS window exits Tessera. Full application-exit notifications remain follow-up work.

A secondary window can also be closed after a callback/rendering failure or Console reset. A stopped VM uses host teardown to detach Mounts and release window-owned Page roots without invoking source close/unmount callbacks; it remains stopped. A preserved picture from a discarded generation is removed without touching a fresh Console VM, even when both generations use the same numeric window ID. Secondary viewport inputs are likewise scoped to their runtime generation. If host teardown fails, that VM is discarded and remaining pictures have input disabled. Native initialization and software-surface failures are reported instead of panicking; a failed secondary creation leaves its managed resource inspectable and closable from Console. Automatic retry and complete application-exit notifications remain future work.

Each window has its own Presentation, layout/Mount/focus state, caches and native viewport. A single VM change batch fans out to all views, all affected frames settle before publication, and shared business references are never copied. Frames and native inputs carry window/session/layout identities; stale requests are rejected. Existing event clients default to window 1, exposed by X-Tessera-Window. Secondary native resize feeds the same worker and updates only its window constraints. Console reset preserves all old pictures with input disabled; replacement sessions discard the old native resources before projecting new resources with the same numeric IDs.

Open Pages appear in the native shell tab bar. Set page.title to label a tab; omitted titles use their open-order number. Clicking a tab or its close control calls the actual Page's activate() or close() method in the existing VM; source overrides participate. Ctrl+Tab / Ctrl+Shift+Tab cycle pages, and Cmd/Ctrl+W closes the active page when the page has keyboard focus. Overflow arrows expose additional pages. Closed Pages follow managed references and GC; a retained Page can be reopened. The tab projection contains opaque session-local IDs and text, with weak reverse lookup in the runtime, and adds no Page roots. Requests carry the published layout version; an old tab event is rejected. Switching creates new presentation Mounts while preserving the business objects. See tabs.endor.

The macOS title strip occupies 44 logical pixels and shares its row with the native window controls. The left rail and title strip form one neutral chrome surface. Content is inset by a 56-pixel rail, an 8-pixel top gap, and 8 pixels at the right and bottom. Page constraints use the remaining height; published Mount geometry and event coordinates remain relative to the content area. Window viewport snapshots continue to report the full native inner size. The host translates drawing and native pointer coordinates at this boundary. Console reset preserves the last picture with input disabled until a new live frame is published.

window.resize(width, height), setTitle(text) and focus() project requests to the GUI thread. id() returns an opaque runtime resource ID and viewport() reports actual logical dimensions. Requests are asynchronous and coalesced by kind; queried dimensions change when the native window reports them, and platform bounds can adjust requested sizes. This host accepts sizes from 1 to 4096 logical pixels per axis, with a native minimum of 720 by 480. Titles are UTF-8 text up to 4096 bytes with no control characters. See window.endor.

Native creation and commands remain local until the input/candidate succeeds. Failed inputs discard pending commands; failed PUT candidates preserve the live native window. Accepted projections carry the runtime session, so replacement/reset/discard clears old requests. GUI publication and native operations are serialized through the namespace boundary. Read-only rendering rejects window side effects. The private transport queue contains only resource IDs/scalars, introduces no Page roots, and is drained without incrementing object versions. Tab UI, move/detach and secondary native windows use the same resource pipeline.

The native window publishes its current logical content size to the existing VM worker on startup, resize and scale changes. The worker coalesces the latest size, updates the managed viewport input, and refreshes the same runtime without adding Console source/history entries. ui.viewport() returns a fresh managed width/height snapshot; reads during rendering depend on the private input object. User changes to a returned snapshot do not resize the window. Unchanged sizes cause no frame; a fresh or unopened Console runtime updates its inputs without replacing a preserved displayed page. Failed resize layouts stop the affected runtime and preserve the displayed frame.

The default Column/Button policy propagates available cross-axis width minus padding and leaves height unconstrained; Row propagates available height and leaves width unconstrained. Page uses both viewport bounds. Native and source desired sizes are clamped to their supplied constraints, while source arrange can still allocate explicit child bounds. The window clips overflow and pointer targeting to its extent; TextBlock wraps to its available width; source layout now settles constraints over repeated measurement/arrangement passes as described below. See layout-cache.endor. GET code also exposes X-Tessera-Viewport-Width/Height headers without evaluating source.

Native pointer clicks now resolve source hitTest(self, mount, point): Bool overrides in reverse paint order, then dispatch through the existing capture/target/bubble route. The Point is local to that occurrence; point.inside(bounds) uses a half-open rectangle and point.inCircle(cx, cy, radius) includes the circle boundary. Each candidate is first restricted to its Mount rectangle, so hit shapes can refine that area, but cannot expand it. A rejected target falls through to another interactive occurrence, including a parent. Ordinary Blocks become interactive through a live click subscription or a source onEvent override; disconnecting the final click subscription updates that status. See source-hit.endor.

The native app sends both press and release positions with the click. Both targets are resolved against the current published frame and heap when the release is processed; only matching Mount identities dispatch a click. These selectors share one read-only VM call and budget, and must return Bool. Callback execution starts after the read-only scope has ended. This is semantic click handling, not a raw pointer-down/up event stream or pointer capture. Keyboard/programmatic activation retains its semantic target and bypasses spatial filtering. API pointer events carry point: [x, y] and press_point: [x, y] together; GET code exposes X-Tessera-Session and X-Tessera-Layout-Version headers for clients to obtain current metadata without evaluating more source.

Native TextBlock measurement now uses the occurrence's maximum width, with word wrapping and glyph fallback for long words. Width changes affect measured height and propagate through native Column/Button sibling placement. The shared text cache includes wrap width; the native shape cache also includes the incoming width. Native base measure queries use the supplied constraints both during source layout and when called directly. Shared Blocks retain separate geometry per Mount and can wrap differently in different windows. See wrapped-text.endor.

Native TextBlock painting carries its allocated width/height into the immutable frame and clips glyphs to those bounds as well as any enclosing scroll clip. Measurement and drawing use the same font/shaping implementation; drawing shapes at the device font size. Zero-width or zero-height paint bounds draw nothing. Management UI text keeps its existing single-line/explicit-newline behavior.

Source PaintCommand text may supply both wrapWidth and wrapHeight as finite nonnegative logical dimensions up to 1000000. Omitting both preserves unwrapped text; supplying only one is rejected. TextBlock.paint returns these fields, so a source override can preserve or explicitly change the boundary. Changing paint bounds does not renegotiate layout: source arrange supplies child Bounds and changed dimensions participate in the layout solve. Bounded flex allocation is described below. ScrollBlock's axis policy now controls cross-axis wrapping and scrolling. Fractional-DPI wrap boundaries need broader platform validation.

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.

The application starts with one managed New tab. The + button and Cmd/Ctrl+T create additional managed Pages; Cmd/Ctrl+W closes the active Page. New tabs expand from left to right while existing tabs interpolate to their new bounds when space becomes limited. Hover and selection use the same capsule geometry, and animated pointer hits use the rendered bounds. Source and page identity remain in the VM; chrome animation retains only projected scalar IDs and rectangles. Tabs are capped at 208 logical pixels wide, with a 40-pixel strip and equal 4-pixel vertical insets. The leading page symbol and close control have matching slots and edge spacing.

The default page is implemented in src/ui_bridge/start_page.endor, compiled once per VM into a private namespace. Its buttons create a Page, open a working shared counter, or show composed Blocks. ui.newTab() constructs the same ordinary Page used by native + and the keyboard shortcut. Layout, callbacks, state and bindings are Endor objects; the host does not paint an introduction over the guest page.

Console, Sources, Network and Settings are independent managed Pages with separate view modules and window-local input/focus/scroll/selection state. They are not opened by default and have no permanent rail/menu launchers. Their future discovery UI is intentionally deferred. Existing integrations can call ui.developerTools("Console"), "Sources", "Network" or "Settings". F12, Cmd-Option-I and Cmd-Shift-I on macOS, or Ctrl-Shift-I elsewhere, open/focus Console. Page closure, placement and GC use the ordinary Page methods. Sources supports read-only drag selection, select-all/copy, and Shift+arrow selection; source line numbers are excluded from copied text. The Console prompt only captures pointer hits over its actual input lines.

See the UI requirement audit for the full change scope, design references, native verification and outstanding evidence.

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.

Source function types

Tessera enables Aster runtime checks for source parameter and return annotations. Object accepts any live value, Unit accepts nil, Number, String/Text and Bool check their value kinds. Function accepts source/native functions and bound methods; callable class definitions remain Type. Newly created functions share the actual managed Function type, so isInstance(callback, Function) works. View is retained as a global compatibility alias for Block.

Other annotation names resolve through the function's own globals and captured builtins on each invocation. A real Type requires nominal inheritance; a Trait requires an explicit implementation. A method's own declaring-class annotation uses its receiver's actual owning definition, allowing saved bound methods on old instances to survive a REPL class-name redefinition. Other global type names are not frozen when a function is defined. TypeMismatch reports the function, parameter/return and expected/actual names. Unknown annotations and argument mismatches reject before the body; a bad return does not roll back completed writes. The per-call return definition remains rooted until success/error cleanup.

See function-types.endor for actual Endor source. Declared-field construction/write checks are described above. Static inference, generics, static effect typing and complete native signature coverage remain pending. Native APIs retain their domain-specific value and resource checks.

Native methods publish positional argument intervals through Aster CallArity. The VM checks counts before invoking them, and trait registration/descendant updates verify that each source or native method accepts the canonical argument count. Optional/default parameters are included; bound receiver counts are removed for callback queries. Generic Type construction still uses its host constructor rules. CallSignature adds optional resolved parameter/return types and host effect metadata, as described below.

Native typed contracts now cover measure(Block, Mount, Constraints): MeasureResult, arrange(Block, Mount, Bounds): Arrangement, paint(Block, Mount, Bounds): DisplayList and hitTest(Block, Mount, Point): Bool, plus Point geometry queries and Window, Block/Container/Page, Mount, Task, Event, Subscription and ObjectList.at methods. The explicit receiver is checked before binding dispatch. Aster checks types before invoking the implementation and checks actual returns, including final results delegated to Endor functions. Signature definitions are traced managed edges; pending delegated return checks retain execution roots and use the existing VM frame/budget machinery. Wrong returns release temporary results but do not roll back earlier writes.

Host CallEffect distinguishes ReadOnly, Delegated, Mutating and External. Delegated means the native wrapper itself is read-only while its continuation has dynamic effects; ui.present uses this category and the actual callback remains in the same VM scope. It is not a static purity proof. A declared mutating/external call is rejected before invocation during read-only rendering; declaring ReadOnly does not bypass the existing heap write barrier. Legacy missing metadata remains unknown, and trusted native code still must obey its I/O contract. CallType supports single types and flat nonempty unions, retaining every branch definition. Page.open accepts Window or nil, Page.window returns Window or nil, and Block.on accepts Bool or String for its optional phase. Mount parent/child/sibling/Page queries have nullable results; bounds() returns an actual Bounds object so Point.inside(mount.bounds()) composes directly. Scope/resource eligibility, numeric ranges and phase names keep their domain checks. This adds native contract metadata, not Endor union syntax.

Public ui functions, captured state set/increment methods and native constructor factories now publish signatures. Bootstrap fills immutable signatures after all referenced types exist, preserving function identities. A native factory first separates the requested subclass fields from native options; its envelope checks the Type receiver/general result/effect, then the concrete options and declared fields are checked before allocation or window effects. Optional Rect/Text options and variadic Row/Column children therefore do not consume subclass fields. Event/ObjectList factories retain their own field/member validation. Generic Type construction still has no single fixed reflected signature. See constructor-contracts.endor.

Source overrides now check contravariant parameters and covariant returns at class construction, method replacement and before invocation with dynamically resolved annotations. Ancestor replacement checks overriding descendants before mutation. Forward names may defer until invocation, where unresolved or incompatible contracts fail before the body. Known native effects may only shrink; source effects remain unknown and read-only execution retains its barriers.

Visual, EventTarget, Children and Presentable now use typed Trait requirements. Registration checks parameter/return substitution and known native effects before recording the declaration, including existing descendants. Runtime calls recheck dynamic annotations and final Trait results, including delegated source/native returns. Concrete implementations retain wider parameters and optional convenience forms. Source functions outside the conservative read-only bytecode proof remain unknown; general effect inference remains pending. See method-contracts.endor for real Endor widening parameters to Object and returning a MeasureResult subclass. Existing visual-protocols.endor and layout-negotiation.endor exercise the typed visual methods with actual Endor code.

Event listeners and onClick callbacks must be Functions with known metadata accepting zero arguments or one Event. If both forms work, the dispatcher passes Event. Binding computes and Task callbacks must accept zero arguments, checked before replacing a binding, adding a connection or scheduling work. Native and source callbacks follow the same rule. See native-signatures.endor for real native one-argument and zero-argument handlers. Event and ObjectList subclass allocators also check declared field suffixes before allocation.

Owner-managed asynchronous tasks

ui.after(owner, key, milliseconds, callback) returns a managed Task and runs its zero-argument callback on the existing VM worker after the monotonic deadline. The callback must return Unit. Delay is an integer from 0 to 86,400,000 ms; zero means a later worker turn. The worker currently polls at up to 100-ms idle intervals, so deadlines are minimum delays rather than exact timing guarantees. No timer thread reads or changes VM objects. Ready work shares the same heap as code and native events; bindings, lifecycle and all affected windows settle before publishing a new frame.

Owners can be Blocks, attached Mounts, open Pages, live Windows, or ordinary objects with attribute storage. Use ui for application lifetime. Each owner owns a traced task scope, and tasks trace callbacks/captures. The scheduler and task-to-owner link are weak. Hidden but still-referenced objects keep updating; keeping a task token alone does not keep its owner alive unless the callback explicitly captures it. A source function's normal global environment remains a managed capture.

The same owner/key supersedes its previous pending task, using the actual managed task identity as the request generation. task.cancel() cancels pending work and returns whether it was active. task.active() queries eligibility; task.status() returns pending/running/completed/cancelled/failed. Cancellation and completion drop the callback and remove the owner slot, including when the caller retained the Task. Already running synchronous callbacks finish before the next input can be processed.

Closing a Page cancels its tasks and its Mount tasks immediately, including closing and reopening in one input. Switching away or replacing content cancels detached Mount tasks; Page and ordinary Block tasks remain. Moving a Page between windows preserves Page tasks and cancels old Mount tasks. Closing a Window cancels its tasks and the Pages it owns. Reset, DELETE and successful program replacement discard the old scheduler with its VM; the worker validates submission/runtime generation before publication, so stale work cannot update the new heap. A failed replacement keeps the previous runtime and its tasks. Callback errors stop the shared runtime, cancel remaining tasks and preserve the last valid picture; completed writes do not roll back. Scheduling/cancelling in read-only render/binding calls is rejected.

Limits are 64 pending tasks per owner, 1024 per runtime, and 64 ready callbacks per worker poll, each with 128 frames/100,000 instructions. Remaining ready work stays pending for later polls. Timers and asynchronous UTF-8 file reads use the same task lifecycle. Network providers, await/coroutines and interrupting a blocked native callback remain unimplemented. See owned-tasks.endor for real Endor code and native buttons.

Typed capability methods cannot be shadowed by instance attributes. Aster checks ordinary writes, type attachment and capability declaration before changing state; queries and method invocation reject backing-store conflicts introduced through trusted host access. The render cache subscribes to these method attribute keys and validates dirty Blocks before native dispatch, so direct storage changes cannot silently leave source calls and rendering on different methods. Removing the conflicting attribute restores the instance. Class method overrides and event subscriptions keep their existing APIs.

Aster ObjectStore::call_effect queries known native or conservatively proved source effects, including bound methods, without executing the function. Proved source ReadOnly methods constrain known native overrides. Shared local assignment, unresolved dynamic calls and other unproved instructions remain unknown; the runtime read-only barrier still follows actual calls and delegation. This does not establish a full source call graph or prove trusted host I/O safe.

Known source call graphs can now be proved ReadOnly using current globals/builtins, closure Cells and resolved receivers. Analysis follows control-flow joins and recursive call graphs with explicit queues, executes no function body and retains no cross-query cache. Rebinding triggers fresh analysis. Unresolved targets/results, shared assignments and calls outside the ReadOnly proof remain unknown; actual runtime barriers and native target checks still apply.

Native layout/paint fast paths and the default event classification inspect the actual ContextNativeFunction implementation and operation. They do not use the current mutable Block method table as an intrinsic baseline. Replacing a base method or a function payload therefore takes the new implementation path. Visual snapshots depend on each protocol function's version and include it in the protocol key, so payload changes at a stable function identity invalidate cached visuals and interactivity.

Block.click returns the generated Event and Block.dispatch returns the delivered Event, preserving the event object from the dispatch pipeline. Native return signatures match these actual results, including GUI activation.

Event dispatch/click and lifecycle entry points publish External as their allowed effect bound: handlers may mutate application state or use host I/O APIs. The built-in no-op bodies do not make the overridable handler contract ReadOnly. These entry points still reject invocation inside read-only rendering.

ContainerBlock.mainAlign is a managed attribute with start (default), center, end and space-between policies. It positions children in the allocated main-axis space after padding and the configured minimum gap. Center/end use remaining positive space; space-between adds it between two or more children. Overflow keeps start positions and existing sizes; alignment does not imply flex sizing. Native arrange queries and rendering use the same actual Bounds. Source arrange overrides retain their explicit placements.

Block.flex is a nonnegative managed weight, default 0. In native containers, fixed children retain measured main-axis sizes; positive weights share the remaining allocated space after padding/gaps with zero basis. This does not request extra parent size. Actual allocated child bounds feed back into measurement before publication. Source arrangement overrides remain authoritative. The additional flexBasis, flexMin, flexMax and flexShrink attributes control bounded allocation and opt-in shrinking along the parent main axis; see the policy below. See flex.endor for shared text reflow and native button updates.

Flex main-axis policy: flexBasis is "auto" by default (zero basis when flex > 0, otherwise the first measured size of this occurrence in the current solve); a number supplies an explicit basis. flexMin defaults to 0, flexMax to 1000000, and flexShrink to 0. All numbers are finite/nonnegative and at most 1000000; flexMin must not exceed flexMax. The parent clamps bases to min/max, distributes positive space by flex, and redistributes space left after a maximum is reached. Deficits shrink by flexShrink times the original basis, freezing at minima. A fixed child can opt into shrink without growth. Unresolvable minimum overflow is preserved, and remaining positive space follows mainAlign. The policy applies to width in a Row and height in Column-like containers; these are not independent width/height bounds. Source arrange overrides remain authoritative. Preferred sizes are derived per occurrence and reset for each new solve, never shared through Block state. See examples/flex-constraints.endor. Layout continuation remains unfinished.

When an occurrence's cross-axis constraint changes during settling, its preferred main-axis size is invalidated and remeasured. Any obsolete native flex allocation on that axis is removed for the replacement measurement, so narrowing a nested Column can increase wrapped text height. Source arrange allocations remain explicit. Preferred-size presence and values participate in convergence/cycle detection.

The source layout driver now uses Aster's resumable read-only call API with a soft quantum of 256 instructions. Pauses retain the same VM frames, operand/local roots, return contracts and read tracking; total instruction/frame limits still apply across slices. ui.stats().layoutSlices counts slices in the most recent solve (native fast path: 1). Viewport and idle-task refresh park Pending across worker turns; newer viewport targets can cancel and replace unpublished work. Source/event callbacks execute atomically; their refresh can park across worker turns. Native callbacks are not preempted. Only the completed settled frame is published.

source_layout::start now returns either Complete(Draft) or Pending(Work). The host-owned Work retains the unpublished frame, derived runtime/cache snapshots, convergence plan and prepare root without borrowing caller frame/cache variables. Work::resume consumes the saved job and advances the same VM continuation; Work::cancel closes that continuation and releases the prepare root without merging a draft. A second start is rejected while any read-only execution is active. Every pending Work must be resumed or explicitly cancelled with its owning VM; dropping it is not cancellation. Synchronous ReplSession operations drain the same saved refresh state. The worker can park viewport and idle-task refresh jobs; cancellation restores still-pending Mount preparation.

UiBridge::begin_layout_frame/resume_layout_frame now expose PendingLayout around the source job. This bridge-level continuation also owns its target window, page snapshot, tab projection, viewport restoration and retained lifecycle hook root. Only the Complete branch projects scroll/focus, captures dependency versions, merges derived caches, commits Mount geometry and runs deferred hooks. Resume restores the saved window context, and failure releases hook roots. ReplSession uses this protocol through its saved refresh state. Viewport refresh supports cross-worker-turn scheduling and prepared-Mount rollback; callers must not drop a pending bridge layout as a substitute for cancellation.

ReplSession now owns a resumable RefreshWork containing window traversal, pending bridge layout, unpublished per-window documents and the cumulative refresh work budget. poll_refresh advances one refresh stage or VM quantum; only a complete stable pass replaces the session's full document map. Synchronous callers drain the same state machine. Failure stops the domain, releases any pending bridge work and preserves the externally published picture. refreshYields counts cooperative refresh returns, not wall-clock time. Only still-pending Mount preparation is rolled back; completed callbacks remain committed.

Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; ready layout/paint or coroutine work checks the request queue without a timed wait and yields to other ready OS threads between turns. An accepted pending input/candidate also yields without adding a fixed millisecond delay per VM quantum; control, epoch and viewport checks still run on every turn. When only external completion/timers are pending, the normal idle wait remains 100 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests wait across worker turns for the existing job, so they cannot run inside its read-only scope. Mutating requests are serialized; newer viewport targets coalesce by cancelling unpublished layout. Initial source execution remains atomic; its layout can park across worker turns. Explicit REPL/event refresh can park across worker turns. Idle task delivery starts at most one ready callback per turn and parks its subsequent layout; callbacks themselves and individual Rust measurement/paint calls remain atomic.

Runtime mutation entry points reject direct calls while refresh work is pending, before executing source or changing metadata; task polling defers. The API worker drains the saved refresh before accepting those mutations, keeping the runtime usable. Incremental refresh keeps the prior complete document map until a stable pass, including across window traversal boundaries where no VM read-only frame is currently active.

Pending bridge layouts now retain a Mount checkpoint. cancel_layout_frame closes the VM continuation, restores the old Mount payloads/links/dictionary and marks the presentation dirty for a new solve; discarded lifecycle hooks do not run. Successful finalization releases checkpoint roots after callbacks. This restores speculative Mount preparation, not arbitrary application writes or already completed lifecycle callbacks.

New viewport targets now coalesce pending refresh work. The runtime cancels only an unfinished bridge layout, restores its Mount checkpoint, preserves completed per-window drafts/callback effects and restarts traversal with the latest viewport map. refreshRestarts counts target replacements; layoutCancels counts replacements that actually closed a pending VM layout. Unchanged targets resume the same job. A new external target resets its convergence budget; internal settling does not. Ordinary source/event mutations still serialize behind refresh rather than roll back application effects.

Cancellation checkpoints own explicit VM roots until restored or released. Initial presentations may have no mount dictionary; cancellation restores that absence too. Saved metadata values retain object-valued attributes. A pending refresh can restart while keeping already completed window drafts; no onMount/onUnmount callback is replayed merely because the viewport target changed. Mount-owned tasks are cancelled immediately before an actual onUnmount callback, never during abandoned preparation.

Large intrinsic layout trees (more than 64 occurrences) now use the resumable per-occurrence driver too. Native measurement caches remain active, and a single native callback remains atomic; VM instruction boundaries between nodes allow the worker to yield and replace an obsolete viewport. Small intrinsic trees retain the allocation-free native fast path. Structural extraction, driver preparation, settling checks and final command projection still run atomically; this is not a wall-clock preemption guarantee.

Idle timer delivery now processes at most one ready callback (or one ineligible task cancellation) before starting an incremental refresh. No further task is delivered while that refresh is pending. Viewport changes can replace its unfinished layout without replaying the completed task callback. Task effects and native window commands publish only after settling; a previously completed resize may publish separately. Explicit input still drains pending work first. Synchronous host callers retain a maximum of 64 task deliveries per poll.

REPL and event mutations now have incremental refresh entry points. They execute source/callbacks once, retain scalar response metadata, then park layout with the current viewport target. While pending, mutation, command extraction and garbage collection wait; layout can be retargeted by a newer native size. Synchronous helpers remain for host regression coverage. Discarding a ReplSession explicitly cancels any still-pending bridge layout before the Candidate releases service/global roots. This does not preempt source/callback execution, structural preparation or paint.

The API worker now owns a pending HTTP request plus its scalar result while REPL/event layout is parked. It advances the layout with current native viewport targets and responds only after the stable full-window publication and command extraction. Later mutation requests remain queued; GET /code can observe the last complete publication. A newer accepted PUT invalidates queued inputs but lets the already-running owner settle before candidate construction, so failed replacement can preserve its consistent heap. DELETE revokes that owner and prevents late publication. This queue ordering does not roll back already-executed source effects.

Complete-source candidates now separate one-time compilation/source execution from initial layout. load_incremental owns the isolated VM while its first layout is pending; the current live VM stays separate. Cancellation uses the same Mount checkpoint/continuation cleanup as later refresh. Synchronous load helpers remain for host regression coverage. This does not slice parsing, compilation, mutating source execution, structural preparation or final command projection.

The API keeps the old runtime alongside a pending PUT candidate. Newer submissions cancel only the obsolete candidate; failures restore the published runtime's submission owner. Background epoch checks distinguish accepted replacements from DELETE revocation, so queued/superseded candidates do not destroy the previous live heap. Candidate resumption reads only the primary shell viewport, avoiding reuse of an old secondary window's dimensions for a new VM's resource with the same numeric ID. Success publishes the complete candidate document and commands together, then replaces the old runtime.

Source paint now uses a resumable read-only VM call with the same 256-instruction quantum and a cumulative 1,000,000-instruction budget. The pending job owns the unpublished frame, visual snapshot, decoded command buffer and prepare root. The refresh worker resumes paint before advancing to another window and publishes only the full stable window set. New viewport targets cancel unfinished paint, discard partial commands and force that window to refresh. Already completed layout/Mount geometry and lifecycle callbacks remain committed; cancelling paint does not replay or undo them. Failure/discard explicitly closes the read-only continuation and releases its roots. paintSlices reports the most recently completed paint solve (1 when the solve completes within its first quantum); paintCancels counts viewport replacements that closed pending paint. Layout checkpoint rollback remains limited to unfinished layout preparation. Native callback bodies, driver preparation, command decoding/projection and rasterization remain atomic.

Layout and paint drivers now prepare one occurrence at a time. Their fixed-size outer bytecode repeats on the same VM frame; a short-lived helper retains only the current call, its bounds/arguments and collector. The helper is released before the next occurrence. Layout keeps bottom-up measure, top-down arrange and the same settling loop. Paint advances even intrinsic method classification through VM quanta. The weak occurrence list and native scratch/output data remain proportional to the actual tree, but preparation no longer allocates managed callables/Bounds for the whole tree before the first slice. All helpers share the original read-only scope and cumulative instruction/frame limits. Weak-list copying, structural extraction, settling and final projection remain bounded atomic native work.

ui.stats().dependencyInvalidations counts observed layout/paint dependency mismatches. lastInvalidDependency records the phase and invalidating identity/name or snapshot-cache cause without rendering its fields or retaining it. windowDependencyDiagnostics reports nonempty reasons from all windows. Source rendering observes the global names it reads, so unrelated global changes do not require repainting; compare idle document versions before diagnosing a refresh loop.

examples/gc-lifecycle.endor demonstrates an actual Page/Block/Task/await-frame reference cycle. Record ui.stats().objects, call mountCycle(), and inspect phase.value (1 while awaiting) and pending.value.status(). Closing the tab removes its navigation root; a retained Block's own workflow continues. Call releaseCycle() to release both external references, then query objects again to trigger the normal between-input collection and compare with the same baseline. Cached layout/paint metadata borrows identities and must not keep that cycle alive. The baseline is specific to that VM/program, not a fixed object count across programs.

ui.readText(owner,key,path[,callback]) returns an owner-scoped Task and reads a regular UTF-8 file off the VM thread. Paths use native filesystem syntax; absolute paths are recommended. The optional callback takes zero arguments and must return Unit; it runs only on successful read and can use task.result(). Without a callback, poll status then read result from code. result() is available while a success callback is running or after completion (timers return Unit); error() returns an empty String until failed, then the provider/callback error. A provider failure marks the task failed and skips its callback without stopping the VM; a source callback failure still stops the shared domain. Owner/key replacement, explicit cancellation, close/reset/VM discard and GC drop the provider lease, remove queued completion and reject late delivery. In-progress OS reads may finish, but cannot deliver after cancellation. Only the owning VM worker updates managed results and rechecks owner/key identity. No VM handles enter provider threads beyond opaque weak identities.

The lazy shared pool has four threads and a 64-job queue. Each runtime permits 16 outstanding I/O requests (one temporary replacement reservation), and each read is capped at 1 MiB by metadata plus actual streaming length; non-regular files, non-UTF-8 data and over-limit content fail rather than truncate. These limits do not constitute a full filesystem sandbox. The provider returns text in the same managed Task, not a second application-state store. Task await is supported through ui.spawn; see the await contract below. See examples/async-file.endor; from REPL call reader.read("/absolute/path.txt") to update its actual TextBlock, even when that object is hidden but retained.

ui.fetchText(owner,key,url[,callback]) performs an asynchronous HTTP(S) GET using the same Task, owner/key replacement and success-only callback protocol as readText. Example: fetcher.fetch("http://127.0.0.1:4737/agent") after loading examples/async-network.endor. The example retains the complete result in fetcher.response and displays completion status; it does not put arbitrary network content into a TextBlock, whose own 16 KiB limit is independent of the provider limit. Only 2xx responses produce a UTF-8 result; HTTP failures, invalid UTF-8, oversize bodies and transport errors set status() to failed with error(). It verifies HTTPS certificates, follows at most five HTTP(S) redirects, has a 10-second connect and 30-second total transfer timeout (starting when a worker takes the request), and bounds decoded text to 1 MiB without truncation. URLs are limited to 8192 bytes and must not contain credentials or control characters. This GET API does not accept request bodies, custom headers or authentication.

The current transport uses a separate system curl process per request (/usr/bin/curl on Unix, curl.exe on Windows); missing or incompatible curl fails the Task. User curlrc and URL globbing are disabled; URL data is passed through escaped stdin configuration rather than command arguments. TLS and normal system proxy behavior remain enabled. Cancellation kills an active HTTP process immediately and the provider worker reaps it; the VM thread never waits for the process. All file and HTTP requests share the per-runtime 16-task limit and global four-worker/64-job queue. In-progress file reads remain cooperatively cancelled at delivery. Network access is not a security sandbox, and this is a text GET provider rather than a general HTTP client. Task await is supported through ui.spawn; see below.

ui.spawn(owner,key,function) creates an owner-scoped cooperative Task for a zero-argument function; its returned value is retained as the Task result. Each owning VM poll resumes at most a 256-instruction quantum per selected Task, rotates runnable tasks fairly, and retains source frames in an Aster-managed coroutine edge. Calls and writes may interleave between quanta; prior writes are not rolled back on cancellation or error. Pending/running Task replacement, owner closure, GC and reset release parked frames through the same managed graph. Native calls remain atomic and all quanta share the 128-frame/100000-instruction callback limit; a native entry may run during preparation. result() is unavailable until spawn completes. A source error still stops the shared runtime. See examples/cooperative-tasks.endor. Endor await expressions now suspend these calls on a Task without replaying the operand. See the await contract below.

Inside a function run by ui.spawn, value = await task waits for any timer/file/network/spawn Task and resumes at the same expression with its retained result. A source function may return a Number, String, managed Object or Unit; normal source annotations still apply. Task.result() and await share the result. ui.sleep(owner,key,milliseconds) creates a callback-free timer Task returning Unit. Plain calls/REPL can await an already completed Task; a pending await requires a coroutine and fails explicitly rather than blocking. There is no new async function declaration: ui.spawn explicitly schedules an ordinary Endor function. See examples/await-file.endor.

A waiting coroutine retains its evaluated operand and frames as managed edges; waiting performs no VM execution or active polling. Source side effects and IO initiation before await are not replayed. When the dependency completes, the owning VM worker resumes the waiter through the existing fair quantum scheduler. Cancelling/replacing/closing the waiter releases its frames and operand; it does not automatically cancel an independently owned or shared dependency. Dependency cancellation propagates cancellation to its waiters without running the remainder. An unhandled dependency failure becomes a source await error (with the original provider message), fails the waiter and follows the existing shared-runtime fault rule; provider failure alone still only fails its own Task. Self-await and cyclic Task waits fail explicitly. Read-only layout/paint cannot await. Waiting state is reported as running, while active() remains true; result() is unavailable until completion. These APIs do not introduce try/finally or change the existing exception model.

While a native tab drag is held over an overflow arrow area, the strip waits 350ms then reveals one hidden tab every 120ms, including in a compatible destination window. The preview scroll does not activate Pages or change business state. Drawing, hit testing and insertion gaps share the same transient visible range; release uses its full-order insertion index. Leaving the strip stops the timer; release, Escape, a new press or runtime generation change restores the normal active-tab-centered strip. A resize or changing tab count clamps the range. Native drag/mixed-DPI behavior still requires platform validation; application-exit cleanup is described below.

Application exit now uses a private VM-worker control channel, independent of HTTP queue capacity. Main native close and a managed primary Window.close request asynchronous cleanup; duplicate requests coalesce and mutation requests are rejected with 503 while exiting. An unpublished candidate/input refresh is cancelled without replay. The current VM cancels all Task entries, closes managed windows, detaches occurrences child-before-parent per window, and invokes normal onUnmount hooks once. It does not run layout/paint during shutdown. All windows are closed before hooks run; new windows and tasks are forbidden. A stopped/faulted VM detaches without source hooks; reset/discarded pictures cannot revive an old VM. A hook error aborts remaining source hooks, releases prepared hook roots, drops the VM and reports failure before native exit. Completed shutdown is acknowledged to the GUI, which then ends the event loop. An OS exit bypassing CloseRequested uses a five-second bounded final fallback; non-cooperative native calls remain atomic and force-kill/crash cannot guarantee notifications. Native stderr records the window/mount/task-entry counts and whether source hooks were enabled, or the cleanup error. See examples/exit-lifecycle.endor. Cross-platform exit/drag validation remains broader follow-up work.

Foreground REPL/events now retain a BeforeInput request while an existing idle layout/paint refresh advances one stage/quantum per worker turn. They execute their source only after that refresh settles, so mutations never enter its read-only scope. New task callbacks are not delivered ahead of this accepted input. Shutdown can cancel the waiting request with 503, and superseded submissions still return 409. Reset cancels/discards the old VM instead of synchronously draining its unpublished refresh. Source parsing/compilation and ordinary synchronous source/native calls remain atomic boundaries; this change removes the worker's synchronous pre-input refresh drain.

Source layout/paint now validate name-level read observations from Aster: each globals/builtins lookup records its text key and observed presence/value, while dictionary kind checks remain separate. Updating the REPL result _ or another unread global no longer invalidates these frames solely because the whole globals dictionary version changed. Reading _ still observes its value; adding a previously missing global correctly invalidates a builtin fallback. Ordinary native dictionary iteration/content reads retain whole-object version dependencies. Measurement cache hits replay their recorded observations into the outer solve; they do not drop dependencies merely because source execution was skipped. Cache observations contain borrowed handles (including object-valued names), add no GC roots, and never write comparison snapshots back into language objects. Invalid-name diagnostics report only identity/key, not captured values. General Block binding and other legacy readers keep their existing conservative object dependencies. See examples/global-dependencies.endor.

Property bind computations also use precise global-name observations. Unrelated REPL/global changes keep valid cached outputs; changing an observed name or business object recalculates the binding, including hidden retained Blocks. A direct overwrite of a bound property is repaired by its compute function. ui.stats().bindingEvaluations counts computations in completed binding refreshes over this VM's lifetime, including initial computations; querying it does not itself force valid bindings to recompute. General dictionary-content reads still conservatively observe the dictionary.

S
Description
No description provided
Readme 16 MiB
Languages
Rust 99.5%
JavaScript 0.4%
Shell 0.1%