perf(macos): reuse cell-grid draw resources - #12
Merged
Conversation
* Implement logical canvas radio groups - Scope nested radios as one roving-focus, single-selection group. - Align radio keyboard, pointer, and handler dispatch behavior. - Expose radiogroup accessibility semantics and document the contract. * Fix radio group accessibility edge cases * Fix radio group focus traversal edge cases * Fix radio group keyboard and naming semantics * fix: preserve radio selection semantics
* fix(canvas): dismiss anchored surfaces without focus * fix(canvas): relayout anchors after scroll restore * fix(canvas): place modal surfaces against root * fix(canvas): address anchored surface review issues * fix(canvas): unify window surface behavior
* fix(runtime): reveal autofocus targets and carets - Share scroll-then-verify focus reveal across keyboard, autofocus, and automation. - Preserve selection while revealing collapsed end carets inside text editors. - Cover scrolled focus flows and document the autofocus contract. * fix(runtime): make focus reveals transactional * fix(runtime): make widget focus reveal transactional
* canvas: allow checkbox and radio label content * canvas: add interaction washes to actionable surfaces * canvas: keep selection control geometry and fills consistent * canvas: separate switch thumb and track colors * canvas: preserve resolved colors through disabled washes * canvas: skip zero-width badge and button strokes * canvas: tint destructive alerts and align compact content * docs: regenerate canvas control previews * canvas: preserve text attributes on text-bearing controls * canvas: fast-path idle layout container rendering * canvas: keep authored row fills as the rest state * bench: recalibrate full-view toggle budget * canvas: complete schema and rendering fixes - Enable checkbox and radio labels consistently across schema, markup engines, accessibility, docs, and previews. - Unify actionable state, disabled-color, variant-accent, and zero-width stroke behavior across controls and surfaces. - Correct compact control geometry and destructive alert styling/alignment with comprehensive golden and gate coverage. * canvas: address schema review findings * canvas: fix actionable state routing * canvas: fix drag menus and disabled control tokens
- Render span underlines only when explicitly enabled. - Preserve conventional underlines for Markdown-generated links. - Cover linked spans with and without underline decoration. Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>
* feat(images): decode photos to fit runtime budgets - Decode encoded photos to fit the app's registered-pixel budget across platform codecs. - Add a validated 1-8 MiB app.zon image budget and independent 8 MiB source bound. - Cover deterministic pixels, 1080p loading, replay, ABI plumbing, and documentation. Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com> * fix(images): honor raised budgets across hosts * fix(images): cover generated and non-mac hosts * fix(images): harden source and replay limits * fix(images): fit Android panoramas before validation --------- Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>
) * fix(macos): report content rects in window frame events - Emit content geometry consistently from the AppKit and CEF hosts. - Preserve layout and restore round-trips without titlebar drift. Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com> * fix(macos): restore CEF content geometry --------- Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>
* fix(macos): place fresh windows correctly - Separate restored, explicit, and default window placement from persistence policy. - Honor explicit origins and restore policy across platform seams and both macOS hosts. - Add placement coverage, update docs, and remove obsolete example workarounds. * fix(macos): honor window placement policies * fix(macos): correct window placement policies * fix(macos): preserve window placement contracts * fix(runtime): restore secondary window placement
- Synchronize CLI, core, platform package, and example versions to 0.9.2. - Add release notes and contributors for all changes since v0.9.1.
- Prefer bound combobox submit handlers over Enter trigger activation. - Cover builder, markup, runtime, menu-selection, and command dispatch paths. - Document the TypeScript-first on-submit composition and keyboard behavior. Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
- Derive comptime scan quotas from Msg shape and identifier bytes across generated shims, persistence, channels, and environment routing. - Keep tag-skew diagnostics precise and teach future generated-code quota failures without blaming contracts. - Add 160-arm compile/link and full TypeScript pipeline regression coverage. Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
* fix(app-runner): load manifest menus and commands - Resolve app.zon commands, menus, and shortcuts consistently across live and replay runners. - Add zero-config TypeScript coverage, automation, and documentation. Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com> * test(app-runner): verify manifest menu registration * fix(automation): escape menu snapshot catalogs --------- Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com>
* fix(macos): preserve file drop targets - Route AppKit drops with labeled, view-local coordinates across canvas, WebView, and window fallback paths. - Preserve drop metadata through Zig and cover widget routing, TypeScript contracts, docs, and examples. Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com> * fix(macos): preserve file drop coordinates * fix: keep file drop targets sized * fix: keep capability webview interactive * fix: constrain capabilities window width --------- Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
* feat: add model-driven theme state - Add the TypeScript themeState helper across checker, generated ABI, and UiApp runtime. - Preserve system accessibility behavior while supporting model pack, scheme, and accent precedence. - Document the API and migrate gpu-components with runtime and replay coverage. Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com> * fix: constrain theme state optional fields --------- Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com>
* Add platform-correct Command Backspace - Add semantic line-start deletion across Zig and TypeScript text mirrors. - Preserve platform keymaps, controlled state, undo, and replay behavior. - Cover all editable widgets and document the shortcut. Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com> * Fix Command Backspace edge cases * Fix platform-specific Command Backspace test --------- Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
- Synchronize CLI, core, platform package, and example versions to 0.9.3. - Add release notes and contributors for all changes since v0.9.2.
* feat(windows): expose model restore policy - Forward restore policy from model-declared windows into platform options. - Expose matching TypeScript types, defaults, validation, and documentation. - Cover fresh center-on-primary windows through the null platform. * docs(windows): clarify restore policy support
* fix(build): keep iterative app rebuilds local - Stabilize generated core and service ABI artifacts by content so implementation-only service edits never rebuild the app core. - Split primary markup and app code into independently cached objects, reducing warm markup rebuilds to data compilation plus linking. - Add rebuild explanations, phase timing/RSS summaries, and forward-compatible scriptc dev profiles through its published binary. * fix(build): launch ScriptC correctly on Windows * fix(build): address PR review findings * fix(build): preserve cached app link inputs * fix(build): invalidate cores on SDK module edits
* chore(core): upgrade scriptc to 0.0.33 - Pin scriptc 0.0.33 across Native packages and refresh compiler surface and calibration artifacts. - Require Node 24 and route check/build tooling through scriptc's published compile-cache bootstrap. - Enable and verify dev/release library profiles with synchronized docs, tests, and package mirrors. * fix(ci): use Node 24 for TypeScript builds
- Synchronize CLI, core, platform package, and example versions to 0.9.4. - Add release notes for model-driven window restore policies and iterative rebuild improvements. - Document the ScriptC 0.0.33 and Node 24 toolchain update.
* feat(tray): add typed rich rows - Add typed metric, segmented-choice, and bounded chart rows across Zig and TypeScript. - Render native macOS controls with command routing and accessible platform fallbacks. - Support independently styled persistent menu-bar titles with configurable size, weight, and number style. * fix(tray): keep rich rows model-owned * fix(tray): harden rich row contracts
* Make app.json the default manifest - Add full-featured app.json parsing, discovery, build conversion, and app.zon fallback. - Scaffold JSON manifests by default across TypeScript, Zig, web, full, and ejected projects. - Publish the JSON Schema and update validation, vendoring, docs, skills, and npm payloads. * Fix app.json manifest regressions * Fix app.json conversion and schema bounds * Publish versioned app schema * Fix app.json vendoring and schema bounds
…abs#389) - Resolve scriptc through npm package resolution so nested, hoisted, and global sibling installs work. - Generate a complete SQLite SDK module family and validate library imports against their actual directory. - Cover global compiler lookup, documented event imports, and generated-core loading with regressions.
…s#388) * feat(schema): redirect non-schema paths to native-sdk.dev - schema.native-sdk.dev now serves only /app/v1.json and its /app.json alias - Every other path catches and redirects (302) to https://native-sdk.dev - Update pinned docs check and schema README to match the new routing * fix(schema): redirect non-schema paths
* feat(canvas): expose registered image source crops - Add image_src to ElementOptions for texture-atlas rendering. - Expose atomic source rectangle attributes through Native markup. - Cover schema, compiler parity, validation, and authoring docs. * fix(markup): complete image crop suggestions * fix(canvas): prevent texture atlas sampling bleed
- Make app.json the default manifest with a published versioned schema. - Add registered-image source cropping and harden installed TypeScript toolchain resolution. - Synchronize v0.9.5 package metadata and release notes.
* feat(canvas): add context menu policy * fix(canvas): retain secondary gesture ownership * fix(canvas): preserve full terminal viewport * fix(canvas): admit complete terminal fonts * fix(canvas): cost underlined terminal runs safely * chore: follow history-driven changelog workflow * fix(canvas): preserve context menu invariants --------- Co-authored-by: phall <phall@noreply.github.com>
A terminal is the densest widget the toolkit hosts, and the per-view frame budgets were sized for desktop chrome. A terminal row costs one display-list command per contiguous same-background run plus one per contiguous same-foreground run, so a styled 200-column row runs 30-60 commands where a whole three-pane view runs a few hundred. Against the old 2,048-command budget (1,792 after the widget chrome reserve) a 60-row viewport had ~30 commands per row to spend, and the painter degraded from the BOTTOM in silence: a realistically colored 200x60 screen (syntax highlighting, htop meters, a colored build log) painted 34 of its 60 rows and the rest showed bare background, with nothing in any log to say a budget had eaten it. Measured on a 200x60 grid at the widget tier (terminal_grid_tests.zig prints both): realistic styling (50 fg runs/row): 34/60 rows -> 60/60 truecolor (distinct fg+bg per cell): 4/60 rows -> 9/60 Changes: - canvas_limits: max_canvas_commands_per_view 2048 -> 4096 and max_canvas_text_bytes_per_view 32 KiB -> 64 KiB (a 300x100 viewport is ~30 KB of presented text before chrome, and a split holds back a share for its sibling). Both host retained-command caps pin the new number (appkit_host.m, gpu_surface_renderer.cpp), and canvas.max_display_list_text_bytes / the new canvas.max_display_list_commands stay in lockstep by test. Measured cost: ~696 B per command slot across the view's retained mirrors, so RuntimeView 3.44 -> 4.83 MiB and the 32-slot Runtime 110.0 -> 154.5 MiB of fixed-capacity address space. - Truncation is never silent. terminal_grid.paintReport returns the rows painted, the rows handed over, and the store that stopped it; paint() keeps its exact signature and every paint records budget stops on the builder (canvas.DisplayListDegradation), which the runtime turns into one teaching log line on the EDGES of a degradation rather than once per frame. - The widget emit path's frame scratch moves off the stack into the per-thread pool the frame planner already uses. At the new command budget the display list, the chrome copy store and the diff output no longer fit a stack frame under the widget emit recursion — a measured segfault inside the button emitter. What this does NOT fix, deliberately: a viewport with a distinct foreground AND background per cell merges nothing and wants two commands per cell — ~24,000 for 200x60, ~60,000 for 300x100. At ~696 B a slot that is 500 MiB and 1.3 GiB across the view slots, and a trial raise to 8,192 alone (255 MiB) crashed runtime construction. That density is not a number in canvas_limits; it needs a packed cell-grid command the host renderers expand themselves. The budgets now carry realistic styling and say so out loud when they cannot. Also: widening a clip no longer leaves stale pixels behind. The render planner erases push_clip/pop_clip into a per-command clip field, so the retained packet baseline holds no key for a clip and the refined dirty rect could not name pixels a growing clip revealed over unchanged content — the frame kept whatever the host last drew there (the stale columns a terminal pane showed after a split collapsed back to full width). The baseline now carries its clip rects and the next frame adds the difference, in both directions. Frames whose clips did not move, including tweens that move content under a stationary clip, keep their region-scoped patches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A terminal is the one surface whose content scales with AREA rather than
with design. Painting it as display-list shapes cost one background
command per contiguous same-colour run plus one text command per
contiguous same-foreground run, which merges into nothing on a styled
screen: a 200x60 truecolor viewport wanted ~24,000 commands against a
per-view budget of 4,096 and painted nine rows of sixty, the rest bare
background. No budget raise reaches that shape — one command slot costs
~700 B across the view's retained mirrors, so 60,000 commands is 40 MiB
per view before a pixel exists.
`CanvasCommand.cell_grid` (src/primitives/canvas/cell_grid.zig) carries
the screen instead: a cols x rows lattice of 20-byte cells, each with its
own background, cluster offset, foreground, and style, which every
renderer expands itself. Geometry is implied by the index, so nothing
per-cell is stored but content.
Measured (terminal_grid_tests.zig prints both):
200x60 truecolor, distinct fg+bg per cell:
before 9/60 rows, ~1,600 commands
after 60/60 rows, 4 commands, 12,000 cells, 36 text bytes
300x100 truecolor, distinct fg+bg per cell:
after 100/100 rows, 4 commands, 30,000 cells, 585 KB, 36 text
The command count is now a constant, not a function of styling. Cluster
bytes are INTERNED, so a screen drawn from a 36-character alphabet costs
36 bytes however many cells it has.
Three bug classes close by construction:
- Reflow is safe. A screen is ONE retained key replaced wholesale, so a
row that loses a run cannot orphan a per-run command keyed by its old
start column. That was the stale-column bug — a split collapsing back
to full width left a duplicated prompt line and a truncated hostname
in the revealed columns. Verified gone by running the consuming app
against this commit and driving split/close twice.
- Cell geometry is exact. A cell's position is its index, so a combining
mark or a wide cluster can no longer advance its neighbours out of
their columns whatever the face does.
- Every SGR attribute has somewhere to live. `TerminalCell` gains bold,
italic, strikethrough, overline, six underline styles and an underline
colour; `TerminalCursor` gains `blinking` and `wide`, and
`TerminalCursorShape` gains `block_hollow` so an emulator-requested
hollow block stops colliding with the focus-driven outline. All
additive: the consuming app builds against this commit unmodified.
The reference CPU renderer is the oracle and is complete: two passes
(every background, then every glyph and decoration, so a neighbour's
background cannot erase an overhanging glyph), each cell's ink through
the same `drawGlyphBox` path a text run takes. Decoration geometry lives
in `CellDecoration` so a future host encoder reads the same source.
The GPU packet layer marks `cell_grid` unsupported, which routes terminal
frames to the CPU pixel path — the reference renderer — on every host.
That is correct everywhere from day one and needs no wire-format change;
it is also slower than a native encoder, and closing that is the next
step. Deliberately not shipping an unverified one: automation screenshots
render through the reference path, so a host encoder cannot be validated
here and a wrong one would diverge silently on the user's glass.
Also fixed, all found by the size increase:
- `paintInto` in the terminal tests returned a `Builder` BY VALUE, which
left every builder-owned slice aimed at a dead stack frame. Text runs
were small enough to survive it; a 38-cell grid was not.
- `Builder.initAt` and `CanvasDisplayListScratch.reset` replace
whole-struct assignment on the widget emit path. Both structs carry a
frame's inline storage, so `x.* = .{}` built a megabyte-scale stack
temporary and overflowed the thread.
- `runtimeViewInfo` took a multi-megabyte `RuntimeView` by value.
Memory: RuntimeView 4.83 -> 5.45 MiB (the retained cell array), so the
32-slot Runtime goes 154.5 -> 174.5 MiB of fixed-capacity address space.
The command budget could now come back down to 2048 and give ~45 MiB of
that back, since the terminal is what drove it up; that is a separate
change with its own measurement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The packed cell grid fixed what a terminal could DRAW and broke how it
PRESENTS. `cell_grid` was unsupported by the GPU packet layer, so one
command dropped the whole view to the CPU pixel path: every frame became
a full-surface upload (~2.8 MB at 1x, 11 MB at 2x) and incremental
dirty-region patching turned off entirely.
Measured on the running app at 1100x640, through the automation
snapshot:
before after
gpu_present_path pixels -> packet
present_mode none -> patch
present_patch_bytes 0 -> 789 (1 upsert on a shell prompt)
present_fallback unsupported_command -> none (0 frames)
gpu_input_latency_ns 19,392,000 -> 5,360,000 (budget 16,666,666)
budget_exceeded 1 -> 0
Three pieces.
ONE GRID COMMAND PER ROW, not per screen. A retained command is the unit
of CHANGE: a screen-wide grid makes a keystroke re-encode and re-upload
every cell, which is the full-surface cost merely moved from the
rasterizer to the wire. A row is the granularity a terminal actually
changes at. Rows are bounded by `max_rows`, so a 300x100 truecolor
screen is 103 commands where per-run painting wanted ~60,000.
Cluster interning moved from per-SCREEN to per-ROW for the same reason,
and this one was measured the hard way: a blob shared across rows put
every row's fingerprint on every other row's characters, so one new
letter re-encoded the screen — 31 upserts and 8,449 bytes per frame.
Per-row it is 1 upsert and ~400-800.
WIRE FORMAT v6 plus the AppKit decoder. `cell_grid` is command kind 14,
its payload implied by the kind (the flag byte is full). Cells encode as
a delta stream — a tag byte per cell whose low bit means "same style as
the previous cell" — so a plain row costs about a byte a column and only
a genuinely per-cell-styled row pays the full 15. Clusters ride inline
per cell, so a row decodes without the rest of the screen.
The host renderer mirrors the reference renderer deliberately, including
its two-pass order (every background, then every glyph and decoration —
one pass lets a neighbour's background erase an overhanging glyph) and
its exact decoration geometry: all six underline styles, underline
colour, strikethrough, overline, wide cells. Stated rather than hidden:
glyph RASTERIZATION differs (CoreText vs the engine's outline filler),
as it already does for every draw_text command; `bold` and `italic` are
carried but not synthesised, matching the reference renderer rather than
getting ahead of it.
Windows moves to v6 and refuses only packets containing a cell grid (an
unknown kind fails validation), so every non-terminal frame keeps the
retained Direct2D path. Not verified on a device — no Windows here.
Also:
- `TerminalCursor.blinking` is real now instead of documented-only. The
runtime arms the same looping opacity animation a text caret uses,
keyed on the new `terminal_grid.cursorCommandId`. A painter has no
clock; blinking is time, so it was never the painter's to stamp.
- The command budget returns to 2048 (the terminal was the only reason
it went to 4096 and now costs ~100 commands), handing back ~45 MiB of
the Runtime's fixed address space. Both host retained caps follow.
- Grid command ids move to `0x60_0000 + row`, out of
`reserved_id_offset` (0x62_0000) where callers layer their focus ring
— the previous commit put the grid and the ring on the same key.
Suite: 2970 pass / 14 skip / 2984 total.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bold` and `italic` reached `CellFlags` and every renderer and then changed no pixels: `drawCellGrid` built its run with the grid's single `font_id`. So `\x1b[1m` and `\x1b[3m` were carried honestly and drawn identically to regular. A cell grid now carries a FONT FAMILY — the regular face plus bold, italic, and bold-italic companion ids — and both renderers pick per CELL off the style flags. One row mixes weights freely. Where the faces come from: the APP registers them (`Runtime.registerCanvasFont`, ids >= 64) and names them in `DesignTokens.typography.mono_bold_font_id` / `mono_italic_font_id` / `mono_bold_italic_font_id`. The SDK bundles only GeistMono-Regular and does not presume to ship a consumer's type; an app that supplies nothing still gets visible weight, because: SYNTHESIS IS THE FALLBACK, and this commit implements it. Stated plainly rather than dressed up as real faces: a missing companion is faked — bold by drawing the glyph a second time offset by max(1, size/14) px, italic by shearing 0.2 about the baseline. Both renderers read the SAME rules (`cell_grid.CellSynthesis`) because a bold run that renders bold on the oracle and regular on the host is worse than no bold at all. A half family is used for the half it covers: a real bold face with no bold-italic is sheared rather than double-faked. Measured through the reference renderer, "mono" at one size: regular 135 inked pixels, bold 189 (+40%), italic 124 (different shape, not a second pass). Rendered a four-row sheet (regular / bold / italic / bold-italic) and looked at it: four visibly distinct weights, columns aligned identically across all four. CELL GEOMETRY DOES NOT MOVE, which is the invariant the packed cell rests on and the thing most likely to break here. A bold face has different advances; in a lattice that must change nothing, because a cell's position is its INDEX. Pinned by a test that compares every cell rect, the cell width, the baseline, and the command's raster extent across regular/bold/italic rows. Faux bold thickens ink inside the cell and faux italic shears about the baseline — neither touches the pen. Italic overhang is real ink and is protected by the existing two-pass order (all backgrounds, then all glyphs). Pinned by rendering an italic 'H' beside a bright background cell and asserting the lean survives. Wire format v7 carries the three companion ids. Windows follows the version and still refuses only packets containing a cell grid. Also: the terminal cursor blink is checked BEFORE the focus-visible gate. A text caret blinks once focus is VISIBLE (the keyboard ring); a terminal cursor blinks whenever the terminal holds focus, however it got it — a click-focused terminal would otherwise sit steady while the program that asked for `\x1b[1 q` waited. Suite: 2975 pass / 14 skip / 2989 total. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…creen verb
Two things a multi-window terminal app cannot do without.
=== A SECONDARY WINDOW CAN PAINT ITS TERMINALS ===
`rebuild` runs the app's `chrome.build` through `installChromeDisplayList`
— the path that produces every terminal cell. `rebuildWindowSlot`, the
secondary-window path, instead published the widget layout and emitted
with `WithChrome(.{})`: a chrome prefix of ZERO. So a second window
opened, laid out correctly, drew its tab strip, its split divider and
its widget bounds, and painted no cells at all. `ChromeOptions.build`
also took no window label, so even reaching it could not have helped —
it could not know which window it was painting.
- `ChromeOptions.build_window` is the per-window builder, taking a
`ChromeContext` (canvas label, window id, that window's size, tokens,
is_main). A struct rather than more parameters so the next thing a
window needs to know does not break every caller again.
- `installChromeDisplayList` is parameterized by window — label, size,
and the tree-currency flag — instead of reading the main canvas's.
- `rebuildWindowSlot` takes that branch, and so do per-window terminal
sizing (`applyTerminalLayout`) and web panes, which were also
main-canvas-only.
- `handleWindowSlotFrame` calls `on_frame` with the slot's own
`gpuSurfaceFrame`, so each window has a viewport/PTY pump.
ADDITIVE: `build` is untouched and still works. The slot path is gated
on `build_window` being set, not merely on `chrome` — a builder that
cannot name a window would paint the MAIN window's content into a
secondary one, which is a different wrong answer, not a fix. An app that
does not migrate sees no change. Migration is one line:
.build = view.buildChrome,
// becomes
.build_window = myBuildWindowChrome, // (model, builder, context)
Verified by running it: a scratch `windows_fn`/`window_view` spike in the
consuming app, built against this commit, opened a second window and
BOTH windows painted live zsh prompts simultaneously — read from the two
automation screenshots. The spike was reverted.
Also fixed on the way: the install path re-emitted with a zero chrome
prefix right after `rebuildWindowSlot`, which would have erased the
chrome it had just installed.
=== THE FULLSCREEN VERB ===
`PlatformServices` had focus/close/minimize/show and no way to enter or
leave fullscreen, so an app could be told it was fullscreen and never ask
to be. Worse, `WindowState.fullscreen` existed but nothing ever filled
it — even the window-state store persisted a constant false.
- `set_window_fullscreen_fn` on `PlatformServices`, `Runtime.setWindowFullscreen`,
`Effects.setWindowFullscreen(label, bool)` and `toggleFullscreenWindow`.
SET rather than toggle so the call is idempotent and an app can restore
a remembered layout without computing parity; macOS compares the style
mask and only calls `toggleFullScreen:` when it differs.
- The READ half now exists: `WindowInfo.fullscreen`, filled from the
window's style mask on every macOS frame emit, so a transition the USER
started from the green button reports exactly like one the app asked
for. `WindowInfo.state()` finally carries it into `WindowState`.
Chose (a) over (b), deliberately. (b) — appending the stock Enter Full
Screen item when an app supplies custom menus — is a real bug, but the
fix changes "you supplied a menu bar" from "you own it" to "you own it
plus items we inject", which is the wrong default for a framework whose
doctrine is explicitness, and it leaves an app still unable to drive
fullscreen from anything but a menu item it does not control. With (a)
in place a custom-menu app binds its own item in one line, and the
capability also serves shortcuts, buttons, and launch-state restore. (b)
remains worth doing as an opt-outable documented policy; it is not this
commit.
Suite: 2979 pass / 14 skip / 2993 total.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every glyph the AppKit host draws lands in a CGBitmapContext, and font smoothing - macOS's stem darkening for text - is off by default on the transparent backing all three of those contexts use. The result is that ALL text renders systematically thin, which is what the faint-text report was about. Measured with phux-cockpit's scripts/measure-glyph-smoothing.m against the bundled JetBrains Mono NL, 13pt: scale 2 solid stem pixels 2341 -> 3164 (+35.2%) scale 1 solid stem pixels 635 -> 940 (+48.0%) Filling an opaque ground under the cells was also considered and is NOT included: measured on the same harness it moves solid stem pixels by -0.3% (2341 -> 2334), so it buys no weight while adding a full-screen fill to every frame. The CPU reference renderer never touches CoreText and blends coverage itself, so it does not share the defect and no reference screenshot can catch a regression here. Re-measure rather than eyeball. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A closed window keeps its runtime table slot until its label or id is re-created -- `removeWindowAt` runs at re-creation, not at close -- so `self.windows[0..window_count]` still carries every window the app has ever declared, with `info.open = false` on the dead ones. The snapshot walked that span unfiltered and published a `window @wN` line for each, while `listViews` and `appendAutomationWidgets` correctly found nothing under them: a ghost window with no views. That misleads exactly the assertion a smoke test wants to make. A test that opens a settings window, closes it, and asserts the window COUNT returned to one passed against a window that is not on screen, and a test asserting the reconciled window set matched saw closed windows in it forever. Filter on `info.open` and advance the output index only for the windows that survive, so the published slice is the windows that are actually up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`widget-key` dispatched a `key_down` and nothing else, so every key-lifetime latch it armed stayed armed. Those latches exist because a physical chord's release carries different modifier flags than its press (Cmd is often already up by the time G comes back), so the classification is latched on the view at the down and retired at the up -- `consumeCanvasWidgetTabInputFocusEntry`, `consumeCanvasWidgetTerminalPasteKeyLifetime`, and any app-level shortcut latch built the same way. With no release ever arriving, the SECOND drive of the same chord found the latch still held from the first and was swallowed as the missing release: `widget-key canvas cmd+g` twice in a row did the work once. Fixed by synthesizing the release, not by adding an explicit release action. Real hardware has no press without a release, so a harness able to emit an unpaired one is a harness able to reach states no user can; and pairing keeps every existing automation script's wire format AND its meaning intact -- one `widget-key` line is still one keystroke. A new release verb would instead have made correctness opt-in and left every script written before it driving half a keystroke. This is the discipline `widget-drag` (down/drag/up) and `widget-pinch` (begin/change/end) already follow, down to sharing one timestamp across the events because press and release are one gesture. The release carries the chord's modifiers but never `text`: `key_up` is deliberately barren in both text paths (`canvasWidgetTextEditEventFromGpuInput` and the target-less commit fallback), so it cannot double-insert what the press already typed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ChromeOptions.build_window` takes a `ChromeContext` naming the window it
is painting; `Options.web_panes` took only the model. But panes are
reconciled PER WINDOW -- `applyWebPanes` runs from the main rebuild, from
every secondary window slot's rebuild, and from the presented-frame
ride-along -- so a hook that could not be told which window it was being
asked about had no choice but to answer with the whole app's pane set
every time.
A webview belongs to exactly one window. Every OTHER window's rebuild
therefore resolved that pane's anchor against a widget tree that does not
contain it and logged
webview pane '<name>': no canvas widget carries semantics label ...
Behaviour was never wrong (the pane is found and snapped in the window
that owns it), but the warning fired on every rebuild of every other
window, which is the kind of noise that trains people to stop reading the
log.
Pass the same `ChromeContext`, built the same way `installChromeDisplayList`
builds it, so an app switches on one context shape whichever per-window
hook it implements: return the panes this window owns, and 0 for a window
that hosts none.
Changed in place rather than added alongside, unlike the `build` ->
`build_window` migration: `build` is a required field that every chrome
app already implemented, while `web_panes` is optional and its every
in-repo implementor is updated here (the ui-app preview test, and the
canvas-preview, workbench, and split-collapse examples). One hook with a
context beats two hooks with a precedence rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`semantics.hidden` was the only way to say "keep this out of the accessibility tree", and it also suppresses painting. Authors reaching for it on chrome — a search field's magnifier glyph, a rendered caret — got a blank gap where the glyph should be, and no way to tell the two intents apart. Add `WidgetSemantics.decorative`: the widget and its subtree leave the semantic tree (and with it automation snapshots) and can never take focus, while paint, layout, hit-testing, and event routing are untouched. It is the `aria-hidden` / `role="presentation"` counterpart. `hidden` is unchanged and now documented for what it actually is: a VISIBILITY flag that keeps the layout box — space reserved, siblings not reflowed — while dropping paint, hit-testing, focus, and announcement. That is exactly what an empty fixed-width slot wants (room held for an affordance that is not currently showing) and what `Ui.nav`'s retained inactive pages want, so every existing call site keeps its behavior: the two engine uses that STAMP the flag (nav retain, anchored-tooltip visibility), the paint/hit-test/routing/dismissal walks that read it, and app-side spacers alike. Both flags now answer one predicate, `WidgetSemantics.concealedFromAccessibility`, so the announcement side cannot drift. The a11y audit's `nodePainted` becomes `nodeAnnounced` and skips decorative subtrees — a glyph declared as decoration must not then be reported as an unnamed image — and `widgetSemanticsEqual` gains the field, so flipping `decorative` marks semantics dirty without a repaint. The field fits existing padding: `@sizeOf(Widget)` stays 776. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`native-sdk.os.openUrl` was a webview bridge command and `openExternalURLIfAllowed:` a webview external-link policy decision, so a Zig core with no web content had no way to ask the OS to open a URL — the platform verb existed on every host (`PlatformServices.openExternalUrl`, NSWorkspace on macOS) with nothing app-facing wired to it. `fx.openUrl(url)` closes that. It follows `showNotification`, the channel's other fire-and-forget platform-service effect: validated on the loop thread, one synchronous platform call, no terminal Msg (the OS owns whether a handler launched, so a success Msg would over-promise), inert under fake execution and session replay. The URL is treated as hostile, because cores build them from terminal output, fetch bodies, and pastes. `validation.validateOpenUrl` refuses whole — never trimmed or coerced into something openable — an empty URL, one past `platform.max_external_url_bytes`, one carrying a NUL or any other control byte / whitespace / DEL, and any scheme outside the allowlist (`http`, `https`, `mailto`, matched case-insensitively so a shouty scheme is honored and a shouty `JavaScript:` still is not). `file:` and `javascript:` are refused by not appearing in it. Unlike `runtime.openExternalUrl`, the effect is not gated on the webview external-link policy: that policy governs links web content follows, while this call comes from the app's own `update`. A test observes it the way the notification effect is observed — bind the null platform's services and read `lastExternalUrl()`, which records the request without opening anything; a refused URL leaves it empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
canvasGpuCommandFingerprint decides whether a retained canvas command must be re-encoded and sent to the GPU host. It hashed kind, bounds, opacity, stroke_width, cap, id, clip, transform, shape, paint, image, text and effect -- and never read command.cells. The cell_grid arm of gpu.zig puts ALL terminal content into .cells and leaves shape, paint, text, image and effect empty; CellGrid.bounds() is purely geometric. So a cell_grid row's packet fingerprint was INVARIANT under every possible content change. Downstream, canvas_frame.zig sets upserts[index]=false on a fingerprint match and then skips re-encoding, and appkit_host.m calls rasterCacheRemoveKey only for evicted or upserted keys -- so the host re-blits the raster it already holds. A terminal emits one cell_grid PER ROW under a stable key, so once a retained baseline existed a row's glyphs could never change on glass again. Content only appeared when something else forced a full present. Reported downstream as: terminal output not appearing when it should, an occasional prompt landing at random, and typed text never showing. Confirmed fixed on the real app by the reporter. The cursor is a separate fill_rect whose bounds and paint ARE hashed, so it kept moving over stale pixels -- which is why this read as "the text is the same colour as the background" rather than as a stuck frame. It also defeated every instrument used to chase it: the CPU reference renderer re-rasterizes from the content-aware display list, so reference screenshots always looked correct, and a background colour change alters a fill_rect PAINT, which IS hashed, so that one did reach the glass. The fix mirrors render_fingerprints.zig's cellGridFingerprint, which already hashes the grid identity plus std.mem.sliceAsBytes(cells) over a full screen every frame, so the cost is known-acceptable. canvas_frame_patch_tests.zig now covers it: the fingerprint must differ for a cluster change, a colour change, and a flag change. Seen failing before the fix (exit 1) and passing after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rror
`packages/core/src/devhost_journal.mjs` arrived with v0.9.0 and bakes the
automation protocol fingerprint as a JS literal, with its own comment
saying it is "kept in lockstep with `zig build print-pins`". Nothing
computes it: the Node suite only ASSERTS the two sides agree.
This fork's first commit bumps `automation.protocol.semantic_epoch` 1 -> 2
(the context-menu action-token generations), which moves the Zig-side
fingerprint from 0x096c8aa4730c11ec to 0x59d66f39803fd602. Upstream had no
JS mirror to update at v0.8.x, so the two only collide now.
Symptom on the rebase, and the only test that failed on it:
ts-services-e2e-tests
test "a devhost-recorded service session replays in the packaged
runtime without a host"
replay refused: the journal was recorded by a build whose automation
protocol differs from this build's (journal 0x096c8aa4730c11ec,
this build 0x59d66f39803fd602)
The devhost RECORDS the journal with the stale JS constant and the Zig
runtime then refuses to replay it -- the skew guard working exactly as
designed, on a skew that is real.
Value taken from `zig build print-pins` on this branch, which is the
source the file names. The journal FORMAT fingerprint
(0x7f56860e36c25e39) is untouched and already agrees; only the automation
protocol pin moved.
Not folded into the commit that bumps the epoch: that commit is the
squashed v0.8.1 restoration PR and predates the file existing, so the
adjustment belongs to the rebase, not to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vely
A TypeScript-core app had no way to put its own Zig beside the generated
runner: CoreOptions.host_calls, the view/markup options and the final
native_sdk.App were built and consumed inside ts_core_main.zig. Cockpit
needs exactly that seam to keep its terminal engine native while its
chrome moves to core.ts, so this adds a narrow one.
AppOptions.native_extension names an app-owned module that exports
configureCoreOptions, configureOptions and app; the runner calls them in
that order between generated setup and launch. HostCallMux is generalized
from the persistence-only pair into a routed/default pair with a route
predicate, so an extension can reserve a command namespace without
touching generated service bindings, and its poll seam alternates so one
busy host cannot starve the other.
Two things the fixture found about testing extensions:
* Zig collects tests from the root module only, never from an imported
one, so an extension's tests cannot ride the app test binary. A canary
test asserting false passed `native test` that way. The build now
emits a second test artifact rooted at the extension, wired into the
test step and exposed as AppArtifacts.extension_tests; the two
extension module instances are exposed too, because a Zig module can
only import below its own root and a real engine lives elsewhere.
* Zig 0.16 refuses `_ = err` on an error value, which turned every
comptime markup diagnostic into "error set is discarded" pointing at
the SDK instead of the author's markup. failComptime now names the
error alongside the positioned message.
The fixture under tests/native-extension-app deliberately declares no
typed service registry: once one exists the frontend refuses any request
name the registry does not spell (NS1067), and an app-owned namespace is
the case that check cannot know about.
Guards, each watched red with its fix removed and green restored:
ts-host-call-mux (test-desktop-runtime-core) and ts-native-extension
(test-native-extension-app; with routing disabled it fails at
"expected 1, found 2").
Validated: test-native-extension-app, test-desktop-runtime-core,
test-example-service-feed-reader, test-canvas, test-desktop-ui-shell.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnTXatcmR52M44ZPS3YfZy
feat(canvas): support vertical split axes
feat(runtime): expose post-present refresh batching
test(runtime): exercise in-flight accessibility deferral
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NSAttributedStringper cellVerification
scripts/test-appkit-cell-grid-host.sh— PASS (cell-grid-host-test: ok)The test emits only existing macOS deployment/deprecation warnings from
appkit_host.m.