425 Commits

Author SHA1 Message Date
Chris Tate 1dfb0db4f2 Single-line inputs strip pasted line breaks and never paint a second line (#140)
* Strip line breaks from single-line text-field inserts at the edit seam

- Sanitize derived edits for input/text_field/search_field/combobox at the keyboard choke point, BEFORE the stamp, so the retained editor, the app's on_input mirror, and replay all hear identical stripped bytes (HTML value-sanitization rule; covers shortcut and context-menu paste, typed/automation text_input, and IME composition).
- Suppress inserts that strip to nothing so pasting bare newlines inserts nothing and a host-stuffed Enter payload never deletes a live selection; the app-side fallback derivation applies the same rule so both derivations agree.
- Sanitize direct runtime editCanvasWidgetText writes through the same shared rule.

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Present line-broken single-line values as one clipped line

- Lay out, measure, and paint a single-line field's value with \n/\r presented as spaces (byte-for-byte, so caret/selection/hit-test offsets address the raw value) — one line on the GPU engine, the reference renderer, and packet hosts alike; copy, semantics, and automation still read the raw model value.
- Force the content-rect clip whenever the raw value holds a line break, the independent guard that keeps even a presentation-scratch fallback inside the field's rounded border.
- Persist presented bytes into a render-walk pool (the chart-label scratch precedent) so emitted commands survive until the runtime copies the display list.

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Pin single-line newline sanitization and containment across the batteries

- Paste, automation set_text, IME composition, and the Tree fallback derivation all pin stripped inserts (textarea keeps its breaks); bare-newline pastes insert nothing and Enter stays not-an-insert even with a host-stuffed newline payload.
- Model-set values holding a newline pin one presented line under a forced clip in both render walks and through the runtime display list, with semantics still reporting the raw value.
- The session reference recording now copies a textarea's two lines and pastes them into the search field, pinning that a recorded multi-line paste replays to the identical sanitized value and fingerprint.

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Sanitize clipboard pastes before clamping them to capacity

- clampCanvasWidgetPasteText (shared by the cmd+V shortcut and the context-menu Paste) now strips single-line targets' line breaks BEFORE measuring against remaining capacity, so a near-limit paste never spends free bytes on breaks the seam strips anyway ("a\nbc" into 3 free bytes lands "abc", not "ab"), and a paste that sanitizes to nothing is not reported truncated.
- Boundary tests pin both paste paths at exactly three free bytes: retained editor and the app's stamped edit hear the identical whole sanitized suffix with no false truncation flag.

* Move emit-built text bytes into the display-list builder

- The builder contract (pinned at Builder.allocPathElements) lets a display list accumulate across several emit calls or be held while another builder emits; presented single-line values and formatted chart labels lived in thread-local pools RESET at each emit entry, so a second emit overwrote text an earlier list still sliced.
- Builder now owns the bytes beside its path-element store: allocChartLabelBytes keeps the chart label budget's loud ChartLabelBytesFull, and allocTextBytes holds presented values under a per-view-text-budget-sized store (lockstep-tested against max_canvas_text_bytes_per_view) with the raw-value fallback intact; both thread-local pools and the per-emit resets are gone.
- Regression tests emit twice — into another builder and accumulated into one — and pin both lists' draw_text bytes intact for presented fields and tick labels; both fail against the pool-reset design.

---------

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>
2026-07-17 10:01:21 -05:00
Chris Tate 1e6b615674 Prepare v0.5.2 release (#138)
- Synchronize the CLI, core, platform packages, and examples at version 0.5.2.

- Merge pending fragments into the marked v0.5.2 release notes with contributor credits.
v0.5.2
2026-07-16 22:39:42 -05:00
Chris Tate a1fa2d0285 Anchored tooltips gain hover intent: show delay, warm window, tooltip-delay attribute (#130)
* Teach the registry the anchored tooltip and its tooltip-delay attribute

- ui_schema: tooltip (39) becomes anchorable; fresh attr code 80 tooltip-delay (.whole, field tooltip_delay) with the pins test re-pinned for the addition
- Options/Widget carry tooltip_delay (ms; -1 follows the new ControlMetricTokens tooltip_show_delay_ms/tooltip_warm_window_ms defaults of 700/300)
- Validator scopes tooltip-delay to tooltip beside anchor with teaching messages, mirrored in the LSP/docs attribute tables and covered in ui_markup_tests

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>

* Land the anchored-tooltip hover-intent state machine in the runtime

- Anchored tooltips become runtime-owned chrome: adoption stamps them hidden, hover on their trigger arms the show delay, leaving disarms, a dwell past the deadline shows on the presented frame's recorded timestamp, and hiding opens the shared warm window that shows the next trigger's tooltip instantly
- Every transition steps on journaled input/frame timestamps (canvasRenderAnimationStartNsForView at pointer dispatch, GpuSurfaceFrameEvent.timestamp_ns at frame advance) and an armed delay rides the render-animation frame pump, so recorded sweeps replay byte-identically
- Escape dismissal clears the intent machine with the surface, and five behavior tests cover sweep-shows-nothing, dwell-shows, leave-disarms, warm transfer/expiry, and tooltip-delay=0

* Hold the engines in parity on the anchored-tooltip declaration

- Interpreter test: anchor + tooltip-delay stamp the widget declaration, the token-default and static leaves keep -1/null so existing documents lower byte-identically
- Compiled-vs-interpreter parity: identical trees and identical anchor/delay stamps across both engines for declared, defaulted, and static tooltips

* Pin the tooltip hover dwell in record-and-replay

- The markup e2e fixture gains an anchored tooltip (tooltip-delay="200") on the Add trigger through the stack pattern
- A recorded dwell arms, shows on the frame at the deadline, and hides on leave; two recordings are byte-identical and the replay verifies every per-frame fingerprint checkpoint on the journaled clock

* Document anchored-tooltip hover intent across docs, vocab, and the UI skill

- Tooltip reference page teaches the anchored stack pattern, the 700ms/300ms hover-intent windows, tooltip-delay=0, and gains the scoped attribute table (tooltip joins the anchor family in the generated vocab)
- native-ui skill enumerates anchor on tooltip and the tooltip-delay attribute; changelog fragment tells the feature story

* Reveal tooltips on keyboard focus, dismiss on press, validate owners

- Keyboard focus-visible now shows a trigger's anchored tooltip instantly (blur hides, never warms), pointer-down and Space/Enter cancel the armed reveal, dismiss the shown tooltip, and close the warm window, and the rebuild prune validates the owning trigger — a removed, rekeyed, disabled, or re-parented owner resets armed/shown/warm state and re-stamps hidden — all per shadcn's Base UI-backed defaults.
- Retune the timing tokens to Base UI parity: tooltip_show_delay_ms 700 -> 600 and tooltip_warm_window_ms 300 -> 400, with the components page, markup vocab, native-ui skill, and changelog fragment updated to match (and to describe the focus and press behaviors).
- Six new canvas_widget_floating_tests cover focus reveal/blur/Escape, press-on-armed, press-on-shown without instant re-show, keyboard activation, and the three owner-invalidation rebuilds; each fails when its fix is reverted.

* Hold anchored tooltips open while the pointer hovers their content

- A shown tooltip's own frame now holds it open, and the anchor gap crosses through a bounded safe-polygon transit corridor (WCAG 1.4.13 hoverable content; Base UI's hoverable default) — the tooltip stays out of hit-testing, so interaction routing and the a11y tree keep treating it as presentation chrome.
- The corridor's grace re-arms on every in-corridor move and resolves on the recorded frame clock (400ms of stillness), so slow deliberate transits never race a timer while parked pointers and replays stay deterministic.
- Motions away from the tooltip keep hiding on the move itself, pinned by the existing sweep/warm-window tests plus two new sabotage-verified travel tests.

* Route every scroll path's hover change through the tooltip machine

- Wheel, kinetic steps, native scroll drivers, and keyboard scrolling now reconcile hover through one wrapper that steps the tooltip intent transition a pointer move would: scrolled-away triggers disarm/hide (usual warm window), newly-arrived triggers arm per normal.
- The step is point-blind on purpose — the content moved, not the pointer, and Base UI closes tooltips on scroll — but the wheel's live position re-seeds the transit-corridor apex for whatever it armed.
- Sabotage-verified tests pin the wheel transition chain (disarm, re-arm, frame-clock show, warm transfer, hide over dead space) and the point-blind path via End-key scrolling a shown trigger out of the tree.

* Make every pointer-down dismiss tooltips, drag and context downs included

- Secondary-button downs consumed by the context-menu gesture and primary downs consumed by a window-drag region now run the same press reset (armed cancels, shown dismisses, warm closes) before their early exits — the documented pointer-down-dismisses contract (Base UI's close-on-press default; macOS help tags vanish on any click) now holds for all buttons.
- The context menu still presents and the OS drag still starts; sabotage-verified tests pin both paths against a shown tooltip.

* Reset tooltip state when a canvas view loses focus

- Both focus seams — per-view focus moves (setFocusedView, input- and command-driven) and window-level focus loss (clearFocusedView) — now drop the blurred view's whole tooltip conversation: armed delay, shown tooltip (keyboard- and pointer-owned), warm window, transit grace, and re-stamp hidden.
- Extends the focus-shown blur-hides contract (shadcn's Base UI-backed default) to the view: a tooltip painted in a view the keyboard left is a stale affordance whose semantics node kept claiming visible.
- Sabotage-verified test pins the keyboard-shown and pointer-shown registers across a sibling-view focus switch, including the semantics tree carrying no stale node.

* Hide focus-owned tooltips when programmatic focus moves the keyboard

- Autofocus, accessibility focus, and automation focus all funnel through focusAutomationCanvasWidget, which now steps updateCanvasTooltipIntentForProgrammaticFocusMove: a focus-owned tooltip hides (no warm window) and the new target's never reveals, the same focus-visible guard rationale as the click-focus exclusion.
- Pointer-owned tooltips stay untouched by focus moves; re-focusing the tooltip's own trigger is not a move and leaves it alone.

* Re-hit-test point-blind scrolls from the stationary pointer's last position

- Kinetic steps, native drivers, and keyboard scrolling borrow the view's last journaled pointer position (canvas_last_pointer_position, cleared on pointer_cancel — the view-exit event): the pointer did not move, so hover and tooltip ownership follow the post-scroll tree honestly — triggers scrolled off the pointer release, ones scrolled under it arm.
- With no trustworthy position (keyboard-only session, or the pointer left the view) the pointer's tooltip intent closes — armed, shown, warm window — instead of guessing; Base UI closes on scroll, we do strictly better only where the re-hit-test is sound.
- The wheel path keeps its live position; whichever position is used re-seeds the transit-corridor apex.

* Normalize runtime tooltip visibility onto the scratch tree before the layout diff

- setCanvasWidgetLayout now prunes tooltip intent against the reconciled tree and stamps runtime-owned anchored-tooltip visibility onto the scratch BEFORE diffing, so an unchanged rebuild carrying a hidden anchored tooltip diffs clean instead of reporting the runtime's own hidden stamp as a spurious visibility invalidation every rebuild.
- A shown tooltip's scratch node is stamped visible (prune-aware), so rebuilds never pass it through a hidden state — no hide-then-show frame pair — while a rebuild that breaks the shown binding still diffs the hide honestly; adoption re-runs prune+stamp as the structural backstop.

* Range-check whole-number attrs against their field type in both engines

- tooltip-delay="2147483648" (or an equally large model binding) trapped in the unchecked @intCast; both engines now teach the grid-lines out-of-range error instead
- the field's own integer type is the honest upper bound - no semantic ms cap is invented, matching resize-duration whose only bound is likewise its u32
- boundary values (0, i32 max) pin as accepted in both engines, literal and binding paths alike

* Collapse degenerate corridor triangles to their boundary segments

- an apex exactly on a tooltip or trigger edge's line zeroed all three cross products for every collinear point, so the sign test read the whole infinite line as in-corridor and motion along it re-armed the transit grace forever
- a zero-area (or epsilon-area) fan triangle now contains only the segments between its actual vertices; the epsilon is half a canvas point over the longest edge, scale-honest for slivers of any length and commented at the constant

* Short-circuit pointer cancel ahead of the tooltip hover-transition gate

- a pointer-shown tooltip held open by its own hovered content reads hovered_id == 0, so cancel-to-0 was no transition and the tooltip stranded visible after the pointer left the view
- .cancel now closes the whole pointer-owned conversation (armed, content-held shown, warm window, corridor state) through the same close the point-blind scroll staleness arm uses; no warmth survives a pointer we cannot place
- the focus-shown tooltip survives a pointer cancel - the keyboard holds it - unlike view blur, where the keyboard itself leaves (commented at the seam)

* Re-hit-test the stationary pointer against every adopted layout

- the adoption prune validates only tooltip/owner identity and hover survives by ID, so a rebuild that MOVED the same-ID trigger away from the stationary pointer left armed intent able to fire and a shown tooltip visible until the next pointer event
- setCanvasWidgetLayout now re-hit-tests canvas_last_pointer_position after the pose restores settle, reusing the point-blind scroll reconcile: armed disarms, shown hides with the usual warmth, a trigger arriving under the pointer arms, and the content hold is re-checked against the tooltip's NEW frame (no transit corridor - the content moved, not the pointer)
- no trustworthy position closes pointer intent exactly like a blind scroll; focus-shown tooltips survive, and a rekeyed trigger under the pointer earns a FRESH dwell instead of inheriting the old widget's show

* Reconcile tooltip intent through one cause-fed choke point

- reconcileCanvasTooltipIntent owns position bookkeeping, hover re-hit-testing, containment, transitions, and the deadline-derived frame-pump kick; every entry point (pointer, consumed streams, scroll, adoption, focus, blur, cancel) is now a thin caller naming its cause
- Closes four input-path holes: transit graces armed by 0-to-0 hovers now pump the idle frame clock, scrolls re-check the content hold even when the hover id is unchanged, a released hold reprocesses the trigger already under the pointer, and consumed secondary/window-drag streams keep the stored pointer position truthful
- Five sabotage-verified regression tests pin the closed holes

* Make the rebuild's tooltip prune transactional against adoption failure

- The pre-diff visibility stamp now reads a pure prune VERDICT (canvasTooltipShownIdSurvivingLayout) instead of mutating the live registers; the mutation lands in copyWidgetLayoutTree's own prune, after the fallible diff and retained-pool validation/copy succeed
- A failed adoption previously left the OLD tree stamped visible with cleared registers: an unhideable tooltip no transition could reach
- Regression test forces the failure through the anchored-surface budget and asserts the shown tooltip stays register-owned and hideable

* Drop tooltips with the whole window on app deactivation and key-loss

- App deactivation and window key-loss now feed the tooltip choke point with the existing view_blur cause for every affected canvas view: setFocusedIndex becomes the one window-key seam (window_focused, frame-change echoes, focusWindow, and native adoption all land there), so a focused→unfocused transition drops that window's tooltip conversations without touching per-window focus memory.
- The stored pointer position deliberately survives the lifecycle blur: pointer truth belongs to the pointer channel, whose own cancel (macOS mouseExited on key-window-scoped tracking) clears it on hosts that stop hover delivery.
- Three tests pin the contract: deactivation hides focus-shown and pointer-shown tooltips (warm dies, armed disarms, no stale a11y visibility), key-loss blurs only the losing window's views, and reactivation/re-key reveals nothing because both reveal paths are transition-edge-triggered.

* Arm and reveal tooltips whose binding changed beneath a stable owner

- A rebuild that mounts, replaces, or rekeys a tooltip under a hovered trigger whose own ID survived produced no hover delta and no stale register, so the new tooltip could never arm until leave/re-enter; setCanvasWidgetLayout now snapshots the outgoing tree's owned-tooltip bindings next to the prune verdict and the layout_adoption arm compares them against the adopted tree, arming a fresh dwell for the hovered owner (never an insta-show; warmth only when genuinely live) and revealing immediately for the standing keyboard focus-visible owner.
- New canvas_widget_focus_visible_keyboard register records whether the ring came from the keyboard contract, so pointer/programmatic/automation rings and the focus-return seams keep the click-focus guard at adoption time too.
- Six floating tests pin the matrix: mount-mid-hover dwell, rekey-under-shown re-earn, focus-visible immediate reveal (pointer-blind path included), caret-ring provenance gate, unmount disarm, and the unchanged-binding rebuild staying inert.

* Gate tooltip reveals and arms on app-active and window-key state

- A rebuild from the deactivation callback (or any adoption/hover in a non-key window) could reveal or arm a tooltip the blur had just cleared; every reveal/arm path now checks the new app_active register plus the owning window's key state, while the focus provenance and stored pointer stay preserved for the next honest transition.
- The frame promote disarms rather than defers under suppression, so re-key and re-activation can never spontaneously reveal.
- Sabotage-verified tests: deactivation-callback rebuild, non-key mount beneath a hovered trigger, and the reworked two-window key-loss fixture.

* Teach the Native UI guide the anchored tooltip's hover intent

- The guide predated anchored tooltips: it scoped the anchored-floating family to dropdown-menu alone and described tooltips as static leaves, contradicting the component page and skill.
- The element table row and the layout-attributes paragraph now carry the runtime-owned hover-intent summary: 600ms dwell, 400ms warm window, immediate focus-visible reveal, and tooltip-delay (0 = instant, a teaching error without anchor).

* Scope anchored-surface lookups so a tooltip never shadows the menu

- A stack can now anchor several surfaces at once (dropdown-menu + tooltip), but the menu lookups still grabbed "the anchored child" and kind-checked the winner: a focus-visible tooltip mounted after the menu made ArrowUp/Down unable to walk into the open menu and Tab departure unable to close it.
- Every consumer of the anchored-child walk now names its population: Escape and automation dismiss keep .any (topmost-first, one surface per gesture), outside-click dismissal and Tab focus scoping take .interactive (tooltips are hover chrome the intent machine owns), and the open-select keymap plus Tab departure take .menu — the scope filters during the scan, never after selection.
- Coexistence tests pin the keymap against a select + open dropdown-menu + focus-shown tooltip stack: arrows enter the menu beneath the tooltip, Tab departure dismisses the menu and leaves the tooltip alone, and Escape peels one surface per press, topmost (tooltip) first.

* Say precisely which tooltip hide opens the warm window

- The runtime warms only when a pointer-hovered tooltip hides on leave — focus departure, Escape, press, blur, and prune are each a deliberate no-warm decision in canvas_widget_events.zig — but the docs claimed "any tooltip hides", contradicting the tooltip page's own next paragraph.
- Corrected the tooltip page, the Native UI guide table, the changelog fragment, the native-ui skill card, and the markup doc strings; regenerated component-vocab.json from them (webp previews untouched).

* Observe window key-loss on the focused flag's own edge

- Windows and GTK announce key changes loss-first: the state echo wrote focused=false directly, so the later gain's dethroning loop saw the old window already unfocused and the tooltip key-loss reset never fired — the tooltip stayed painted and a11y-visible in the inactive window.
- setWindowFocused is now the one writer of a tracked window's focused flag (setFocusedIndex, applyNativeInfo, and updateWindowState all route through it); the reset fires on the flag's true→false edge regardless of event ordering, including a loss with no subsequent gain.
- closeWindow's transactional flip stays outside the seam deliberately: its views are removed with the window on success, and the rollback on platform failure must not have fired a reset.

* Spend the standing focus reveal on explicit tooltip dismissal

- Keyboard activation and Escape preserved keyboard provenance, so the adoption binding-reconcile treated the ring as standing intent: an activation whose own model rebuild replaced or rekeyed the tooltip resurrected it one frame after dismissal, contradicting "stays down while focus rests on the trigger".
- Both dismissal seams now consume canvas_widget_focus_visible_keyboard when the ring rests on the dismissed tooltip's owner — safe because its only readers are the two tooltip reveal gates, while the focus ring renders from canvas_widget_focus_visible_id and stays painted; Tab away and back re-grants the contract at the one provenance write, and pointer hover re-earns its dwell untouched.

* Say which hide warms in the token and state doc comments

- The two source doc comments still claimed ANY tooltip hide opens the warm window; only a pointer-hovered tooltip hiding on pointer leave warms, matching the implementation and user docs.

---------

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>
2026-07-16 22:12:50 -05:00
Chris Tate eefa3690c9 Polish the live-smoke ledger: grid slots, dark focus ring, system monitor states (#128)
* Keep declared grid column slots when children run short

- gridColumnCount no longer clamps the declared column count to the child count, so a filtered grid's children keep their column-slot width instead of stretching across the freed row
- pin the rule in the widget layout tests and in both soundboard e2e batteries (Zig example + TS port), which search the album grid down to one match and hold the tile at its natural width

* Desaturate the accent focus ring in dark appearance

- canvas.accentOverrides now takes the resolved scheme and derives the dark focus ring at half the accent's HSL saturation (canvas.accentFocusRing), matching the built-in packs' per-scheme ring moves
- the Zig soundboard theme states the same derivation so both authoring tiers land the identical ring; pins in the token tests and both soundboard suites

* Polish the system monitor footer and empty state in both tiers

- label the footer sample time UTC: local rendering would need a journaled tz channel to keep replay byte-identical, so the honest label wins for this sweep
- clear the transient 'terminate request delivered' notice on the next applied sample (failure notes still stick); pinned in both e2e batteries
- hint the top-128-by-CPU search scope in the no-match empty state, derived from the sampler cap

* Note the per-scheme accent ring in the soundboard-ts README

- the theme_accent bullet now names the dark scheme's desaturated focus-ring derivation

* Floor the dark accent ring at 3:1 and harden grid row math

- accentFocusRing's dark step now lifts the desaturated ring's HSL lightness until it holds 3:1 (WCAG non-text) against the default dark background whenever the accent itself cleared that bar, and never below the accent's own contrast when it did not — halving saturation alone dropped #008000 from 3.9:1 to ~2.6:1; a hue-sweep test pins the floor, and the changelog fragment now states the accentOverrides ColorScheme parameter as a deliberate pre-1.0 break with the .light migration line.
- gridRowCount ceil-divides as 1 + (count - 1) / columns so an unclamped huge declared column count no longer overflows the additive form in safe builds; both the layout and intrinsic-size paths already share the helper, and a unit test covers maxInt columns and zero children.

* Floor the accent ring on the lightest adjacent dark tone and finish the grid row sweep

- accentFocusRing's dark floor now measures against the lightest tone controls commonly sit on (house surface_subtle #262626) instead of the background, so rings drawn on cards and muted chrome clear 3:1 too — green's ring rises from ~2.72:1 to 3.49:1 on the dark surface, and the never-invent escape hatch caps at the accent's own contrast against that same reference
- Route intrinsicGridChildrenSize's row count through gridRowCount: the additive ceil-div still panicked when a parent intrinsically measured a nonempty grid with maxInt columns (the earlier fix only covered the placement path)
- Extend the hue sweep to assert 3:1 per adjacent tone across both packs and add the nested-grid intrinsic regression (verified to panic before the fix)

* Gate the transient-note clear on a sample launched after the kill

- A ps sample already in flight when the kill confirmed collected its rows before the signal, so its ps_done must not retire the delivered notice; both cores now bump a sample generation at launch and clear only when an applied sample's generation exceeds the kill_done stamp (pure Msg-driven state, replay-deterministic)
- Pin the race in both batteries: the stale in-flight sample applies with the notice surviving into the rendered footer (kill exit and stale ps exit drained in one batch), and the first sample launched after the kill retires it
- Rework the live kill-note pins to drive real launches through the cadence so delivered-clears and failure-persists keep their semantics under the generation gate
2026-07-16 11:01:50 -05:00
Chris Tate 4d83ce2f0d Derive every editor mutation through one seam so Escape reaches your core (#129)
* Derive every keyboard editor mutation through one stamped-edit seam

- updateCanvasWidgetTextFromKeyboard now stamps the edit it derives and applies onto the routed event, so the app dispatch hears exactly what the retained editor did — Escape's search-field clear, its composition cancel, and the single-line ArrowUp/Down caret jumps were runtime-only before and never reached the model.
- The app-side msgForKeyboard consumes the stamped edit first and keeps its own derivation only as the fallback for events that never crossed the runtime.
- The context-menu edit path routes through the same seam and dispatches the stamped event.

* Route automation composition verbs through the real ime input path

- widget-action set_composition/commit_composition/cancel_composition now dispatch the ime gpu input events a live IME session produces: journaled for replay, applied by the editor, and mirrored to the core's on_input — direct editor writes kept the model out of the loop.
- set_selection synthesizes the stamped keyboard event the clipboard edits use, so the core's selection mirror follows; it stays un-journaled (no selection input kind exists on the wire).
- The accessibility action dispatch and the mobile widget-action ABI funnel through the same verbs, so all three surfaces converge.

* Pin the edit-derivation seam across mirror, replay, and both tiers

- ui_app tests drive a search field through Escape, ArrowUp, ime cancel, and the automation composition/selection verbs, asserting the model mirror and the retained editor agree.
- The reference record/replay session types into a search field and Escape-clears it, so replay must re-derive the same clear from the raw journaled events.
- Soundboard e2e batteries (Zig live app and transpiled TS core) pin Escape clearing the query and unfiltering the library.

* Add changelog fragment for the Escape edit-derivation fix

- User-facing story: Escape in a search field now reaches your core, plus the automation composition verbs riding the real input path.

* Journal accessibility actions outer-wins so replay dispatches once

- Suppress event records staged while a journaled widget_accessibility_action dispatch is on the recorder's staging stack: the verb's synthesized key/text/drop children are deterministic derivations the replayed action re-runs, so recording both dispatched every child twice on replay.
- Effect results still write through mid-action in feed-then-dispatch order, and event_count/checkpoint ordinals stay coherent with the records a replay reader actually sees.
- Pin the class end to end: repeated AX press and set_text plus the composition verbs record and replay with exactly-once input counts and one journal record per action.

* Journal direct-surface accessibility verbs as outer-wins action records

- Stage a synthetic widget_accessibility_action inside dispatchCanvasWidgetAccessibilityAction when no platform tag-23 event is on the recorder's staging stack: the direct verb surfaces (embed widgetAction, automation widget_action — now delegated through the same dispatch) journaled only untargeted focus-routed children while the verb's focus write stayed unjournaled, so a first-in-session composition or set_text replayed against the wrong editor or none at all; nested inside a staged platform AX event the synthetic stage is a suppressed placeholder, keeping exactly one record.
- Add the composition kinds to the platform WidgetAccessibilityActionKind enum (values 11-13, additive) with both widget_bridge mappings, so the journal and replay's tag-23 arm carry every verb.
- Pin the class: composition first-in-session, composition while another field holds focus, and set_text first-in-session all record action-only journals and replay onto their target editor; a recorder mechanics test pins the nested placeholder.

* Suppress the open arrow's caret edit and bump the journal to v4

- A CLOSED combobox's ArrowUp/Down are its open keys: the app dispatch resolves the press before any stamped edit, so the caret derivation now yields no edit there and the retained editor agrees with the model mirror; an open picker's arrows already walk into the mounted menu, and an expanded trigger without one keeps the caret jump both sides hear.
- Bump the session journal format to v4: this branch serializes the composition action codes 11-13 into accessibility-action records, which a v3 reader would have called corrupt instead of refusing as version skew; the skew message now names the version this build reads, and the skew test pins refusal of older journals too.
- Rewrite the journal's stale coverage note: every automation verb journals now (direct-surface verbs as the outer-wins widget_accessibility_action record), so the v1 do-not-journal caveat is gone.
2026-07-16 10:00:35 -05:00
Chris Tate f7aa92af6d Prepare v0.5.1 release (#127)
- Bump CLI, core, platform, and example versions to 0.5.1
- Merge pending changelog fragments into the marked release entry
- Refresh exact package pins for reproducible publishing
v0.5.1
2026-07-13 14:17:59 -05:00
Chris Tate 4b9d40b871 Carry the TypeScript toolchain as a CLI dependency (#123)
* Carry the transpiler's TypeScript toolchain as exactly-pinned CLI dependencies

- @native-sdk/cli gains @typescript/typescript6 6.0.2 and @typescript/old npm:typescript@6.0.3 as regular dependencies, so npm installs the whole toolchain (the wrapper AND the real compiler it re-exports) in the same transaction as the CLI
- packages/core pins the same two exact versions as devDependencies (replacing the ^6.0.2 range) and its lockfile follows
- check-version-sync asserts both pins string-equal across the two manifests and shape-exact: X.Y.Z for the wrapper, npm:typescript@X.Y.Z for the alias

* Gate TS verbs and direct zig build on toolchain resolution, teach per layout

- transpilerResolves walks node's ancestor node_modules from packages/core and requires the wrapper's manifest + entrypoint AND the aliased real compiler resolving from the wrapper's own directory (nested, hoisted, and global layouts alike); partial extractions read as unresolved
- the gate runs before any zig spawn (check, dev --core, build-graph verbs) and never runs npm: checkouts (the packages/core/test signal) are taught the one npm ci --include=dev against a resolved absolute path, npm layouts are taught the reinstall (BrokenToolchainInstall)
- build/app.zig carries the twin predicate for direct zig build and fails configure with a clean teaching instead of a panic; test-ts-toolchain-twins pins the twins' alias probes and teachings in lockstep

* Add the toolchain-as-dependency changelog fragment

- first TS-verb use needs no network, no install step, and never runs npm
- repo checkouts are taught the one npm ci command; direct zig build teaches instead of panicking

* Exit quietly on the broken-install teaching

- BrokenToolchainInstall already prints its reinstall guidance; list it in failVerb's expected-error switch so the CLI exits without a Zig error-return trace.

* Scope the pre-spawn TS toolchain gate to CLI-generated graphs

- Ejected apps pin their own SDK in build.zig.zon, so gating the CLI's
  resolved SDK false-failed healthy apps whose direct zig build works;
  they now flow to the spawn, where build/app.zig's tsCoreStage teaching
  names the app's actual dependency SDK.
- Name the one generated-vs-ejected decision (isEjectedAt) so the
  preflight and the argv assembly share a single predicate, and keep
  check/dev --core gated: they always transpile against the CLI's SDK.
- Cover both paths in a verbs test: an ejected-shaped TS layout skips
  the gate against a toolchain-less SDK; the generated layout still
  teaches, and passes once the toolchain resolves.

* Raise the TypeScript-tier node floor to 22.15 with a fail-fast teaching

- ts_run.mjs now teaches "upgrade to Node.js 22.15+" and exits before importing a node_modules-resident target when module.registerHooks is missing, instead of dying inside node with the raw ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING; repo-checkout targets keep running natively on any Node 22.
- Every "Node.js 22+" surface (nodeMissing teaching, build/app.zig panic, scaffold README, quick-start docs, changelog fragment) now names 22.15.
- packages/core/test/ts_run.test.ts pins both tiers by spawning the runner with a --import preload that deletes module.registerHooks, plus the real hook-stripping path against the package's own toolchain install.

* Strip every .ts through the runner hook and pin-check the resolved compiler

- build/ts_run.mjs now strips ALL .ts modules (node's default stripping is 22.18+, so the 22.15-22.17 checkout fall-through died raw); hooks-absent teaches for any .ts target, and the hook requires @typescript/old directly with a sane direct-run teaching when the dev install is missing
- typed_ast.ts imports @typescript/old instead of the wrapper, so a consumer tree's conflicting hoisted copy can never shadow the SDK's exact pin (the wrapper stays the declared dependency)
- both resolution twins (ts_core.zig transpilerResolution, build/app.zig tsToolchainResolution) read the resolved @typescript/old version and hold it against the npm:typescript@X.Y.Z pin parsed from packages/core/package.json, teaching resolved-vs-pinned on mismatch; fixtures gain version/pin manifests and mismatch tests cover both layouts

* Validate the compiler from the packages/core origin runtime resolves from

- Both resolution twins now walk @typescript/old from packages/core — the origin typed_ast.ts and ts_run.mjs actually load from — instead of holding the wrapper's origin against the pin, which false-rejected npm's own conflict shape (consumer's hoisted compiler + our exact pin nested under the CLI); the unused @typescript/typescript6 wrapper is no longer probed at all, though it stays a declared dependency.
- Make the node requirement branch-aware everywhere it is taught (22.15+ admits 23.0-23.4 numerically, but registerHooks only landed in 22.15 and 23.5): one phrasing across ts_run.mjs, both gate teachings, the templates README, the quick-start docs, and the changelog fragment.
- Correct the changelog fragment's claim that repo checkouts run on any Node 22: every .ts module rides the same registerHooks stripping, so the floor is uniform across layouts.
2026-07-13 14:00:54 -05:00
Chris Tate 6915fc440a Join effect worker threads before teardown frees the channel (#124)
* Join effect worker threads before teardown frees the channel

- Store spawn/fetch/file worker handles on their slots instead of detaching, join them in reclaim and unconditionally in deinit: the old ~5s give-up abandoned workers still holding slot/queue/io pointers into memory the owner frees right after, which is how a torn-down harness segfaulted the next test inside a stale slot's child_mutex.
- Spawn each child into its own POSIX process group and kill the group on cancel/teardown, so shell-wrapped commands' grandchildren cannot hold the stdout pipe open and stall the worker (and now the join) past the cancel.
- Regression tests: teardown mid-stream joins and returns promptly with no running slots, and an 8-round teardown storm (immediate and cancel-racing) over recycled harness memory.

* Make the ts-core e2e battery tolerant of congested CI runners

- Scale the host battery's real-child wait budget 10x (20s -> 200s): the waits prove correctness and poll, so a healthy run still returns in milliseconds while a loaded runner gets the slack it needs to schedule and reap /bin/sh children.
- A blown wait budget now tears the effects channel down (kill children, join workers) before surfacing TestTimedOut, so one slow test can never cascade a straggling child into the next test's harness.
- The soundboard dispatch-latency test asserts the best of three attempts with unchanged budgets: scheduler contention only adds time, so real regressions still fail while parallel-suite noise no longer does.

* Bound file-worker teardown: interrupt, then abandon-and-leak, never hang

- Effects.deinit joined every worker unconditionally, but a file worker blocked in I/O that nothing converges (a write to a FIFO with no reader, a stalled network filesystem) made teardown hang forever behind it; file workers now get an injectable budget (file_join_deadline_ms, 15s default) with a best-effort cancel of the blocked task at the halfway mark, and past it teardown detaches the thread, warns once naming the stuck op and path, and deliberately leaks everything the worker can still reach (its context, its data buffer, the executor io) so the owner can free the channel safely. Spawn and fetch joins stay unconditional.
- The blocking phase moved out of the channel: each file worker supervises its op as a cancelable Io task (mirroring fetchWorkerMain, which also supplies the platform interruption: SIG.IO signaling on POSIX, NtCancelSynchronousIoFile on Windows, via the threaded io) against a heap FileWorkerContext holding the path copy, buffer, and a commit/abandon handshake - the worker only touches the slot and queue after committing under the context mutex, so an abandoned worker that wakes later walks only leaked memory.
- Regression tests: a write against a reader-less FIFO is abandoned within a tiny injected deadline (loud counter, healthy process after, the woken worker exercised under the leak invariant), the default interruption path joins the same posture with no leak, and the happy-path teardown pins the abandon counter at zero.

* Move the abandonable leak into process-lifetime storage and reject uncancelable fetches

- Allocate FileWorkerContext, its private data buffer, and the shared IoThreaded executor from a process-lifetime allocator (page_allocator) so an abandoned worker never walks memory that dies with the owner's arena or GPA; the happy path frees all three through the same seam (joinWorker, deinit), and a committed read publishes its bytes into the slot's channel-owned delivery buffer.
- Refuse a fetch whose exchange cannot start as a cancelable task instead of running it inline: an inline exchange observes neither cancel nor the timeout and would hang deinit's unconditional fetch join, so the honest terminal is one journaled .rejected (replay reproduces it like any transport failure), with an injectable fetch_concurrent_start seam and a warn-once counter.
- Pin both with tests: an arena-backed channel abandons a FIFO-stuck worker, dies, and the woken worker walks only process-lived memory (leak-checked happy path alongside); the fetch rejection seam delivers exactly one .rejected and teardown returns promptly.

* Bound spawn-worker teardown: interrupt, then abandon-and-leak, never hang

- Group-kill cannot guarantee spawn convergence: a descendant that leaves the child's process group (setsid, a shell's set -m background job) keeps the inherited stdout write end open, so the worker's read never sees EOF and deinit's unconditional join hung forever; spawn workers now get the file workers' full discipline — an injectable budget (spawn_join_deadline_ms, same 15s default) with a best-effort cancel of the blocked task at the halfway mark, then a detach-warn-and-leak abandon with its own abandoned_spawn_workers counter, so spawn, fetch, and file teardown all share one terminal guarantee: bounded return, and every byte a live thread can still touch stays valid forever (Windows, where the direct-handle terminate never reached descendants, is bounded by the same net; job objects remain the future strengthening).
- The blocking phase's world moved out of the channel into a process-lived SpawnWorkerContext (argv/stdin copies, the published-child kill handshake, private framing/collect buffers, the stderr tail ring, drop accounting), supervised as a cancelable Io task exactly like file ops; unlike a file op a spawn DELIVERS while it blocks, so streaming lines enqueue under the context's abandon fence (produceSpawnLine re-checks the abandon with every channel touch) and the committed epilogue publishes collect payloads into the slot's channel-owned delivery buffer, preserving cancel semantics, the stale-event window, and record/replay byte-identity.
- Regression tests pin the escaped-descendant shape (/bin/bash -c 'set -m; sleep 300 & echo $!' — portable setsid): the default interruption path joins it with no leak, the disabled-interruption path abandons it within a tiny injected deadline (loud counter, healthy process after, the woken worker proven to reap its zombie child through only leaked memory), the arena-lifetime test wakes an abandoned worker after the owner's allocator died, and the existing mid-stream teardown pins abandon-count zero.
2026-07-13 10:25:13 -05:00
Chris Tate 8e37536acb Windows fixes: caption clearance, GUI subsystem, elision budget, dark titlebars (#122)
* Hand the full snap quantum back to the text wrap/elision budget

- Edge snapping rounds each frame edge independently, so an exact-fit text frame at a fractional position can lose up to a FULL device pixel of width — the old 0.5/scale hand-back plus the 0.125 elision slack under-covered it, and the TS scaffold's centered counter digit painted as "…" on Windows at scale 1
- textWrapMaxWidth now returns width + 1/scale with the epsilon policy documented at the seam; regression tests sweep fractional origins for every digit at scales 1/1.25/1.5/2 plus the exact centered-row scaffold shape

* Reserve the Windows caption cluster in drag-header layout

- A window-drag header that never consumed the chrome channel's trailing inset laid right-aligned content under the DWM min/max/close cluster; the caption punch-out then visibly truncated it (system-monitor-ts's header status)
- The runtime now runs a one-retry clearance pass: when the built layout leaves drag-header CONTENT under the platform-reported control cluster, it stamps the cluster into DesignTokens.window_controls and rebuilds once, and widget layout trims the drag row's content box on the cluster's side - apps that already pad (soundboard) never trigger and keep byte-identical layout
- Regression: engine tests for both cluster sides and the content-only trigger, plus runtime tests faking Windows-shaped chrome through the null platform for the naive and the contract header shapes

* Give Windows release exes the GUI subsystem and pin it at packaging

- Every app exe was console-subsystem (zig's default), so dev AND packaged apps flashed a terminal behind the window; the posture is now: Debug keeps the console (dev logs live there), release-shaped builds get /SUBSYSTEM:WINDOWS - redirected logging still works because only console AUTO-allocation is subsystem-gated
- native package reads the wrapped exe's PE subsystem (peSubsystem), warns with the rebuild teaching when it wraps a console binary, and reports it in PackageStats.windows_console_subsystem; tests pin the reader and both packaging outcomes over synthetic PE headers
- Verified on the Windows box: ReleaseFast ts-smoke.exe reads subsystem=2, launches headed with no console, automation and file-redirected logs intact

* Dark-mode titlebars on Windows and a kinder scaffold empty state

- Standard-chrome windows set DWMWA_USE_IMMERSIVE_DARK_MODE from the OS app scheme at creation (pre-show, no light-caption flash) and on appearance broadcasts; hidden-titlebar windows keep their pixel-sampled caption fidelity
- The TS scaffold's status bar branches on {stampedMs < 0} to say "press Stamp for a timestamp" instead of "stamped: -1ms"; fresh scaffold verified through native check and the ts-core e2e scaffold pins

* Emit the GUI-subsystem posture into the scaffold's standalone build.zig

- The web-frontend scaffold (Next/Vite/React/Svelte/Vue, native init --full) generates its own build.zig and never set exe.subsystem, so those apps still shipped console-subsystem Windows release exes - and the packaging warn had no remedy, since rebuilding can't fix a scaffold that never sets it
- The generated build.zig now carries the same release-only assignment as the SDK build graph (Debug keeps the console for dev logs), with the condensed rationale
- The Vite template test pins the emitted condition and assignment lines

* Resolve a drag header's anchored children against the cleared rect

- The collision scan counts anchored descendants of the drag header, but the remedy only trimmed the flow content box - an anchored floater triggered the one retry and then stayed under the caption cluster (retry paid, nothing moved)
- layoutWidgetDepth now passes the anchor base through windowControlsClearedContent for window_drag widgets only, covering both the trailing (Windows) and leading (macOS) clusters; non-drag widgets' anchored children are untouched by construction
- Engine test pins the anchored-only collision converging in one pass (scan fires, remedy moves it, re-scan stays quiet), the macOS mirror, and byte-identical non-drag anchoring under stamped tokens

* Run the window-control clearance retry in secondary windows too

- rebuildWindowSlot laid out exactly once, so a model-declared hidden-inset window's drag header still rendered content under the OS caption cluster; it now runs the same collision scan + one-retry pass as the main rebuild, stamping the cluster into a local copy of the slot's tokens.
- windowControlsReservation takes the canvas label so both rebuild paths share it, and the slot's build+layout pass is factored into buildWindowSlotPass (same arena reuse as the main path's buildLayoutPass retry).
- ui_app_window_tests: a colliding secondary drag header re-lays clear of the cluster while the main canvas stays unstamped, and a padded secondary header keeps its own layout.

* Read only the PE headers for the packaging subsystem check

- peIsConsoleSubsystem slurped the whole exe through readPath (capped at 128 MiB) and swallowed every error as false, so a console exe over the cap packaged without the promised warning and packaging allocated the entire binary to read 2 bytes.
- peSubsystemAtPath reads the 0x40-byte DOS header, bounds the rest to e_lfanew + 94 bytes through the existing pure peSubsystem parser, and rejects e_lfanew past a 1 MiB ceiling as not-a-real-PE; only genuinely-unreadable files still degrade to no-claim.
- Tests: a sparse console exe past the old cap still warns, and a bogus 8 MiB e_lfanew answers nothing under the failing allocator (no full-file or offset-sized allocation).

* Persist the subsystem verdict in the report and stats summary

- package-manifest.zon now carries .subsystem = "gui"/"console" whenever the posture check ran (a Windows package with a binary), threaded through writeReport like asset_count; other targets make no claim
- the stats summary prints "subsystem: gui" or "subsystem: console (a terminal window opens behind the app - rebuild with `native build`)" alongside the web-layer and signing verdicts
- PackageStats.windows_console_subsystem is now a ternary (?bool) so an unprobed package cannot pass for a GUI verdict; the synthetic-PE tests pin both report fields and the no-binary null

* Stop claiming a gui subsystem the PE parse never established

- Replace the bool verdict with WindowsSubsystem { gui, console, unknown }: gui and console only when the optional header said so, unknown for non-PE/truncated bytes, benign read errors, and unmodeled subsystem values; null still means the check never ran
- Diagnostic prints "subsystem: unknown (unrecognized executable format)" and the report writes .subsystem = "unknown" instead of affirming gui; console warning behavior and OutOfMemory propagation unchanged
- Pin unknown for a non-PE file, a truncated PE, and a native-subsystem exe; retarget the sparse-oversized and offset-ceiling probes onto the verdict helper

* Package a release-shaped exe from the web-frontend scaffold

- The emitted build.zig defaulted -Doptimize to Debug for everything, so the documented `zig build package` wrapped a Debug, console-subsystem exe; the package step now builds its own exe that defaults to ReleaseFast (mirroring `native build`) while an explicit -Doptimize or --release still pins both roles.
- Registers -Doptimize by hand instead of standardOptimizeOption so the graph can tell unset from explicit, the same optimizeMode split build/app.zig uses; the --optimize arg, the artifact name, and the Windows GUI-subsystem posture all key on the package exe's actual mode.
- Extends the Vite template pins to the package-exe shape and forbids the stale dev-exe wiring.

* Judge drag-header text by its painted bounds in the caption scan

- The collision scan intersected each candidate's full frame with the caption cluster, so a grow/stretch centered title spanning the header row false-positived while its glyphs sat clear — and the paid retry visibly shifted the title.
- Single-line text leaves now intersect their aligned painted bounds: measured width through the same tokens seam the layout ran with, capped at the frame, placed per text_alignment; controls keep the frame test, and span paragraphs or explicit newlines fall back to it conservatively.
- Threads tokens from both windowControlsReservation call sites, pins the centered-title no-retry case at engine and runtime level, and keeps the trailing true-positive, the macOS leading mirror, and post-remedy re-scan convergence green.
2026-07-13 09:00:17 -05:00
Chris Tate 07b259f4f3 Give @native-sdk/core the provenance repository metadata npm requires (#121)
- npm publish --provenance rejected @native-sdk/core@0.5.0: the manifest carried no repository.url to validate against the workflow's repository. Add the repository (with the monorepo directory) and homepage, matching the CLI package.

- check-version-sync now pins packages/core repository.url and homepage to the main package, exactly as it already did for the eight platform packages, and the package-manifest suite pins the fields as publish contract.
2026-07-12 22:54:05 -05:00
Chris Tate e2627ee07f Prepare v0.5.0 release (#120)
- Bump the CLI, platform packages, @native-sdk/core, and runtime version to 0.5.0, and drop the private flag from @native-sdk/core — the release workflow publishes it from this version on.

- Fold the pending changelog fragments into the marked v0.5.0 entry: TypeScript authoring (#119) plus the signing (#118) and static-TLS (#117) fixes.

- Credit co-authors from the release range in the v0.5.0 contributor list.
v0.5.0
2026-07-12 22:45:05 -05:00
Chris Tate 584dbbbaa9 TypeScript authoring: write app cores in TypeScript (#119)
* TypeScript authoring: write app cores in TypeScript

- App cores can be authored in TypeScript and compiled ahead of time to arena-backed native code: the complete language minus the ecosystem and purity violations, checked by tsc plus a teaching checker (NS1001-NS1060), emitting readable Zig with 83ns dispatch, no JS engine, and no GC
- The full platform surface reaches TS cores: the Cmd and Sub effects vocabulary bridged to the real engine, markup views binding the committed model, record and replay byte-identical to node semantics, stock-IDE support, multi-file cores with @native-sdk/core library modules, and native init scaffolding TypeScript by default with Zig first-class by choice
- Two showcase ports prove the bar with zero hand-written Zig: soundboard-ts at pixel parity with its Zig original and system-monitor-ts sampling the real OS, each with end-to-end batteries including replayed sessions with zero host calls
- Docs lead TypeScript-first with a segmented language toggle and a markup-first components reference; the eval suite gains dual-track realistic cases measuring both authoring tiers' health and efficiency

* Ship packages/core in the npm package and run its .ts modules from any layout

- copy-framework.js stages the @native-sdk/core closure (src/, sdk/, rt/, package.json + package-lock.json; test/ and scripts/ stay out), the sync check pins each staged entry plus the dep.path coverage, and package.json "files" covers the mirrored paths
- build/ts_run.mjs runs the transpiler tier's .ts modules on every layout: node refuses builtin type stripping under node_modules, so the runner strips those modules with the transpiler's own installed TypeScript and passes repo checkouts through untouched; build/app.zig, native check, and native dev --core all invoke through it
- the missing-dependency teaching now names the real dependency root (works verbatim on the npm-installed layout, where npm ci runs in the shipped packages/core against its shipped lockfile)

* TS scaffolds ship a CI workflow

- the --full ts-core template now writes the Zig full template's workflow (logic tests + Linux automation smoke, no WebKitGTK) with the node tier added to both jobs: setup-node and one npm ci in the fetched SDK's packages/core, the same install native build's teaching names
- slim scaffolds keep shipping no workflow (zero-config parity with the slim Zig template), now pinned by the ts slim template test

* Wire @native-sdk/core into the release automation

- sync-version.js stamps packages/core (manifest + lockfile own-package fields) and the committed TS examples' pins with the CLI release version, check-version-sync.js refuses a half-bumped tree, and the npm version script stages the stamped files; packages/core rides 0.4.4 from here on and scaffold pins follow the bundled manifest automatically
- the release publish step gains the packages/core publish gated on its "private" flag: private (until the 0.5.0 cut, by design) skips with a loud flip-requirement comment; dropping the flag is the publish switch, no workflow edit needed
- the TS scaffold README notes npm install is optional (the CLI materializes and refreshes the editor package itself), closing the pre-publish gap window honestly

* Provide node to the CI jobs that build TypeScript cores

- The Native Examples job panicked on the missing transpiler dependency, and the Zig Core and tooling jobs were silently skipping every node-gated ts-core suite; all three now set up node and npm ci packages/core
2026-07-12 21:37:07 -05:00
Chris Tate 2d623ac4f6 Move large per-thread canvas scratch out of static TLS (#117)
* Move large per-thread canvas scratch out of static TLS

- Add canvas.lazy_tls.LazyTls: per-thread scratch behind one TLS pointer, heap-allocated and default-initialized on a thread's first use
- Convert the planner/diff/cache scratch giants (advance cache, span wrap cache, frame planner arrays, image decode buffer, probe tables) to lazy per-thread state; only threads that actually plan frames pay for them
- Windows cloned the full static TLS template per thread (~6.5 MiB x every window-host/COM/accessibility/worker thread); the template now carries pointers instead

Co-authored-by: SunkenInTime <76637177+SunkenInTime@users.noreply.github.com>

* Add changelog fragment for the static-TLS working-set fix

- Working-set drop, .tls shrink, and smaller executables, told from the user's side

---------

Co-authored-by: SunkenInTime <76637177+SunkenInTime@users.noreply.github.com>
2026-07-12 19:38:07 -05:00
Chris Tate 87d859fd4f Fail packaging loudly when codesign fails and sign spaced output paths (#118)
- codesign/ditto/notarytool/stapler now exec argv arrays instead of sh -c strings, so bundle paths, identities, and entitlements with spaces reach each tool as one argument
- a signing mode that claims to sign (adhoc or identity) either delivers a signature that passes codesign --verify --deep --strict or fails the package with codesign's own stderr; the report proves the outcome with a "signing: <mode> (signed, verified)" line
- tests pin spaced-path adhoc packaging end to end on darwin hosts, the loud failure paths, and every pipeline argv shape

Co-authored-by: sepehr-safari <25853688+sepehr-safari@users.noreply.github.com>
2026-07-12 19:32:21 -05:00
Chris Tate ce3e42dfd5 Prepare v0.4.4 release (#111)
- Bump the CLI, platform packages, and runtime version to 0.4.4.

- Fold the v0.4.4 release notes for #105, #106, #107, and #110 into the marked changelog entry.

- Credit co-authors in the v0.4.4 release notes and repair the v0.4.3 contributor list.
v0.4.4
2026-07-11 13:20:26 -05:00
Chris Tate 49aa5e7481 Native-only Linux host: compile out WebKitGTK when no web intent is declared (#110)
* Compile the GTK host without WebKitGTK for native-only apps

- NATIVE_SDK_ALLOW_WEBKITGTK_STUB mirrors the Windows WebView2 seam: the define wins over header visibility, compiles out every WebKit-touching path (opaque never-non-NULL web-view pointers keep the GTK-only bookkeeping shape), and stubs the exported webview entry points
- Both Linux build graphs (build/app.zig and the generated template) compile gtk_host.c with the stub and drop the webkitgtk-6.0 link when the web layer is excluded; the native scaffold's generated CI stops installing libwebkitgtk-6.0-dev

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Audit the web layer in Linux ELF executables

- tools/audit_web_layer.zig auto-detects PE vs ELF and hand-rolls an ELF64 reader over the section headers: DT_NEEDED entries naming libwebkitgtk/libjavascriptcoregtk and webkit_/jsc_ dynamic symbols are the evidence, refusal (never a pass) for anything it cannot parse
- native package grows the ELF twin of the PE guard: a WebKitGTK-linking binary packaged under a native-only decision is refused with the same teaching message, pinned by synthetic-ELF tests covering both evidence channels

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Prove the Linux seam in CI from both directions

- linux-webkitgtk gains test-linux-web-layer-audit (seam under webkit-PRESENT conditions: native-only ELF scans clean, web ELF keeps its references) and linux-canvas-smoke drops libwebkitgtk-6.0-dev so the build itself is the native-only link test, with the ELF audit run on the real binary
- vendor pins keep the stub define wired through gtk_host.c and both build graphs; the macOS gpu-dashboard smoke asserts a native-only session spawns zero new WebKit helper processes

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Pin native-only behavior: automation sessions, record/replay, docs

- An automation-driven session on a native-only canvas app proves normal command dispatch plus the WebViewLayerNotBuilt teaching error on a webview attempt, with zero webviews reaching the platform host
- The session record/replay reference journal round-trips identically under web_layer=false; the capabilities and app.zon pages note the user-visible Linux consequence (no WebKitGTK to build or run)

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Forward the web engine to native package from the SDK build graph

- A Chromium exe packaged through zig build package shipped no CEF runtime because the CLI defaults to the system engine; forward --web-engine, --cef-dir, and --cef-auto-install exactly as the generated build graph already does

---------

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
2026-07-11 12:53:47 -05:00
Chris Tate 9b4f62d040 Infer the web layer and enforce native-only builds end to end (#107)
* Infer the web layer and enforce native-only builds end to end

- The build graph parses app.zon and strips the Windows webview layer, loader staging, and dev PATH wiring when nothing declares web use; a webview_layer manifest field and -Dweb-layer flag override inference in both directions
- Conflicting declarations are rejected with one teaching message at validate, configure, runner compile, and package time, and a native-only build that reaches webview creation fails fast with WebViewLayerNotBuilt instead of a blank window
- A PE cross-audit build step pins that native-only Windows exes never reference the loader while webview apps must, and native check prints the web-layer verdict

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Unify web-layer inference behind one shared contract

- app_manifest.web_layer owns the declaration scan, engine folding, and include/exclude decision, usable at comptime by the runner and at runtime by the build graph, validator, CLI, and generated scaffolds, with boundary ownership documented where each adapter lives
- Packaging decides from the resolved engine so --web-engine overrides cannot skew the layer, the runner guard covers shell views and manifest chromium, and the full scaffold emits the same inference, conflict panic, and conditional Windows wiring as the SDK graph
- A contract matrix test runs every manifest shape through every boundary form so the definitions can never diverge again

* Document the webview_layer override and Chromium web intent

- The capabilities page counts a Chromium-resolved engine as web intent and points at the override; the app.zon reference gains the webview_layer field, its inference and include/exclude semantics, and the exclude-conflict rule with the shipped error's remedy

* Carry the web-layer resolution into packaging

- Both build graphs forward their computed web-layer decision to native package via a new --web-layer flag, so the exe and the package can never disagree; a confirming flag keeps the manifest's reason while an overriding one names itself
- Packaging PE-scans Windows binaries and refuses to package a loader-referencing exe under a loaderless decision, closing the mismatch for hand-built binaries too
- Fixes an adjacent buildgraph bug where a sentinel-terminated path allocation was returned as a plain slice

* Honor the webview stub define before header visibility

- NATIVE_SDK_ALLOW_WEBVIEW2_STUB now excludes the embedded layer even when WebView2 headers are globally visible, so a native-only build can never reintroduce the loader reference; the vendor pins lock the guard order
- The stub message says the layer is excluded by configuration instead of claiming the header is missing

---------

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
2026-07-11 11:12:12 -05:00
Chris Tate edbb2045b2 Create the GTK main webview lazily (#106)
- Window create builds only GTK chrome; the main WebKitWebView materializes on first web use with the zero scheme and bridge registration riding along, so canvas apps never start WebKit processes on Linux
- The canvas smoke now asserts zero WebKit processes and drops the sandbox workaround it existed for; child-webview bridge responses no longer require a main webview to exist

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
2026-07-10 17:31:31 -05:00
Chris Tate d1852d5dc2 Teach the Zig 0.16 idioms agents and humans trip on (#105)
- New zig skill and a docs reference page pair each pre-0.16 habit with its verbatim compile error and the repo idiom that replaces it, all compile-verified against the pinned toolchain
- native build points at the guidance when a failed step's errors name missing std members
2026-07-10 17:23:59 -05:00
Chris Tate c5bb87afc2 Prepare v0.4.3 release (#96)
- Bump @native-sdk/cli and platform packages to 0.4.3.

- Mark the v0.4.3 changelog entry for #89, #90, and #92.

- Remove changelog fragments merged into the release notes.
v0.4.3
2026-07-10 14:24:08 -05:00
Chris Tate 36d295ac5c Route cross-volume SDK dependencies through a junction on Windows (#92)
* Route cross-volume SDK dependencies through a junction on Windows

- A project and the npm-global SDK on different drives have no relative path, so the generated build graph now creates a .native/sdk directory junction and references the framework through it; same-volume layouts keep plain relative paths
- Junctions are created via the NT reparse API (no admin rights), refreshed idempotently on retarget or dangling, and never replace real directories; ejected and full-scaffold builds get a teaching error instead since the CLI cannot keep a junction fresh in user-owned build files

Co-authored-by: fleeting-zone <44354736+fleeting-zone@users.noreply.github.com>

* Tidy fallback ownership and route cross-volume errors in mobile packaging

- nativeDependencyPath dupes the dot fallback before freeing the empty relative path, so the errdefer owns it exactly once on every path
- package --target ios and android exit quietly on CrossVolumeFramework instead of dumping an error trace after the teaching text; a failed junction means the generated project cannot build, so no libraryless package is produced

---------

Co-authored-by: fleeting-zone <44354736+fleeting-zone@users.noreply.github.com>
2026-07-10 13:51:10 -05:00
Chris Tate 908e3deb58 Scroll and clip overflowing single-line text inputs (#90)
- Single-line fields clip their text, selection, and caret to the content rect when the value overflows, matching the textarea treatment
- A retained horizontal offset keeps the caret in view: edits, programmatic set-text, pointer-placed carets, and field resizes all re-run ensure-visible, and the offset rides the existing retained value channel so replay and schema pins are untouched

Co-authored-by: kvnwdev <47703820+kvnwdev@users.noreply.github.com>
2026-07-10 12:04:17 -05:00
Chris Tate e71338f872 Blend geometry edge coverage in linear light (#89)
- Anti-aliased fringes of opaque geometry (rounded rects, path fills and strokes) now composite in linear light through a LUT, removing the dark rim sRGB-space blending grows on curved edges; interiors stay byte-identical via an opaque fast path
- Glyph coverage and translucent sources keep sRGB blending so text weight and overlay brightness are unchanged, and tests pin the split in both directions
2026-07-10 08:26:57 -05:00
Chris Tate 20bc1eb6f3 Prepare v0.4.2 release (#88)
- Bump @native-sdk/cli and platform package pins to 0.4.2

- Mark the 0.4.2 changelog entry with release notes from the full post-0.4.1 range

- Merge and remove the pending WebView2 changelog fragment
v0.4.2
2026-07-09 23:46:47 -05:00
Chris Tate 33da7101fa Stroke the checkbox mark through the vector core (#87)
* Stroke the checkbox mark through the vector core

- The check was two diagonal draw_line commands, and the line rasterizer's binary capsule test has no anti-aliasing, so the mark stair-stepped at every scale while the stroke icons around it rendered clean
- One stroked polyline with round caps and a round join now rasterizes the same shape with real coverage; a test pins the anti-aliased property so the mark can never regress to hard edges

* Own path elements in the builder and carry the stroke cap to the GPU host

- Path elements now live in builder-owned storage instead of threadlocal frame scratch, so commands from separately emitted trees can never alias each other's geometry; charts and the spinner adopt the same lifetime
- The stroke cap rides the packet command, both wire encodings (v4), and the fingerprint, and the AppKit host applies it plus the reference renderer's round join when stroking paths
2026-07-09 23:14:12 -05:00
Chris Tate 26df3687f4 Make the Windows embedded WebView layer real (#86)
* Make the Windows embedded WebView layer real

- Vendor the WebView2 SDK header and loader under third_party/webview2 (BSD-3-Clause, license preserved) so repo state alone compiles the embedded layer; a missing header is now a hard error instead of a silent stub
- Fix the conformance blockers behind the guard: a local WRL callback factory for mingw, the uncaptured bridge-handler variable, an EventToken shim, and STA COM initialization on the host thread
- Stage the arch-matched loader beside built, run, packaged, and scaffolded apps, mirror it into the npm payload, and pin the wiring with vendor and loader-layout tests

* Carry the SDK root through package shortcuts and generated builds

- packageShortcut and package-ios now pass the environ map into createPackage like the package verb, so NATIVE_SDK_PATH resolves the framework root for loader staging from standalone binaries
- Generated frontend builds export NATIVE_SDK_PATH on the package command and stage the loader dir on the dev command's PATH, mirroring the SDK-dependency graph
2026-07-09 21:48:41 -05:00
Chris Tate 512298b474 Anti-alias rounded primitives and adopt Per-Monitor V2 DPI on Windows (#81)
* Anti-alias rounded primitives and adopt Per-Monitor V2 DPI on Windows

- Rounded-rect fills and strokes render through one continuous signed-distance coverage field, so curved edges ramp smoothly with no silhouette drift; a supersampled ground-truth test pins shape fidelity and radius-0 rects stay bit-identical
- Hairline borders snap to whole device pixel columns at emit time and geometry snapping is on by default in the house and Geist packs, keeping 1px borders crisp while arcs stay smooth; pure-SDF geometry remains available by disabling pixel_snap.geometry
- Windows apps declare Per-Monitor V2 DPI awareness in the embedded manifest and the Win32 host sizes windows, child views, and surfaces in physical pixels with WM_DPICHANGED re-rasterization, so canvases render at device scale instead of being bitmap-stretched

* Re-pin example reference signatures for the snap default

- gpu-dashboard and gpu-components pin their reference-surface signatures inside the example suites, which only test-examples-native runs; the geometry-snap default changed those pixels

* Re-apply explicit webview frames on DPI change and stamp static tokens with surface scale

- WM_DPICHANGED now re-applies explicit child webview frames rooted at the message window, matching the native-view pass
- effectiveTokens stamps pixel_snap.scale onto a copy of static tokens and scale changes rebuild static-token apps, so hairline snapping stays on the device grid across monitor density changes

* Lay out the components scene with the tokens it renders with

- The catalog laid out under default tokens (geometry snapping off) and rendered under pack tokens (snapping on), so the ceil rule for label-exact widths no-oped and per-edge frame rounding elided the third theme tab
- Layout builders now take the token set, the live app lays out with its surface tokens, and a regression test asserts the theme strip never elides under snapping in either pack

* Snap hairline borders to the lighter whole-pixel width

- Within the existing snap-eligibility window, fractional hairline widths now floor to the lighter device-pixel count instead of rounding, so a 1px border at 1.5x covers one crisp device column instead of two
- Sub-half-pixel strokes still never snap and 1x, 1.25x, and 2x rendering is pinned byte-identical by the updated tests

* Adopt resize-carried density and give each window its own snap scale

- handleResize adopts the event's scale factor before rebuilding, so a DPI-only monitor move re-stamps tokens and re-emits even when the logical size is unchanged
- Window slots own a per-window pixel_snap_scale stamped into their token emission, so secondary windows on different-density monitors snap on their own device grid

* Round native view frames once from accumulated logical coordinates

- Native child view origins accumulate logical x/y through the parent chain and every physical edge rounds exactly once, so nested controls no longer drift a pixel at fractional scales and abutting frames share pixel columns
- The app manifest declares an ordered DPI awareness chain (PerMonitorV2, PerMonitor, legacy dpiAware) so pre-1607 systems degrade gracefully instead of losing awareness entirely

* Round hidden-titlebar content sizes like the standard path

- hiddenOuterSizeForContent rounds scaled content extents through a shared helper instead of truncating, so hidden-titlebar windows and min-size floors match the requested logical size at fractional scales
- check-framework-sync now explains that the package mirror is generated and points at copy-framework.js instead of implying the mirror should be committed

* Chain window DPI resolution through monitor and system fallbacks

- dpiForWindow now mirrors the manifest's awareness chain: GetDpiForWindow, then GetDpiForMonitor via shcore, then the system DPI, so pre-1607 systems that the manifest makes DPI-aware no longer render tiny 1x content
- gpuSurfaceScale delegates to the shared helper and the build pin asserts the chain alongside the manifest elements
2026-07-09 21:32:50 -05:00
Chris Tate c17c64e4c9 Align repository URLs with the renamed GitHub repo (#80)
* Align repository URLs with the renamed GitHub repo

- Point repository.url at vercel-labs/native in all eight platform packages so npm provenance validation passes (the v0.4.1 publish failed on the first package)
- Guard in check-version-sync.js: platform repository.url and homepage must match the main package
- Sweep remaining vercel-labs/zero-native links and zero-native.dev domains across docs, templates, and fixtures to the new canonical names

* Sync repository and homepage fields in sync-version.js

- version:sync now stamps repository and homepage from the main package into each platform package, making the check script's remediation hint accurate
2026-07-09 10:29:58 -05:00
Chris Tate 750824580b Change repository URL in package.json (#78)
Updated the repository URL in package.json to point to the new GitHub repository.
2026-07-09 08:46:57 -05:00
Chris Tate bd8fb61e8c Prepare v0.4.1 release (#77)
- Bump the Native SDK CLI and all platform package versions to 0.4.1.

- Mark the v0.4.1 changelog entry for the npm assets packaging fix.

- Leave the previous v0.4.0 changelog entry unmarked for release publishing.
v0.4.1
2026-07-09 07:46:02 -05:00
Chris Tate 79afe58156 Ship assets/ in the npm package (#75)
- Stage the repo-root assets/ dir in copy-framework.js and list it in package.json files so generated apps can resolve assets/native-sdk.manifest (and the macOS icon/entitlements) from the installed package
- Guard against regressions: check-framework-sync.js now fails when any dep.path reference in build/app.zig is missing from the staged mirror or the files array

Fixes #72

Co-authored-by: lzitser23 <10744132+lzitser23@users.noreply.github.com>
2026-07-09 07:36:26 -05:00
Chris Tate a9ba74a5d4 Add a mobile menu to the doc site header (#69)
- Add a hamburger button on mobile that opens the site nav (Home, Docs, Components) in a sheet
- Hide the search box and theme toggle below the md breakpoint so the mobile header stays minimal
2026-07-08 20:02:48 -05:00
Chris Tate b47111069c Native SDK: the complete toolkit for building native desktop apps (#67)
zero-native becomes the Native SDK. Apps are authored as native markup plus Zig on a deterministic runtime and rendered by the toolkit's own engine into real OS windows — no browser, no WebView, no interpreter in the binary.

- Desktop is complete on macOS, Windows, and Linux: native rendering with per-platform titlebar fidelity, audio playback with streaming, a verified track cache, and real spectrum analysis, native context menus, packaging with sealed code signing, and a deterministic automation and record-replay story.
- Experimental iOS and Android host tiers ship behind the same app manifest: simulator and emulator dev loops, archive-ready packaging, real platform tab bars and push navigation, with embedding over the C ABI underneath.
- The docs site, component catalog, theme packs, showcase apps, and CHANGELOG carry the full account.
v0.4.0
2026-07-08 18:51:43 -05:00
Chris Tate c915991175 Merge pull request #66 from vercel-labs/fix/docs-dependency-security-bumps
Bump docs next and postcss past security advisories
2026-07-02 17:18:16 -05:00
Chris Tate e75e6337a5 Bump docs next and postcss past security advisories
- next 16.2.5 -> 16.2.9 (middleware/proxy bypass advisory, needs >= 16.2.6)
- pnpm override pins transitive postcss >= 8.5.10 (stringify XSS advisory)
2026-07-02 14:16:04 -05:00
Chris Tate a9c6008ced Merge pull request #65 from vercel-labs/ctate/native-list-rows
Add native list item view kind
2026-06-27 20:03:41 -05:00
Chris Tate 3c03182503 Add native list item view kind
- Add list_item to manifest, runtime, platform enums, and desktop host rendering.

- Route list_item commands like native row selections across macOS, Linux, and Windows.

- Document the new view kind and expose it through TypeScript view APIs.
2026-06-27 19:42:30 -05:00
Chris Tate c24b33859f Merge pull request #64 from vercel-labs/ctate/native-first
Advance Zero Native’s native-first app model
2026-06-27 08:56:57 -05:00
Chris Tate 95827f9c6b Fix package mirror check in CI 2026-06-27 02:27:49 -05:00
Chris Tate f92e4d976b Harden CEF external URL wildcards 2026-06-27 02:10:00 -05:00
Chris Tate 54b8c59a46 Harden external URL wildcard policies 2026-06-27 02:02:38 -05:00
Chris Tate 3270b8a9f4 Add package framework sync check 2026-06-27 01:44:04 -05:00
Chris Tate 660b87cd91 Validate native view parent links 2026-06-27 01:18:05 -05:00
Chris Tate 69d8f29ccc Fix dialog buffer overflow handling 2026-06-27 00:57:49 -05:00
Chris Tate f5d49207cd Fix Linux core test credential probe 2026-06-27 00:39:14 -05:00
Chris Tate f8aee3a96a Fix CI content checks and GTK prototype 2026-06-27 00:34:29 -05:00
Chris Tate a32d206884 Fix parented WebView view frames 2026-06-27 00:18:06 -05:00
Chris Tate 137193f113 Fix bridge validation regressions 2026-06-26 22:04:44 -05:00
Chris Tate 5b272a28e9 Fix Android mobile stop lifecycle 2026-06-26 21:54:21 -05:00
Chris Tate ebdc79e6ac Restore main webview state on shell rollback 2026-06-26 21:33:21 -05:00