feat(ghostwright): add Freedom tty semantic inspection#6
Draft
rauhryan wants to merge 48 commits into
Draft
Conversation
Self-eval deadlock avoidance used NodeContext to detect re-entrant calls, but NodeContext spans the whole node scope — including the root event loop task — so the event loop's dispatch wrongly short-circuited into its own scope, where middleware (registered in the eval-loop scope) was invisible. Use a dedicated EvalOwner context set inside spawnEvalLoop, which is present only while executing within that loop. Genuine re-entrant self-evals still run inline; the event loop's dispatch routes through the channel as before. Adds a regression test for re-entrant self-eval (previously uncovered).
This reverts commit 087d137.
This reverts commit 0758092.
Each node captures its task scope (useScope); eval repoints the current routine's scope at the node's scope, runs the op inline, and restores — immediate, non-serial, re-entrant by nesting. Removes the channel/eval-loop, CallEval, box, and EvalOwner. Focus stays synchronous and no longer deadlocks on cross-node setFocused during dispatch. GA3 now waits a turn (sleep 0) for concurrent component init, like its siblings — inline eval no longer grants the incidental scheduler turn the channel did.
…Tree/append/eval) - focus.ts: sync functions taking a node; useFocus installs a remove-interceptor via node.scope.around(NodeApi) - NodeApi gains sync remove; Node.remove() returns the teardown Promise - delete freedom.ts (FreedomApi/append/operations) and tree.ts (useTree) - drop Component/Tree/Node.eval from types; mod.ts exports the new surface - focus.test.ts rewritten imperatively (synchronous)
Add an options arg to createChild so a fresh child can be spliced before an
existing sibling: createChild(name, { before }). Rebuilds the children Set to
splice in position. Throws when before is not a current child. options is left
open for future positioning hints (after, at).
Spec: C14/C15, N7, N22.
Brings in the examples/ + experiments/ restructure and the Ghostwright package (experiments/ghostwright) while preserving the focus-stack work under packages/. Conflict resolutions: - packages/demo/package.json: kept focus-stack version (holds pizza.ts, use-input.ts, use-stdin.ts, freedom-focus-text-input.ts). Renamed the package "demo" -> "focus-demo" to avoid a duplicate workspace package name with main's examples/demo. - package.json: took main's newer root config (playground script, bsh test excluding ghostwright, catalog deps); re-added the `demo` script. - pnpm-lock.yaml: took main's and regenerated via pnpm install.
Adds ghostwright as a workspace devDependency of focus-demo and a smoke test proving the loop works: pizza.ts boots under a real PTY, paints its form, and echoes typed input. This is scaffolding, not real coverage - replace/extend with actual acceptance tests.
…safe Three defects made `bun run build:artifacts` unusable from a clean clone. 1. build:artifacts destroyed ghostty.lock.json. update-manifest.ts rebuilt the whole lockfile from a hardcoded copy that had drifted from the committed one, so every run downgraded bindingVersion 2 -> 1, deleted the `graphics` block, dropped the 8 Kitty entries from requiredWasmExports, and discarded checksums for targets not built locally. The lockfile is now the source of truth: only the `artifacts` checksum map is rewritten, only for artifacts built on this machine, and entries for other targets are preserved. This also made the build non-idempotent. Because build-ghostty-vt.ts reads graphics.freestandingPatchSha256, a second build:artifacts died with GW_PATCH_CHECKSUM. 2. build:ghostty-vt failed on first run. It copied the wasm into artifacts/, which is gitignored and absent in a clean clone, so it died at `cp` after a successful ~1min Zig build. It now creates the directory. 3. verify:artifacts could never pass locally. It required every artifact in the manifest to exist, which is false on any single platform, and it demanded bindingVersion 2 while update-manifest wrote 1. It now skips and reports absent artifacts, and still fails hard on any present artifact whose checksum, protocol marker, exports, or ABI does not match. Also adds `bun run setup` (build + verify) and `bun run update:manifest`, friendly preflight errors when Zig or the Ghostty checkout is missing, and documents the maintainer path in the README. Manifest writes are normalized through `bsh format` so a no-op build produces no diff. Verified: two consecutive build:artifacts runs from a wiped artifacts/ and .cache/ both succeed with zero lockfile churn, verify:artifacts passes, and a full rebuild reproduces the committed checksums bit for bit. Lint and format gates match the pre-change baseline.
Drives the demo through a real PTY and asserts focus purely from visible terminal state, with no application instrumentation. pizza.ts expresses text-field focus two independent ways: the field's box border is WHITE when focused and GRAY otherwise, and only the focused field renders a caret so the terminal cursor sits inside its box. Both are asserted, so the tests fail if either indicator regresses. Field boxes are located by walking down from their label row rather than by hardcoded coordinates, so layout changes above them do not break the tests. Predicates passed to toSatisfy are total, since they are evaluated against pre-paint frames as well. Verified by mutation: removing the Tab keypress, and swapping the focused/unfocused border colours, each make all three tests fail.
…ates
Addresses the friction found while writing the pizza focus tests, where
asserting "is this field focused" required abandoning locators and
hand-rolling cell scraping.
Locators can now express visual state:
- expectTerminal(locator).toHaveStyle(query) asserts how text is drawn,
and reports the actual style on failure
- expectTerminal(locator).toContainCursor() asserts caret position
- getByText(text, { style }) filters by appearance, which disambiguates
repeated text without positional .nth()
- LocatorMatch now carries the cells backing the match, so styles are
reachable without re-deriving geometry from the raw snapshot
- screen.getCells(rect) returns a rectangle of cells
Colours accept '#rrggbb', 'rgb(r,g,b)', 'default', 'palette:N', or the
structured TerminalColor. Omitted StyleQuery fields are ignored.
Default assertion timeout is now DEFAULT_ASSERTION_TIMEOUT_MS (4000ms),
below the 5000ms default of Bun/Jest/Vitest. At equal values the runner
won the race and reported a bare "timed out", discarding Ghostwright's
screen diagnostic as an unattributed unhandled error.
toSatisfy/toHaveShown predicates run against every revision, including
blank pre-paint frames. A throwing predicate is now treated as
unsatisfied instead of aborting the assertion, and the last error is
included in the diagnostic. Previously a predicate that read a not yet
rendered layout failed the whole assertion for an unrelated reason.
keyboard.press() accepts 'Shift+Tab', 'Ctrl+c', 'Alt+x', 'Cmd+k', and
rejects unknown key names with InvalidKeyError. KeyName widens to string,
so a typo could not be caught at compile time and previously encoded
nothing, surfacing much later as an assertion timeout. The functional key
table is shared with the encoder so validation cannot drift from it.
screen.snapshot() is added as an alias of screen.current(), matching
AsyncRegion.snapshot(); the two names for one concept caused repeated
wrong guesses.
Docs: matches(), getCells(), style/cursor assertions, key syntax, and the
timeout rationale were undocumented or absent.
29 new tests. The pizza focus test is rewritten onto the new API as a
usability check, and still fails under both mutations.
Proves the minimum end-to-end path for semantic tty locators: - a tty program renders a named 30x5 Clay element - result.info.get(id) supplies its computed Clay bounding box - the program appends a versioned, namespaced custom OSC to the same stdout write as the visible rendering - a test-side stand-in for a future Ghostwright extension parses the accumulated raw PTY stream and exposes getById(id) - the resulting element locator scopes Ghostwright text locators and exposes the corresponding screen cells The acceptance test proves the metadata rect is not merely plausible: it matches all four visible box corners and scopes the expected text. It also proves the unknown OSC remains available in screen.rawOutput() while Ghostty ignores it and does not paint its namespace or payload. The parser tolerates an OSC split before its ST terminator. It reparses the accumulated stream for simplicity; a real extension should keep an incremental parser and attach completed metadata frames to screen revisions. OSC 7777 is explicitly experimental for this spike, not a proposed permanent allocation.
Proves a browser-like semantic locator can be built by joining two existing authoritative sources on Freedom's generated node ID: - Freedom contributes name, parent/child order, safe semantic attributes, current focus, and the active modal focus root - tty/Clay contributes the computed bounding rectangle through RenderInfo.get(node.id) - an in-band, namespaced OSC carries one complete snapshot per render - a test-side stand-in for a Ghostwright extension exposes lazy CSS-like locators that re-resolve against the newest complete frame The selector spike supports semantic names, generated #IDs, *, attribute selectors, :root, :focus, :focus-root, descendant combinators, direct children, and nth() disambiguation. The acceptance test drives the real pizza modal lifecycle. It proves: - initial `form > name:focus` - opening Add card creates `card-modal:focus-root` - `[role=dialog]:focus-root` and the generated ID locate the same node - three `[role=textbox]` children have Clay geometry - focus is trapped inside the modal and wraps through five controls - no form node is focused while the modal is active - Cancel removes the modal from a pre-existing lazy locator - focus is restored to `form > card:focus` - every remaining semantic node has corresponding Clay geometry - complete frame numbers are monotonic The emitted schema deliberately whitelists role, label, input, and focusability. `value` and `caret` are excluded; the test enters a card number and verifies it is absent from decoded semantic metadata. The spike also found a critical ordering constraint. Although Node makes one stdout.write, the PTY split visual bytes and a trailing OSC into separate host frames. Ghostwright publishes no revision for an unknown OSC that causes no terminal-state change, so semantic waits never woke. Metadata now precedes the visual bytes it describes. A future streaming extension can hold that frame as pending and attach it to the next visual revision without retroactively mutating an already-published revision. Adds inspectFocus(), a read-only Freedom API for active root, focused node, focus boundaries, and restoration targets. Freedom tests prove the inspection before push, during modal capture, and after pop.
…etry Exercises four geometry cases through real tty output and Ghostwright's terminal grid: 1. RenderOptions.row shifts emitted CUP rows but Clay_GetElementData remains local to the render surface. A 1-based row origin must be applied before using Clay bounds as a Ghostwright region. 2. Centering an odd 5x3 element in an even 20x10 surface yields fractional Clay coordinates [7.5, 3.5, 5, 3]. clayterm rasterizes by casting each edge to int, producing visible bounds [7, 3, 5, 3]. 3. A 20x7 child under a 12x5 clipping ancestor retains its full Clay layout bounds while only the ancestor's 12x5 intersection is painted. Logical bounds and visible bounds must be represented separately. 4. A floating element at x=-5 with width 40 retains negative/oversized Clay bounds while viewport clipping paints only columns 0..29. The tests locate actual visible geometry by cell background and box corners, so metadata is checked against terminal output rather than trusted at face value. All four measurements pass through the custom OSC spike transport.
Removes the spike's custom CSS parser and recursive matcher. Lazy Freedom+tty locators now parse with css-what and compile once with css-select, then re-run the compiled predicate against each newest immutable semantic frame. The custom adapter maps Freedom metadata to css-select's generic tree contract: semantic names as element names, generated keys as IDs, whitelisted attributes, ordered parent/children/siblings, safe labels as text, and identity by frame-local key. Custom pseudos provide :focus, :focus-root, and :visible. xmlMode preserves semantic-name case and cacheResults is disabled because frames are immutable replacements. The extension owns an explicit supported subset. It rejects pseudo-elements, the nonstandard parent combinator and != operator, unknown/jQuery pseudos, selectors over 4096 bytes, overlarge selector lists/token counts, nesting beyond 8, and :has nesting beyond 2. The unchanged pizza lifecycle remains green. Additional coverage proves :has(), :nth-child(), adjacent/general siblings, selector lists, attribute-prefix matching, :not(), custom pseudos, generated IDs, and rejection/complexity diagnostics. Adds css-select 7 and css-what 8 as demo-only dev dependencies. Direct research verified the adapter approach under Node/Bun/Deno; keeping the dependencies in the extension avoids imposing CSS semantics on Ghostwright core.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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
Validation
bun test experiments/ghostwright/test/extensions.test.tsbun test packages/freedom-tty/test/geometry.test.tsbun test packages/demo/test/freedom-tty-modal-spike.test.tsbun build packages/freedom-tty/src/index.ts --outdir /tmp/gw-semantic-build --target node --format esm --packages externalbun build packages/freedom-tty/src/producer.ts --outdir /tmp/gw-semantic-build --target node --format esm --packages externalFollow-up
visibleBoundsis intentionally omitted unless tty supplies authoritative clipping geometry. Extending ttyRenderInfowith resolved clipping bounds is tracked as follow-up work.