diff --git a/.github/workflows/desktop-release-gates.yml b/.github/workflows/desktop-release-gates.yml index b3b44fd3..a5c34774 100644 --- a/.github/workflows/desktop-release-gates.yml +++ b/.github/workflows/desktop-release-gates.yml @@ -18,10 +18,12 @@ concurrency: jobs: verify: - runs-on: macos-15 + # The Ghostty 1.4 recipe pins Xcode 26.6, which the macOS 15 image lacks. + runs-on: macos-26 timeout-minutes: 45 env: - GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer + GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer GHOSTTEA_SOAK_SESSIONS: ${{ github.event_name == 'schedule' && '1024' || '256' }} GHOSTTEA_SOAK_ROUNDS: ${{ github.event_name == 'schedule' && '4' || '1' }} steps: diff --git a/.github/workflows/ghosttea-apple-native-artifact.yml b/.github/workflows/ghosttea-apple-native-artifact.yml index fcd9f42a..3b979e1e 100644 --- a/.github/workflows/ghosttea-apple-native-artifact.yml +++ b/.github/workflows/ghosttea-apple-native-artifact.yml @@ -33,6 +33,7 @@ on: - "native/ghostty.lock.json" - "native/ssh.lock.json" - "scripts/build-ghosttea-apple-native.mjs" + - "scripts/cargo-target-directory.mjs" - "scripts/build-ghostty-vt-apple.mjs" - "scripts/ghostty-vt-target.mjs" - "scripts/build-ssh-candidate-apple.mjs" @@ -108,7 +109,7 @@ jobs: timeout-minutes: 90 env: DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer - GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer permissions: attestations: write contents: read diff --git a/.github/workflows/ghostty-vt-artifact.yml b/.github/workflows/ghostty-vt-artifact.yml index 91ab6217..f34ef443 100644 --- a/.github/workflows/ghostty-vt-artifact.yml +++ b/.github/workflows/ghostty-vt-artifact.yml @@ -74,6 +74,23 @@ jobs: if: github.ref_type != 'tag' run: npm run package:ghostty-vt -- --target=aarch64-apple-darwin --allow-mismatch + - if: github.ref_type != 'tag' + uses: dtolnay/rust-toolchain@1.99.1 + + - name: Test consumers of the normalized macOS archive + if: github.ref_type != 'tag' + env: + GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer + RUSTFLAGS: -Dlinker-messages + run: | + npm run bootstrap:ghostty-vt:apple + npm run sync:fonts + export GHOSTTY_VT_PREFIX="$PWD/native/build/ghostty/aarch64-apple-darwin/install" + export GHOSTTEA_GHOSTTY_VT_OFFLINE=1 + export GHOSTTEA_FONT_DIR="$PWD/native/build/ghosttea-fonts" + cargo test --locked -p ghosttea-vt -p ghosttea-text -p ghosttea-core -p ghosttea-ffi + cargo test --locked -p ghosttea-ffi --test parity -- --ignored + - name: Resolve locked macOS candidate run if: github.ref_type == 'tag' id: macos_candidate @@ -174,6 +191,26 @@ jobs: - run: npm ci --ignore-scripts + # Zig 0.17 queries Windows system libraries through pkg-config. The + # runner's Strawberry Perl .BAT shim fails when spawned by Zig; use a + # native executable and record the tool version in the build recipe. + - name: Select native pkg-config + if: github.ref_type != 'tag' + shell: pwsh + run: | + $recipe = (Get-Content native/ghostty.lock.json | ConvertFrom-Json).builder.windowsPkgConfig + for ($attempt = 1; $attempt -le 3; $attempt++) { + choco install $recipe.package --version=$($recipe.version) --yes --no-progress + if ($LASTEXITCODE -eq 0) { break } + if ($attempt -eq 3) { throw "pkg-config installation failed" } + Start-Sleep -Seconds 10 + } + $exe = Get-ChildItem "$env:ChocolateyInstall/lib/$($recipe.package)" -Recurse -Filter pkg-config.exe | Select-Object -First 1 + if (-not $exe) { throw "native pkg-config executable was not installed" } + & $exe.FullName --version + if ($LASTEXITCODE -ne 0) { throw "native pkg-config is not executable" } + "PKG_CONFIG=$($exe.FullName)" >> $env:GITHUB_ENV + - name: Build native Windows candidate if: github.ref_type != 'tag' run: | @@ -181,6 +218,29 @@ jobs: npm run build:ghostty-vt -- --target="$TARGET" npm run package:ghostty-vt -- --target="$TARGET" --allow-mismatch + - name: Test Rust consumers with the selected Windows runtime + if: github.ref_type != 'tag' + timeout-minutes: 20 + run: | + export GHOSTTY_VT_PREFIX="$(cygpath -w "$PWD/native/build/ghostty/$TARGET/install")" + export GHOSTTEA_GHOSTTY_VT_OFFLINE=1 + export GHOSTTEA_FONT_DIR="$(cygpath -w "$PWD/native/build/ghosttea-fonts")" + if [ "$TARGET" = "x86_64-pc-windows-msvc-crt-static" ]; then + export RUSTFLAGS="-C target-feature=+crt-static" + fi + node scripts/sync-ghosttea-fonts.mjs + cargo test --locked --target x86_64-pc-windows-msvc -p ghosttea-vt -p ghosttea-text -p ghosttea-core -p ghosttea-ffi + cargo test --locked --target x86_64-pc-windows-msvc -p ghosttea-ffi --test parity -- --ignored + + - name: Preserve rejected Windows build for diagnosis + if: failure() && github.ref_type != 'tag' + uses: actions/upload-artifact@v7 + with: + name: rejected-${{ matrix.target }} + path: native/build/ghostty/${{ matrix.target }}/install/ + retention-days: 7 + if-no-files-found: ignore + - name: Resolve locked Windows candidate run if: github.ref_type == 'tag' id: windows_candidate diff --git a/.github/workflows/ios-release-hardening.yml b/.github/workflows/ios-release-hardening.yml index 5f641dc5..2d21d7e5 100644 --- a/.github/workflows/ios-release-hardening.yml +++ b/.github/workflows/ios-release-hardening.yml @@ -98,7 +98,7 @@ jobs: timeout-minutes: 90 env: DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer - GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index cedc45d4..4552624c 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -136,7 +136,7 @@ jobs: timeout-minutes: 75 env: DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer - GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }} steps: - uses: actions/checkout@v7 @@ -281,7 +281,7 @@ jobs: timeout-minutes: 75 env: DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer - GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + GHOSTTY_DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }} steps: - uses: actions/checkout@v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 569187ff..78c0dfa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,45 @@ All notable changes to Ghosttea are documented here. The Rust and npm packages share one version. +## 0.14.0 - 2026-11-02 + +### Changed + +- Upgrade the terminal engine to Ghostty 1.4 at + `83fb95624b0439ee660c2679b43b38c8b63769cd`, with Unicode 18 cell widths and + the checksum-locked Zig 0.17 build recipe. The native build and Apple renderer + retain Ghosttea's host-owned terminal geometry and embedding patches. +- Rebuild the Apple native artifact from the release sources. Windows keeps + separate dynamic-CRT and static-CRT VT archives; macOS archives apply the + pinned upstream Darwin runtime-symbol normalization before packaging. + +### Added + +- Routed terminal selection copy through the optional `selection-text` + capability. Copy uses read authority, supports Unicode and scrollback, and + rejects stale, oversized or timed-out results without replaying requests. +- Apple remote viewers can declare the source frame's text metrics, so changing + the viewer's font size scales incoming glyphs with its terminal grid. + +### Fixed + +- Foreground routed panes recover after background retry exhaustion. Input + remains gated until resumed demand is accepted and a fresh cell status arrives. +- Solid block and quadrant characters fill their cells in the Apple renderer, + removing horizontal gaps in TUI artwork at different font sizes and display + scales. Selection, cursor colors and faint opacity remain consistent. +- Joined emoji follow host cell boundaries while keeping their intended visual + span. Selection backgrounds cover the host's complete glyph spans, and graphics + classification preserves host glyphs when Unicode widths are ambiguous. +- Native packaging locates Cargo outputs through the configured target directory. + +### Qualification + +- Migration evidence and remaining limits are recorded in + `docs/ghostty-1.4-migration.md` and `docs/ghostty-1.4-qualification/`. + Latency remains unqualified; physical Windows GPU and real Wi-Fi/cellular + handoff are not claimed. Release CI verifies the final published artifacts. + ## 0.13.8 - 2026-11-02 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index b681a704..2e4b6728 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ Seven Rust crates, ten `@vibecook/*` npm packages and the SwiftPM package share - Rust 1.88+, the workspace minimum. There is no rust-toolchain file. - Apple work also needs: - Xcode 26.6. - - Xcode 26.3 installed side by side at `/Applications/Xcode_26.3.app`. It is used only for the Ghostty/Zig 0.15.2 build; override the path with `GHOSTTY_DEVELOPER_DIR`. + - Ghostty 1.4 uses the locked Zig 0.17.0 and Xcode 26.6 toolchain; `GHOSTTY_DEVELOPER_DIR` can select that Xcode installation. - Rust 1.97.1 with the `aarch64-apple-ios` and `aarch64-apple-ios-sim` targets. **Get started** @@ -36,7 +36,7 @@ npm run dev # build:sdk, then the Electron demo (apps/desktop, workspace "ghos 2. `test:desktop`: vitest in every workspace, `cargo test --workspace --locked`, the FFI parity/ASan probe, the release-script tests, the integration smokes and the bench tests. 3. `package:check:built`: packs every package and crate, then builds consumers from those archives. 4. `test:lifecycle:soak`. -- **Before running locally:** run `npm run bootstrap:ghostty-vt` once before `ci:desktop` or `test:desktop`. `test:ghosttea-core:ffi` syncs fonts from `native/vendor/ghostty` at the locked commit and fails without it. CI on macOS uses the `:apple` variant, which needs Xcode 26.3. +- **Before running locally:** run `npm run bootstrap:ghostty-vt` once before `ci:desktop` or `test:desktop`. `test:ghosttea-core:ffi` syncs fonts from `native/vendor/ghostty` at the locked commit and fails without it. CI on macOS uses the `:apple` variant, which uses the locked Xcode 26.6 toolchain. - **Format and lint:** `npm run format` writes prettier and `cargo fmt`. `npm run lint` runs eslint with `--max-warnings 0`. Prettier (width 120, LF line endings enforced by `.gitattributes`) covers only the paths named in the `format` script, not `docs/`, `draft/`, `apple/` or `native/`. - **Full checks:** `npm run check` runs the lint and type checks plus the iOS release-metadata gates: BOM, resources, App Store, beta matrix and SwiftPM manifests. diff --git a/Cargo.lock b/Cargo.lock index f49d8adc..2bff19dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,7 +581,7 @@ dependencies = [ [[package]] name = "ghosttea" -version = "0.13.8" +version = "0.14.0" dependencies = [ "anyhow", "async-trait", @@ -603,7 +603,7 @@ dependencies = [ [[package]] name = "ghosttea-apple-ffi" -version = "0.13.8" +version = "0.14.0" dependencies = [ "ghosttea-ffi", "ghosttea-font-fixture-ffi", @@ -611,7 +611,7 @@ dependencies = [ [[package]] name = "ghosttea-config" -version = "0.13.8" +version = "0.14.0" dependencies = [ "libc", "serde", @@ -622,7 +622,7 @@ dependencies = [ [[package]] name = "ghosttea-core" -version = "0.13.8" +version = "0.14.0" dependencies = [ "anyhow", "ghosttea-text", @@ -634,7 +634,7 @@ dependencies = [ [[package]] name = "ghosttea-ffi" -version = "0.13.8" +version = "0.14.0" dependencies = [ "ghosttea-config", "ghosttea-core", @@ -644,7 +644,7 @@ dependencies = [ [[package]] name = "ghosttea-font-fixture-ffi" -version = "0.13.8" +version = "0.14.0" dependencies = [ "ghosttea-text", "serde_json", @@ -652,7 +652,7 @@ dependencies = [ [[package]] name = "ghosttea-text" -version = "0.13.8" +version = "0.14.0" dependencies = [ "anyhow", "fontdb", @@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "ghosttea-truffle" -version = "0.13.8" +version = "0.14.0" dependencies = [ "anyhow", "async-trait", @@ -688,14 +688,14 @@ dependencies = [ [[package]] name = "ghosttea-vt" -version = "0.13.8" +version = "0.14.0" dependencies = [ "ghosttea-vt-sys", ] [[package]] name = "ghosttea-vt-crt-witness" -version = "0.13.8" +version = "0.14.0" dependencies = [ "cc", "ghosttea-vt", @@ -703,7 +703,7 @@ dependencies = [ [[package]] name = "ghosttea-vt-sys" -version = "0.13.8" +version = "0.14.0" dependencies = [ "cc", "serde", @@ -715,7 +715,7 @@ dependencies = [ [[package]] name = "ghosttead" -version = "0.13.8" +version = "0.14.0" dependencies = [ "anyhow", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 47db5d97..6caea13f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = [ ] [workspace.package] -version = "0.13.8" +version = "0.14.0" edition = "2024" rust-version = "1.88" authors = ["James Yong"] diff --git a/Package.swift b/Package.swift index 23bd80f2..0aeb8649 100644 --- a/Package.swift +++ b/Package.swift @@ -44,8 +44,8 @@ import PackageDescription let appleNativeURL = - "https://github.com/vibecook-dev/ghosttea/releases/download/ghosttea-apple-native-be8c80c9cfd3/GhostteaAppleNative.xcframework.zip" -let appleNativeChecksum = "2a9e41d98e7e746c61cc0a4c61d6505cee5b69977ff4796db6f4de93a51362f9" + "https://github.com/vibecook-dev/ghosttea/releases/download/ghosttea-apple-native-fcf9a17cce90/GhostteaAppleNative.xcframework.zip" +let appleNativeChecksum = "5a4a72b9aeb5d11a05170e2f423327f9cd3405d7eaf64f7d3e01467e68ec896c" // Truffle is consumed from its published repository, pinned to an exact version // rather than a bare revision. @@ -65,8 +65,8 @@ let appleNativeChecksum = "2a9e41d98e7e746c61cc0a4c61d6505cee5b69977ff4796db6f4d // // `exact:` rather than `from:` keeps the lockstep discipline the revision pin // had: exactly one Truffle across both planes, moved deliberately. Truffle's -// remote carries a plain `v0.7.23` tag on the same commit its release-please -// `truffle-v0.7.23` tag names, so this resolves that exact build through a +// remote carries a plain `v0.7.26` tag on the same commit its release-please +// `truffle-v0.7.26` tag names, so this resolves that exact build through a // stable requirement. // // Keep this equal to `package.version` in @@ -74,7 +74,7 @@ let appleNativeChecksum = "2a9e41d98e7e746c61cc0a4c61d6505cee5b69977ff4796db6f4d // `package.revision` records the commit that version resolves to; the App Store // readiness check compares the resolved pin against both and fails closed. let truffleRepository = "https://github.com/vibecook-dev/truffle.git" -let truffleVersion: Version = "0.7.23" +let truffleVersion: Version = "0.7.26" let sources = "apple/GhostteaKit/Sources" let tests = "apple/GhostteaKit/Tests" diff --git a/apple/GhostteaApp/Resources/Ghosttea-iOS.cdx.json b/apple/GhostteaApp/Resources/Ghosttea-iOS.cdx.json index 20fba14d..397708c5 100644 --- a/apple/GhostteaApp/Resources/Ghosttea-iOS.cdx.json +++ b/apple/GhostteaApp/Resources/Ghosttea-iOS.cdx.json @@ -8,9 +8,9 @@ "timestamp": "2026-07-18T00:00:00Z", "component": { "type": "application", - "bom-ref": "pkg:npm/ghosttea@0.13.8", + "bom-ref": "pkg:npm/ghosttea@0.14.0", "name": "Ghosttea iOS", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -18,7 +18,7 @@ } } ], - "purl": "pkg:npm/ghosttea@0.13.8", + "purl": "pkg:npm/ghosttea@0.14.0", "properties": [ { "name": "ghosttea:scope", @@ -129,7 +129,7 @@ }, { "name": "ghosttea:cargo-lock:sha256", - "value": "15a04aafd08da63abec539d27785e821a443a5f7fd68d4e4f8012a716865fbae" + "value": "ff4e9a2275cc360aa57093d0c3dbd989c34e1f321d9114254386b1195c1533dd" }, { "name": "ghosttea:rust-target", @@ -160,9 +160,9 @@ "components": [ { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-apple-ffi@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-apple-ffi@0.14.0", "name": "ghosttea-apple-ffi", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -170,13 +170,13 @@ } } ], - "purl": "pkg:cargo/ghosttea-apple-ffi@0.13.8" + "purl": "pkg:cargo/ghosttea-apple-ffi@0.14.0" }, { "type": "library", - "bom-ref": "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "Ghostty VT", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -184,7 +184,7 @@ } } ], - "purl": "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "purl": "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd", "externalReferences": [ { "type": "vcs", @@ -328,9 +328,9 @@ }, { "type": "library", - "bom-ref": "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "bom-ref": "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "name": "Truffle Swift", - "version": "0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "version": "101889f676d32989e2c32295c3b608ccd5972c02", "licenses": [ { "license": { @@ -338,7 +338,7 @@ } } ], - "purl": "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "purl": "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "externalReferences": [ { "type": "vcs", @@ -1194,9 +1194,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-config@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-config@0.14.0", "name": "ghosttea-config", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1204,7 +1204,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-config@0.13.8", + "purl": "pkg:cargo/ghosttea-config@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1218,9 +1218,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-core@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-core@0.14.0", "name": "ghosttea-core", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1228,7 +1228,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-core@0.13.8", + "purl": "pkg:cargo/ghosttea-core@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1242,9 +1242,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-ffi@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-ffi@0.14.0", "name": "ghosttea-ffi", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1252,7 +1252,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-ffi@0.13.8", + "purl": "pkg:cargo/ghosttea-ffi@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1266,9 +1266,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", "name": "ghosttea-font-fixture-ffi", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1276,7 +1276,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", + "purl": "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1290,9 +1290,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-text@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-text@0.14.0", "name": "ghosttea-text", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1300,7 +1300,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-text@0.13.8", + "purl": "pkg:cargo/ghosttea-text@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1314,9 +1314,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-vt-sys@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-vt-sys@0.14.0", "name": "ghosttea-vt-sys", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1324,7 +1324,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-vt-sys@0.13.8", + "purl": "pkg:cargo/ghosttea-vt-sys@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1338,9 +1338,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-vt@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-vt@0.14.0", "name": "ghosttea-vt", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1348,7 +1348,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-vt@0.13.8", + "purl": "pkg:cargo/ghosttea-vt@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -3038,9 +3038,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/regular@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/regular@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-Regular.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3073,9 +3073,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/bold@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/bold@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-Bold.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3108,9 +3108,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-Italic.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3143,9 +3143,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/bold-italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/bold-italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-BoldItalic.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3178,9 +3178,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/fallback-emoji@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/fallback-emoji@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "NotoColorEmoji.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3191,7 +3191,7 @@ "hashes": [ { "alg": "SHA-256", - "content": "bf2a8506b80614ba190a34c7b037af1269a7d614fe9f3b613cc15cdeec6f814b" + "content": "72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b" } ], "externalReferences": [ @@ -3283,9 +3283,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/fallback-emoji-text@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/fallback-emoji-text@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "NotoEmoji-Regular.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3296,7 +3296,7 @@ "hashes": [ { "alg": "SHA-256", - "content": "84a38f2f02374efb72cac2b7eb5589102e06fffc33f7b531ed708f4a08236c91" + "content": "cd07743a4c93d8da929afdd28c1d368f11da478a1093146a13610081c2a58440" } ], "externalReferences": [ @@ -3319,34 +3319,34 @@ ], "dependencies": [ { - "ref": "pkg:npm/ghosttea@0.13.8", + "ref": "pkg:npm/ghosttea@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-apple-ffi@0.13.8", + "pkg:cargo/ghosttea-apple-ffi@0.14.0", "pkg:github/openssl/openssl@8cf17aaeb4599f8af87fefd810b5b5fee90fe69e", "pkg:github/libssh2/libssh2@a312b43325e3383c865a87bb1d26cb52e3292641", - "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "pkg:github/mbadolato/iTerm2-Color-Schemes@875a82f0fdc773ae45099ce683a11c56bb0f8b3d", "pkg:github/0xhckr/ghostty-shaders@85898f08fcf4a9274e418912098e99e00a5f8350", - "ghosttea:font/regular@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/bold@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/bold-italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/fallback-emoji@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ghosttea:font/regular@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/bold@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/italic@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/bold-italic@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/fallback-emoji@83fb95624b0439ee660c2679b43b38c8b63769cd", "ghosttea:font/fallback-symbols-math@v2.13b171", "ghosttea:font/fallback-symbols@NotoSansSymbols2-v2.008", - "ghosttea:font/fallback-emoji-text@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69" + "ghosttea:font/fallback-emoji-text@83fb95624b0439ee660c2679b43b38c8b63769cd" ] }, { - "ref": "pkg:cargo/ghosttea-apple-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-apple-ffi@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-ffi@0.13.8", - "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", - "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69" + "pkg:cargo/ghosttea-ffi@0.14.0", + "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", + "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd" ] }, { - "ref": "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { @@ -3360,7 +3360,7 @@ ] }, { - "ref": "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "ref": "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "dependsOn": [ "pkg:github/tailscale/libtailscale@d1dc45d039edb7759ecbee6ba2aea0c4bca884b8" ] @@ -3520,7 +3520,7 @@ ] }, { - "ref": "pkg:cargo/ghosttea-config@0.13.8", + "ref": "pkg:cargo/ghosttea-config@0.14.0", "dependsOn": [ "pkg:cargo/libc@0.2.186", "pkg:cargo/serde@1.0.228", @@ -3530,33 +3530,33 @@ ] }, { - "ref": "pkg:cargo/ghosttea-core@0.13.8", + "ref": "pkg:cargo/ghosttea-core@0.14.0", "dependsOn": [ "pkg:cargo/anyhow@1.0.103", - "pkg:cargo/ghosttea-text@0.13.8", - "pkg:cargo/ghosttea-vt@0.13.8", + "pkg:cargo/ghosttea-text@0.14.0", + "pkg:cargo/ghosttea-vt@0.14.0", "pkg:cargo/serde@1.0.228", "pkg:cargo/smallvec@1.15.2" ] }, { - "ref": "pkg:cargo/ghosttea-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-ffi@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-config@0.13.8", - "pkg:cargo/ghosttea-core@0.13.8", - "pkg:cargo/ghosttea-text@0.13.8", + "pkg:cargo/ghosttea-config@0.14.0", + "pkg:cargo/ghosttea-core@0.14.0", + "pkg:cargo/ghosttea-text@0.14.0", "pkg:cargo/serde_json@1.0.150" ] }, { - "ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-text@0.13.8", + "pkg:cargo/ghosttea-text@0.14.0", "pkg:cargo/serde_json@1.0.150" ] }, { - "ref": "pkg:cargo/ghosttea-text@0.13.8", + "ref": "pkg:cargo/ghosttea-text@0.14.0", "dependsOn": [ "pkg:cargo/anyhow@1.0.103", "pkg:cargo/fontdb@0.24.0", @@ -3570,7 +3570,7 @@ ] }, { - "ref": "pkg:cargo/ghosttea-vt-sys@0.13.8", + "ref": "pkg:cargo/ghosttea-vt-sys@0.14.0", "dependsOn": [ "pkg:cargo/cc@1.2.67", "pkg:cargo/serde@1.0.228", @@ -3581,9 +3581,9 @@ ] }, { - "ref": "pkg:cargo/ghosttea-vt@0.13.8", + "ref": "pkg:cargo/ghosttea-vt@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-vt-sys@0.13.8" + "pkg:cargo/ghosttea-vt-sys@0.14.0" ] }, { @@ -3952,23 +3952,23 @@ "dependsOn": [] }, { - "ref": "ghosttea:font/regular@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/regular@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/bold@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/bold@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/bold-italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/bold-italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/fallback-emoji@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/fallback-emoji@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { @@ -3980,7 +3980,7 @@ "dependsOn": [] }, { - "ref": "ghosttea:font/fallback-emoji-text@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/fallback-emoji-text@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] } ] diff --git a/apple/GhostteaApp/Resources/THIRD-PARTY-NOTICES.txt b/apple/GhostteaApp/Resources/THIRD-PARTY-NOTICES.txt index 18ab3403..d368b281 100644 --- a/apple/GhostteaApp/Resources/THIRD-PARTY-NOTICES.txt +++ b/apple/GhostteaApp/Resources/THIRD-PARTY-NOTICES.txt @@ -2,18 +2,18 @@ GHOSTTEA iOS THIRD-PARTY NOTICES ================================ This file is generated from the reviewed iOS release dependency graph. -CycloneDX SHA-256: 37cdaa0687700c43f0dc331d8941518498cdc654ba921b618a7913bec7207303 +CycloneDX SHA-256: 68e3f82671269ca505e9be9099df1621cfb1b7463b1906e91f0714acdc99cb27 Components: 106 Unique license documents: 95 COMPONENT INDEX =============== -ghosttea-apple-ffi 0.13.8 +ghosttea-apple-ffi 0.14.0 License: MIT Documents: LICENSE-079 -Ghostty VT f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +Ghostty VT 83fb95624b0439ee660c2679b43b38c8b63769cd License: MIT Documents: LICENSE-020 @@ -25,7 +25,7 @@ libssh2 libssh2-1.11.1 License: BSD-3-Clause and bundled notices Documents: LICENSE-001, LICENSE-022, LICENSE-025, LICENSE-030, LICENSE-078, LICENSE-095 -Truffle Swift 0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc +Truffle Swift 101889f676d32989e2c32295c3b608ccd5972c02 License: MIT Documents: LICENSE-079 @@ -137,31 +137,31 @@ getrandom 0.4.3 License: MIT OR Apache-2.0 Documents: LICENSE-031, LICENSE-068 -ghosttea-config 0.13.8 +ghosttea-config 0.14.0 License: MIT Documents: LICENSE-079 -ghosttea-core 0.13.8 +ghosttea-core 0.14.0 License: MIT Documents: LICENSE-079 -ghosttea-ffi 0.13.8 +ghosttea-ffi 0.14.0 License: MIT Documents: LICENSE-079 -ghosttea-font-fixture-ffi 0.13.8 +ghosttea-font-fixture-ffi 0.14.0 License: MIT Documents: LICENSE-079 -ghosttea-text 0.13.8 +ghosttea-text 0.14.0 License: MIT Documents: LICENSE-079 -ghosttea-vt-sys 0.13.8 +ghosttea-vt-sys 0.14.0 License: MIT Documents: LICENSE-079 -ghosttea-vt 0.13.8 +ghosttea-vt 0.14.0 License: MIT Documents: LICENSE-079 @@ -401,23 +401,23 @@ zmij 1.0.23 License: MIT Documents: LICENSE-016 -JetBrainsMonoNerdFont-Regular.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +JetBrainsMonoNerdFont-Regular.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd License: OFL-1.1 Documents: LICENSE-043, LICENSE-092 -JetBrainsMonoNerdFont-Bold.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +JetBrainsMonoNerdFont-Bold.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd License: OFL-1.1 Documents: LICENSE-043, LICENSE-092 -JetBrainsMonoNerdFont-Italic.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +JetBrainsMonoNerdFont-Italic.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd License: OFL-1.1 Documents: LICENSE-043, LICENSE-092 -JetBrainsMonoNerdFont-BoldItalic.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +JetBrainsMonoNerdFont-BoldItalic.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd License: OFL-1.1 Documents: LICENSE-043, LICENSE-092 -NotoColorEmoji.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +NotoColorEmoji.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd License: OFL-1.1 Documents: LICENSE-043, LICENSE-092 @@ -429,7 +429,7 @@ NotoSansSymbols2-Regular.ttf NotoSansSymbols2-v2.008 License: OFL-1.1 Documents: LICENSE-043, LICENSE-092 -NotoEmoji-Regular.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +NotoEmoji-Regular.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd License: OFL-1.1 Documents: LICENSE-043, LICENSE-092 @@ -1576,7 +1576,7 @@ DEALINGS IN THE SOFTWARE. ================================================================================ LICENSE-020 · LICENSE SHA-256: 386211873e5b7a02f663ae4d7adf96285999f91608f8f9f31fecfd0f4095e6f1 -Used by: Ghostty VT f8041e849b36efbbb9736b6ecf0ccfcb01d94e69 +Used by: Ghostty VT 83fb95624b0439ee660c2679b43b38c8b63769cd -------------------------------------------------------------------------------- MIT License @@ -2843,7 +2843,7 @@ DEALINGS IN THE SOFTWARE. ================================================================================ LICENSE-043 · FONT-NOTICES.md SHA-256: 74a171c239592ee050b78f675550e837814715caee6c4133f2b072c57b6579e6 -Used by: JetBrainsMonoNerdFont-Bold.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, JetBrainsMonoNerdFont-BoldItalic.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, JetBrainsMonoNerdFont-Italic.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, JetBrainsMonoNerdFont-Regular.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, NotoColorEmoji.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, NotoEmoji-Regular.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, NotoSansSymbols2-Regular.ttf NotoSansSymbols2-v2.008, STIXTwoMath-Regular.otf v2.13b171 +Used by: JetBrainsMonoNerdFont-Bold.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, JetBrainsMonoNerdFont-BoldItalic.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, JetBrainsMonoNerdFont-Italic.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, JetBrainsMonoNerdFont-Regular.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, NotoColorEmoji.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, NotoEmoji-Regular.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, NotoSansSymbols2-Regular.ttf NotoSansSymbols2-v2.008, STIXTwoMath-Regular.otf v2.13b171 -------------------------------------------------------------------------------- # Fonts and Licenses @@ -5275,7 +5275,7 @@ in this Software without prior written authorization of the copyright holder. ================================================================================ LICENSE-079 · LICENSE, TRUFFLE-LICENSE SHA-256: d5988fd09ab1f97d46de9b2678a4fd2950286601f0a5995e739ac926c5a8eae0 -Used by: Truffle Swift 0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc, ghosttea-apple-ffi 0.13.8, ghosttea-config 0.13.8, ghosttea-core 0.13.8, ghosttea-ffi 0.13.8, ghosttea-font-fixture-ffi 0.13.8, ghosttea-text 0.13.8, ghosttea-vt 0.13.8, ghosttea-vt-sys 0.13.8 +Used by: Truffle Swift 101889f676d32989e2c32295c3b608ccd5972c02, ghosttea-apple-ffi 0.14.0, ghosttea-config 0.14.0, ghosttea-core 0.14.0, ghosttea-ffi 0.14.0, ghosttea-font-fixture-ffi 0.14.0, ghosttea-text 0.14.0, ghosttea-vt 0.14.0, ghosttea-vt-sys 0.14.0 -------------------------------------------------------------------------------- MIT License @@ -5879,7 +5879,7 @@ in the source collection may be redistributed. ================================================================================ LICENSE-092 · OFL-1.1.txt SHA-256: fb755fa3280d1d215a20df2e64fe6a0f75f8208439b11671978214aae60d2997 -Used by: JetBrainsMonoNerdFont-Bold.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, JetBrainsMonoNerdFont-BoldItalic.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, JetBrainsMonoNerdFont-Italic.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, JetBrainsMonoNerdFont-Regular.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, NotoColorEmoji.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, NotoEmoji-Regular.ttf f8041e849b36efbbb9736b6ecf0ccfcb01d94e69, NotoSansSymbols2-Regular.ttf NotoSansSymbols2-v2.008, STIXTwoMath-Regular.otf v2.13b171 +Used by: JetBrainsMonoNerdFont-Bold.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, JetBrainsMonoNerdFont-BoldItalic.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, JetBrainsMonoNerdFont-Italic.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, JetBrainsMonoNerdFont-Regular.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, NotoColorEmoji.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, NotoEmoji-Regular.ttf 83fb95624b0439ee660c2679b43b38c8b63769cd, NotoSansSymbols2-Regular.ttf NotoSansSymbols2-v2.008, STIXTwoMath-Regular.otf v2.13b171 -------------------------------------------------------------------------------- This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: diff --git a/apple/GhostteaKit/Compatibility/apple-native-artifact.lock.json b/apple/GhostteaKit/Compatibility/apple-native-artifact.lock.json index 16684b3f..79dc845f 100644 --- a/apple/GhostteaKit/Compatibility/apple-native-artifact.lock.json +++ b/apple/GhostteaKit/Compatibility/apple-native-artifact.lock.json @@ -3,22 +3,22 @@ "binaryTarget": "GhostteaAppleNative", "bundleName": "GhostteaAppleNative.xcframework", "repository": "https://github.com/vibecook-dev/ghosttea", - "tag": "ghosttea-apple-native-be8c80c9cfd3", + "tag": "ghosttea-apple-native-fcf9a17cce90", "filename": "GhostteaAppleNative.xcframework.zip", - "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghosttea-apple-native-be8c80c9cfd3/GhostteaAppleNative.xcframework.zip", - "checksum": "2a9e41d98e7e746c61cc0a4c61d6505cee5b69977ff4796db6f4de93a51362f9", - "size": 46886842, - "contentDigest": "be8c80c9cfd3d88a093510fe41d7a3b4d5f2d8cee761c7d27527360ece24528f", - "sourceDigest": "8da9b23c182171801014155f98e418fa8befbf1368afe58ce9f9d1b3cb16a0c3", - "entries": 137, + "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghosttea-apple-native-fcf9a17cce90/GhostteaAppleNative.xcframework.zip", + "checksum": "5a4a72b9aeb5d11a05170e2f423327f9cd3405d7eaf64f7d3e01467e68ec896c", + "size": 50688666, + "contentDigest": "fcf9a17cce9034d8258c2a247be5dc5c79e4b1411aea97632274eb7dec07b3d7", + "sourceDigest": "5c36ad009576b86696efe06e1c1cc359fdaf7c8076dae57c367bbc8bdb270282", + "entries": 146, "notes": { - "checksum": "The SHA-256 of the archive file, which is exactly what `swift package compute-checksum` reports and what SwiftPM enforces before unpacking a URL binary target. Verified equal on 2026-11-02.", + "checksum": "The SHA-256 of the archive file, which is exactly what `swift package compute-checksum` reports and what SwiftPM enforces before unpacking a URL binary target. Verified equal on 2026-11-03.", "contentDigest": "A digest of paths, modes, and file bytes, independent of how the tree is archived. The archive's own checksum also depends on zlib, so this is the value a rebuild can honestly re-derive on another machine.", "tag": "Content-addressed rather than keyed to a ghosttea release. `.binaryTarget(url:checksum:)` needs a checksum already valid at the commit SwiftPM resolves, but release assets are built after their release commit — so a per-release artifact could never carry a valid checksum at its own tag.", "sourceDigest": "A digest of the sources the artifact was built from: the crates compiled into it, the pins fixing its vendored inputs, and the scripts that compile and compose them. The digests above answer which bytes are published; only this answers whether those bytes contain the source being shipped, because a digest taken from a stale build agrees with a lock written from that same stale build. A mismatch means republish, not merely a field update — the Apple build is not byte-reproducible, measured rather than assumed: recomposing identical sources on one machine and toolchain moved the archive by two bytes. `scripts/ghosttea-apple-native-artifact.mjs` owns the input list and explains what it deliberately omits.", - "verification": "There is no hand-set `published` flag. `check:apple-native-artifact --release` fetches this URL, checks the archive's size and SHA-256 against the fields above, unpacks it, and re-derives the content digest and every slice digest from the bytes that are actually served. It replaced a boolean a human set, which stayed true across a release that changed the artifact's contents. Last confirmed 2026-11-02, together with the complete local-artifact SwiftPM suite and an external consumer linking every public product; the promotion job then re-downloaded and independently verified the served release bytes. These bytes are never replaced in place: every consumer's SwiftPM checksum is pinned to them, so a changed artifact takes a new content digest and a new tag.", - "supersedes": "ghosttea-apple-native-74b3ccb87e58, the 0.13.7 artifact. The native source digest moved from 6fa6d1833b64 to 8da9b23c1821 through native/ghosttea/crates/ghosttea-vt, native/ghosttea/crates/ghosttea-core, Cargo.lock, Cargo.toml, and native/ghosttea/Cargo.toml, so the release sources require a new immutable publication rather than a field-only update.", - "provenance": "Built, qualified, attested, and published by ghosttea-apple-native-artifact.yml run https://github.com/vibecook-dev/ghosttea/actions/runs/41529382182 from clean source commit dd0806f6284513c7f69e8e87366458c4c33c2309 (release/0.13.8), promoted through the release environment, and pinned by the same run. Attestation: https://github.com/vibecook-dev/ghosttea/attestations/62846784 (subject sha256:2a9e41d98e7e746c61cc0a4c61d6505cee5b69977ff4796db6f4de93a51362f9)." + "verification": "There is no hand-set `published` flag. `check:apple-native-artifact --release` fetches this URL, checks the archive's size and SHA-256 against the fields above, unpacks it, and re-derives the content digest and every slice digest from the bytes that are actually served. It replaced a boolean a human set, which stayed true across a release that changed the artifact's contents. Last confirmed 2026-11-03, together with the complete local-artifact SwiftPM suite and an external consumer linking every public product; the promotion job then re-downloaded and independently verified the served release bytes. These bytes are never replaced in place: every consumer's SwiftPM checksum is pinned to them, so a changed artifact takes a new content digest and a new tag.", + "supersedes": "ghosttea-apple-native-be8c80c9cfd3, the 0.13.8 artifact. The native source digest moved from 8da9b23c1821 to 5c36ad009576 through native/ghosttea/crates/ghosttea-ffi, native/ghosttea/crates/ghosttea-vt-sys, native/ghosttea/crates/ghosttea-vt, native/ghosttea/crates/ghosttea-core, native/ghosttea/crates/ghosttea-text, native/ghostty.lock.json, native/fonts.lock.json, native/ghosttea/crates/ghosttea-vt-sys/artifacts.json, Cargo.lock, Cargo.toml, native/ghosttea/Cargo.toml, scripts/build-ghosttea-apple-native.mjs, scripts/cargo-target-directory.mjs, scripts/build-ghostty-vt-apple.mjs, and scripts/ghostty-vt-target.mjs, so the release sources require a new immutable publication rather than a field-only update.", + "provenance": "Built, qualified, attested, and published by ghosttea-apple-native-artifact.yml run https://github.com/vibecook-dev/ghosttea/actions/runs/41552793124 from clean source commit 40376eb1d8382fefeec2c53cb4c04b4abdb87381 (release/0.14.0), promoted through the release environment, and pinned by the same run. Attestation: https://github.com/vibecook-dev/ghosttea/attestations/62899521 (subject sha256:5a4a72b9aeb5d11a05170e2f423327f9cd3405d7eaf64f7d3e01467e68ec896c)." }, "producedBy": { "composer": "scripts/build-ghosttea-apple-native.mjs", @@ -26,8 +26,8 @@ "sourceLocks": ["native/ghostty.lock.json", "native/ssh.lock.json", "native/fonts.lock.json"] }, "slices": { - "ios-arm64-simulator/libghosttea-apple-native.a": "a4a8e5cb041643d2db70a1481b98c2fa9f22708029a5d19d77d9e80d8695ca3a", - "ios-arm64/libghosttea-apple-native.a": "ec803a217b66f08f7fb653ff40947e63d6cfa7dab71d6289a1f6099789b7c761", - "macos-arm64/libghosttea-apple-native.a": "f16bd9696a237e998ef76edc3b6f0bde61bbc323c18060c0d4b6dc87d0bdb3c2" + "ios-arm64-simulator/libghosttea-apple-native.a": "2c3adc52c6b8e1f07ed2e2ce2754a8938716816ceff0cb1d14a4ae261f7248c2", + "ios-arm64/libghosttea-apple-native.a": "8f5e086aec71f1f4c6b25f92d813dec67313f2380dca475e2ad877a5ce9f6b21", + "macos-arm64/libghosttea-apple-native.a": "5df0ef023428048fcc4dac5310ed65520ba76cca2aed787f6e2fe8de5cf4498b" } } diff --git a/apple/GhostteaKit/Compatibility/apple-native-spm-publishing.md b/apple/GhostteaKit/Compatibility/apple-native-spm-publishing.md index 823744b8..6a9a0c2f 100644 --- a/apple/GhostteaKit/Compatibility/apple-native-spm-publishing.md +++ b/apple/GhostteaKit/Compatibility/apple-native-spm-publishing.md @@ -73,19 +73,18 @@ or Git LFS content. ## Local candidate qualification Use the toolchain recorded in -`Compatibility/ios-toolchain.lock.json` on Apple Silicon macOS. Ghostty's -pinned Zig 0.15.2 build additionally requires the side-by-side Xcode recorded -in `native/ghostty.lock.json`; Xcode 26.4 and later changed the macOS system -stubs in a way Zig 0.15.2 cannot link. Keep the main release toolchain on Xcode -26.6 and point only the two Ghostty commands at Xcode 26.3: +`Compatibility/ios-toolchain.lock.json` on Apple Silicon macOS. Ghostty 1.4 +uses the checksum-pinned Zig 0.17.0 and Xcode 26.6 / SDK 26.5 recorded in +`native/ghostty.lock.json`. The Ghostty and Rust/Swift builds now use the same +reviewed Xcode installation: ```sh npm ci --ignore-scripts npm run check:ios-release-toolchain -GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.3.app/Contents/Developer \ +GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.6.app/Contents/Developer \ npm run bootstrap:ghostty-vt:apple -GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.3.app/Contents/Developer \ +GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.6.app/Contents/Developer \ npm run build:ghostty-vt:apple npm run bootstrap:ssh:apple npm run build:ssh:apple @@ -101,14 +100,15 @@ npm run verify:ghosttea-apple-native-package CI selects the `macos-26` ARM runner and `/Applications/Xcode_26.6.app/Contents/Developer` explicitly for Rust, Swift, -packaging, and tests. It selects -`/Applications/Xcode_26.3.app/Contents/Developer` only for the Ghostty/Zig -input. Both versions and SDK builds are verified against their committed -locks. Do not replace either path with the runner's moving -`/Applications/Xcode.app` default; the checks are intended to fail on drift. -The split is the reviewed workaround for +packaging, tests, and the Ghostty/Zig input. Versions and SDK builds remain +verified against their committed locks. Do not replace the CI path with the +runner's moving `/Applications/Xcode.app` default; the checks intentionally +fail on drift. A workstation may set `GHOSTTY_DEVELOPER_DIR` to its main Xcode +installation when it matches those exact checks. + +The previous Zig 0.15.2 / Xcode 26.3 split worked around [Zig issue 31658](https://codeberg.org/ziglang/zig/issues/31658), fixed -upstream after Zig 0.15.2 by +upstream before this compiler migration by [PR 31673](https://codeberg.org/ziglang/zig/pulls/31673). The external-consumer test imports and links every public library product into diff --git a/apple/GhostteaKit/Compatibility/ghostty-upgrade-procedure.md b/apple/GhostteaKit/Compatibility/ghostty-upgrade-procedure.md index c2827178..6ec4ca15 100644 --- a/apple/GhostteaKit/Compatibility/ghostty-upgrade-procedure.md +++ b/apple/GhostteaKit/Compatibility/ghostty-upgrade-procedure.md @@ -54,7 +54,7 @@ then let both bootstrap commands reproduce the patched tree: git -C native/vendor/ghostty fetch --depth=1 origin NEW_FULL_COMMIT git -C native/vendor/ghostty checkout --detach NEW_FULL_COMMIT npm run bootstrap:ghostty-vt -GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.3.app/Contents/Developer \ +GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.6.app/Contents/Developer \ npm run bootstrap:ghostty-vt:apple ``` @@ -103,7 +103,7 @@ an unstamped witness links clean against a library built for either runtime. ## 3. Rebuild the downloadable and Apple artifacts Build the desktop/server artifact in the pinned container with the locked -single-job schedule, dependency seed, and `baseline` CPU target; normalize it +single-job schedule, `fast` optimization, and `baseline` CPU target; normalize it with the locked Xcode build; and first package it as a diagnostic candidate. On a workstation where that Xcode is the main application rather than side-by-side, point the normalizer at it: @@ -148,7 +148,7 @@ Build and validate every Apple slice, then recompose the local combined binary used by SwiftPM: ```sh -GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.3.app/Contents/Developer \ +GHOSTTY_DEVELOPER_DIR=/Applications/Xcode_26.6.app/Contents/Developer \ npm run build:ghostty-vt:apple npm run check:ghostty-vt:apple npm run test:ghostty-vt:apple diff --git a/apple/GhostteaKit/Compatibility/ios-release-resources.lock.json b/apple/GhostteaKit/Compatibility/ios-release-resources.lock.json index df7e499e..b63d3d24 100644 --- a/apple/GhostteaKit/Compatibility/ios-release-resources.lock.json +++ b/apple/GhostteaKit/Compatibility/ios-release-resources.lock.json @@ -2,15 +2,15 @@ "schemaVersion": 1, "sourceBom": { "path": "apple/GhostteaKit/Compatibility/ios-release.cdx.json", - "sha256": "37cdaa0687700c43f0dc331d8941518498cdc654ba921b618a7913bec7207303" + "sha256": "68e3f82671269ca505e9be9099df1621cfb1b7463b1906e91f0714acdc99cb27" }, "bundledBom": { "path": "apple/GhostteaApp/Resources/Ghosttea-iOS.cdx.json", - "sha256": "37cdaa0687700c43f0dc331d8941518498cdc654ba921b618a7913bec7207303" + "sha256": "68e3f82671269ca505e9be9099df1621cfb1b7463b1906e91f0714acdc99cb27" }, "notices": { "path": "apple/GhostteaApp/Resources/THIRD-PARTY-NOTICES.txt", - "sha256": "1fe37b0acfcf072bb0ab7ac5e85353dd593408b7dfc601dc982a19fb9eaf11a8", + "sha256": "15a21512348061a95a22c842c282116cc7e0f80d7a3fccd76cb0c1171a0bebe2", "componentCount": 106, "licenseDocumentCount": 95 } diff --git a/apple/GhostteaKit/Compatibility/ios-release.cdx.json b/apple/GhostteaKit/Compatibility/ios-release.cdx.json index 20fba14d..397708c5 100644 --- a/apple/GhostteaKit/Compatibility/ios-release.cdx.json +++ b/apple/GhostteaKit/Compatibility/ios-release.cdx.json @@ -8,9 +8,9 @@ "timestamp": "2026-07-18T00:00:00Z", "component": { "type": "application", - "bom-ref": "pkg:npm/ghosttea@0.13.8", + "bom-ref": "pkg:npm/ghosttea@0.14.0", "name": "Ghosttea iOS", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -18,7 +18,7 @@ } } ], - "purl": "pkg:npm/ghosttea@0.13.8", + "purl": "pkg:npm/ghosttea@0.14.0", "properties": [ { "name": "ghosttea:scope", @@ -129,7 +129,7 @@ }, { "name": "ghosttea:cargo-lock:sha256", - "value": "15a04aafd08da63abec539d27785e821a443a5f7fd68d4e4f8012a716865fbae" + "value": "ff4e9a2275cc360aa57093d0c3dbd989c34e1f321d9114254386b1195c1533dd" }, { "name": "ghosttea:rust-target", @@ -160,9 +160,9 @@ "components": [ { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-apple-ffi@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-apple-ffi@0.14.0", "name": "ghosttea-apple-ffi", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -170,13 +170,13 @@ } } ], - "purl": "pkg:cargo/ghosttea-apple-ffi@0.13.8" + "purl": "pkg:cargo/ghosttea-apple-ffi@0.14.0" }, { "type": "library", - "bom-ref": "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "Ghostty VT", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -184,7 +184,7 @@ } } ], - "purl": "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "purl": "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd", "externalReferences": [ { "type": "vcs", @@ -328,9 +328,9 @@ }, { "type": "library", - "bom-ref": "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "bom-ref": "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "name": "Truffle Swift", - "version": "0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "version": "101889f676d32989e2c32295c3b608ccd5972c02", "licenses": [ { "license": { @@ -338,7 +338,7 @@ } } ], - "purl": "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "purl": "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "externalReferences": [ { "type": "vcs", @@ -1194,9 +1194,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-config@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-config@0.14.0", "name": "ghosttea-config", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1204,7 +1204,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-config@0.13.8", + "purl": "pkg:cargo/ghosttea-config@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1218,9 +1218,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-core@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-core@0.14.0", "name": "ghosttea-core", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1228,7 +1228,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-core@0.13.8", + "purl": "pkg:cargo/ghosttea-core@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1242,9 +1242,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-ffi@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-ffi@0.14.0", "name": "ghosttea-ffi", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1252,7 +1252,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-ffi@0.13.8", + "purl": "pkg:cargo/ghosttea-ffi@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1266,9 +1266,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", "name": "ghosttea-font-fixture-ffi", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1276,7 +1276,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", + "purl": "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1290,9 +1290,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-text@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-text@0.14.0", "name": "ghosttea-text", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1300,7 +1300,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-text@0.13.8", + "purl": "pkg:cargo/ghosttea-text@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1314,9 +1314,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-vt-sys@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-vt-sys@0.14.0", "name": "ghosttea-vt-sys", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1324,7 +1324,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-vt-sys@0.13.8", + "purl": "pkg:cargo/ghosttea-vt-sys@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -1338,9 +1338,9 @@ }, { "type": "library", - "bom-ref": "pkg:cargo/ghosttea-vt@0.13.8", + "bom-ref": "pkg:cargo/ghosttea-vt@0.14.0", "name": "ghosttea-vt", - "version": "0.13.8", + "version": "0.14.0", "licenses": [ { "license": { @@ -1348,7 +1348,7 @@ } } ], - "purl": "pkg:cargo/ghosttea-vt@0.13.8", + "purl": "pkg:cargo/ghosttea-vt@0.14.0", "properties": [ { "name": "ghosttea:cargo-source", @@ -3038,9 +3038,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/regular@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/regular@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-Regular.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3073,9 +3073,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/bold@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/bold@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-Bold.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3108,9 +3108,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-Italic.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3143,9 +3143,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/bold-italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/bold-italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "JetBrainsMonoNerdFont-BoldItalic.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3178,9 +3178,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/fallback-emoji@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/fallback-emoji@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "NotoColorEmoji.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3191,7 +3191,7 @@ "hashes": [ { "alg": "SHA-256", - "content": "bf2a8506b80614ba190a34c7b037af1269a7d614fe9f3b613cc15cdeec6f814b" + "content": "72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b" } ], "externalReferences": [ @@ -3283,9 +3283,9 @@ }, { "type": "file", - "bom-ref": "ghosttea:font/fallback-emoji-text@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "bom-ref": "ghosttea:font/fallback-emoji-text@83fb95624b0439ee660c2679b43b38c8b63769cd", "name": "NotoEmoji-Regular.ttf", - "version": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "version": "83fb95624b0439ee660c2679b43b38c8b63769cd", "licenses": [ { "license": { @@ -3296,7 +3296,7 @@ "hashes": [ { "alg": "SHA-256", - "content": "84a38f2f02374efb72cac2b7eb5589102e06fffc33f7b531ed708f4a08236c91" + "content": "cd07743a4c93d8da929afdd28c1d368f11da478a1093146a13610081c2a58440" } ], "externalReferences": [ @@ -3319,34 +3319,34 @@ ], "dependencies": [ { - "ref": "pkg:npm/ghosttea@0.13.8", + "ref": "pkg:npm/ghosttea@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-apple-ffi@0.13.8", + "pkg:cargo/ghosttea-apple-ffi@0.14.0", "pkg:github/openssl/openssl@8cf17aaeb4599f8af87fefd810b5b5fee90fe69e", "pkg:github/libssh2/libssh2@a312b43325e3383c865a87bb1d26cb52e3292641", - "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "pkg:github/mbadolato/iTerm2-Color-Schemes@875a82f0fdc773ae45099ce683a11c56bb0f8b3d", "pkg:github/0xhckr/ghostty-shaders@85898f08fcf4a9274e418912098e99e00a5f8350", - "ghosttea:font/regular@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/bold@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/bold-italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", - "ghosttea:font/fallback-emoji@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ghosttea:font/regular@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/bold@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/italic@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/bold-italic@83fb95624b0439ee660c2679b43b38c8b63769cd", + "ghosttea:font/fallback-emoji@83fb95624b0439ee660c2679b43b38c8b63769cd", "ghosttea:font/fallback-symbols-math@v2.13b171", "ghosttea:font/fallback-symbols@NotoSansSymbols2-v2.008", - "ghosttea:font/fallback-emoji-text@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69" + "ghosttea:font/fallback-emoji-text@83fb95624b0439ee660c2679b43b38c8b63769cd" ] }, { - "ref": "pkg:cargo/ghosttea-apple-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-apple-ffi@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-ffi@0.13.8", - "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", - "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69" + "pkg:cargo/ghosttea-ffi@0.14.0", + "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", + "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd" ] }, { - "ref": "pkg:github/ghostty-org/ghostty@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "pkg:github/ghostty-org/ghostty@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { @@ -3360,7 +3360,7 @@ ] }, { - "ref": "pkg:github/vibecook-dev/truffle@0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "ref": "pkg:github/vibecook-dev/truffle@101889f676d32989e2c32295c3b608ccd5972c02", "dependsOn": [ "pkg:github/tailscale/libtailscale@d1dc45d039edb7759ecbee6ba2aea0c4bca884b8" ] @@ -3520,7 +3520,7 @@ ] }, { - "ref": "pkg:cargo/ghosttea-config@0.13.8", + "ref": "pkg:cargo/ghosttea-config@0.14.0", "dependsOn": [ "pkg:cargo/libc@0.2.186", "pkg:cargo/serde@1.0.228", @@ -3530,33 +3530,33 @@ ] }, { - "ref": "pkg:cargo/ghosttea-core@0.13.8", + "ref": "pkg:cargo/ghosttea-core@0.14.0", "dependsOn": [ "pkg:cargo/anyhow@1.0.103", - "pkg:cargo/ghosttea-text@0.13.8", - "pkg:cargo/ghosttea-vt@0.13.8", + "pkg:cargo/ghosttea-text@0.14.0", + "pkg:cargo/ghosttea-vt@0.14.0", "pkg:cargo/serde@1.0.228", "pkg:cargo/smallvec@1.15.2" ] }, { - "ref": "pkg:cargo/ghosttea-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-ffi@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-config@0.13.8", - "pkg:cargo/ghosttea-core@0.13.8", - "pkg:cargo/ghosttea-text@0.13.8", + "pkg:cargo/ghosttea-config@0.14.0", + "pkg:cargo/ghosttea-core@0.14.0", + "pkg:cargo/ghosttea-text@0.14.0", "pkg:cargo/serde_json@1.0.150" ] }, { - "ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-text@0.13.8", + "pkg:cargo/ghosttea-text@0.14.0", "pkg:cargo/serde_json@1.0.150" ] }, { - "ref": "pkg:cargo/ghosttea-text@0.13.8", + "ref": "pkg:cargo/ghosttea-text@0.14.0", "dependsOn": [ "pkg:cargo/anyhow@1.0.103", "pkg:cargo/fontdb@0.24.0", @@ -3570,7 +3570,7 @@ ] }, { - "ref": "pkg:cargo/ghosttea-vt-sys@0.13.8", + "ref": "pkg:cargo/ghosttea-vt-sys@0.14.0", "dependsOn": [ "pkg:cargo/cc@1.2.67", "pkg:cargo/serde@1.0.228", @@ -3581,9 +3581,9 @@ ] }, { - "ref": "pkg:cargo/ghosttea-vt@0.13.8", + "ref": "pkg:cargo/ghosttea-vt@0.14.0", "dependsOn": [ - "pkg:cargo/ghosttea-vt-sys@0.13.8" + "pkg:cargo/ghosttea-vt-sys@0.14.0" ] }, { @@ -3952,23 +3952,23 @@ "dependsOn": [] }, { - "ref": "ghosttea:font/regular@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/regular@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/bold@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/bold@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/bold-italic@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/bold-italic@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { - "ref": "ghosttea:font/fallback-emoji@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/fallback-emoji@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] }, { @@ -3980,7 +3980,7 @@ "dependsOn": [] }, { - "ref": "ghosttea:font/fallback-emoji-text@f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "ref": "ghosttea:font/fallback-emoji-text@83fb95624b0439ee660c2679b43b38c8b63769cd", "dependsOn": [] } ] diff --git a/apple/GhostteaKit/Compatibility/ios-rust-components.lock.json b/apple/GhostteaKit/Compatibility/ios-rust-components.lock.json index 45ad3d2b..eca6502f 100644 --- a/apple/GhostteaKit/Compatibility/ios-rust-components.lock.json +++ b/apple/GhostteaKit/Compatibility/ios-rust-components.lock.json @@ -2,15 +2,15 @@ "schemaVersion": 1, "target": "aarch64-apple-ios", "root": { - "ref": "pkg:cargo/ghosttea-apple-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-apple-ffi@0.14.0", "name": "ghosttea-apple-ffi", - "version": "0.13.8", + "version": "0.14.0", "dependencies": [ - "pkg:cargo/ghosttea-ffi@0.13.8", - "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8" + "pkg:cargo/ghosttea-ffi@0.14.0", + "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0" ] }, - "cargoLockSha256": "15a04aafd08da63abec539d27785e821a443a5f7fd68d4e4f8012a716865fbae", + "cargoLockSha256": "ff4e9a2275cc360aa57093d0c3dbd989c34e1f321d9114254386b1195c1533dd", "components": [ { "ref": "pkg:cargo/anyhow@1.0.103", @@ -275,9 +275,9 @@ ] }, { - "ref": "pkg:cargo/ghosttea-config@0.13.8", + "ref": "pkg:cargo/ghosttea-config@0.14.0", "name": "ghosttea-config", - "version": "0.13.8", + "version": "0.14.0", "source": "workspace", "license": "MIT", "dependencies": [ @@ -289,47 +289,47 @@ ] }, { - "ref": "pkg:cargo/ghosttea-core@0.13.8", + "ref": "pkg:cargo/ghosttea-core@0.14.0", "name": "ghosttea-core", - "version": "0.13.8", + "version": "0.14.0", "source": "workspace", "license": "MIT", "dependencies": [ "pkg:cargo/anyhow@1.0.103", - "pkg:cargo/ghosttea-text@0.13.8", - "pkg:cargo/ghosttea-vt@0.13.8", + "pkg:cargo/ghosttea-text@0.14.0", + "pkg:cargo/ghosttea-vt@0.14.0", "pkg:cargo/serde@1.0.228", "pkg:cargo/smallvec@1.15.2" ] }, { - "ref": "pkg:cargo/ghosttea-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-ffi@0.14.0", "name": "ghosttea-ffi", - "version": "0.13.8", + "version": "0.14.0", "source": "workspace", "license": "MIT", "dependencies": [ - "pkg:cargo/ghosttea-config@0.13.8", - "pkg:cargo/ghosttea-core@0.13.8", - "pkg:cargo/ghosttea-text@0.13.8", + "pkg:cargo/ghosttea-config@0.14.0", + "pkg:cargo/ghosttea-core@0.14.0", + "pkg:cargo/ghosttea-text@0.14.0", "pkg:cargo/serde_json@1.0.150" ] }, { - "ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.13.8", + "ref": "pkg:cargo/ghosttea-font-fixture-ffi@0.14.0", "name": "ghosttea-font-fixture-ffi", - "version": "0.13.8", + "version": "0.14.0", "source": "workspace", "license": "MIT", "dependencies": [ - "pkg:cargo/ghosttea-text@0.13.8", + "pkg:cargo/ghosttea-text@0.14.0", "pkg:cargo/serde_json@1.0.150" ] }, { - "ref": "pkg:cargo/ghosttea-text@0.13.8", + "ref": "pkg:cargo/ghosttea-text@0.14.0", "name": "ghosttea-text", - "version": "0.13.8", + "version": "0.14.0", "source": "workspace", "license": "MIT", "dependencies": [ @@ -345,9 +345,9 @@ ] }, { - "ref": "pkg:cargo/ghosttea-vt-sys@0.13.8", + "ref": "pkg:cargo/ghosttea-vt-sys@0.14.0", "name": "ghosttea-vt-sys", - "version": "0.13.8", + "version": "0.14.0", "source": "workspace", "license": "MIT", "dependencies": [ @@ -360,13 +360,13 @@ ] }, { - "ref": "pkg:cargo/ghosttea-vt@0.13.8", + "ref": "pkg:cargo/ghosttea-vt@0.14.0", "name": "ghosttea-vt", - "version": "0.13.8", + "version": "0.14.0", "source": "workspace", "license": "MIT", "dependencies": [ - "pkg:cargo/ghosttea-vt-sys@0.13.8" + "pkg:cargo/ghosttea-vt-sys@0.14.0" ] }, { diff --git a/apple/GhostteaKit/Compatibility/truffle-swift.lock.json b/apple/GhostteaKit/Compatibility/truffle-swift.lock.json index b44dee19..e6eb4628 100644 --- a/apple/GhostteaKit/Compatibility/truffle-swift.lock.json +++ b/apple/GhostteaKit/Compatibility/truffle-swift.lock.json @@ -4,8 +4,8 @@ "name": "Truffle", "repository": "https://github.com/vibecook-dev/truffle.git", "pinStyle": "exact-version", - "version": "0.7.23", - "revision": "0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", + "version": "0.7.26", + "revision": "101889f676d32989e2c32295c3b608ccd5972c02", "packagePath": "", "packageIdentity": "truffle", "license": "MIT", diff --git a/apple/GhostteaKit/Package.resolved b/apple/GhostteaKit/Package.resolved index 7b4082fc..ff120b8c 100644 --- a/apple/GhostteaKit/Package.resolved +++ b/apple/GhostteaKit/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "2463745b152d73a8239487d146df19b52ba10801eb6b8e75fc4acd06d4ef5890", + "originHash" : "b7c826bf084652811014f567c278bf60e1a648249f7b0a13e053ac70538c4e26", "pins" : [ { "identity" : "truffle", "kind" : "remoteSourceControl", "location" : "https://github.com/vibecook-dev/truffle.git", "state" : { - "revision" : "0b83a2ceac54b60c54c01ab5b95fad480bc7d6bc", - "version" : "0.7.23" + "revision" : "101889f676d32989e2c32295c3b608ccd5972c02", + "version" : "0.7.26" } } ], diff --git a/apple/GhostteaKit/Package.swift b/apple/GhostteaKit/Package.swift index 268789f1..fea517e8 100644 --- a/apple/GhostteaKit/Package.swift +++ b/apple/GhostteaKit/Package.swift @@ -18,8 +18,8 @@ import PackageDescription // // `exact:` rather than `from:` keeps the lockstep discipline the revision pin // had: exactly one Truffle across both planes, moved deliberately. Truffle's -// remote carries a plain `v0.7.23` tag on the same commit its release-please -// `truffle-v0.7.23` tag names, so this resolves that exact build through a +// remote carries a plain `v0.7.26` tag on the same commit its release-please +// `truffle-v0.7.26` tag names, so this resolves that exact build through a // stable requirement. // // Keep this equal to `package.version` in @@ -27,7 +27,7 @@ import PackageDescription // commit that version resolves to; the App Store readiness check compares the // resolved pin against both and fails closed on drift. let truffleRepository = "https://github.com/vibecook-dev/truffle.git" -let truffleVersion: Version = "0.7.23" +let truffleVersion: Version = "0.7.26" let package = Package( name: "GhostteaKit", diff --git a/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoColorEmoji.ttf b/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoColorEmoji.ttf index 2c1f1043..943741df 100644 Binary files a/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoColorEmoji.ttf and b/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoColorEmoji.ttf differ diff --git a/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoEmoji-Regular.ttf b/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoEmoji-Regular.ttf index 850d972b..b50618f0 100644 Binary files a/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoEmoji-Regular.ttf and b/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/Fonts/NotoEmoji-Regular.ttf differ diff --git a/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/font-parity.json b/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/font-parity.json index 8587b72a..4e5dfb3c 100644 --- a/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/font-parity.json +++ b/apple/GhostteaKit/Sources/GhostteaFontProof/Resources/font-parity.json @@ -38,7 +38,7 @@ "role": "fallback-0", "family": "Noto Color Emoji", "faceIndex": 0, - "sha256": "bf2a8506b80614ba190a34c7b037af1269a7d614fe9f3b613cc15cdeec6f814b" + "sha256": "72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b" }, { "role": "fallback-1", @@ -56,7 +56,7 @@ "role": "fallback-3", "family": "Noto Emoji", "faceIndex": 0, - "sha256": "84a38f2f02374efb72cac2b7eb5589102e06fffc33f7b531ed708f4a08236c91" + "sha256": "cd07743a4c93d8da929afdd28c1d368f11da478a1093146a13610081c2a58440" } ], "cases": { @@ -83,12 +83,12 @@ "emoji-fallback": { "glyphCount": 9, "definitionCount": 9, - "sha256": "00247df6157df4b8e1f2c09380f68b66f990108964d2c89280bcbd6b147d1f61" + "sha256": "e7c0afd6aae4c49e0d58653427346f4088810bd8807b247021e58feabbb1b1d5" }, "emoji-presentation": { "glyphCount": 4, "definitionCount": 3, - "sha256": "9d1d5ba7323a77090d1ebbe26d71c5f712e4e21274f8a3a8217b50d10ede17ef" + "sha256": "8460b1ccad4691ee6363560ce62fbb1508c29c4e9a408c15213e5eb96108b3be" }, "italic": { "glyphCount": 14, diff --git a/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaBlockElements.swift b/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaBlockElements.swift new file mode 100644 index 00000000..08c07321 --- /dev/null +++ b/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaBlockElements.swift @@ -0,0 +1,52 @@ +import Foundation + +/// Cell graphics shared in meaning with WebGPU's `blockElementRectangles`. +/// Font ink does not fill terminal line spacing (Menlo leaves 5.5pt at the +/// default size), so solid blocks must use cell fractions instead of bitmaps. +enum GhostteaMetalBlockElements { + /// Fractions are left, top, right, bottom. Shade characters keep their font + /// pattern; clusters carrying combining marks or selectors keep their glyphs. + static func rectangles(for character: Character) -> [SIMD4]? { + guard character.unicodeScalars.count == 1, + let code = character.unicodeScalars.first?.value + else { return nil } + switch code { + case 0x2580: return [SIMD4(0, 0, 1, 0.5)] + case 0x2581...0x2587: return [SIMD4(0, 1 - Float(code - 0x2580) / 8, 1, 1)] + case 0x2588: return [SIMD4(0, 0, 1, 1)] + case 0x2589...0x258F: return [SIMD4(0, 0, Float(0x2590 - code) / 8, 1)] + case 0x2590: return [SIMD4(0.5, 0, 1, 1)] + case 0x2594: return [SIMD4(0, 0, 1, 0.125)] + case 0x2595: return [SIMD4(0.875, 0, 1, 1)] + case 0x2596...0x259F: + let masks = [4, 8, 1, 13, 9, 7, 11, 2, 6, 14] + let mask = masks[Int(code - 0x2596)] + let quadrants: [SIMD4] = [ + SIMD4(0, 0, 0.5, 0.5), SIMD4(0.5, 0, 1, 0.5), + SIMD4(0, 0.5, 0.5, 1), SIMD4(0.5, 0.5, 1, 1), + ] + return (0..<4).filter { mask & (1 << $0) != 0 }.map { quadrants[$0] } + default: return nil + } + } + + static func cells(in text: String, nativeCellEnd: @autoclosure () -> Int) -> [Int: [SIMD4]] + { + guard text.unicodeScalars.contains(where: { (0x2580...0x259F).contains($0.value) }) + else { return [:] } + // TRF1 supplies glyph positions, but no complete text-to-cell map. Keep the + // host glyph when local Unicode widths disagree, rather than replacing an + // unrelated cell after a ZWJ or a host-wide emoji with a rectangle. + let measured = text.reversed().drop(while: { $0.isWhitespace }) + .reduce(0) { $0 + GhostteaMetalGraphicCells.cellWidth($1) } + guard !text.unicodeScalars.contains(where: { $0.value == 0x200D }), measured == nativeCellEnd() + else { return [:] } + var output: [Int: [SIMD4]] = [:] + var column = 0 + for character in text { + if let rectangles = rectangles(for: character) { output[column] = rectangles } + column += GhostteaMetalGraphicCells.cellWidth(character) + } + return output + } +} diff --git a/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaMetalRenderer.swift b/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaMetalRenderer.swift index 82852686..64d4ab75 100644 --- a/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaMetalRenderer.swift +++ b/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaMetalRenderer.swift @@ -418,6 +418,8 @@ final class GhostteaMetalRenderer { let shaderFunctionNames: Set let cellWidth: Float let lineHeight: Float + private let glyphScaleX: Float + private let glyphScaleY: Float private let rectanglePipeline: any MTLRenderPipelineState private let alphaGlyphPipeline: any MTLRenderPipelineState private let colorGlyphPipeline: any MTLRenderPipelineState @@ -455,6 +457,8 @@ final class GhostteaMetalRenderer { colorAtlasSize: Int = 2048, cellWidth: Float = GhostteaTerminalLayout.cellWidth, lineHeight: Float = GhostteaTerminalLayout.lineHeight, + sourceCellWidth: Float? = nil, + sourceLineHeight: Float? = nil, encodedGeometryReuseEnabled: Bool = true, instancedSubmissionEnabled: Bool = true, rowGeometryReuseEnabled: Bool = true, @@ -466,6 +470,15 @@ final class GhostteaMetalRenderer { lineHeight: lineHeight ) } + let sourceWidth = sourceCellWidth ?? cellWidth + let sourceHeight = sourceLineHeight ?? lineHeight + guard sourceWidth.isFinite, sourceWidth > 0, sourceHeight.isFinite, sourceHeight > 0, + (cellWidth / sourceWidth).isFinite, (lineHeight / sourceHeight).isFinite + else { + throw GhostteaMetalError.invalidTextMetrics(cellWidth: sourceWidth, lineHeight: sourceHeight) + } + glyphScaleX = cellWidth / sourceWidth + glyphScaleY = lineHeight / sourceHeight self.runtime = runtime self.cellWidth = cellWidth self.lineHeight = lineHeight @@ -1066,12 +1079,19 @@ final class GhostteaMetalRenderer { styles: GhostteaMetalStyleResolver ) throws -> GhostteaMetalRowMesh { var mesh = GhostteaMetalRowMesh() + var nativeCellEnd: Int { + row.glyphs.reduce(0) { max($0, Int($1.cellStart) + Int($1.cellSpan)) } + } + let blocks = GhostteaMetalBlockElements.cells(in: row.text, nativeCellEnd: nativeCellEnd) // Graphics (box drawing, block elements) keep the raw foreground rather // than the contrast-adjusted text color, as in Ghostty; block elements // are areas, so light adaptation maps them as surfaces. Only computed // when those colors can differ. let graphics = - styles.distinguishesGraphics ? GhostteaMetalGraphicCells.cells(in: row.text) : [:] + styles.distinguishesGraphics + ? GhostteaMetalGraphicCells.cells( + in: row.text, + nativeCellEnd: nativeCellEnd) : [:] for run in row.styles { let style = styles[run.styleID] if let background = style.background { @@ -1099,6 +1119,32 @@ final class GhostteaMetalRenderer { } ?? false let selected = selectionContains(selection, row: rowIndex, column: glyphStart) let foreground = selected ? theme.selectionForeground : style.foreground + if definition.format == .alpha8, instance.cellSpan == 1, + let rectangles = blocks[glyphStart] + { + let left = (originX + Float(glyphStart) * cellWidth) * scale + let right = (originX + Float(glyphStart + 1) * cellWidth) * scale + let top = (originY + Float(rowIndex) * lineHeight) * scale + let bottom = (originY + Float(rowIndex + 1) * lineHeight) * scale + for rectangle in rectangles { + // Adjacent cells and quadrants share the same rounded boundary, so + // neither line spacing nor linear atlas filtering can open a seam. + let x0 = floor(left + (right - left) * rectangle.x + 0.5) + let x1 = floor(left + (right - left) * rectangle.z + 0.5) + let y0 = floor(top + (bottom - top) * rectangle.y + 0.5) + let y1 = floor(top + (bottom - top) * rectangle.w + 0.5) + pushRectangle( + into: &mesh.decorations, x: x0, y: y0, width: x1 - x0, height: y1 - y0, + color: selected ? foreground : style.fill, + viewportWidth: width, viewportHeight: height) + if cursorCoversGlyph { + pushRectangle( + into: &mesh.cursorDecorations, x: x0, y: y0, width: x1 - x0, height: y1 - y0, + color: theme.cursorText, viewportWidth: width, viewportHeight: height) + } + } + continue + } guard let location = atlases.location(for: definition) else { throw TRF1DecodingError("visible glyph \(definition.id) is absent from its atlas") } @@ -1115,10 +1161,10 @@ final class GhostteaMetalRenderer { } pushGlyph( into: &mesh.alphaGlyphs, - x: (originX + instance.x) * scale, - y: (originY + Float(rowIndex) * lineHeight + instance.y) * scale, - width: instance.width * scale, - height: instance.height * scale, + x: (originX + instance.x * glyphScaleX) * scale, + y: (originY + Float(rowIndex) * lineHeight + instance.y * glyphScaleY) * scale, + width: instance.width * glyphScaleX * scale, + height: instance.height * glyphScaleY * scale, location: location, color: glyphColor, viewportWidth: width, @@ -1127,10 +1173,10 @@ final class GhostteaMetalRenderer { if cursorCoversGlyph { pushGlyph( into: &mesh.cursorAlphaGlyphs, - x: (originX + instance.x) * scale, - y: (originY + Float(rowIndex) * lineHeight + instance.y) * scale, - width: instance.width * scale, - height: instance.height * scale, + x: (originX + instance.x * glyphScaleX) * scale, + y: (originY + Float(rowIndex) * lineHeight + instance.y * glyphScaleY) * scale, + width: instance.width * glyphScaleX * scale, + height: instance.height * glyphScaleY * scale, location: location, color: theme.cursorText, viewportWidth: width, @@ -1140,10 +1186,10 @@ final class GhostteaMetalRenderer { } else { pushGlyph( into: &mesh.colorGlyphs, - x: (originX + instance.x) * scale, - y: (originY + Float(rowIndex) * lineHeight + instance.y) * scale, - width: instance.width * scale, - height: instance.height * scale, + x: (originX + instance.x * glyphScaleX) * scale, + y: (originY + Float(rowIndex) * lineHeight + instance.y * glyphScaleY) * scale, + width: instance.width * glyphScaleX * scale, + height: instance.height * glyphScaleY * scale, location: location, color: foreground, viewportWidth: width, @@ -1154,10 +1200,10 @@ final class GhostteaMetalRenderer { // must be replayed after the block background so they remain visible. pushGlyph( into: &mesh.cursorColorGlyphs, - x: (originX + instance.x) * scale, - y: (originY + Float(rowIndex) * lineHeight + instance.y) * scale, - width: instance.width * scale, - height: instance.height * scale, + x: (originX + instance.x * glyphScaleX) * scale, + y: (originY + Float(rowIndex) * lineHeight + instance.y * glyphScaleY) * scale, + width: instance.width * glyphScaleX * scale, + height: instance.height * glyphScaleY * scale, location: location, color: foreground, viewportWidth: width, @@ -2010,7 +2056,7 @@ private final class GhostteaMetalStyleResolver { } } -private enum GhostteaMetalGraphicCell { +enum GhostteaMetalGraphicCell { case box case block } @@ -2018,8 +2064,17 @@ private enum GhostteaMetalGraphicCell { enum GhostteaMetalGraphicCells { /// Columns holding box-drawing (U+2500–257F) or block-element (U+2580–259F) /// characters, found by walking the row text with the desktop's cell widths. - fileprivate static func cells(in text: String) -> [Int: GhostteaMetalGraphicCell] { + static func cells(in text: String, nativeCellEnd: Int? = nil) -> [Int: GhostteaMetalGraphicCell] { if text.utf8.allSatisfy({ $0 < 0x80 }) { return [:] } + // The host grid owns placement. TRF1 has no text-to-cell mapping, so do + // not assign graphics colors to an unrelated letter after a ZWJ or when + // the host's Unicode widths differ. The native glyph's own style remains. + if let nativeCellEnd { + let measured = text.reversed().drop(while: { $0.isWhitespace }) + .reduce(0) { $0 + cellWidth($1) } + guard !text.unicodeScalars.contains(where: { $0.value == 0x200D }), measured == nativeCellEnd + else { return [:] } + } var cells: [Int: GhostteaMetalGraphicCell] = [:] var column = 0 for character in text { diff --git a/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaTerminalMetalView.swift b/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaTerminalMetalView.swift index 36e72e64..86710a64 100644 --- a/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaTerminalMetalView.swift +++ b/apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaTerminalMetalView.swift @@ -222,6 +222,16 @@ } } public private(set) var terminalTextMetrics = GhostteaTextMetrics() + + /// Metrics used to shape incoming glyph coordinates. Set this for a remote + /// display list whose host keeps its font metrics while this viewer changes + /// font size. Nil preserves local runtimes shaped at the viewer's own size. + public var frameTextMetrics: GhostteaTextMetrics? { + didSet { + evictRendererResources() + requestEventDrivenDraw() + } + } public private(set) var configurationWarnings: [String] = [] public private(set) var diagnostics = GhostteaTerminalMetalDiagnostics() public private(set) var currentGridSize: GhostteaTerminalGridSize? @@ -1666,6 +1676,8 @@ runtime: metalRuntime, cellWidth: terminalTextMetrics.cellWidthPixels, lineHeight: terminalTextMetrics.lineHeightPixels, + sourceCellWidth: frameTextMetrics?.cellWidthPixels, + sourceLineHeight: frameTextMetrics?.lineHeightPixels, encodedGeometryReuseEnabled: encodedGeometryReuseEnabled, instancedSubmissionEnabled: instancedSubmissionEnabled, rowGeometryReuseEnabled: rowGeometryReuseEnabled, diff --git a/apple/GhostteaKit/Sources/GhostteaTerminal/Resources/terminal-visual-golden.json b/apple/GhostteaKit/Sources/GhostteaTerminal/Resources/terminal-visual-golden.json index 4257e4d5..e86168da 100644 --- a/apple/GhostteaKit/Sources/GhostteaTerminal/Resources/terminal-visual-golden.json +++ b/apple/GhostteaKit/Sources/GhostteaTerminal/Resources/terminal-visual-golden.json @@ -13,7 +13,7 @@ "width" : 787 }, "fixture" : "phase4-styled-unicode-v1", - "referencePixelHash" : "6eda391be8a068fc", + "referencePixelHash" : "22286852f3cdfa6b", "schemaVersion" : 1, "tolerance" : { "maxEdgeHammingDistance" : 48, diff --git a/apple/GhostteaKit/Sources/GhosttyVtProof/GhosttyVtProofSession.swift b/apple/GhostteaKit/Sources/GhosttyVtProof/GhosttyVtProofSession.swift index 9051c5e0..7f8b64f3 100644 --- a/apple/GhostteaKit/Sources/GhosttyVtProof/GhosttyVtProofSession.swift +++ b/apple/GhostteaKit/Sources/GhosttyVtProof/GhosttyVtProofSession.swift @@ -32,13 +32,8 @@ public final class GhosttyVtProofSession { maxScrollbackBytes: Int ) throws { var handle: GhosttyTerminal? - let options = GhosttyTerminalOptions( - cols: columns, - rows: rows, - max_scrollback: maxScrollbackBytes - ) try ghosttyRequireSuccess( - ghostty_terminal_new(nil, &handle, options), + ghostty_terminal_new(nil, &handle, columns, rows), operation: "create terminal" ) guard let handle else { @@ -47,6 +42,11 @@ public final class GhosttyVtProofSession { let collector = ReplyCollector() do { + var scrollbackBytes = maxScrollbackBytes + try ghosttyRequireSuccess( + ghostty_terminal_set(handle, GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES, &scrollbackBytes), + operation: "set scrollback limit" + ) try ghosttyRequireSuccess( ghostty_terminal_set( handle, diff --git a/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaBlockElementTests.swift b/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaBlockElementTests.swift new file mode 100644 index 00000000..fce4dee1 --- /dev/null +++ b/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaBlockElementTests.swift @@ -0,0 +1,137 @@ +import Foundation +import GhostteaCore +import Testing + +@testable import GhostteaFrame +@testable import GhostteaTerminal + +@Test func claudeBlockRowsHaveNoGapsAtPhoneFontSizesAndDisplayScales() async throws { + let terminal = try GhostteaTerminal( + runtime: GhostteaRuntime(), configuration: .init(sessionHandle: 801, columns: 8, rows: 4)) + let update = try await terminal.feed( + Data("\u{1b}[?25l\u{1b}[38;5;208m██\r\n██".utf8), render: .full) + var state = RetainedTRF1State() + for effect in update.effects where effect.kind == .frameReady { + _ = try state.apply(effect.payload) + } + let metal = try GhostteaMetalRuntime() + for pointSize: Float in [9, 13, 16, 20] { + for scale: Float in [1, 2, 3] { + for instanced in [false, true] { + let cellWidth: Float = 7.83 * pointSize / 13 + let lineHeight: Float = 19 * pointSize / 13 + let renderer = try GhostteaMetalRenderer( + runtime: metal, cellWidth: cellWidth, lineHeight: lineHeight, + sourceCellWidth: 7.83, sourceLineHeight: 19, + instancedSubmissionEnabled: instanced) + for appearance in ["dark", "light", "transparent", "selected"] { + var theme = GhostteaMetalTheme() + if appearance == "light" { + theme.background = .init(red: 0.96, green: 0.96, blue: 0.94, alpha: 1) + } else if appearance == "transparent" { + theme.background = .init(red: 0.1, green: 0.1, blue: 0.1, alpha: 0.65) + } + let selection: GhostteaMetalSelection? = + appearance == "selected" + ? .init(anchor: .init(column: 0, row: 0), focus: .init(column: 1, row: 1)) : nil + let width = 400 + let height = 260 + let (_, pixels) = try renderer.renderWithPixels( + state: state, width: width, height: height, scale: scale, + theme: theme, selection: selection) + func pixel(_ x: Int, _ y: Int) -> ArraySlice { + let offset = (y * width + x) * 4 + return pixels[offset.. [UInt8] { + Array(pixels[(y * 80 + x) * 4..<(y * 80 + x) * 4 + 4]) + } + let ink = pixel(6, 6) + let background = pixel(75, 45) + #expect(ink != background) + // ▛ upper-left/right and lower-left; ▜ upper-left/right and lower-right; + // ▝ upper-right; ▘ upper-left. Every occupied pixel has the same faint alpha, + // including the shared quadrant edges, where double painting darkens a join. + for (column, occupied) in [ + [true, true, true, false], [true, true, false, true], + [false, true, false, false], [true, false, false, false], + ].enumerated() { + var mismatch = false + for y in 0..<40 { + for x in 0..<16 { + let quadrant = (x >= 8 ? 1 : 0) + (y >= 20 ? 2 : 0) + mismatch = + mismatch || pixel(4 + column * 16 + x, 4 + y) != (occupied[quadrant] ? ink : background) + } + } + #expect(!mismatch, "incorrect quadrant or alpha at column \(column)") + } +} + +@Test func claudeBlockCursorUsesCursorTextColor() async throws { + let terminal = try GhostteaTerminal( + runtime: GhostteaRuntime(), configuration: .init(sessionHandle: 803, columns: 8, rows: 4)) + let update = try await terminal.feed( + Data("\u{1b}[38;5;208m█\u{1b}[H\u{1b}[2 q\u{1b}[?25h".utf8), render: .full) + var state = RetainedTRF1State() + for effect in update.effects where effect.kind == .frameReady { + _ = try state.apply(effect.payload) + } + #expect(state.cursor?.style == .block) + #expect(state.cursor?.visible == true) + var theme = GhostteaMetalTheme() + theme.cursorText = .init(red: 0, green: 1, blue: 1, alpha: 1) + let renderer = try GhostteaMetalRenderer( + runtime: GhostteaMetalRuntime(), cellWidth: 8, lineHeight: 20) + let (_, pixels) = try renderer.renderWithPixels( + state: state, width: 80, height: 50, scale: 2, theme: theme) + var mismatch = false + for y in 4..<44 { + for x in 4..<20 { + mismatch = + mismatch || Array(pixels[(y * 80 + x) * 4..<(y * 80 + x) * 4 + 4]) != [0, 255, 255, 255] + } + } + #expect(!mismatch) +} + +@Test func claudeBlockMappingPreservesAmbiguousHostWidthsAndComplexClusters() { + #expect(GhostteaMetalBlockElements.cells(in: "A█", nativeCellEnd: 2)[1] != nil) + #expect(GhostteaMetalBlockElements.cells(in: "A█", nativeCellEnd: 4).isEmpty) + #expect(GhostteaMetalBlockElements.cells(in: "👩‍💻█", nativeCellEnd: 3).isEmpty) + #expect(GhostteaMetalBlockElements.cells(in: "█\u{301}", nativeCellEnd: 1).isEmpty) + #expect(GhostteaMetalBlockElements.cells(in: "█\u{fe0e}", nativeCellEnd: 1).isEmpty) + for character: Character in ["░", "▒", "▓"] { + #expect(GhostteaMetalBlockElements.rectangles(for: character) == nil) + } +} diff --git a/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaMetalTests.swift b/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaMetalTests.swift index ee4e1bad..751e7dd0 100644 --- a/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaMetalTests.swift +++ b/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaMetalTests.swift @@ -7,6 +7,13 @@ import Testing @testable import GhostteaFrame @testable import GhostteaTerminal +@Test func graphicsClassificationDoesNotGuessAcrossHostWidthDifferences() { + #expect(GhostteaMetalGraphicCells.cells(in: "👩‍💻ab▀X", nativeCellEnd: 8).isEmpty) + #expect(GhostteaMetalGraphicCells.cells(in: "🛙a▀", nativeCellEnd: 3).isEmpty) + #expect(GhostteaMetalGraphicCells.cells(in: "日▀e\u{0301}╭", nativeCellEnd: 5).keys.sorted() == [2, 4]) + #expect(GhostteaMetalGraphicCells.cells(in: " 日▀e\u{0301}╭ ", nativeCellEnd: 7).keys.sorted() == [4, 6]) +} + @Test func terminalDamageUnionsRowsAndPresentationCategories() { var damage = GhostteaTerminalRenderDamage.rows([1, 2]) damage.formUnion(.cursor) diff --git a/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaRenderEvidenceTests.swift b/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaRenderEvidenceTests.swift index b5a9725c..ac5d135c 100644 --- a/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaRenderEvidenceTests.swift +++ b/apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaRenderEvidenceTests.swift @@ -1,6 +1,7 @@ import CoreGraphics import Foundation import GhostteaCore +import GhostteaFonts import ImageIO import Testing @@ -70,6 +71,69 @@ func rendersTheFixtureRowsAsEvidence() async throws { .write(to: directory.appending(path: "render.json")) } +// Exercise live resize damage against a fresh full snapshot, including the +// adjacent block rows used by TUI logos. A static visual golden cannot catch +// a stale row left behind when the phone keyboard changes the viewport. +@Test func resizedLogoAndTextMatchAFreshFrameInEveryAppearance() async throws { + let terminal = try GhostteaTerminal( + runtime: try GhostteaRuntime(), + configuration: .init(sessionHandle: 111, columns: 80, rows: 24)) + let metal = try GhostteaMetalRuntime() + let cached = try GhostteaMetalRenderer(runtime: metal) + var state = RetainedTRF1State() + let sizes: [(UInt16, UInt16)] = [(80, 24), (46, 18), (46, 7), (90, 30), (46, 18)] + var records: [[String: Any]] = [] + for (index, size) in sizes.enumerated() { + let resized = try await terminal.resize( + columns: size.0, rows: size.1, layoutEpoch: UInt64(index + 1)) + for effect in resized.effects where effect.kind == .frameReady { _ = try state.apply(effect.payload) } + let lines = [ + "\u{1b}[38;5;208m████████ ▄▄▄▄ ▐▛███▜▌\u{1b}[0m", + "\u{1b}[38;5;208m████████ ████ ▝▜███▛▘\u{1b}[0m", + "\u{1b}[38;5;208m████████ ▀▀▀▀ ▘▘ ▝▝\u{1b}[0m", + "resize \(size.0)x\(size.1) — frame \(index)", + "normal text / \u{1b}[1mbold\u{1b}[0m / e\u{301} 界", + "┌───────────────┐ │ box drawing │", + ] + let update = try await terminal.feed( + Data(("\u{1b}[?25l\u{1b}[2J\u{1b}[H" + lines.joined(separator: "\r\n")).utf8), render: .damage) + for effect in update.effects where effect.kind == .frameReady { _ = try state.apply(effect.payload) } + let full = try await terminal.refresh(.full) + var fresh = RetainedTRF1State() + for effect in full.effects where effect.kind == .frameReady { _ = try fresh.apply(effect.payload) } + let width = Int(ceil(GhostteaMetalRenderer.originX * 2 + Float(size.0) * cached.cellWidth)) + let height = Int(ceil(GhostteaMetalRenderer.originY * 2 + Float(size.1) * cached.lineHeight)) + for appearance in ["dark", "light", "transparent"] { + var theme = GhostteaMetalTheme() + if appearance == "light" { + theme.background = .init(red: 0.94, green: 0.94, blue: 0.94, alpha: 1) + theme.foreground = .init(red: 0.15, green: 0.15, blue: 0.15, alpha: 1) + theme.lightAdaptation = true + } else if appearance == "transparent" { + theme.background = .init(red: 0.08, green: 0.08, blue: 0.08, alpha: 0.4) + } + let (_, pixels) = try cached.renderWithPixels(state: state, width: width, height: height, theme: theme) + let reference = try GhostteaMetalRenderer(runtime: metal) + let (_, expected) = try reference.renderWithPixels(state: fresh, width: width, height: height, theme: theme) + #expect(pixels == expected, "resize \(index), \(appearance): retained pixels differ from a full scene") + if let evidenceDirectory, !evidenceDirectory.isEmpty { + let directory = URL(filePath: evidenceDirectory, directoryHint: .isDirectory) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let name = "resize-\(index)-\(appearance).png" + try writePNG(pixels, width: width, height: height, to: directory.appending(path: name)) + records.append(["file": name, "columns": size.0, "rows": size.1, + "width": width, "height": height, "freshPixelsEqual": pixels == expected]) + } + } + // The renderer receiving the full snapshot must converge as well. + for effect in full.effects where effect.kind == .frameReady { _ = try state.apply(effect.payload) } + } + if let evidenceDirectory, !evidenceDirectory.isEmpty { + try JSONSerialization.data(withJSONObject: records, options: [.prettyPrinted, .sortedKeys]) + .write(to: URL(filePath: evidenceDirectory).appending(path: "resize-matrix.json")) + } +} + private func writePNG(_ pixels: [UInt8], width: Int, height: Int, to url: URL) throws { let provider = try #require(CGDataProvider(data: Data(pixels) as CFData)) let image = try #require( @@ -91,3 +155,70 @@ private func writePNG(_ pixels: [UInt8], width: Int, height: Int, to url: URL) t CGImageDestinationAddImage(destination, image, nil) #expect(CGImageDestinationFinalize(destination)) } + +// A remote frame is shaped at the host's metrics. Changing the viewer's font +// size must scale its glyphs along with cell backgrounds, cursor and row pitch. +@Test func remoteFontResizeMatchesUniformlyScaledHostPixels() async throws { + let terminal = try GhostteaTerminal( + runtime: try GhostteaRuntime(), + configuration: .init(sessionHandle: 112, columns: 40, rows: 8)) + let update = try await terminal.feed( + Data("AB e\u{301} 中 😀\r\n\u{1b}[31;44m██\u{1b}[0m".utf8), render: .full) + var state = RetainedTRF1State() + for effect in update.effects where effect.kind == .frameReady { _ = try state.apply(effect.payload) } + let metal = try GhostteaMetalRuntime() + let reference = try GhostteaMetalRenderer(runtime: metal) + let resized = try GhostteaMetalRenderer( + runtime: metal, cellWidth: GhostteaTerminalLayout.cellWidth * 2, + lineHeight: GhostteaTerminalLayout.lineHeight * 2, + sourceCellWidth: GhostteaTerminalLayout.cellWidth, + sourceLineHeight: GhostteaTerminalLayout.lineHeight) + let (_, expected) = try reference.renderWithPixels(state: state, width: 680, height: 340, scale: 2) + let (_, actual) = try resized.renderWithPixels( + state: state, width: 680, height: 340, contentInsets: .init(top: 2, left: 2)) + let identical = actual == expected + if let evidenceDirectory, !evidenceDirectory.isEmpty { + let directory = URL(filePath: evidenceDirectory, directoryHint: .isDirectory) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try writePNG(actual, width: 680, height: 340, to: directory.appending(path: "remote-font-actual.png")) + try writePNG(expected, width: 680, height: 340, to: directory.appending(path: "remote-font-expected.png")) + } + #expect(identical, "remote glyphs must grow together with their cells") + let (_, cached) = try resized.renderWithPixels( + state: state, width: 680, height: 340, contentInsets: .init(top: 2, left: 2)) + #expect(cached == actual) +} + +#if os(macOS) +// Compare the actual host font as well as the bundled-font fixture. Menlo's +// full-block ink does not fill this host's 19pt line box. +@Test(.enabled(if: evidenceDirectory?.isEmpty == false)) +func rendersHostMenloBlocksAsEvidence() async throws { + let bytes = try Data(contentsOf: URL(filePath: "/System/Library/Fonts/Menlo.ttc")) + let fonts = try GhostteaBundledFonts.load().map { font in + switch font.role { + case .regular, .bold, .italic, .boldItalic: + GhostteaBundledFont(role: font.role, data: bytes, faceIndex: font.role.rawValue) + default: font + } + } + let terminal = try GhostteaTerminal( + runtime: try GhostteaRuntime(fonts: fonts, metrics: .init()), + configuration: .init(sessionHandle: 113, columns: 46, rows: 18)) + let lines = [ + "\u{1b}[38;5;208m████████ ▄▄▄▄ ▐▛███▜▌", + "████████ ████ ▝▜███▛▘", + "████████ ▀▀▀▀ ▘▘ ▝▝\u{1b}[0m", + "normal text / bold / 中 e\u{301}", + ] + let update = try await terminal.feed( + Data(("\u{1b}[?25l" + lines.joined(separator: "\r\n")).utf8), render: .full) + var state = RetainedTRF1State() + for effect in update.effects where effect.kind == .frameReady { _ = try state.apply(effect.payload) } + let renderer = try GhostteaMetalRenderer(runtime: GhostteaMetalRuntime()) + let (_, pixels) = try renderer.renderWithPixels(state: state, width: 730, height: 690, scale: 2) + let directory = URL(filePath: try #require(evidenceDirectory), directoryHint: .isDirectory) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try writePNG(pixels, width: 730, height: 690, to: directory.appending(path: "host-menlo.png")) +} +#endif diff --git a/apps/desktop-experiment/package.json b/apps/desktop-experiment/package.json index 656f4a79..51c235f8 100644 --- a/apps/desktop-experiment/package.json +++ b/apps/desktop-experiment/package.json @@ -1,6 +1,6 @@ { "name": "ghosttea-desktop-experiment", - "version": "0.13.8", + "version": "0.14.0", "description": "An isolated Ghosttea desktop experimentation application.", "author": "James Yong", "license": "MIT", @@ -23,11 +23,11 @@ "test": "vitest run" }, "dependencies": { - "@vibecook/ghosttea": "0.13.8", - "@vibecook/ghosttea-electron": "0.13.8", - "@vibecook/ghosttea-frame": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8", - "@vibecook/ghosttea-react": "0.13.8", + "@vibecook/ghosttea": "0.14.0", + "@vibecook/ghosttea-electron": "0.14.0", + "@vibecook/ghosttea-frame": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0", + "@vibecook/ghosttea-react": "0.14.0", "react": "19.2.7", "react-dom": "19.2.7" }, diff --git a/apps/desktop-pipeline-lab/package.json b/apps/desktop-pipeline-lab/package.json index c6c35c79..4e7ef2cf 100644 --- a/apps/desktop-pipeline-lab/package.json +++ b/apps/desktop-pipeline-lab/package.json @@ -1,6 +1,6 @@ { "name": "ghosttea-desktop-pipeline-lab", - "version": "0.13.8", + "version": "0.14.0", "description": "Electron compositor lab: race terminal scene/view architectures.", "author": "James Yong", "license": "MIT", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1dc5e20e..84a33454 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ghosttea-demo", - "version": "0.13.8", + "version": "0.14.0", "description": "The Ghosttea desktop example application.", "author": "James Yong", "license": "MIT", @@ -23,11 +23,11 @@ "test": "vitest run" }, "dependencies": { - "@vibecook/ghosttea": "0.13.8", - "@vibecook/ghosttea-electron": "0.13.8", - "@vibecook/ghosttea-frame": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8", - "@vibecook/ghosttea-react": "0.13.8", + "@vibecook/ghosttea": "0.14.0", + "@vibecook/ghosttea-electron": "0.14.0", + "@vibecook/ghosttea-frame": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0", + "@vibecook/ghosttea-react": "0.14.0", "react": "19.2.7", "react-dom": "19.2.7" }, diff --git a/bench/ghostty-ux/README.md b/bench/ghostty-ux/README.md index 419bbdb8..d98ee753 100644 --- a/bench/ghostty-ux/README.md +++ b/bench/ghostty-ux/README.md @@ -26,14 +26,14 @@ This bench is the first step of TDD: ## Sources of truth (priority order) -| Priority | Source | Location / command | -| -------- | ------------------------------- | ---------------------------------------------------- | -| 1 | Vendored Ghostty source at lock | `native/vendor/ghostty` @ `native/ghostty.lock.json` | -| 2 | Default keybinds in source | `src/config/Config.zig` → `Keybinds.init` | -| 3 | Action enum + docs | `src/input/Binding.zig` → `Action` | -| 4 | Installed Ghostty CLI dump | `ghostty +list-keybinds --default --plain` | -| 5 | Default config dump | `ghostty +show-config --default` | -| 6 | Action list dump | `ghostty +list-actions` / `--docs` | +| Priority | Source | Location / command | +| -------- | ------------------------------- | ---------------------------------------------------------------- | +| 1 | Vendored Ghostty source at lock | `native/ghostty-config.lock.json` → `uxGroundTruth.vendorCommit` | +| 2 | Default keybinds in source | `src/config/Config.zig` → `Keybinds.init` | +| 3 | Action enum + docs | `src/input/Binding.zig` → `Action` | +| 4 | Installed Ghostty CLI dump | `ghostty +list-keybinds --default --plain` | +| 5 | Default config dump | `ghostty +show-config --default` | +| 6 | Action list dump | `ghostty +list-actions` / `--docs` | CLI dumps are convenient snapshots. When they disagree with the locked vendor commit, **the locked source wins**. @@ -42,16 +42,16 @@ vendor commit, **the locked source wins**. Captured under [`ground-truth/`](./ground-truth/): -| File | Contents | -| ----------------------------- | ---------------------------------------------- | -| `vendor-commit.txt` | Pinned Ghostty git commit used for builds | -| `ghostty-version.txt` | Installed app version that produced CLI dumps | -| `keybinds-macos-default.txt` | Raw `+list-keybinds --default --plain` | -| `keybinds-macos-default.json` | Structured trigger → action pairs | -| `actions.txt` | Full action names (`+list-actions`) | -| `actions-docs.txt` | Actions with documentation | -| `actions-from-source.json` | Actions parsed from `Binding.zig` | -| `config-macos-default.txt` | Full default config (`+show-config --default`) | +| File | Contents | +| ----------------------------- | ------------------------------------------------- | +| `vendor-commit.txt` | Reviewed source commit when the dump was captured | +| `ghostty-version.txt` | Installed app version that produced CLI dumps | +| `keybinds-macos-default.txt` | Raw `+list-keybinds --default --plain` | +| `keybinds-macos-default.json` | Structured trigger → action pairs | +| `actions.txt` | Full action names (`+list-actions`) | +| `actions-docs.txt` | Actions with documentation | +| `actions-from-source.json` | Actions parsed from `Binding.zig` | +| `config-macos-default.txt` | Full default config (`+show-config --default`) | **Scale (macOS defaults):** @@ -59,6 +59,12 @@ Captured under [`ground-truth/`](./ground-truth/): - **93** default keybind entries (includes digit_N physical + unicode aliases) - **200** non-keybind config keys with defaults +The Ghostty 1.4 VT migration retains this 1.3.1 application UX baseline. The +VT source, config schema source, and historical UX capture have distinct pins; +upgrading the parser does not silently change shortcuts or application defaults. +The CLI version is recorded independently of the source checkout and does not +claim that the installed app was built from that checkout. + ## Refresh procedure ```sh @@ -68,9 +74,12 @@ npm run extract:ghostty-ux Or with an explicit binary: ```sh -GHOSTTY=/Applications/Ghostty.app/Contents/MacOS/ghostty npm run extract:ghostty-ux +GHOSTTY=/path/to/Ghostty-1.3.1.app/Contents/MacOS/ghostty \ +GHOSTTY_UX_SOURCE=/path/to/reviewed-ux-checkout npm run extract:ghostty-ux ``` +The extractor first checks the CLI version and source commit against the UX lock. +A deliberate UX upgrade must review and update that lock separately. This rewrites `bench/ghostty-ux/ground-truth/*` and copies keybind fixtures into `packages/ghosttea-react/src/bindings/fixtures/` for the golden suite. `extensions.json` (⌘⇧O remote sessions) is always re-emitted as intentional. diff --git a/docs/ghostty-1.4-migration.md b/docs/ghostty-1.4-migration.md new file mode 100644 index 00000000..05e1060f --- /dev/null +++ b/docs/ghostty-1.4-migration.md @@ -0,0 +1,414 @@ +# Ghostty 1.4 migration for Vibefield + +Status, **2026-11-02**: migration and regression fixes are committed for Ghosttea +0.14.0, rebased on released 0.13.8. Final release CI and Vibefield's published +dependency qualification remain open. No product release tag is recorded here. + +The qualification table below records the **October 30 candidate**, not the +subsequent 0.14.0 release artifact. Later manual regressions and their fixes are +recorded in [the regression history](./ghostty-1.4-regression-history.md), including +its November 2 publication addendum. Passing an earlier candidate does not qualify +later source changes or a different native archive. + +The release branch is `release/0.14.0`. It retains 0.13.8's conditional session +release and attachment-disposal fixes. The reviewed VT candidates from run +40972570187 were promoted without rebuilding to +[`ghostty-vt-b2a959db6a0d`](https://github.com/vibecook-dev/ghosttea/releases/tag/ghostty-vt-b2a959db6a0d) +on November 2; promotion and both Windows CRT checks passed in +[run 41552795762](https://github.com/vibecook-dev/ghosttea/actions/runs/41552795762). +The Apple artifact +[`ghosttea-apple-native-fcf9a17cce90`](https://github.com/vibecook-dev/ghosttea/releases/tag/ghosttea-apple-native-fcf9a17cce90) +was built, tested, attested, published and independently checked in +[run 41552793124](https://github.com/vibecook-dev/ghosttea/actions/runs/41552793124). +Commit `50e5c39` pins it to the 0.14.0 source digest +`5c36ad009576b86696efe06e1c1cc359fdaf7c8076dae57c367bbc8bdb270282`. +The complete Swift suite and all-products consumer passed before publication; +the served archive and all-products consumer passed again before the pin commit. + +[Vibefield PR #12](https://github.com/vibefield/vibefield/pull/12) contains the +consumer fixes and remains a draft until the published npm, Cargo and SwiftPM +dependencies are pinned together and the final consumer gates pass. + +The clean consumer build exposed one more release dependency: the DNS fix uses +`Truffle.Peer.dnsName`, which was present in the manual build's local Truffle +patch but is absent from published 0.7.23 and 0.7.25. That prerequisite is now +released: [Truffle PR #247](https://github.com/vibecook-dev/truffle/pull/247) passed +all 116 Swift tests and shipped in +[Truffle 0.7.26](https://github.com/vibecook-dev/truffle/releases/tag/truffle-v0.7.26). +Its release pipeline passed CI, real-network tests, Apple package/iOS builds, +binary checks and registry publication before creating the SwiftPM `v0.7.26` +tag at `101889f676d32989e2c32295c3b608ccd5972c02`. + +Both Ghosttea Swift manifests and their provenance lock now pin that verified +version. The Truffle license, TailscaleKit artifact and Ghosttea native source +digest are unchanged. Vibefield must use the same Swift pin when adopting +Ghosttea 0.14.0; its desktop Truffle Rust/sidecar pin remains 0.7.24. Final +published-package and product checks still cover this dependency separately +from the already-published Ghosttea native archive. + +## Baselines and scope + +- Ghosttea: `3eee581c14b05850c90bea09536d4bc6b856223b`, released as 0.13.7. +- Vibefield: `e259f1fe31d9b3a01dca70dc33a5dad99841f6f3`. +- Existing Ghostty: `f8041e849b36efbbb9736b6ecf0ccfcb01d94e69`, with G15/G21 and MSVC runtime patches. +- Candidate Ghostty: 1.4.0, `83fb95624b0439ee660c2679b43b38c8b63769cd`. +- Migration branch: `codex/libghostty-1.4-vibefield`. + +The earlier library qualification used Ghosttea 0.12.0. Its evidence is useful for +the VT port, but does not qualify Vibefield's current 0.13.7 consumers. This work +starts from the exact released consumer baseline and preserves subsequent fixes. + +Vibefield's terminal host cells own Ghosttea's terminal model and frame production. +Godview presents those sessions through its terminal pool and panes. The Swift app +uses FieldRoutedTerminal and FieldFreeway over Truffle and consumes rendered TRF1 +frames through FieldTerminal and the Ghosttea renderer. Ghosttea's own Truffle +demo is additional coverage; it does not substitute for Vibefield's current route. + +Vibefield deliberately pins Truffle Rust/sidecars to 0.7.24 and Swift to 0.7.23. +Preserve that split and validate the resolved dependency graphs. A VT migration +must not silently change transport versions or activate a second Rust node type. + +## Required evidence + +| Area | Required check | Status | +| --- | --- | --- | +| Source | Rebase both local patch contracts; checksum-verified Zig 0.17; deterministic bootstrap diff | Passed: both patches ported, nine extension exports retained; prepared-source diff reproduced twice. | +| Artifacts | macOS ARM64 and both Windows MSVC runtime variants; immutable CI candidates and provenance | Passed in corrected run 40972570187: macOS and both Windows CRT consumers, signed provenance, all 40 inventoried files per bundle, header/library checksums, SBOM consistency and three CRT witnesses per Windows variant. All three candidates are pinned atomically. Run 40969422970 remains rejected for its macOS duplicate `memset`. | +| Apple | All framework slices, complete combined Rust/Swift artifact, reviewed toolchain | Final combined candidate package verified: `ghosttea-apple-native-1611ad6b0820`, source digest `56dc58d73c6d319af6759ef6f1e465e011ae58b32ec5e7b50295a1c2d4f49ac8`. All 18 public Swift products link against it. The exact signed iPhone package now passes all 833 production terminal tests with no failures or skips, and its native font golden passes on device. Both staged iOS archives match the package and both baseline/candidate app signatures verify. No artifact published. | +| Core | Full Rust tests, C ABI, allocation/lifetime checks, parser chunking and ordered replies | Passed against the corrected CI macOS archive: candidate 424 Rust tests / 3 ignored and 623 JS tests; baseline 414 Rust / 3 ignored. ABI 197 types / 1,916 assertions; four strict C shims. C FFI parity and AddressSanitizer gate passed without duplicate-symbol warnings. | +| Fonts | Unicode policy, explicit missing-glyph handling, desktop and iOS rendered comparisons | Eight bundled files verified; two Noto fonts updated. Unicode 18 cell-width regression reproduced and fixed. Reviewed pixel comparison: all eight cases retain identical layout; two emoji cases have intentional font-pixel changes. New engine with old fonts reproduces the old golden exactly. Native iPhone font goldens passed in all four old/new client-host combinations. Joined-emoji artwork now stays at its natural cell width while following text retains the host grid; Godview and iPhone images show the corrected artwork/grid. The complete Metal/frame suite passes 80 tests. Its one font-dependent golden hash changed; restoring only the old fonts reproduced the old pixel hash exactly, with layout, edge map, mean channels, occupied pixels and tolerances unchanged. | +| Godview | Pane creation/split/close, focus/input, resize, scrollback, selection/copy, freeze/resume, mirrors and restored panes | Passed: final full field-app suite (2,807 tests, no skips). Final exact-CI-artifact pane lab copies Unicode and 160-row scrollback, switches alternate screens, verifies hidden output, splits/closes panes, preserves used sessions, checks fullscreen/canvas sizing and reattaches eight times. Screenshot reviewed; clean process exit, PTYs 93 before/after and zero surviving owned processes. | +| Real desktop | Isolated-profile Godview and terminal-direct smoke with candidate host binary | All 13 landing gates passed against the frozen graph and exact CI archive, including Godview, routed/direct terminals, Studio, graphics fallback, plugin recovery and bundled-package checks. All 14 emitted process censuses were empty. The routed-terminal shutdown passed at its unchanged 30-second watchdog; the earlier failed attempt remains recorded. Windows evidence covers native CI consumers, not a physical Windows GPU/product-UI run. | +| Host routing | Seat/geometry authority, activation/generation fences, drawn acknowledgements, credits and transfer bounds | Passed: baseline 493 originally; candidate 498 tests passed, with 15 existing ignored cases including a doctest. Passing coverage includes read-only copy, identity/lease fences and oversized replies. Shared Freeway fix is included in both comparison hosts. | +| Swift | FieldTerminal, FieldInput, FieldRoutedTerminal, FieldFreeway and relevant FieldMesh tests; real simulator rendering | Simulator: 1,306 passed / 2 skipped on both. iPhone baseline: 826 passed after correcting a demonstrated test-only acknowledgment race. The final rebuilt candidate now passes all 833 production tests, including seven new selection tests, with zero failures and zero skips. The tested app, debug library, xctestrun and native archives match their recorded final-package hashes. | +| Remote | Concurrent desktop/Swift mirrors; slow viewers, disconnect/reconnect, foreground resync, stale frames and control handoff | Passed on the exact final package: all four old/new client-host pairings, eight tests, zero failures or skips. The probe exercises two independent Truffle nodes and production Metal surfaces, real PTY input, accepted/rendered frames, resize and seat handoff, park/resume, fresh-link reconnect and live Unicode copy. All isolated apps were uninstalled and both hosts exited cleanly. Earlier phone tailnet-join failures remain recorded; their cause was not established. | +| Compatibility | Baseline/candidate host-client combinations without wire-version drift | Passed: all four final-package combinations. Wire versions unchanged. New client/new host copies exact Unicode and live PTY text through both production attachments; new client/old host returns explicit unsupported copy status while mirroring and reconnect continue. Old clients remain usable with both hosts. The comparison baseline includes the documented shared Freeway correction. | +| Workspace | Complete verification with the exact locked dependency graph | Static/build checks and 26 of 27 JavaScript test projects passed. The remaining agent-cleanup assertion reported 1,999 ms against a 2,000 ms bound; source is unchanged from baseline. Full `pnpm verify` remains red, with matched timing controls recorded separately. | +| Performance | Same-product pane workloads and frame latency; no acceptance based solely on old microbenchmarks | All 16 paired scenario processes completed without errors or unpresentable arms; approximately 120 Hz, frame p95 around 9.1–9.3 ms. Latency is explicitly unqualified: the user chose to continue under the heavily loaded host instead of arranging a quiet repeat. Injection-to-PTY measurements are not input-to-photon latency, and these results are not a performance sign-off. | +| Release | Source/font/artifact/BOM consistency, device evidence and atomic upgrade/rollback | Candidate source, font parity, BOM, resources, Swift manifests, App Store and beta metadata checks passed, as did final-package production and remote iPhone checks. Desktop landing-gate results and remaining release limits are recorded below. No coordinated artifact/version promotion. Config/UX retain their separately recorded 1.3.1 baseline. | + +Use isolated application profiles and sessions. Do not operate on existing user +terminals or rewrite goldens to hide differences. Record intentional upstream +corrections separately from regressions. Missing platform evidence remains an open +gate; passing library tests cannot establish that every product behavior works. + +## Regressions found during qualification + +**Unicode grid alignment (fixed in the candidate).** Ghostty 1.4 uses Unicode 18, +while the Rust width/segmentation tables do not classify all newly assigned +characters the same way. U+1F6D9 followed by `X` reproduced a misplaced second +glyph. Terminal shaping now consumes the host's authoritative cell boundaries, +columns, widths and styles; cache identity also includes that geometry. Tests +cover all nine newly classified emoji, missing artwork, grid gaps, malformed +metadata, and old/new-host widths in logical replicas. The new emoji still have +no artwork in the pinned fonts; the missing-glyph representation preserves the +correct cell span. TRF1 and the routed protocol versions are unchanged. + +**Physical-device focus test race (test-only fix).** The original baseline failed +`theCellASessionOpensOrComesBackOnIsSaidFocused` on iPhone. Its helper sent a frames +acknowledgment but returned before the socket reader processed it; immediate +reopen correctly took the fresh-attach path instead of the fast path the test +expected. Waiting explicitly for both legs to acknowledge made all five repeats +(35 suite test executions) pass, followed by all 826 device tests. No production +resume behavior was changed. Original failing results are retained. + +**Candidate build gates.** Zig 0.17's package directory must be writable when the +source mount is read-only. Its Windows system-library discovery also encounters +a broken Strawberry Perl pkg-config shim on the runner. The candidate workflow +now selects a versioned native executable, retains rejected binaries for diagnosis, +and still rejects missing or conflicting MSVC runtime evidence. Ghostty 1.4's +`-fno-autolink` removes runtime stamps from every production C++ object. Each of +the three compile groups now adds a metadata-only witness using its actual flags +plus `-fautolink`; the compiler's `_MT`/`_DLL` macros determine the stamp. Packaging +requires all three witnesses, and CI links/runs Rust VT, text, core and FFI tests +for both CRT selections. Both Windows archives passed symbol/CRT checks, native Rust consumer tests and +signed provenance again in corrected run 40972570187, which also carries the final Rust +emoji correction. + +## Routed copy and transport corrections + +**Godview and Swift copy (existing gaps, fixed in this work).** Released Godview +returned an empty string for routed copy, and the Swift home card did not wire +its selection callback. Both now request exact host text over the authenticated +control leg. `selection-text` is negotiated independently of the core wire +version; copy needs read authority, never input or geometry authority. Correlated +request/session/activation/lease identities, bounded queues, five-second timeout, +no replay and cancellation prevent delayed responses from reaching a replacement +pane or overwriting a newer copy. Empty selections preserve the clipboard; +oversized selections return `TOO_LARGE` instead of silently truncating. The UI +uses its existing feedback surface for failures. Protocol fixtures are shared +byte-for-byte across TypeScript, Rust and Swift. + +**Truffle proxy trailing slash (existing interoperability defect).** The pinned +proxy forwards the Freeway root as `/f//`; the old door accepted only +`/f/`, returning 404 before authentication. The door now accepts exactly +one trailing slash while preserving the complete secret comparison and identity +gate. Wrong secrets, double slashes, subpaths and missing identity remain refused. +Both comparison hosts include the same fix, so the mixed-version baseline is +explicitly not a pristine released product. Original failure evidence is retained. + +**Joined emoji and host cell accounting.** Unicode 18 widened nine newly assigned +emoji relative to the Rust tables. Host metadata now remains authoritative. +Legacy-width hosts may reserve four columns for a joined emoji: the shaper keeps +one combined symbol at a two-cell visual width and anchors subsequent clusters +at their actual host columns. Splitting the emoji into separate glyphs was an +intermediate attempt and is superseded. Tests cover legacy and mode-2027 hosts, +adjacent emoji, replica geometry and cache invalidation. + +**Selection and procedural graphics grid (fixed).** Live Godview review found that +browser-derived text width ended a selected row’s backdrop before the host’s +last glyph, making its selected `X` invisible. Both WebGPU backdrop paths now +cover host glyph spans and trailing spaces. Box/block classification falls back +to host glyphs when Unicode widths disagree or ZWJ mapping is ambiguous, so an +unrelated following letter cannot become a block. Metal uses the same conservative +classification rule for graphics colors. New regression tests cover joined emoji, +Unicode 18, CJK, combining text, leading/trailing spaces and adjacent graphics; +reviewed Godview images preserve `👩‍💻ab▀X` and the highlighted final `X`. + +**Normalized macOS archive.** Linux cross-builds skip upstream's Darwin-only +`libsystem_override.sh`. Simply stripping that archive leaves both compiler-rt +and Ghostty's override defining `memset`. The candidate now runs the exact +checksum-pinned upstream script during Apple normalization, preserving compiler +intrinsics while localizing compiler-rt's libc/libm symbols. The script is part +of the artifact source inventory and content-addressed recipe. CI now runs Rust +consumers of that normalized archive and rejects linker diagnostics. A matching +checksum or signed provenance alone was insufficient to qualify the earlier run. + +**Physical QA host lifecycle.** A competing root Metal drawable loop could block +XCTest teardown even after assertions passed. The isolated harness pauses those +background views while mounting the two real terminal surfaces, restores the +original controller and leaves its Metal views paused until QA process exit. +All four pairings subsequently exited cleanly. This changes only the generated +QA host. Vibefield's reusable runner consumes/deletes the test configuration and +uninstalls the isolated app even after launch failure. + +**Cargo output directory (packaging correction).** The machine's Cargo config +places its shared target directory on SamsungRed. A checkout's `target` symlink +does not override that setting. Two scripts wrote to Cargo's configured directory +but then read `root/target`, causing the package gate to read an older crate's +artifact manifest. The package gate now creates its archives in a fresh fixture +directory. The Apple composer asks Cargo for its actual target directory and +uses that same path for compilation and library composition. A real Cargo +regression check covers the default path, a configured path containing spaces +and an environment override. The Apple source digest and workflow inputs also +cover the new resolver. + +**Earlier workspace gate under load.** The earlier Vibefield `pnpm verify` invocation +passed its static, type, lint, Rust clippy and bundle stages, then reported five +JavaScript test failures. The four fieldd failures passed on a focused rerun +(81 passed, seven skipped); three were polluted by Node's conflicting +`NO_COLOR`/`FORCE_COLOR` warning, removed from the rerun environment. The remaining +fieldd test and the field-app document test were timing-sensitive. Both baseline +and candidate passed the document first-run/restart controls at their original +five-second timeout; the complete 19-test document suite passed with only its +outer test timeout raised to 30 seconds. Product assertions were unchanged. +The final exact-artifact Rust run passed all 498 tests (15 ignored, including one +documentation test), followed by generated-contract freshness. This is qualified +stage/recheck evidence, not a claim that the original full invocation was green +or a latency sign-off. A disk-full build attempt is retained separately. + +A final full repeat after SSD relocation ran Nx projects serially with the color +conflict removed. All static, type, lint, clippy and bundle stages passed. The +complete fieldd suite passed 2,608 tests (11 skipped), and field-app passed all +2,807 with no skips. Six five-second helper-process timeouts remained in the +screenshot and Electron-shell projects, so the full invocation still exited 1. +The affected tests and launcher/helper source files match the baseline exactly. +All 41 tests in those three files then passed on both baseline and candidate, +one file at a time, with the original timeouts and unchanged assertions. Preserve +both results; these rechecks do not turn the full-run exit status into a pass. + +**Controlled dependency restoration (2026-10-30).** Early repeat attempts +exposed missing/mismatched React links and the candidate renderer resolving the +released protocol, which lacks the new copy capability. The next full run then +reported three document-compaction assertions, a missing Loro JavaScript snippet +in a logging fixture, and a terminal environment assertion. Investigation found +that pnpm automatically reinstalled the local dev overlay before running commands: +its resolutions had drifted to Loro 1.17.1 and React 19.3.1 while the committed +lock specifies Loro 1.13.8 and React 19.2.7. That run was stopped before any +landing gates; its failures are retained as an invalid dependency comparison. + +A clean offline frozen install restored the complete committed graph. Stale +hoists required recreating only the isolated generated dependency directory. +`pnpm_config_verify_deps_before_run=false` prevents pnpm from automatically +undoing that restore; it does not disable the dependency guard or any test. +Host peers and the candidate renderer's internal Ghosttea source links were then +restored. The single-copy guard passes, the installed lock is byte-identical to +the committed lock, and actual baseline/candidate package manifests and versions +match, including Loro's WASM bytes. Production pins and tracked source did not +change. Repeating this candidate setup must preserve the frozen graph and these +source links; a plain auto-install of the dev overlay can reintroduce drift. + +The terminal assertion also exposed a build-only flag inherited from this run: +`GHOSTTEA_GHOSTTY_VT_OFFLINE=1`. The unchanged service privacy filter removes +registered secret names and prefixes, not that build flag. It is now absent from +runtime test environments; `GHOSTTY_VT_PREFIX` still selects the exact verified +CI archive. Original assertions and timeouts remain intact. This correction is +to the qualification environment, with no product environment-policy change. + +**Full frozen workspace run (2026-10-30).** With the actual locked dependencies +restored, all static, type, lint, formatting, Rust clippy and bundle checks passed. +Of 27 JavaScript test projects, 26 passed: 9,982 tests passed, one failed and +17 were skipped under the existing test conditions. Field-app passed all 2,807 tests with +no skips; Electron-shell passed 1,983 with three existing skips. The previous +screenshot/helper process timeouts did not recur. Fieldd passed 2,607 tests with +11 existing skips and one failure: its agent-cleanup warning recorded `pendingMs` +as 1,999 against a 2,000 ms minimum. The test had already verified that exit was +reported and cleanup remained pending. Its test and implementation are identical +to the baseline. The full invocation remains exit 1; focused controls do not +turn that result green. Test workers were capped at two, with the original +assertions and timeouts; this is not a latency sign-off. Because `verify` stops +at its first failed stage, its Rust and generated-file stages did not run in +that invocation. Running those stages separately against the same frozen graph +and exact CI archive passed: 498 Rust tests, 15 existing ignored cases including +the doctest, zero failures, and current generated files. Their success does not +change the original full-invocation exit status. + +**Matched agent-cleanup controls.** A predetermined set of three alternating +baseline/candidate pairs passed all 12 selected test executions (six per build). +Each invocation selects the same two item-87 cases and excludes the other 54; +those exclusions are not additional product test results. Test source and the +agent-cleanup implementation are byte-identical across the comparison. Original +assertions and timeouts were retained. The only setup difference is skipping the +native rebuild in global setup and reusing the already-qualified retained binaries, +whose hashes were checked before and after each run. No candidate-specific +behavior difference was observed in these controls. They do not establish the +cause of the original 1 ms failure or turn the full workspace gate green. + +**Full desktop landing run (2026-10-30).** All 13 required landing gates passed +serially in 1,742.53 seconds, against the frozen graph and exact CI native archive: +stills, basic smoke, full canvas, both graphics fallback modes, kind faults, +plugin restart, plugin-author try, Godview, routed terminal door, direct terminal, +Studio and bundled plugins. All 14 emitted final process censuses were empty. +The routed-terminal probe passed its original 30-second shutdown watchdog. +The plugin-author light/dark clock and full-board images were also reviewed; +clock geometry is intact in both, with markedly dimmer existing dark styling. +The bundle gate accepted the valid fixture and refused corrupt content and a +forged index. No assertions or timeouts changed. Historical red invocations +remain evidence of those attempts, independently of this complete passing run. + +**Earlier desktop shutdown evidence.** The final Godview and direct-terminal smoke +commands both exited zero. The terminal-door probe accepted document/control and +worker/frames connections from the expected origin, then its teardown exceeded +the existing 30-second watchdog and exited 3. An unchanged baseline control and +candidate recheck both subsequently exited zero, with the original assertions +and watchdog. Their probe, supervisor and teardown source files are identical. +Both controls used the 20-second daemon drain budget and needed forced cleanup +inside the smoke; the launcher's final process census was empty in every run. +The initial failure is retained, its root cause is not established, and the +rechecks do not make the initial three-command gate green. The final host's +Cargo dependency fingerprints trace to the corrected CI archive's exact SHA-256; +it is a later build from the same sources than the host recorded before full +workspace verification. + +**Final build identity and cleanup.** The complete Rust test command enables +Tokio's `test-util` feature through dependency feature unification, producing +different test-feature binaries at Cargo's usual executable paths. The originally +qualified runtime binaries remained intact in `debug/deps`. A normal +`cargo build -p field-native --bins` restored all three top-level executable hashes +exactly to the bytes used by the final phone matrix, desktop gates and timing +controls. Their dependency fingerprints still trace to the corrected CI VT archive. +No product source changed. The final owned-process census was empty. + +**Final physical-device qualification (2026-10-30).** The exact final signed +candidate passed all 833 production terminal, input, routed, Freeway and mesh +tests, with zero failures and zero skips. On the user's next requested retry, +all four old/new client-host pairings passed their native-font and real-Truffle +checks: eight tests, no failures or skips. App, debug-library, xctestrun and +native-archive hashes were reverified; the candidate is the same package as the +833-test run. No product code, assertions or timeouts changed for the retry. + +Each pairing exercised real PTY input, two independent Truffle nodes and +production Metal surfaces, advances in both accepted and rendered frames, +seat/geometry handoff, resize, park/resume, fresh-link redial and fresh output. +The new client copied exact Unicode and live PTY text from both attachments on +the new host; the old host returned explicit unsupported-copy status while its +stream and reconnect remained usable. The old clients passed against both hosts. +The test reaches the production selection API; a physical tap on the Swift home +card's copy menu was not separately exercised. + +Both candidate-client screenshots were reviewed at original resolution: +[new host](ghostty-1.4-qualification/iphone-final-new-client-new-host.png) and +[old host](ghostty-1.4-qualification/iphone-final-new-client-old-host.png). +Mirrors match, joined emoji keep their natural artwork width, and the missing +Unicode 18 glyph and following `X` preserve each host's actual cell grid. The +pinned fonts still lack artwork for the newly assigned emoji. + +The preceding four final-package attempts timed out at the phone's 60-second +Truffle `waitUntilRunning` bound, before peer discovery or terminal attachment. +Both old/new apps failed there and both Mac hosts joined. Those failures remain +in `iphone-final-package.json`; their environmental cause is not established. +The successful retry is recorded separately in +`iphone-final-package-retry-20261030.json`, with no passing claim backfilled into +the failed attempts. All four QA apps were uninstalled, temporary configuration +was consumed/deleted, and both fixtures exited zero. Complete retry result bundles +and logs are archived on SamsungRed with per-file checksum verification; the +approved test key was checked absent from the archive. + +The first baseline fixture startup in this resumed pass failed because its retained +executable was launched outside Cargo's original `debug/deps` layout. The embedded +floor locates its terminal and Freeway helpers beside the current executable or +one directory above it. Restoring a hard link to the same fixture bytes at their +original location resolved startup; all three adjacent helpers were verified. +Product code and test assertions were unchanged. The failure and successful +fixture recovery are retained separately from the phone's tailnet-join failures. + +**Storage recovery.** The user requested cache cleanup and use of SamsungRed. +Disposable incremental/obsolete builds were removed; 20.98 GiB of isolated Rust, +native and iPhone build caches were then copied to +`/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29`. +Each copy passed a complete content-checksum comparison before its original was +removed, and the original paths remain symlinks. Source and qualification evidence +remain internal. Xcode's path-bound precompiled caches required a fresh candidate +build after relocation; that build succeeded and its two staged native libraries +match the verified package. Internal free space was about 64 GiB at completion; +that total also reflects other work on the shared machine. +The exact final Apple candidate ZIP is also retained on SamsungRed, with its +50,692,910 bytes and SHA-256 `de397a043efee2cb3f8d0e2cecf320cb345af475c4ea9b6d70b50f13f789dc5d` +reverified after copying. It remains unpublished. + +## Promotion and rollback + +No release has been published. The candidate's development source pin is separate +from Vibefield's exact released npm, Cargo and Swift versions. All 13 desktop +landing gates passed, as did the separately completed Rust/generated-file stages. +Before landing, resolve the remaining full-workspace +agent-cleanup timing failure. Its matched controls passed with unchanged checks, +but no full `pnpm verify` pass is claimed. Earlier helper-timeout and terminal-door +shutdown failures remain preserved; they did not recur in the final frozen run. +Corrected native packages and their consumers have passed. The exact final +rebuilt iPhone package passed all 833 production tests and all four real-Truffle +client/host combinations. Latency remains explicitly unqualified; do not convert +the noisy measurements into a performance guarantee. +Publish coordinated Ghosttea native/npm/Cargo/Swift artifacts only after release +authorization, then move every Vibefield production pin together and run its +landing gates. Do not commit development overlay locks or nil Swift release lines. +Rollback must restore the complete previous Ghosttea version and all associated +native/font artifacts, while retaining independently validated product fixes. + +Three pre-existing SSH production-readiness blockers remain (the pinned libssh2 +security baseline, missing upstream fixes and incomplete revalidation). They are +not caused by this VT migration and have not been waived. Configuration and UX +compatibility remain on their separately reviewed 1.3.1 baseline. A passing +terminal suite is not a claim that every TUI, network handoff or suspension case +has been exercised. + +## Evidence location + +The isolated run and its full logs/result bundles are under +`/private/tmp/vibefield-libghostty-migration-2026-10-29/evidence`. Baseline and +candidate checkouts, native build directories, simulator and iPhone app IDs are +separate from the user's running applications. Compact result summaries belong +in `docs/ghostty-1.4-qualification`; preserve failed attempts as well as passes. + +The final frozen desktop runs, six timing-control runs, generated-file checks, +plugin-author images and exact comparison harnesses are retained in the SSD's +`qualification-evidence/desktop-final-20261030/desktop-results.tar.gz`: +69 files, 7,691,046 bytes, SHA-256 +`c8f770c7480e130578243bf4260f834eebd65a3b28344bbf508b4a46caaa6d7c`. +Every archive member was checked against its original; the approved test key is +absent. The obsolete generated dependency directory used to diagnose the overlay +drift was removed after qualification; its diagnostic locks and logs remain. +Archive locations and hashes are recorded in +[desktop archive metadata](ghostty-1.4-qualification/desktop-final-20261030-archive.json), +[device retry metadata](ghostty-1.4-qualification/device-retry-20261030-archive.json) +and [final Apple asset metadata](ghostty-1.4-qualification/final-candidate-assets-20261030.json). diff --git a/docs/ghostty-1.4-qualification/agent-timing-controls-20261030.json b/docs/ghostty-1.4-qualification/agent-timing-controls-20261030.json new file mode 100644 index 00000000..373c6ec9 --- /dev/null +++ b/docs/ghostty-1.4-qualification/agent-timing-controls-20261030.json @@ -0,0 +1,110 @@ +[ + { + "label": "baseline", + "repetition": 1, + "exitCode": 0, + "elapsedSeconds": 13.59, + "summary": "Tests 2 passed | 54 skipped (56)", + "log": "agent-timing-baseline-1-20261030.log", + "binaries": { + "field-native": "79d9f93b73bfb27c09ebff104fb0bef8392499f8fc66b53403e63d0cb9f2148f", + "field-terminal-host": "938dd252b26a6783eb5205b56ee689ec0b0250667ce075199655a54ab37b48c6", + "field-freeway-host": "e54860095ae7ba929f5a502ed0d92e6bb8e354ffbdc4cf1b3f20adf57ff6f217" + }, + "setupChange": "reuse already-qualified retained binaries instead of rebuilding them", + "testSourceChanged": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "plannedPairedRepetitions": 3 + }, + { + "label": "candidate", + "repetition": 1, + "exitCode": 0, + "elapsedSeconds": 12.13, + "summary": "Tests 2 passed | 54 skipped (56)", + "log": "agent-timing-candidate-1-20261030.log", + "binaries": { + "field-native": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "field-terminal-host": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1", + "field-freeway-host": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6" + }, + "setupChange": "reuse already-qualified retained binaries instead of rebuilding them", + "testSourceChanged": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "plannedPairedRepetitions": 3 + }, + { + "label": "baseline", + "repetition": 2, + "exitCode": 0, + "elapsedSeconds": 12.14, + "summary": "Tests 2 passed | 54 skipped (56)", + "log": "agent-timing-baseline-2-20261030.log", + "binaries": { + "field-native": "79d9f93b73bfb27c09ebff104fb0bef8392499f8fc66b53403e63d0cb9f2148f", + "field-terminal-host": "938dd252b26a6783eb5205b56ee689ec0b0250667ce075199655a54ab37b48c6", + "field-freeway-host": "e54860095ae7ba929f5a502ed0d92e6bb8e354ffbdc4cf1b3f20adf57ff6f217" + }, + "setupChange": "reuse already-qualified retained binaries instead of rebuilding them", + "testSourceChanged": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "plannedPairedRepetitions": 3 + }, + { + "label": "candidate", + "repetition": 2, + "exitCode": 0, + "elapsedSeconds": 12.03, + "summary": "Tests 2 passed | 54 skipped (56)", + "log": "agent-timing-candidate-2-20261030.log", + "binaries": { + "field-native": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "field-terminal-host": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1", + "field-freeway-host": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6" + }, + "setupChange": "reuse already-qualified retained binaries instead of rebuilding them", + "testSourceChanged": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "plannedPairedRepetitions": 3 + }, + { + "label": "baseline", + "repetition": 3, + "exitCode": 0, + "elapsedSeconds": 11.68, + "summary": "Tests 2 passed | 54 skipped (56)", + "log": "agent-timing-baseline-3-20261030.log", + "binaries": { + "field-native": "79d9f93b73bfb27c09ebff104fb0bef8392499f8fc66b53403e63d0cb9f2148f", + "field-terminal-host": "938dd252b26a6783eb5205b56ee689ec0b0250667ce075199655a54ab37b48c6", + "field-freeway-host": "e54860095ae7ba929f5a502ed0d92e6bb8e354ffbdc4cf1b3f20adf57ff6f217" + }, + "setupChange": "reuse already-qualified retained binaries instead of rebuilding them", + "testSourceChanged": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "plannedPairedRepetitions": 3 + }, + { + "label": "candidate", + "repetition": 3, + "exitCode": 0, + "elapsedSeconds": 12.46, + "summary": "Tests 2 passed | 54 skipped (56)", + "log": "agent-timing-candidate-3-20261030.log", + "binaries": { + "field-native": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "field-terminal-host": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1", + "field-freeway-host": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6" + }, + "setupChange": "reuse already-qualified retained binaries instead of rebuilding them", + "testSourceChanged": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "plannedPairedRepetitions": 3 + } +] diff --git a/docs/ghostty-1.4-qualification/agent-timing-source-control-20261030.json b/docs/ghostty-1.4-qualification/agent-timing-source-control-20261030.json new file mode 100644 index 00000000..b675e5b8 --- /dev/null +++ b/docs/ghostty-1.4-qualification/agent-timing-source-control-20261030.json @@ -0,0 +1,12 @@ +[ + { + "path": "packages/fieldd/src/agent-runtime.ts", + "identicalToBaseline": true, + "sha256": "7cca2cdd5377182132a3d4ba8ecfcc3cf1238eae1e9fa2ec7cbff2e732b3a547" + }, + { + "path": "packages/fieldd/test/agent-session.test.ts", + "identicalToBaseline": true, + "sha256": "d197550af6a87d4c3f8febc009a1c06f0727fc87022a2ef29d8dbc3477d43c39" + } +] diff --git a/docs/ghostty-1.4-qualification/apple-final-candidate.json b/docs/ghostty-1.4-qualification/apple-final-candidate.json new file mode 100644 index 00000000..4c314165 --- /dev/null +++ b/docs/ghostty-1.4-qualification/apple-final-candidate.json @@ -0,0 +1,15 @@ +{ + "buildPassed": true, + "isolatedBundleIdentifier": "com.jamesyong.vibefield.ghosttyqualification.candidate", + "appExecutableSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrun": "/private/tmp/vibefield-libghostty-migration-2026-10-29/derived-device-candidate/Build/Products/VibeField_iphoneos26.5-arm64.xctestrun", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "stagedLibrarySha256": "0aea3d134386cccfd5381e8a7a2bd11c6383182bf008168babdcfa0a2bc76f61", + "stagedLibraryMatchesVerifiedPackage": true, + "deviceRun": "passed: 833 production tests, zero failures or skips; native device font golden passed", + "signatureVerification": "baseline and candidate passed codesign --verify --deep --strict with system trust access", + "swiftAllProductsLinked": 18, + "packageZipSha256": "de397a043efee2cb3f8d0e2cecf320cb345af475c4ea9b6d70b50f13f789dc5d", + "realTruffleReconfirmation": "passed: all four final-package client/host pairings; eight tests, zero failures or skips; exact same app and native package" +} diff --git a/docs/ghostty-1.4-qualification/baseline-pane-regression-summary.json b/docs/ghostty-1.4-qualification/baseline-pane-regression-summary.json new file mode 100644 index 00000000..b204d289 --- /dev/null +++ b/docs/ghostty-1.4-qualification/baseline-pane-regression-summary.json @@ -0,0 +1,8 @@ +{ + "passed": true, + "usedSessionRetained": true, + "survivingWidths": [ + 581, + 581 + ] +} diff --git a/docs/ghostty-1.4-qualification/candidate-artifacts-40969422970-rejected.json b/docs/ghostty-1.4-qualification/candidate-artifacts-40969422970-rejected.json new file mode 100644 index 00000000..4b289464 --- /dev/null +++ b/docs/ghostty-1.4-qualification/candidate-artifacts-40969422970-rejected.json @@ -0,0 +1,37 @@ +{ + "candidateRunId": 40969422970, + "headSha": "4c674131158053a184b02e9d451bf35a26075e2b", + "sourceDigest": "4bf5fbebe3f2f961ccdd550aa1d71151ad51f3b305f569d15af0f568b2b37b50", + "targets": [ + { + "target": "aarch64-apple-darwin", + "sha256": "704d69addca21ecf33164ac2b740e10e29f1a34916c5775dd56fa5230f46c021", + "librarySha256": "f202bdb37ebb79747248ca03c7a864ef8b0603b8df80de02076359478d44808f", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", + "filesVerified": 39, + "runtime": null, + "provenanceVerified": true + }, + { + "target": "x86_64-pc-windows-msvc", + "sha256": "e479a6d4dc4ef389cc5a22fab80ba485ae252e1574b518569a5005566c6c11c7", + "librarySha256": "383326b227dc9a4c72f486d696f43c3d60704d98fd71bd6e42eeb469c60543dd", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", + "filesVerified": 39, + "runtime": "dll", + "provenanceVerified": true + }, + { + "target": "x86_64-pc-windows-msvc-crt-static", + "sha256": "3ada5088a79fe821f9ba7a93185686e7f8b8fee001aac8a48c3269b9a0d09539", + "librarySha256": "86e1d1178e258506d86d6a92f65bccc45b3b84d00e9de79f7c294e7e04e20e7e", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", + "filesVerified": 39, + "runtime": "static", + "provenanceVerified": true + } + ], + "published": false, + "qualificationStatus": "rejected for promotion", + "reason": "Normalized macOS archive still exposes duplicate memset definitions; corrected Darwin postprocessing candidate run 40972570187 is pending." +} diff --git a/docs/ghostty-1.4-qualification/candidate-artifacts-40972570187-verified.json b/docs/ghostty-1.4-qualification/candidate-artifacts-40972570187-verified.json new file mode 100644 index 00000000..6c8ca818 --- /dev/null +++ b/docs/ghostty-1.4-qualification/candidate-artifacts-40972570187-verified.json @@ -0,0 +1,47 @@ +{ + "workflow": "https://github.com/vibecook-dev/ghosttea/actions/runs/40972570187", + "sourceCommit": "905ffb875008df4c3c03a54608ad3515bb49b49a", + "sourceDigest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7", + "ci": "passed", + "targets": [ + { + "target": "aarch64-apple-darwin", + "sourceDigest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7", + "bundleSha256": "b7d95901b38bf490a4f9808c019b91df9731d43913e880cb5e4c4df7218b6b7b", + "librarySha256": "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", + "verifiedFiles": 40, + "fileInventory": "passed", + "sourceIdentity": "passed", + "sbomConsistency": "passed", + "provenance": "passed", + "candidateRunId": 40972570187 + }, + { + "target": "x86_64-pc-windows-msvc", + "sourceDigest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7", + "bundleSha256": "b8f364d60521f13f8fd8b3ec361b11585985e00542ca9bb2019cac6503402ea0", + "librarySha256": "99da659f3a7f5d495700c1f527b8f7e8a5dc5b6e624fc102aff82c8b1420f834", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", + "verifiedFiles": 40, + "fileInventory": "passed", + "sourceIdentity": "passed", + "sbomConsistency": "passed", + "provenance": "passed", + "candidateRunId": 40972570187 + }, + { + "target": "x86_64-pc-windows-msvc-crt-static", + "sourceDigest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7", + "bundleSha256": "0249b96bec6f04b29bba09637c7cfdc375b4e05754e0162ee22128c4c5a475c7", + "librarySha256": "94c149ed42fb8ea5d18c3717821b4c713af41dc77181d33fd167b58929d524ca", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", + "verifiedFiles": 40, + "fileInventory": "passed", + "sourceIdentity": "passed", + "sbomConsistency": "passed", + "provenance": "passed", + "candidateRunId": 40972570187 + } + ] +} diff --git a/docs/ghostty-1.4-qualification/candidate-js-link-repair-20261030.json b/docs/ghostty-1.4-qualification/candidate-js-link-repair-20261030.json new file mode 100644 index 00000000..24ba9d7a --- /dev/null +++ b/docs/ghostty-1.4-qualification/candidate-js-link-repair-20261030.json @@ -0,0 +1,23 @@ +{ + "changedOnly": "generated links inside the isolated candidate renderer node_modules", + "sourceAndLocksChanged": false, + "releasePinsChanged": false, + "selectionCapabilityVerified": true, + "changes": [ + { + "package": "@vibecook/ghosttea", + "before": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@vibecook+ghosttea@0.13.7/node_modules/@vibecook/ghosttea", + "after": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/packages/ghosttea" + }, + { + "package": "@vibecook/ghosttea-frame", + "before": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@vibecook+ghosttea-frame@0.13.7/node_modules/@vibecook/ghosttea-frame", + "after": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/packages/ghosttea-frame" + }, + { + "package": "@vibecook/ghosttea-protocol", + "before": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@vibecook+ghosttea-protocol@0.13.7/node_modules/@vibecook/ghosttea-protocol", + "after": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/packages/ghosttea-protocol" + } + ] +} diff --git a/docs/ghostty-1.4-qualification/candidate-pane-regression-summary.json b/docs/ghostty-1.4-qualification/candidate-pane-regression-summary.json new file mode 100644 index 00000000..b204d289 --- /dev/null +++ b/docs/ghostty-1.4-qualification/candidate-pane-regression-summary.json @@ -0,0 +1,8 @@ +{ + "passed": true, + "usedSessionRetained": true, + "survivingWidths": [ + 581, + 581 + ] +} diff --git a/docs/ghostty-1.4-qualification/desktop-echo-analysis.json b/docs/ghostty-1.4-qualification/desktop-echo-analysis.json new file mode 100644 index 00000000..d40376b5 --- /dev/null +++ b/docs/ghostty-1.4-qualification/desktop-echo-analysis.json @@ -0,0 +1,61 @@ +{ + "method": "Exclude only fixture receipts strictly before first injection; then require exact probe identity in arrival order and nonnegative latency <=1000ms. Original reports remain unchanged. This measures injection to PTY receipt, not input to visible pixels.", + "runs": [ + { + "run": "echo-probe-baseline-1", + "rawReportPaired": 0, + "excludedBeforeFirstInjection": 3, + "injected": 60, + "receivedAfterFirstInjection": 60, + "strictOrderMismatches": 0, + "matched": 60, + "inputToPtyMs": { + "p50": 0.8425205, + "p95": 1.531542, + "max": 2.115417 + } + }, + { + "run": "echo-probe-baseline-4", + "rawReportPaired": 60, + "excludedBeforeFirstInjection": 0, + "injected": 60, + "receivedAfterFirstInjection": 60, + "strictOrderMismatches": 0, + "matched": 60, + "inputToPtyMs": { + "p50": 0.8565834999999999, + "p95": 1.583666, + "max": 17.426708 + } + }, + { + "run": "echo-probe-candidate-2", + "rawReportPaired": 60, + "excludedBeforeFirstInjection": 0, + "injected": 60, + "receivedAfterFirstInjection": 60, + "strictOrderMismatches": 0, + "matched": 60, + "inputToPtyMs": { + "p50": 0.875292, + "p95": 1.503, + "max": 2.2365 + } + }, + { + "run": "echo-probe-candidate-3", + "rawReportPaired": 60, + "excludedBeforeFirstInjection": 0, + "injected": 60, + "receivedAfterFirstInjection": 60, + "strictOrderMismatches": 0, + "matched": 60, + "inputToPtyMs": { + "p50": 1.152187, + "p95": 3.015583, + "max": 9.5705 + } + } + ] +} diff --git a/docs/ghostty-1.4-qualification/desktop-final-20261030-archive.json b/docs/ghostty-1.4-qualification/desktop-final-20261030-archive.json new file mode 100644 index 00000000..646c2a5f --- /dev/null +++ b/docs/ghostty-1.4-qualification/desktop-final-20261030-archive.json @@ -0,0 +1,9 @@ +{ + "archive": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/qualification-evidence/desktop-final-20261030/desktop-results.tar.gz", + "sha256": "c8f770c7480e130578243bf4260f834eebd65a3b28344bbf508b4a46caaa6d7c", + "bytes": 7691046, + "files": 69, + "everyFileVerifiedAgainstOriginal": true, + "approvedTestKeyAbsent": true, + "manifest": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/qualification-evidence/desktop-final-20261030/manifest.json" +} diff --git a/docs/ghostty-1.4-qualification/desktop-final-ci-smokes.json b/docs/ghostty-1.4-qualification/desktop-final-ci-smokes.json new file mode 100644 index 00000000..322e4af9 --- /dev/null +++ b/docs/ghostty-1.4-qualification/desktop-final-ci-smokes.json @@ -0,0 +1,56 @@ +{ + "initialThreeGateExitCode": 1, + "godview": { + "exitCode": 0, + "seconds": 157, + "finalSurvivingProcesses": 0 + }, + "terminalDirect": { + "exitCode": 0, + "seconds": 48, + "finalSurvivingProcesses": 0 + }, + "terminalDoor": { + "exitCode": 3, + "seconds": 45, + "connectionAndOriginChecksPassed": true, + "failure": "30-second teardown watchdog", + "finalSurvivingProcesses": 0 + }, + "terminalDoorControls": [ + { + "label": "baseline", + "exitCode": 0, + "durationSeconds": 45.71, + "nativeSha256": "79d9f93b73bfb27c09ebff104fb0bef8392499f8fc66b53403e63d0cb9f2148f", + "connectionAndOriginChecksPassed": true, + "watchdogFired": false, + "timeoutChanged": false, + "assertionsChanged": false, + "drainWaitMs": 20129, + "forcedCleanupCount": 1, + "finalSurvivingProcesses": 0, + "finalReapedProcesses": 0, + "log": "final-baseline-terminal-door-recheck.log" + }, + { + "label": "candidate", + "exitCode": 0, + "durationSeconds": 49.8, + "nativeSha256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "connectionAndOriginChecksPassed": true, + "watchdogFired": false, + "timeoutChanged": false, + "assertionsChanged": false, + "drainWaitMs": 20412, + "forcedCleanupCount": 1, + "finalSurvivingProcesses": 0, + "finalReapedProcesses": 0, + "log": "final-candidate-terminal-door-recheck.log" + } + ], + "sourceFilesIdentical": true, + "rootCause": "not established; initial intermittent shutdown failure retained", + "fullLandingGateRun": false, + "finalHostLinkIdentity": "final-desktop-host-link-identity.json" +} diff --git a/docs/ghostty-1.4-qualification/desktop-final-integrity-20261030.json b/docs/ghostty-1.4-qualification/desktop-final-integrity-20261030.json new file mode 100644 index 00000000..7a061246 --- /dev/null +++ b/docs/ghostty-1.4-qualification/desktop-final-integrity-20261030.json @@ -0,0 +1,39 @@ +{ + "revisions": { + "ghosttea-0.11.2": { + "head": "f535d6f679affe274aa49997f63125c5ce9c5213", + "status": [] + }, + "vibe-field": { + "head": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "status": [ + " M apps/ios/VibeFieldKit/Package.swift" + ] + } + }, + "nativeHelpers": [ + { + "binary": "field-native", + "sha256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "matchesFinalDeviceAndDesktopTestedBinary": true + }, + { + "binary": "field-terminal-host", + "sha256": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1", + "matchesFinalDeviceAndDesktopTestedBinary": true + }, + { + "binary": "field-freeway-host", + "sha256": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6", + "matchesFinalDeviceAndDesktopTestedBinary": true + } + ], + "correctedCIArchiveSha256": "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd", + "ownedPathProcessCensus": [], + "smokeProcessCensuses": 14, + "nonemptySmokeProcessCensuses": 0, + "productionPinsPromoted": false, + "temporarySwiftOverlayKeptLocal": true, + "runtimeOutputRestorationEvidence": "restore-qualified-runtime-binaries-20261030.json", + "fullVerifyExitCodeUnchanged": 1 +} diff --git a/docs/ghostty-1.4-qualification/desktop-matrix.json b/docs/ghostty-1.4-qualification/desktop-matrix.json new file mode 100644 index 00000000..30f35dcd --- /dev/null +++ b/docs/ghostty-1.4-qualification/desktop-matrix.json @@ -0,0 +1,246 @@ +{ + "conditions": "Isolated product profiles, two 12-second rotations per run, WebGPU, same Mac and unchanged rosettad. Background builds overlapped some runs. Lab-internal derived-null verdicts are not baseline-versus-candidate comparisons.", + "deadlineNote": "windowsTimedOut means the metrics window ended at its deadline instead of waiting for idle; busy flood/resize workloads produced these in both versions. No windowErrors or unpresentableArms were reported.", + "runs": [ + { + "scenario": "echo-probe", + "flavor": "baseline", + "repetition": 1, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.9760047990401, + "rendererFrameP95Ms": 9.100000000000364, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 3.470213996529786 + }, + { + "scenario": "echo-probe", + "flavor": "candidate", + "repetition": 2, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.98800119987997, + "rendererFrameP95Ms": 9.199999999998909, + "droppedFrames": 4, + "unpresentableArms": 0, + "framesPerSecond": 3.4698126301179735 + }, + { + "scenario": "echo-probe", + "flavor": "candidate", + "repetition": 3, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.98800119987975, + "rendererFrameP95Ms": 9.299999999999272, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 3.471705599365174 + }, + { + "scenario": "echo-probe", + "flavor": "baseline", + "repetition": 4, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.97600479904033, + "rendererFrameP95Ms": 9.200000000000728, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 3.633300853825701 + }, + { + "scenario": "flood-in-a-deck", + "flavor": "baseline", + "repetition": 1, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 3, + "windowErrors": 0, + "rendererFps": 119.96401079676106, + "rendererFrameP95Ms": 9.100000000002183, + "droppedFrames": 3, + "unpresentableArms": 0, + "framesPerSecond": 74.64467831181513 + }, + { + "scenario": "flood-in-a-deck", + "flavor": "candidate", + "repetition": 2, + "exitCode": 0, + "backend": "webgpu", + "windows": 3, + "windowsTimedOut": 2, + "windowErrors": 0, + "rendererFps": 119.98800119987997, + "rendererFrameP95Ms": 9.200000000000728, + "droppedFrames": 3, + "unpresentableArms": 0, + "framesPerSecond": 89.55687878135686 + }, + { + "scenario": "flood-in-a-deck", + "flavor": "candidate", + "repetition": 3, + "exitCode": 0, + "backend": "webgpu", + "windows": 5, + "windowsTimedOut": 2, + "windowErrors": 0, + "rendererFps": 119.98800119987997, + "rendererFrameP95Ms": 9.100000000002183, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 61.87456338513741 + }, + { + "scenario": "flood-in-a-deck", + "flavor": "baseline", + "repetition": 4, + "exitCode": 0, + "backend": "webgpu", + "windows": 4, + "windowsTimedOut": 3, + "windowErrors": 0, + "rendererFps": 119.98800119987997, + "rendererFrameP95Ms": 9.199999999998909, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 89.96124746263149 + }, + { + "scenario": "resize-storm", + "flavor": "baseline", + "repetition": 1, + "exitCode": 0, + "backend": "webgpu", + "windows": 4, + "windowsTimedOut": 4, + "windowErrors": 0, + "rendererFps": 119.9760047990401, + "rendererFrameP95Ms": 9.100000000002183, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 231.52647975077883 + }, + { + "scenario": "resize-storm", + "flavor": "candidate", + "repetition": 2, + "exitCode": 0, + "backend": "webgpu", + "windows": 4, + "windowsTimedOut": 4, + "windowErrors": 0, + "rendererFps": 119.98800119987997, + "rendererFrameP95Ms": 9.100000000002183, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 235.48389779067546 + }, + { + "scenario": "scroll-storm", + "flavor": "baseline", + "repetition": 1, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.98800119988019, + "rendererFrameP95Ms": 9.199999999998909, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 3.4800477263688183 + }, + { + "scenario": "scroll-storm", + "flavor": "candidate", + "repetition": 2, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.98016856717898, + "rendererFrameP95Ms": 9.200000000000728, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 3.4684945082170286 + }, + { + "scenario": "focus-alternation", + "flavor": "baseline", + "repetition": 1, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.98800119988019, + "rendererFrameP95Ms": 9.200000000000728, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 3.4829913920355597 + }, + { + "scenario": "focus-alternation", + "flavor": "candidate", + "repetition": 2, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.96401079676106, + "rendererFrameP95Ms": 9.200000000000728, + "droppedFrames": 4, + "unpresentableArms": 0, + "framesPerSecond": 3.471131754243871 + }, + { + "scenario": "startup", + "flavor": "baseline", + "repetition": 1, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 119.98016856717898, + "rendererFrameP95Ms": 9.100000000002183, + "droppedFrames": 0, + "unpresentableArms": 0, + "framesPerSecond": 3.9272798677815777 + }, + { + "scenario": "startup", + "flavor": "candidate", + "repetition": 2, + "exitCode": 0, + "backend": "webgpu", + "windows": 6, + "windowsTimedOut": 0, + "windowErrors": 0, + "rendererFps": 120, + "rendererFrameP95Ms": 9.200000000000728, + "droppedFrames": 3, + "unpresentableArms": 0, + "framesPerSecond": 3.465289351660864 + } + ] +} diff --git a/docs/ghostty-1.4-qualification/device-retry-20261030-archive.json b/docs/ghostty-1.4-qualification/device-retry-20261030-archive.json new file mode 100644 index 00000000..aaf906b7 --- /dev/null +++ b/docs/ghostty-1.4-qualification/device-retry-20261030-archive.json @@ -0,0 +1,7 @@ +{ + "path": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/qualification-evidence/device-retry-20261030/device-results.tar.gz", + "sha256": "f13b2fa205a28f2cc481e47dd2418f6eb4a94b90ffba80277db5eec1a0024ff0", + "bytes": 3583258, + "sourceFilesVerified": 160, + "testCredentialAbsent": true +} diff --git a/docs/ghostty-1.4-qualification/final-candidate-assets-20261030.json b/docs/ghostty-1.4-qualification/final-candidate-assets-20261030.json new file mode 100644 index 00000000..bdc9b1cf --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-candidate-assets-20261030.json @@ -0,0 +1,9 @@ +{ + "artifact": "GhostteaAppleNative.xcframework.zip", + "sha256": "de397a043efee2cb3f8d0e2cecf320cb345af475c4ea9b6d70b50f13f789dc5d", + "bytes": 50692910, + "retainedAt": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/qualification-evidence/final-candidate-assets-20261030/GhostteaAppleNative.xcframework.zip", + "sourceDigest": "56dc58d73c6d319af6759ef6f1e465e011ae58b32ec5e7b50295a1c2d4f49ac8", + "tag": "ghosttea-apple-native-1611ad6b0820", + "published": false +} diff --git a/docs/ghostty-1.4-qualification/final-desktop-host-link-identity.json b/docs/ghostty-1.4-qualification/final-desktop-host-link-identity.json new file mode 100644 index 00000000..ceed94cf --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-desktop-host-link-identity.json @@ -0,0 +1,58 @@ +{ + "hostBinarySHA256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "cargoBinary": "field_native-fd04087cf9a7a49b", + "dependencyChain": [ + { + "fingerprint": "field-native-fd04087cf9a7a49b/bin-field-native.json", + "features": "[\"default\", \"truffle-egress\"]", + "profile": 9469513639638652478 + }, + { + "fingerprint": "ghosttea-5728064a0c9a2c12/lib-ghosttea.json", + "features": "[]", + "profile": 4636522799852498062 + }, + { + "fingerprint": "ghosttea-text-1a264baadb494f4a/lib-ghosttea_text.json", + "features": "[]", + "profile": 4636522799852498062 + }, + { + "fingerprint": "ghosttea-vt-584b6105eca830a7/lib-ghosttea_vt.json", + "features": "[]", + "profile": 4636522799852498062 + }, + { + "fingerprint": "ghosttea-vt-sys-ff44760716521748/lib-ghosttea_vt_sys.json", + "features": "[]", + "profile": 4636522799852498062 + }, + { + "fingerprint": "ghosttea-vt-sys-6a5c1e706c6cecc5/run-build-script-build-script-build.json", + "features": "", + "profile": 0 + }, + { + "fingerprint": "ghosttea-vt-sys-7a091ab055249261/build-script-build-script-build.json", + "features": "[]", + "profile": 5509259098233294680 + }, + { + "fingerprint": "ghosttea-core-b9d2a8a7d6fe097c/lib-ghosttea_core.json", + "features": "[]", + "profile": 4636522799852498062 + }, + { + "fingerprint": "ghosttea-config-ff29e399594dd5c8/lib-ghosttea_config.json", + "features": "[]", + "profile": 4636522799852498062 + } + ], + "vtArchives": [ + { + "archive": "/private/tmp/vibefield-libghostty-migration-2026-10-29/target-vibefield-candidate/debug/build/ghosttea-vt-sys-6a5c1e706c6cecc5/out/libghosttea_ghostty_vt_static.a", + "sha256": "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd" + } + ], + "expectedCIArchiveSHA256": "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd" +} diff --git a/docs/ghostty-1.4-qualification/final-device-fixture-layout-recovery.json b/docs/ghostty-1.4-qualification/final-device-fixture-layout-recovery.json new file mode 100644 index 00000000..9f988f9b --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-device-fixture-layout-recovery.json @@ -0,0 +1,9 @@ +{ + "initialBaselineFixtureExitCode": 101, + "initialBaselineMatrixReachedPhone": false, + "reason": "Retained fixture was launched outside its original debug/deps layout; NativeConfig locates cell and Freeway helpers beside or one directory above the current executable. The storage-recovery copy had neither helper at those relative locations.", + "correction": "Restored a hard link to the same verified fixture bytes at target-vibefield-baseline/debug/deps; all three original adjacent helper binaries verified before retry.", + "productSourceChanged": false, + "fixtureAssertionsChanged": false, + "initialCandidateFixtureStoppedCleanly": true +} diff --git a/docs/ghostty-1.4-qualification/final-device-helper-link-identities.json b/docs/ghostty-1.4-qualification/final-device-helper-link-identities.json new file mode 100644 index 00000000..c2916949 --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-device-helper-link-identities.json @@ -0,0 +1,59 @@ +[ + { + "binary": "field-native", + "sha256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "cargoDependencyChain": [ + "field-native-fd04087cf9a7a49b/bin-field-native.json", + "ghosttea-5728064a0c9a2c12/lib-ghosttea.json", + "ghosttea-text-1a264baadb494f4a/lib-ghosttea_text.json", + "ghosttea-vt-584b6105eca830a7/lib-ghosttea_vt.json", + "ghosttea-vt-sys-ff44760716521748/lib-ghosttea_vt_sys.json", + "ghosttea-vt-sys-6a5c1e706c6cecc5/run-build-script-build-script-build.json", + "ghosttea-vt-sys-7a091ab055249261/build-script-build-script-build.json", + "ghosttea-core-b9d2a8a7d6fe097c/lib-ghosttea_core.json", + "ghosttea-config-ff29e399594dd5c8/lib-ghosttea_config.json", + "field-native-4b5be8d0e0cf8add/lib-field_native.json" + ], + "vtArchiveSha256": [ + "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd" + ] + }, + { + "binary": "field-terminal-host", + "sha256": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1", + "cargoDependencyChain": [ + "field-native-ab71da0921531b03/bin-field-terminal-host.json", + "ghosttea-5728064a0c9a2c12/lib-ghosttea.json", + "ghosttea-text-1a264baadb494f4a/lib-ghosttea_text.json", + "ghosttea-vt-584b6105eca830a7/lib-ghosttea_vt.json", + "ghosttea-vt-sys-ff44760716521748/lib-ghosttea_vt_sys.json", + "ghosttea-vt-sys-6a5c1e706c6cecc5/run-build-script-build-script-build.json", + "ghosttea-vt-sys-7a091ab055249261/build-script-build-script-build.json", + "ghosttea-core-b9d2a8a7d6fe097c/lib-ghosttea_core.json", + "ghosttea-config-ff29e399594dd5c8/lib-ghosttea_config.json", + "field-native-4b5be8d0e0cf8add/lib-field_native.json" + ], + "vtArchiveSha256": [ + "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd" + ] + }, + { + "binary": "field-freeway-host", + "sha256": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6", + "cargoDependencyChain": [ + "field-native-50ff0e340d5e31b0/bin-field-freeway-host.json", + "ghosttea-5728064a0c9a2c12/lib-ghosttea.json", + "ghosttea-text-1a264baadb494f4a/lib-ghosttea_text.json", + "ghosttea-vt-584b6105eca830a7/lib-ghosttea_vt.json", + "ghosttea-vt-sys-ff44760716521748/lib-ghosttea_vt_sys.json", + "ghosttea-vt-sys-6a5c1e706c6cecc5/run-build-script-build-script-build.json", + "ghosttea-vt-sys-7a091ab055249261/build-script-build-script-build.json", + "ghosttea-core-b9d2a8a7d6fe097c/lib-ghosttea_core.json", + "ghosttea-config-ff29e399594dd5c8/lib-ghosttea_config.json", + "field-native-4b5be8d0e0cf8add/lib-field_native.json" + ], + "vtArchiveSha256": [ + "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd" + ] + } +] diff --git a/docs/ghostty-1.4-qualification/final-ssd-source-consistency-with-recheck.json b/docs/ghostty-1.4-qualification/final-ssd-source-consistency-with-recheck.json new file mode 100644 index 00000000..f30632eb --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-ssd-source-consistency-with-recheck.json @@ -0,0 +1,40 @@ +[ + { + "check": "check:ghostty-upgrade", + "exitCode": 0 + }, + { + "check": "check:bundled-fonts", + "exitCode": 0 + }, + { + "check": "check:font-parity", + "exitCode": 1 + }, + { + "check": "check:ios-release-bom", + "exitCode": 0 + }, + { + "check": "check:ios-release-resources", + "exitCode": 0 + }, + { + "check": "check:swiftpm:manifests", + "exitCode": 0 + }, + { + "check": "check:ios-app-store", + "exitCode": 0 + }, + { + "check": "check:ios-beta-matrix", + "exitCode": 0 + }, + { + "check": "check:font-parity", + "exitCode": 0, + "recheck": "final-ssd-check-font-parity-permitted.log", + "reason": "Original sandboxed invocation could not access the external Cargo target; permitted invocation explicitly used the isolated candidate target." + } +] diff --git a/docs/ghostty-1.4-qualification/final-tail-stages-frozen-20261030.json b/docs/ghostty-1.4-qualification/final-tail-stages-frozen-20261030.json new file mode 100644 index 00000000..dcf3e05c --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-tail-stages-frozen-20261030.json @@ -0,0 +1,33 @@ +{ + "command": [ + "node", + "scripts/gate-landing.mjs", + "--only", + "test:rust,gen:check", + "--logs", + "/private/tmp/vibefield-libghostty-migration-2026-10-29/evidence/final-tail-stages-frozen-20261030" + ], + "exitCode": 0, + "elapsedSeconds": 313.14, + "gates": [ + "GATE test:rust ok exit=0 308s load 20/21/17\u219222/22/19", + "GATE gen:check ok exit=0 5s load 22/22/19\u219221/22/19" + ], + "final": { + "sha": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ok": true, + "red": [], + "at": "2026-10-30T18:26:16.596Z" + }, + "frozenDependencyGraphVerifiedBeforeAndAfter": true, + "automaticDependencyReinstallDisabled": true, + "buildOnlyOfflineFlagAbsent": true, + "originalVerifyExitCodeUnchanged": 1, + "log": "final-tail-stages-frozen-20261030.log", + "rustTestTotals": { + "passed": 498, + "failed": 0, + "ignoredIncludingDocTests": 15 + }, + "generatedFilesCurrent": true +} diff --git a/docs/ghostty-1.4-qualification/final-terminal-door-source-control.json b/docs/ghostty-1.4-qualification/final-terminal-door-source-control.json new file mode 100644 index 00000000..8b56f68e --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-terminal-door-source-control.json @@ -0,0 +1,32 @@ +[ + { + "path": "packages/electron-shell/src/testing/terminal-door-probe.ts", + "baselineSha256": "3aa367173e49d85d0c966a6d9bf50e416e42fb6ca6a3cd6f7fd13d756014419d", + "candidateSha256": "3aa367173e49d85d0c966a6d9bf50e416e42fb6ca6a3cd6f7fd13d756014419d", + "identical": true + }, + { + "path": "packages/electron-shell/src/testing/smoke-daemons.ts", + "baselineSha256": "b7ac14e13221aed51364553ce3460560e98cdbfcb4a154617ad3555c4de50680", + "candidateSha256": "b7ac14e13221aed51364553ce3460560e98cdbfcb4a154617ad3555c4de50680", + "identical": true + }, + { + "path": "packages/fieldd-supervisor/src/supervisor.ts", + "baselineSha256": "d3c469b4c9035bbbdff268219b7f0e450fd52c4dc5c162f44e82ad0de26385d2", + "candidateSha256": "d3c469b4c9035bbbdff268219b7f0e450fd52c4dc5c162f44e82ad0de26385d2", + "identical": true + }, + { + "path": "apps/desktop/packaging/run-smoke.mjs", + "baselineSha256": "2aeb3c5b60374b4d68349331abd6e17dd6166271e435bdf2705b47e667a457cd", + "candidateSha256": "2aeb3c5b60374b4d68349331abd6e17dd6166271e435bdf2705b47e667a457cd", + "identical": true + }, + { + "path": "apps/desktop/packaging/smoke-daemons.mjs", + "baselineSha256": "070e853e019582731c0cec877bb3f5d4064575e2c85d1d3539a69f1327d38c63", + "candidateSha256": "070e853e019582731c0cec877bb3f5d4064575e2c85d1d3539a69f1327d38c63", + "identical": true + } +] diff --git a/docs/ghostty-1.4-qualification/final-verify-timeout-baseline-source-control.json b/docs/ghostty-1.4-qualification/final-verify-timeout-baseline-source-control.json new file mode 100644 index 00000000..620fa80a --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-verify-timeout-baseline-source-control.json @@ -0,0 +1,32 @@ +[ + { + "path": "packages/screenshot/test/tool.test.ts", + "candidateSha256": "473da502f13b294b20e42c57c0b148c9fc5b6cb381173c30481d8108ae412ead", + "baselineSha256": "473da502f13b294b20e42c57c0b148c9fc5b6cb381173c30481d8108ae412ead", + "identical": true + }, + { + "path": "packages/electron-shell/test/smoke-profile.test.ts", + "candidateSha256": "c6450e2f9f070e759a5c3651eee5d81fc630da94f4e9ef66958ca4a048866c79", + "baselineSha256": "c6450e2f9f070e759a5c3651eee5d81fc630da94f4e9ef66958ca4a048866c79", + "identical": true + }, + { + "path": "packages/electron-shell/test/typed-agent.test.ts", + "candidateSha256": "99ec59cad7ffcc56f50ed151560680d7f8db73d9ce767a4906e569e17740772e", + "baselineSha256": "99ec59cad7ffcc56f50ed151560680d7f8db73d9ce767a4906e569e17740772e", + "identical": true + }, + { + "path": "apps/desktop/packaging/run-smoke.mjs", + "candidateSha256": "2aeb3c5b60374b4d68349331abd6e17dd6166271e435bdf2705b47e667a457cd", + "baselineSha256": "2aeb3c5b60374b4d68349331abd6e17dd6166271e435bdf2705b47e667a457cd", + "identical": true + }, + { + "path": "packages/electron-shell/src/testing/typed-agent.ts", + "candidateSha256": "d6d86b01ec44490f87db08ba7e0c3e629cedc9f90891f8c50f7f1e247c3c03e3", + "baselineSha256": "d6d86b01ec44490f87db08ba7e0c3e629cedc9f90891f8c50f7f1e247c3c03e3", + "identical": true + } +] diff --git a/docs/ghostty-1.4-qualification/final-verify-timeout-rechecks.json b/docs/ghostty-1.4-qualification/final-verify-timeout-rechecks.json new file mode 100644 index 00000000..d7a74277 --- /dev/null +++ b/docs/ghostty-1.4-qualification/final-verify-timeout-rechecks.json @@ -0,0 +1,34 @@ +[ + { + "tree": "baseline", + "suite": "screenshot", + "exitCode": 0, + "log": "final-baseline-screenshot-recheck.log", + "testTimeoutChanged": false, + "assertionsChanged": false + }, + { + "tree": "baseline", + "suite": "shell-helpers", + "exitCode": 0, + "log": "final-baseline-shell-helpers-recheck.log", + "testTimeoutChanged": false, + "assertionsChanged": false + }, + { + "tree": "candidate", + "suite": "screenshot", + "exitCode": 0, + "log": "final-candidate-screenshot-recheck.log", + "testTimeoutChanged": false, + "assertionsChanged": false + }, + { + "tree": "candidate", + "suite": "shell-helpers", + "exitCode": 0, + "log": "final-candidate-shell-helpers-recheck.log", + "testTimeoutChanged": false, + "assertionsChanged": false + } +] diff --git a/docs/ghostty-1.4-qualification/font-comparison.json b/docs/ghostty-1.4-qualification/font-comparison.json new file mode 100644 index 00000000..fd5231f4 --- /dev/null +++ b/docs/ghostty-1.4-qualification/font-comparison.json @@ -0,0 +1,83 @@ +{ + "oldFontsWithNewEngineExactlyMatchGolden": true, + "cases": [ + { + "case": "ascii-ligatures", + "layoutUnchanged": true, + "changedGlyphs": [] + }, + { + "case": "bold", + "layoutUnchanged": true, + "changedGlyphs": [] + }, + { + "case": "italic", + "layoutUnchanged": true, + "changedGlyphs": [] + }, + { + "case": "bold-italic", + "layoutUnchanged": true, + "changedGlyphs": [] + }, + { + "case": "combining-wide-missing", + "layoutUnchanged": true, + "changedGlyphs": [] + }, + { + "case": "emoji-fallback", + "layoutUnchanged": true, + "changedGlyphs": [ + { + "glyphId": 56, + "dimensionsBefore": [ + 32, + 30 + ], + "dimensionsAfter": [ + 32, + 30 + ], + "onlyPixelsChanged": true + } + ] + }, + { + "case": "emoji-presentation", + "layoutUnchanged": true, + "changedGlyphs": [ + { + "glyphId": 56, + "dimensionsBefore": [ + 32, + 30 + ], + "dimensionsAfter": [ + 32, + 30 + ], + "onlyPixelsChanged": true + }, + { + "glyphId": 58, + "dimensionsBefore": [ + 32, + 30 + ], + "dimensionsAfter": [ + 32, + 30 + ], + "onlyPixelsChanged": true + } + ] + }, + { + "case": "symbol-fallback", + "layoutUnchanged": true, + "changedGlyphs": [] + } + ] +} diff --git a/docs/ghostty-1.4-qualification/font-comparison.png b/docs/ghostty-1.4-qualification/font-comparison.png new file mode 100644 index 00000000..8ac3cc25 Binary files /dev/null and b/docs/ghostty-1.4-qualification/font-comparison.png differ diff --git a/docs/ghostty-1.4-qualification/freeway-trailing-slash-origin.json b/docs/ghostty-1.4-qualification/freeway-trailing-slash-origin.json new file mode 100644 index 00000000..340b4f8b --- /dev/null +++ b/docs/ghostty-1.4-qualification/freeway-trailing-slash-origin.json @@ -0,0 +1,15 @@ +{ + "baselineCommit": "e259f1fe31d9b3a01dca70dc33a5dad99841f6f3", + "observedSidecarVersion": "0.7.24", + "observedRoute": "exact=false trailing_slash=true", + "files": [ + { + "flavor": "baseline", + "originalDoorSha256": "56c1671a4260fac4705da8bfd47785208784f15a6d1f8c8c835002a9dda0ee40" + }, + { + "flavor": "candidate", + "originalDoorSha256": "56c1671a4260fac4705da8bfd47785208784f15a6d1f8c8c835002a9dda0ee40" + } + ] +} diff --git a/docs/ghostty-1.4-qualification/frozen-actual-dependency-graph-20261030.json b/docs/ghostty-1.4-qualification/frozen-actual-dependency-graph-20261030.json new file mode 100644 index 00000000..333731fa --- /dev/null +++ b/docs/ghostty-1.4-qualification/frozen-actual-dependency-graph-20261030.json @@ -0,0 +1,131 @@ +{ + "lockedGraphByteIdentical": true, + "lockSha256": "8201e7c39ee67791cc88c5ddb10aecabd2f205a5855b3103aff2184f12dda6c4", + "actualCriticalDependenciesMatchBaseline": true, + "packages": [ + { + "importer": "packages/fieldd", + "package": "loro-crdt", + "matchesBaseline": true, + "baseline": { + "version": "1.13.8", + "manifestSha256": "4fedb3ddfa41e34508b3d1856fbf92baad41833e897cfeb0d5ed6e62b6de3a2b", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/loro-crdt@1.13.8/node_modules/loro-crdt", + "wasmSha256": "44c208ef73a2b26f8adf541677ea644ece0a41229976ce5bedc81aa466da98a6" + }, + "candidate": { + "version": "1.13.8", + "manifestSha256": "4fedb3ddfa41e34508b3d1856fbf92baad41833e897cfeb0d5ed6e62b6de3a2b", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/loro-crdt@1.13.8/node_modules/loro-crdt", + "wasmSha256": "44c208ef73a2b26f8adf541677ea644ece0a41229976ce5bedc81aa466da98a6" + } + }, + { + "importer": "packages/field-app", + "package": "react", + "matchesBaseline": true, + "baseline": { + "version": "19.2.7", + "manifestSha256": "51966ae9c8c377ff592b5d69c980966992101bc6791bf83a1e5c7ad9bcefa7f4", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/react@19.2.7/node_modules/react" + }, + "candidate": { + "version": "19.2.7", + "manifestSha256": "51966ae9c8c377ff592b5d69c980966992101bc6791bf83a1e5c7ad9bcefa7f4", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react@19.2.7/node_modules/react" + } + }, + { + "importer": "packages/field-app", + "package": "react-dom", + "matchesBaseline": true, + "baseline": { + "version": "19.2.7", + "manifestSha256": "447acd72cdb6a966d9db967729e286e7cbb4a09ea61fddd45e12c2a97fd4cfb6", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/react-dom@19.2.7_react@19.2.7/node_modules/react-dom" + }, + "candidate": { + "version": "19.2.7", + "manifestSha256": "447acd72cdb6a966d9db967729e286e7cbb4a09ea61fddd45e12c2a97fd4cfb6", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react-dom@19.2.7_react@19.2.7/node_modules/react-dom" + } + }, + { + "importer": "packages/field-app", + "package": "@types/react", + "matchesBaseline": true, + "baseline": { + "version": "19.2.17", + "manifestSha256": "36496fc09a9dcf31b0ca969ce5677c9ce9ebb5c89da9bd6212e0c89b6fae3f88", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/@types+react@19.2.17/node_modules/@types/react" + }, + "candidate": { + "version": "19.2.17", + "manifestSha256": "36496fc09a9dcf31b0ca969ce5677c9ce9ebb5c89da9bd6212e0c89b6fae3f88", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react@19.2.17/node_modules/@types/react" + } + }, + { + "importer": "packages/field-app", + "package": "@types/react-dom", + "matchesBaseline": true, + "baseline": { + "version": "19.2.3", + "manifestSha256": "bf90986beaec5f757c164bf1d374f3038dc41b0b06673de4a55fd975d1e5deca", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.17/node_modules/@types/react-dom" + }, + "candidate": { + "version": "19.2.3", + "manifestSha256": "bf90986beaec5f757c164bf1d374f3038dc41b0b06673de4a55fd975d1e5deca", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.17/node_modules/@types/react-dom" + } + }, + { + "importer": "packages/field-app", + "package": "@vibecook/ice", + "matchesBaseline": true, + "baseline": { + "version": "0.16.0", + "manifestSha256": "7c9f10c6d8ba4d74eeb189328baa390b1c01bb6c89910e0780325b49504cf43b", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/@vibecook+ice@0.16.0_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@vibecook/ice" + }, + "candidate": { + "version": "0.16.0", + "manifestSha256": "7c9f10c6d8ba4d74eeb189328baa390b1c01bb6c89910e0780325b49504cf43b", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@vibecook+ice@0.16.0_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@vibecook/ice" + } + }, + { + "importer": "packages/fieldd", + "package": "vitest", + "matchesBaseline": true, + "baseline": { + "version": "3.2.7", + "manifestSha256": "3f3dc1ef7acc6ffac8d0a7f10c4460f98e36026837b57f774426d962d546d740", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/vitest@3.2.7_@types+debug@4.1.13_@types+node@24.13.3_happy-dom@20.11.0_jiti@2.7.0_light_7a6200b28a02c3610313c1c816f82b7a/node_modules/vitest" + }, + "candidate": { + "version": "3.2.7", + "manifestSha256": "3f3dc1ef7acc6ffac8d0a7f10c4460f98e36026837b57f774426d962d546d740", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/vitest@3.2.7_@types+debug@4.1.13_@types+node@24.13.3_happy-dom@20.11.0_jiti@2.7.0_light_7a6200b28a02c3610313c1c816f82b7a/node_modules/vitest" + } + }, + { + "importer": "apps/desktop", + "package": "electron", + "matchesBaseline": true, + "baseline": { + "version": "43.1.1", + "manifestSha256": "b27a9ed2dd26e96b0eb3e8cf0efa46992dfb0e9e9c7d4a10ae26dfcdf4c82aaa", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/baseline/vibe-field/node_modules/.pnpm/electron@43.1.1_supports-color@7.2.0/node_modules/electron" + }, + "candidate": { + "version": "43.1.1", + "manifestSha256": "b27a9ed2dd26e96b0eb3e8cf0efa46992dfb0e9e9c7d4a10ae26dfcdf4c82aaa", + "path": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/electron@43.1.1_supports-color@7.2.0/node_modules/electron" + } + } + ], + "automaticReinstallDisabledBy": "pnpm_config_verify_deps_before_run=false", + "sourceAndProductionPinsChanged": false +} diff --git a/docs/ghostty-1.4-qualification/frozen-affected-daemon-recheck-20261030.json b/docs/ghostty-1.4-qualification/frozen-affected-daemon-recheck-20261030.json new file mode 100644 index 00000000..95d2c372 --- /dev/null +++ b/docs/ghostty-1.4-qualification/frozen-affected-daemon-recheck-20261030.json @@ -0,0 +1,21 @@ +{ + "command": [ + "pnpm", + "exec", + "vitest", + "run", + "test/doc-compaction.test.ts", + "test/loro-host.test.ts", + "test/pair-log-root.test.ts", + "test/terminal-kill-matrix.test.ts", + "--maxWorkers=1", + "--no-file-parallelism" + ], + "exitCode": 0, + "elapsedSeconds": 107.28, + "log": "frozen-affected-daemon-recheck-20261030.log", + "dependencyGraphRestored": true, + "buildOnlyOfflineFlagAbsent": true, + "timeoutsChanged": false, + "assertionsChanged": false +} diff --git a/docs/ghostty-1.4-qualification/frozen-recheck-source-control-20261030.json b/docs/ghostty-1.4-qualification/frozen-recheck-source-control-20261030.json new file mode 100644 index 00000000..9b9a6b9b --- /dev/null +++ b/docs/ghostty-1.4-qualification/frozen-recheck-source-control-20261030.json @@ -0,0 +1,27 @@ +[ + { + "path": "packages/fieldd/test/doc-compaction.test.ts", + "identical": true, + "sha256": "f8324b22871d607a5c9b5602c8fd43e86a1df1e258b44387d6a22492a574525b" + }, + { + "path": "packages/fieldd/test/loro-host.test.ts", + "identical": true, + "sha256": "794a50ef1cbf6f661ce169bfe4fc92fbdad11def8c48e7bc76abefb7674c65b1" + }, + { + "path": "packages/fieldd/test/pair-log-root.test.ts", + "identical": true, + "sha256": "d414f69e05e5db27863b0cd6545f7518582c2ff35eb65adb7a254c87d273c1b4" + }, + { + "path": "packages/fieldd/test/terminal-kill-matrix.test.ts", + "identical": true, + "sha256": "8aaf69d7e7f07977a82f243677ee48d854f0f76fa369e7ad066852f1c60a3549" + }, + { + "path": "ghosttea/native/ghosttea/src/session.rs", + "identical": true, + "sha256": "325a3106bceffb44962066bb30f1278c975eb9f8f955847bb4f7b1117632de9e" + } +] diff --git a/docs/ghostty-1.4-qualification/godview-final-ci-ssd.json b/docs/ghostty-1.4-qualification/godview-final-ci-ssd.json new file mode 100644 index 00000000..96f91827 --- /dev/null +++ b/docs/ghostty-1.4-qualification/godview-final-ci-ssd.json @@ -0,0 +1,30 @@ +{ + "passed": true, + "selectionCopy": { + "status": "passed" + }, + "behaviors": { + "scrollTopAndBottom": true, + "alternateScreenRoundTrip": true, + "hiddenProcessRetained": true, + "hiddenOutputText": "copy-verified" + }, + "processExitCode": 0, + "ptyCensus": { + "before": 93, + "after": 93, + "leaked": 0, + "settled": true, + "survivingProcesses": 0, + "survivingLabElectrons": 0, + "note": "clean" + }, + "artifactLibrarySha256": "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd", + "screenshotReviewed": "terminal-behaviors.png; selected final X, joined emoji, adjacent ab and block all preserve host grid", + "screenshotSha256": "2f6486e7dc0141e19c4b45d28f86015863c6b87a9c21624b9a77d7f5a2a25327", + "sourceRecordSha256": "b96ca2a9ed78addee681fef0b0b71a4e3dee2183a208f2e25083cc8bf9da7f7a", + "fullEvidenceDirectory": "/private/tmp/vibefield-libghostty-migration-2026-10-29/evidence/candidate-ci-ssd-pane-behaviors", + "latencyQualified": false, + "usedSessionRetained": true, + "reattachmentChecks": 8 +} diff --git a/docs/ghostty-1.4-qualification/host-peer-overlay-repair-20261030.json b/docs/ghostty-1.4-qualification/host-peer-overlay-repair-20261030.json new file mode 100644 index 00000000..d616aa39 --- /dev/null +++ b/docs/ghostty-1.4-qualification/host-peer-overlay-repair-20261030.json @@ -0,0 +1,182 @@ +{ + "changedOnly": "generated node_modules links in isolated checkouts", + "sourceAndLocksChanged": false, + "before": [ + { + "folder": "../mille-pin", + "packages": { + "react": null, + "react-dom": null, + "@types/react": null, + "@types/react-dom": null + } + }, + { + "folder": "../mille-pin/packages/mille", + "packages": { + "react": null, + "react-dom": null, + "@types/react": null, + "@types/react-dom": null + } + }, + { + "folder": "../mille-pin/packages/mille-ui", + "packages": { + "react": null, + "react-dom": null, + "@types/react": null, + "@types/react-dom": null + } + }, + { + "folder": "../ghosttea-0.11.2", + "packages": { + "react": { + "version": "19.2.7", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/react/package.json" + }, + "react-dom": { + "version": "19.2.7", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/react-dom/package.json" + }, + "@types/react": { + "version": "19.2.17", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/@types/react/package.json" + }, + "@types/react-dom": { + "version": "19.2.3", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/@types/react-dom/package.json" + } + } + }, + { + "folder": "../ghosttea-0.11.2/packages/ghosttea-react", + "packages": { + "react": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react@19.3.1/node_modules/react/package.json" + }, + "react-dom": { + "version": "19.2.7", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/react-dom/package.json" + }, + "@types/react": { + "version": "19.2.17", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/@types/react/package.json" + }, + "@types/react-dom": { + "version": "19.2.3", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/@types/react-dom/package.json" + } + } + } + ], + "after": [ + { + "folder": "../mille-pin", + "packages": { + "react": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react@19.3.1/node_modules/react/package.json" + }, + "react-dom": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react-dom@19.3.1_react@19.3.1/node_modules/react-dom/package.json" + }, + "@types/react": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/package.json" + }, + "@types/react-dom": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/@types/react-dom/package.json" + } + } + }, + { + "folder": "../mille-pin/packages/mille", + "packages": { + "react": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react@19.3.1/node_modules/react/package.json" + }, + "react-dom": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react-dom@19.3.1_react@19.3.1/node_modules/react-dom/package.json" + }, + "@types/react": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/package.json" + }, + "@types/react-dom": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/@types/react-dom/package.json" + } + } + }, + { + "folder": "../mille-pin/packages/mille-ui", + "packages": { + "react": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react@19.3.1/node_modules/react/package.json" + }, + "react-dom": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react-dom@19.3.1_react@19.3.1/node_modules/react-dom/package.json" + }, + "@types/react": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/package.json" + }, + "@types/react-dom": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/@types/react-dom/package.json" + } + } + }, + { + "folder": "../ghosttea-0.11.2", + "packages": { + "react": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/react/package.json" + }, + "react-dom": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/react-dom/package.json" + }, + "@types/react": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/@types/react/package.json" + }, + "@types/react-dom": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/node_modules/@types/react-dom/package.json" + } + } + }, + { + "folder": "../ghosttea-0.11.2/packages/ghosttea-react", + "packages": { + "react": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react@19.3.1/node_modules/react/package.json" + }, + "react-dom": { + "version": "19.3.1", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/react-dom@19.3.1_react@19.3.1/node_modules/react-dom/package.json" + }, + "@types/react": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/package.json" + }, + "@types/react-dom": { + "version": "19.3.0", + "file": "/private/tmp/vibefield-libghostty-migration-2026-10-29/vibe-field/node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/@types/react-dom/package.json" + } + } + } + ] +} diff --git a/docs/ghostty-1.4-qualification/iphone-final-new-client-new-host.png b/docs/ghostty-1.4-qualification/iphone-final-new-client-new-host.png new file mode 100644 index 00000000..df08ac47 Binary files /dev/null and b/docs/ghostty-1.4-qualification/iphone-final-new-client-new-host.png differ diff --git a/docs/ghostty-1.4-qualification/iphone-final-new-client-old-host.png b/docs/ghostty-1.4-qualification/iphone-final-new-client-old-host.png new file mode 100644 index 00000000..7ad65536 Binary files /dev/null and b/docs/ghostty-1.4-qualification/iphone-final-new-client-old-host.png differ diff --git a/docs/ghostty-1.4-qualification/iphone-final-package-font-proof.json b/docs/ghostty-1.4-qualification/iphone-final-package-font-proof.json new file mode 100644 index 00000000..276affe3 --- /dev/null +++ b/docs/ghostty-1.4-qualification/iphone-final-package-font-proof.json @@ -0,0 +1,104 @@ +{ + "schemaVersion": 1, + "fontMode": "bundled", + "primaryFamily": "JetBrainsMono Nerd Font", + "metrics": { + "fontSizePx": 13.0, + "cellWidthPx": 7.83, + "lineHeightPx": 19.0, + "baselinePx": 14.0 + }, + "rasterScale": 2.0, + "faces": [ + { + "role": "regular", + "family": "JetBrainsMono Nerd Font", + "faceIndex": 0, + "sha256": "a2f4268a1719b95d1f1448376173a42317919cb598423f18ea327eaca13dd916" + }, + { + "role": "bold", + "family": "JetBrainsMono Nerd Font", + "faceIndex": 0, + "sha256": "ece02baa478a98f3b6ea0c276c1a9207c6bbdcbde04b8010542f2b0671432438" + }, + { + "role": "italic", + "family": "JetBrainsMono Nerd Font", + "faceIndex": 0, + "sha256": "7fab948d4bea3441d6dbff7f726b4fa1e27087e98286902b53ddf523be7a20fc" + }, + { + "role": "bold-italic", + "family": "JetBrainsMono Nerd Font", + "faceIndex": 0, + "sha256": "a70e3862b5d186548b8cc0badc65016ac8040205cc9753f31329c4b51dd131e3" + }, + { + "role": "fallback-0", + "family": "Noto Color Emoji", + "faceIndex": 0, + "sha256": "72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b" + }, + { + "role": "fallback-1", + "family": "STIX Two Math", + "faceIndex": 0, + "sha256": "3a5f3f26f40d5698b3c62dd085d48d6663696a3f80825aab8b553d5097518e8c" + }, + { + "role": "fallback-2", + "family": "Noto Sans Symbols 2", + "faceIndex": 0, + "sha256": "7d5fb73b7ca67a6798101741f5d280a3d016a56a197afcd4199dbb57b4b82a21" + }, + { + "role": "fallback-3", + "family": "Noto Emoji", + "faceIndex": 0, + "sha256": "cd07743a4c93d8da929afdd28c1d368f11da478a1093146a13610081c2a58440" + } + ], + "cases": { + "ascii-ligatures": { + "glyphCount": 23, + "definitionCount": 21, + "sha256": "79a7b72d7024b817c671b92cfe8b0f42bfad984772fe53e8f633d4db86b1e55f" + }, + "bold": { + "glyphCount": 12, + "definitionCount": 11, + "sha256": "da697ccd71d2a16827895c3de43edcd42be6e69afec017b7dc0fc66a3342f8d8" + }, + "bold-italic": { + "glyphCount": 10, + "definitionCount": 8, + "sha256": "812e51ae0b2f312a0c5a86a3ce1432366709f55298cbc7a112dd1531a5137130" + }, + "combining-wide-missing": { + "glyphCount": 2, + "definitionCount": 2, + "sha256": "857968032f9c4499ff9dc2aaf48fc223a48f889cf829113a7504464570d957c4" + }, + "emoji-fallback": { + "glyphCount": 9, + "definitionCount": 9, + "sha256": "e7c0afd6aae4c49e0d58653427346f4088810bd8807b247021e58feabbb1b1d5" + }, + "emoji-presentation": { + "glyphCount": 4, + "definitionCount": 3, + "sha256": "8460b1ccad4691ee6363560ce62fbb1508c29c4e9a408c15213e5eb96108b3be" + }, + "italic": { + "glyphCount": 14, + "definitionCount": 9, + "sha256": "99863813f334d93f19f402a3589d27989325994d5b2dfda38d77f756fdff6f6c" + }, + "symbol-fallback": { + "glyphCount": 5, + "definitionCount": 5, + "sha256": "6f5f73702797bae5335d5cc99e909055debb0f18c3e7dafdbf5654e9d55fdf80" + } + } +} \ No newline at end of file diff --git a/docs/ghostty-1.4-qualification/iphone-final-package-retry-20261030.json b/docs/ghostty-1.4-qualification/iphone-final-package-retry-20261030.json new file mode 100644 index 00000000..0d657eff --- /dev/null +++ b/docs/ghostty-1.4-qualification/iphone-final-package-retry-20261030.json @@ -0,0 +1,133 @@ +{ + "ghostteaTestedSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "vibefieldTestedSourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "pairedCases": 4, + "passedCases": 4, + "passedTests": 8, + "failedTests": 0, + "skippedTests": 0, + "matrix": [ + { + "client": "candidate", + "host": "candidate", + "runnerExitCode": 0, + "elapsedSeconds": 33.6, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-candidate-host-retry-20261030.xcresult", + "selectionTextSupported": true, + "copyChecksEnabled": true, + "copyExercised": true, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "exact Unicode/live-PTY text returned by both production attachments" + }, + { + "client": "candidate", + "host": "baseline", + "runnerExitCode": 0, + "elapsedSeconds": 35.19, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-baseline-host-retry-20261030.xcresult", + "selectionTextSupported": false, + "copyChecksEnabled": true, + "copyExercised": true, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "explicit unsupported result; stream and reconnect passed" + }, + { + "client": "baseline", + "host": "candidate", + "runnerExitCode": 0, + "elapsedSeconds": 35.53, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-candidate-host-retry-20261030.xcresult", + "selectionTextSupported": true, + "copyChecksEnabled": false, + "copyExercised": false, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "older client; optional capability not requested" + }, + { + "client": "baseline", + "host": "baseline", + "runnerExitCode": 0, + "elapsedSeconds": 40.54, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-baseline-host-retry-20261030.xcresult", + "selectionTextSupported": false, + "copyChecksEnabled": false, + "copyExercised": false, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "older client; optional capability not requested" + } + ], + "candidateAppMatchesPreviouslyPassed833TestSuite": true, + "productCodeChangedForRetry": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "previousTailnetJoinFailuresPreserved": true, + "previousJoinFailureCause": "not established; all pairings passed on the user-requested retry with unchanged binaries and test timeouts", + "visualReview": { + "newClientNewHost": "two matching Metal surfaces; CJK and combining text readable; joined emoji at natural artwork width; Unicode 18 missing-glyph box and following X preserve the host grid", + "newClientOldHost": "matching Metal mirrors; legacy host missing-glyph width preserved; following X remains aligned", + "imageEditingApplied": false + }, + "cleanup": { + "baselineFixtureExitCode": 0, + "candidateFixtureExitCode": 0, + "allFourQAAppsUninstalled": true, + "phoneConfigurationDeleted": true + }, + "latencyQualification": "unchanged: explicitly unqualified" +} diff --git a/docs/ghostty-1.4-qualification/iphone-final-package.json b/docs/ghostty-1.4-qualification/iphone-final-package.json new file mode 100644 index 00000000..51a63cb7 --- /dev/null +++ b/docs/ghostty-1.4-qualification/iphone-final-package.json @@ -0,0 +1,138 @@ +{ + "ghostteaTestedSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "vibefieldTestedSourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "production": { + "xcodebuildExitCode": 0, + "cleanupExitCode": 0, + "elapsedSeconds": 63.4, + "resultBundle": "iphone-final-package-production.xcresult", + "log": "iphone-final-package-production.log", + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "nativeIosArchiveSha256": "0aea3d134386cccfd5381e8a7a2bd11c6383182bf008168babdcfa0a2bc76f61", + "testKeyTransferred": false, + "remoteProbeExcludedForSeparateMatrix": true, + "tests": { + "result": "Passed", + "totalTestCount": 833, + "passedTests": 833, + "failedTests": 0, + "skippedTests": 0 + } + }, + "finalRemoteMatrix": [ + { + "client": "candidate", + "host": "candidate", + "runnerExitCode": 65, + "elapsedSeconds": 86.5, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-candidate-host.xcresult", + "selectionTextSupported": true, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": true, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + }, + { + "client": "candidate", + "host": "baseline", + "runnerExitCode": 65, + "elapsedSeconds": 89.29, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-baseline-host.xcresult", + "selectionTextSupported": false, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": true, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + }, + { + "client": "baseline", + "host": "candidate", + "runnerExitCode": 65, + "elapsedSeconds": 85.33, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-candidate-host.xcresult", + "selectionTextSupported": true, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": false, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + }, + { + "client": "baseline", + "host": "baseline", + "runnerExitCode": 65, + "elapsedSeconds": 86.75, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-baseline-host.xcresult", + "selectionTextSupported": false, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": false, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + } + ], + "finalRemoteVerdict": "unqualified: all four phone nodes timed out joining the tailnet before peer discovery or terminal connection", + "earlierRemoteMatricesPassed": true, + "hostFixturesJoinedTailnet": true, + "approvedKeyParsersMatched": true, + "networkAndPermissionPromptClarificationRequested": true, + "environmentalCauseEstablished": false, + "cleanup": { + "remainingIsolatedWorktreeProcesses": [], + "baselineFixtureExitCode": 0, + "candidateFixtureExitCode": 0, + "allFourQAAppsUninstalled": true, + "temporaryPhoneConfigRetained": false + } +} diff --git a/docs/ghostty-1.4-qualification/metal-golden-font-control.json b/docs/ghostty-1.4-qualification/metal-golden-font-control.json new file mode 100644 index 00000000..726b957d --- /dev/null +++ b/docs/ghostty-1.4-qualification/metal-golden-font-control.json @@ -0,0 +1,13 @@ +{ + "onlyChangedField": "referencePixelHash", + "changes": { + "referencePixelHash": { + "before": "6eda391be8a068fc", + "after": "22286852f3cdfa6b" + } + }, + "oldFontsWithNewRendererRestoreExactOldGolden": true, + "candidateFontsRestoredAfterControl": true, + "layoutEdgesMeanChannelsAndNonBackgroundCount": "unchanged", + "sourceOfDifference": "reviewed upstream Noto emoji font pixel changes" +} diff --git a/docs/ghostty-1.4-qualification/obsolete-dependency-cache-cleanup-20261030.json b/docs/ghostty-1.4-qualification/obsolete-dependency-cache-cleanup-20261030.json new file mode 100644 index 00000000..ecabb98a --- /dev/null +++ b/docs/ghostty-1.4-qualification/obsolete-dependency-cache-cleanup-20261030.json @@ -0,0 +1,7 @@ +{ + "removed": "/private/tmp/vibefield-libghostty-migration-2026-10-29/dependency-drift-node-modules-20261030", + "kind": "obsolete generated node_modules moved aside during frozen dependency repair", + "sourceFilesRemoved": false, + "diagnosticLocksAndLogsRetained": true, + "internalFreeGiBAfter": 66.23 +} diff --git a/docs/ghostty-1.4-qualification/product-qualification.json b/docs/ghostty-1.4-qualification/product-qualification.json new file mode 100644 index 00000000..dba2b946 --- /dev/null +++ b/docs/ghostty-1.4-qualification/product-qualification.json @@ -0,0 +1,1148 @@ +{ + "status": "qualification recorded with open gates; not promoted", + "ghostteaBaseline": "3eee581c14b05850c90bea09536d4bc6b856223b", + "vibefieldBaseline": "e259f1fe31d9b3a01dca70dc33a5dad99841f6f3", + "physicalDevice": { + "model": "iPhone 14 Pro", + "os": "iOS 26.6", + "productionTests": { + "passed": 833, + "failed": 0, + "skipped": 0 + }, + "baselineProductionTests": { + "passed": 826, + "failed": 0 + }, + "finalRebuiltArtifactProbe": { + "status": "passed", + "productionTestsPassed": 833, + "mixedPairingsPassed": 4, + "mixedTestsPassed": 8, + "failed": 0, + "skipped": 0, + "exactSameFinalPackage": true, + "evidence": "iphone-final-package-retry-20261030.json" + } + }, + "baselineAdjustments": [ + "Freeway accepts one trailing slash inserted by the pinned Truffle proxy, preserving secret and identity validation.", + "The Swift test helper awaits both leg acknowledgments before reopening; no product resume behavior changed." + ], + "mixedMatrixBeforeCopy": { + "combinations": 4, + "passed": 4, + "testsPerCombination": 2, + "checks": [ + "native font golden", + "real PTY input", + "two independent Truffle nodes and Metal views", + "accepted and rendered frame advances", + "resize/seat handoff", + "park/resume", + "fresh-link redial" + ] + }, + "newClientOldHost": { + "passed": 2, + "failed": 0, + "selectionTextSupported": false, + "copyVerdict": "explicit unsupported result; stream, mirrors and reconnect stay usable", + "runnerUninstalledQAApp": true + }, + "copy": { + "nativeTests": 498, + "nativeIgnored": 15, + "contractTests": 1214, + "fieldAppTests": 2807, + "fieldAppSkipped": 0, + "swiftNewSelectionTests": 7, + "ghostteaReactTests": 445, + "ghostteaProtocolTests": 27, + "controls": [ + "optional selection-text capability", + "authenticated read grant", + "session/activation/lease correlation", + "no input or geometry authority required", + "bounded queued responses and client pending requests", + "negotiated byte limit with explicit TOO_LARGE", + "timeout/cancellation; no replay", + "newest copy owns clipboard; empty text preserves clipboard" + ] + }, + "godviewCopyLab": { + "passed": true, + "unicodeAndScrollback": true, + "alternateScreen": true, + "hiddenOutput": true, + "finalEmojiVisualRerun": "exact CI archive passed; selection and graphics screenshot reviewed; clean exit", + "exactCIArtifact": true, + "cleanProcessExit": true, + "ptyBefore": 93, + "ptyAfter": 93, + "survivingOwnedProcesses": 0 + }, + "artifactGate": { + "rejectedRun": 40969422970, + "reason": "Duplicate memset in the normalized cross-built macOS library; checksums and provenance alone did not reveal consumer-link warning.", + "correction": "Apply the exact upstream Darwin libSystem normalization script; checksum it in the build recipe and package it with the artifact; test normalized macOS Rust consumers in CI.", + "newCandidateRun": 40972570187, + "status": "all three corrected CI candidates passed consumers, inventories, provenance, SBOM and CRT verification; local macOS check:desktop, 424 Rust/623 JS tests, FFI/ASan and packaged npm/Rust consumers passed" + }, + "limits": [ + "No release or product dependency version promotion.", + "The final package passed 833 production iPhone tests and all four real-Truffle pairings; all 13 desktop landing gates and separate Rust/generated-file stages passed. Full pnpm verify remains red at one unchanged 1999 ms versus 2000 ms agent-cleanup assertion; all 12 selected baseline/candidate control executions passed without changing assertions or timeouts.", + "Latency is explicitly unqualified by user choice.", + "Physical Windows GPU/product UI was not tested; native Windows CI consumers passed.", + "The physical-phone probe exercises the production selection API; a tap on the Swift home copy menu was not separately exercised. Real Wi-Fi/cellular handoff and long suspension are not claimed.", + "Pre-existing SSH production-readiness blockers remain separately recorded and unwaived." + ], + "metalRenderer": { + "passed": 80, + "failed": 0, + "reviewedGolden": "only emoji-font pixel hash changed; old fonts restore exact old golden", + "exactFinalAppleArtifact": true, + "finalRunPassed": 80, + "finalRunFailed": 0, + "log": "swift-metal-exact-final-artifact.log" + }, + "newClientNewHost": { + "passed": 2, + "failed": 0, + "runnerUninstalledQAApp": true, + "selectionTextSupported": true, + "copyExercised": true + }, + "oldClientNewHost": { + "passed": 2, + "failed": 0, + "runnerUninstalledQAApp": true, + "selectionTextSupported": true, + "copyExercised": false + }, + "performanceQualification": { + "status": "unqualified", + "reason": "shared host load; user explicitly chose to continue without a quiet comparison" + }, + "workspaceGate": { + "originalVerify": "static/type/lint/clippy/bundle stages passed; five JavaScript failures retained", + "fielddRecheck": { + "passed": 81, + "skipped": 7, + "environment": "NO_COLOR unset to remove conflict with FORCE_COLOR" + }, + "documentControls": { + "baselinePassed": 2, + "candidatePassed": 2, + "originalTimeoutSeconds": 5 + }, + "documentFullFunctionalRecheck": { + "passed": 19, + "outerTestTimeoutSeconds": 30, + "assertionsChanged": false + }, + "finalExactArtifactRust": { + "passed": 498, + "ignoredIncludingDocTest": 15, + "failed": 0 + }, + "generatedContracts": "passed", + "oneShotVerifyGreen": false, + "finalSerialVerify": { + "exitCode": 1, + "projects": 27, + "failedProjects": [ + "@vibefield/screenshot", + "@vibefield/electron-shell" + ], + "fiveSecondHelperTimeouts": 6, + "fielddPassed": 2608, + "fielddSkipped": 11, + "fieldAppPassed": 2807, + "fieldAppSkipped": 0, + "originalTimeoutsRetained": true, + "staticAndBundleStagesPassed": true + }, + "finalTimeoutControls": { + "baselinePassed": 41, + "candidatePassed": 41, + "failed": 0, + "testSourceAndHelpersIdenticalToBaseline": true, + "testTimeoutChanged": false, + "assertionsChanged": false, + "oneTestFileAtATime": true + }, + "dependencyDriftAttempt": { + "status": "interrupted; invalid as a pinned comparison", + "loroLocked": "1.13.8", + "loroDrifted": "1.17.1", + "reactLocked": "19.2.7", + "reactDrifted": "19.3.1", + "cause": "pnpm automatic overlay install before command execution", + "restore": "clean frozen offline install plus candidate-only source links", + "actualDependenciesMatchBaseline": true, + "productionPinsChanged": false, + "buildOnlyOfflineFlagRemovedFromRuntime": true, + "evidence": "frozen-actual-dependency-graph-20261030.json", + "focusedRecheck": { + "passed": 56, + "failed": 0, + "skipped": 1, + "skipReason": "existing Windows-only case", + "assertionsChanged": false, + "timeoutsChanged": false + } + }, + "finalFrozenVerify": { + "sourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ghostteaSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "command": [ + "pnpm", + "verify" + ], + "exitCode": 1, + "elapsedSeconds": 971.26, + "log": "vibefield-final-verify-frozen-20261030.log", + "projectsSerial": true, + "maxVitestWorkers": 2, + "frozenDependencyGraphVerifiedBeforeAndAfter": true, + "automaticDependencyReinstallDisabled": true, + "buildOnlyOfflineFlagAbsent": true, + "timeoutsChanged": false, + "assertionsChanged": false, + "projects": [ + { + "project": "@vibefield/contracts", + "summary": "Tests 1214 passed (1214)", + "passed": 1214, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/trust", + "summary": "Tests 3 passed (3)", + "passed": 3, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-build", + "summary": "Tests 77 passed (77)", + "passed": 77, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-sdk", + "summary": "Tests 80 passed (80)", + "passed": 80, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/logging", + "summary": "Tests 54 passed | 1 skipped (55)", + "passed": 54, + "failed": 0, + "skipped": 1, + "todo": 0 + }, + { + "project": "@vibefield/fieldd", + "summary": "Tests 1 failed | 2607 passed | 11 skipped (2619)", + "passed": 2607, + "failed": 1, + "skipped": 11, + "todo": 0 + }, + { + "project": "@vibefield/fieldd-client", + "summary": "Tests 47 passed (47)", + "passed": 47, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/users", + "summary": "Tests 40 passed | 1 skipped (41)", + "passed": 40, + "failed": 0, + "skipped": 1, + "todo": 0 + }, + { + "project": "@vibefield/plugin-example-kv", + "summary": "Tests 5 passed (5)", + "passed": 5, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-runtime", + "summary": "Tests 55 passed (55)", + "passed": 55, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/audit", + "summary": "Tests 6 passed | 1 skipped (7)", + "passed": 6, + "failed": 0, + "skipped": 1, + "todo": 0 + }, + { + "project": "@vibefield/test-env", + "summary": "Tests 5 passed (5)", + "passed": 5, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-browser-pane", + "summary": "Tests 151 passed (151)", + "passed": 151, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-example-desk-clock", + "summary": "Tests 36 passed (36)", + "passed": 36, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-comment-pad", + "summary": "Tests 37 passed (37)", + "passed": 37, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/live-surfaces", + "summary": "Tests 38 passed (38)", + "passed": 38, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-browser", + "summary": "Tests 9 passed (9)", + "passed": 9, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-behavior-conformance", + "summary": "Tests 4 passed (4)", + "passed": 4, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/field-app", + "summary": "Tests 2807 passed (2807)", + "passed": 2807, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/fieldd-supervisor", + "summary": "Tests 74 passed (74)", + "passed": 74, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/screenshot", + "summary": "Tests 38 passed (38)", + "passed": 38, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/terminal-perf", + "summary": "Tests 92 passed (92)", + "passed": 92, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/electron-shell", + "summary": "Tests 1983 passed | 3 skipped (1986)", + "passed": 1983, + "failed": 0, + "skipped": 3, + "todo": 0 + }, + { + "project": "@vibefield/plugin-playground", + "summary": "Tests 68 passed (68)", + "passed": 68, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/dev-runner", + "passed": 257, + "failed": 0, + "skipped": 0 + }, + { + "project": "@vibefield/plugin-cli", + "summary": "Tests 115 passed (115)", + "passed": 115, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/create-plugin", + "summary": "Tests 80 passed (80)", + "passed": 80, + "failed": 0, + "skipped": 0, + "todo": 0 + } + ], + "failedProjects": [ + "@vibefield/fieldd" + ], + "singleFailure": { + "test": "agent-session.test.ts item 87 cleanup-pending warning", + "expectedPendingMsAtLeast": 2000, + "observedPendingMs": 1999, + "behaviorChecksBeforeTimingAssertionPassed": true + }, + "remainingChainStagesNotRun": [ + "test:rust", + "gen:check" + ], + "testTotals": { + "passed": 9982, + "failed": 1, + "skipped": 17 + } + }, + "latestCompleteInvocation": "finalFrozenVerify; earlier runs remain historical evidence", + "agentTimingControls": { + "pairedRepetitions": 3, + "baselineSelectedPassed": 6, + "candidateSelectedPassed": 6, + "failed": 0, + "excludedPerInvocation": 54, + "testSourceAndImplementationIdenticalToBaseline": true, + "assertionsChanged": false, + "timeoutsChanged": false, + "setupChange": "reuse previously qualified retained native binaries instead of rebuilding in global setup", + "fullVerifyExitCodeUnchanged": 1, + "evidence": "agent-timing-controls-20261030.json" + }, + "separateFinalTailStages": { + "command": [ + "node", + "scripts/gate-landing.mjs", + "--only", + "test:rust,gen:check", + "--logs", + "/private/tmp/vibefield-libghostty-migration-2026-10-29/evidence/final-tail-stages-frozen-20261030" + ], + "exitCode": 0, + "elapsedSeconds": 313.14, + "gates": [ + "GATE test:rust ok exit=0 308s load 20/21/17\u219222/22/19", + "GATE gen:check ok exit=0 5s load 22/22/19\u219221/22/19" + ], + "final": { + "sha": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ok": true, + "red": [], + "at": "2026-10-30T18:26:16.596Z" + }, + "frozenDependencyGraphVerifiedBeforeAndAfter": true, + "automaticDependencyReinstallDisabled": true, + "buildOnlyOfflineFlagAbsent": true, + "originalVerifyExitCodeUnchanged": 1, + "log": "final-tail-stages-frozen-20261030.log", + "rustTestTotals": { + "passed": 498, + "failed": 0, + "ignoredIncludingDocTests": 15 + }, + "generatedFilesCurrent": true + } + }, + "cargoOutputCorrection": { + "cause": "Cargo shared target config overrides the checkout target symlink; old scripts read a different output directory", + "appleComposer": "resolve metadata target_directory and use it for producer and consumer", + "packageConsumer": "fresh fixture-owned archive directory; never reuse stale target/package archives", + "regressionTestsPassed": 4, + "packageConsumerPassed": true + }, + "appleNativePackage": { + "verified": true, + "tag": "ghosttea-apple-native-1611ad6b0820", + "bytes": 50692910, + "sha256": "de397a043efee2cb3f8d0e2cecf320cb345af475c4ea9b6d70b50f13f789dc5d", + "nativeSourceDigest": "56dc58d73c6d319af6759ef6f1e465e011ae58b32ec5e7b50295a1c2d4f49ac8", + "published": false, + "slices": [ + "macos-arm64", + "ios-arm64", + "ios-arm64-simulator" + ], + "allPublicSwiftProductsLinked": 18, + "cleanIPhoneBuildPassed": true, + "stagedIPhoneArchiveMatchesPackage": true, + "baselineAndCandidateSignaturesVerified": true, + "finalIPhoneProductionTestsPassed": 833, + "finalIPhoneProductionTestsFailed": 0, + "finalIPhoneProductionTestsSkipped": 0 + }, + "ghostteaNativeBuildRevision": "a71e171409276412b1ff8f4546a3b6fe5a4bec4b", + "sourceIdentityNote": "The containing source revision also includes the reviewed Swift font golden and qualification records; these do not change the native source digest.", + "desktopFinalSmokes": { + "initialThreeGateExitCode": 1, + "godview": { + "exitCode": 0, + "seconds": 157, + "finalSurvivingProcesses": 0 + }, + "terminalDirect": { + "exitCode": 0, + "seconds": 48, + "finalSurvivingProcesses": 0 + }, + "terminalDoor": { + "exitCode": 3, + "seconds": 45, + "connectionAndOriginChecksPassed": true, + "failure": "30-second teardown watchdog", + "finalSurvivingProcesses": 0 + }, + "terminalDoorControls": [ + { + "label": "baseline", + "exitCode": 0, + "durationSeconds": 45.71, + "nativeSha256": "79d9f93b73bfb27c09ebff104fb0bef8392499f8fc66b53403e63d0cb9f2148f", + "connectionAndOriginChecksPassed": true, + "watchdogFired": false, + "timeoutChanged": false, + "assertionsChanged": false, + "drainWaitMs": 20129, + "forcedCleanupCount": 1, + "finalSurvivingProcesses": 0, + "finalReapedProcesses": 0, + "log": "final-baseline-terminal-door-recheck.log" + }, + { + "label": "candidate", + "exitCode": 0, + "durationSeconds": 49.8, + "nativeSha256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "connectionAndOriginChecksPassed": true, + "watchdogFired": false, + "timeoutChanged": false, + "assertionsChanged": false, + "drainWaitMs": 20412, + "forcedCleanupCount": 1, + "finalSurvivingProcesses": 0, + "finalReapedProcesses": 0, + "log": "final-candidate-terminal-door-recheck.log" + } + ], + "sourceFilesIdentical": true, + "rootCause": "not established; initial intermittent shutdown failure retained", + "fullLandingGateRun": false, + "finalHostLinkIdentity": "final-desktop-host-link-identity.json" + }, + "candidateConsistencyChecks": "passed: upgrade source, bundled fonts, font parity, iOS BOM/resources, Swift manifests, App Store and beta metadata; initial sandbox font-parity attempt is retained separately", + "finalPhysicalDeviceQualification": { + "ghostteaTestedSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "vibefieldTestedSourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "pairedCases": 4, + "passedCases": 4, + "passedTests": 8, + "failedTests": 0, + "skippedTests": 0, + "matrix": [ + { + "client": "candidate", + "host": "candidate", + "runnerExitCode": 0, + "elapsedSeconds": 33.6, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-candidate-host-retry-20261030.xcresult", + "selectionTextSupported": true, + "copyChecksEnabled": true, + "copyExercised": true, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "exact Unicode/live-PTY text returned by both production attachments" + }, + { + "client": "candidate", + "host": "baseline", + "runnerExitCode": 0, + "elapsedSeconds": 35.19, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-baseline-host-retry-20261030.xcresult", + "selectionTextSupported": false, + "copyChecksEnabled": true, + "copyExercised": true, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "explicit unsupported result; stream and reconnect passed" + }, + { + "client": "baseline", + "host": "candidate", + "runnerExitCode": 0, + "elapsedSeconds": 35.53, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-candidate-host-retry-20261030.xcresult", + "selectionTextSupported": true, + "copyChecksEnabled": false, + "copyExercised": false, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "older client; optional capability not requested" + }, + { + "client": "baseline", + "host": "baseline", + "runnerExitCode": 0, + "elapsedSeconds": 40.54, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-baseline-host-retry-20261030.xcresult", + "selectionTextSupported": false, + "copyChecksEnabled": false, + "copyExercised": false, + "mirroringExercised": true, + "remoteProbeCompleted": true, + "summaryExitCode": 0, + "tests": { + "result": "Passed", + "totalTestCount": 2, + "passedTests": 2, + "failedTests": 0, + "skippedTests": 0 + }, + "qaAppUninstalled": true, + "nativeFontGoldenPassed": true, + "copyVerdict": "older client; optional capability not requested" + } + ], + "candidateAppMatchesPreviouslyPassed833TestSuite": true, + "productCodeChangedForRetry": false, + "timeoutsChanged": false, + "assertionsChanged": false, + "previousTailnetJoinFailuresPreserved": true, + "previousJoinFailureCause": "not established; all pairings passed on the user-requested retry with unchanged binaries and test timeouts", + "visualReview": { + "newClientNewHost": "two matching Metal surfaces; CJK and combining text readable; joined emoji at natural artwork width; Unicode 18 missing-glyph box and following X preserve the host grid", + "newClientOldHost": "matching Metal mirrors; legacy host missing-glyph width preserved; following X remains aligned", + "imageEditingApplied": false + }, + "cleanup": { + "baselineFixtureExitCode": 0, + "candidateFixtureExitCode": 0, + "allFourQAAppsUninstalled": true, + "phoneConfigurationDeleted": true + }, + "latencyQualification": "unchanged: explicitly unqualified", + "production": { + "xcodebuildExitCode": 0, + "cleanupExitCode": 0, + "elapsedSeconds": 63.4, + "resultBundle": "iphone-final-package-production.xcresult", + "log": "iphone-final-package-production.log", + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "nativeIosArchiveSha256": "0aea3d134386cccfd5381e8a7a2bd11c6383182bf008168babdcfa0a2bc76f61", + "testKeyTransferred": false, + "remoteProbeExcludedForSeparateMatrix": true, + "tests": { + "result": "Passed", + "totalTestCount": 833, + "passedTests": 833, + "failedTests": 0, + "skippedTests": 0 + } + }, + "finalRemoteVerdict": "passed: all four final-package old/new client-host pairings; eight tests, no failures or skips", + "priorFailedAttemptEvidence": "iphone-final-package.json" + }, + "previousFinalPhysicalDeviceQualification": { + "ghostteaTestedSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "vibefieldTestedSourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "production": { + "xcodebuildExitCode": 0, + "cleanupExitCode": 0, + "elapsedSeconds": 63.4, + "resultBundle": "iphone-final-package-production.xcresult", + "log": "iphone-final-package-production.log", + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "nativeIosArchiveSha256": "0aea3d134386cccfd5381e8a7a2bd11c6383182bf008168babdcfa0a2bc76f61", + "testKeyTransferred": false, + "remoteProbeExcludedForSeparateMatrix": true, + "tests": { + "result": "Passed", + "totalTestCount": 833, + "passedTests": 833, + "failedTests": 0, + "skippedTests": 0 + } + }, + "finalRemoteMatrix": [ + { + "client": "candidate", + "host": "candidate", + "runnerExitCode": 65, + "elapsedSeconds": 86.5, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-candidate-host.xcresult", + "selectionTextSupported": true, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": true, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + }, + { + "client": "candidate", + "host": "baseline", + "runnerExitCode": 65, + "elapsedSeconds": 89.29, + "appSha256": "7498599981210cc3e0a1954c0f667cf1997c4ebd20dc6ab2b1ae69958000cb3d", + "debugLibrarySha256": "480f1bec1861a0c7bb46014a85bbc7a41052f6ac1a0d90d9e272a3118e599a5e", + "xctestrunSha256": "3838d7ce226d2792ae21157dfc0441e14d89342a39c9597ae7c45a3c7a62bec9", + "resultBundle": "iphone-final-candidate-client-baseline-host.xcresult", + "selectionTextSupported": false, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": true, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + }, + { + "client": "baseline", + "host": "candidate", + "runnerExitCode": 65, + "elapsedSeconds": 85.33, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-candidate-host.xcresult", + "selectionTextSupported": true, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": false, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + }, + { + "client": "baseline", + "host": "baseline", + "runnerExitCode": 65, + "elapsedSeconds": 86.75, + "appSha256": "3414ce7fc235e197b0201ff3c84a9ac440fa32fb5f85ef0ee18ce4c4d04e3217", + "debugLibrarySha256": "ec4187ab43f03ebc460ca5fceb14511985a9e6958a7f656742ae0d5730d74649", + "xctestrunSha256": "1107814d5afcb766ac88f223e1b774893c7b614a4634f1fea318f8be0642afcf", + "resultBundle": "iphone-final-baseline-client-baseline-host.xcresult", + "selectionTextSupported": false, + "copyExercised": false, + "summaryExitCode": 0, + "tests": { + "result": "Failed", + "totalTestCount": 2, + "passedTests": 1, + "failedTests": 1, + "skippedTests": 0 + }, + "copyChecksEnabled": false, + "mirroringExercised": false, + "remoteProbeCompleted": false, + "failureStage": "iPhone Truffle waitUntilRunning, before peer discovery or terminal connection", + "qaAppUninstalled": true + } + ], + "finalRemoteVerdict": "unqualified: all four phone nodes timed out joining the tailnet before peer discovery or terminal connection", + "earlierRemoteMatricesPassed": true, + "hostFixturesJoinedTailnet": true, + "approvedKeyParsersMatched": true, + "networkAndPermissionPromptClarificationRequested": true, + "environmentalCauseEstablished": false, + "cleanup": { + "remainingIsolatedWorktreeProcesses": [], + "baselineFixtureExitCode": 0, + "candidateFixtureExitCode": 0, + "allFourQAAppsUninstalled": true, + "temporaryPhoneConfigRetained": false + } + }, + "desktopFullLandingGate": { + "sourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ghostteaSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "command": [ + "node", + "scripts/gate-landing.mjs", + "--logs", + "/private/tmp/vibefield-libghostty-migration-2026-10-29/evidence/landing-frozen-20261030" + ], + "exitCode": 0, + "elapsedSeconds": 1742.53, + "log": "vibefield-final-landing-frozen-20261030.log", + "projectsSerial": true, + "maxVitestWorkers": 2, + "frozenDependencyGraphVerifiedBeforeAndAfter": true, + "automaticDependencyReinstallDisabled": true, + "buildOnlyOfflineFlagAbsent": true, + "timeoutsChanged": false, + "assertionsChanged": false, + "gates": [ + { + "gate": "still", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 40, + "log": "01-still.log", + "processCensuses": [] + }, + { + "gate": "smoke", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 41, + "log": "02-smoke.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-t9SHaL", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:canvas", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 933, + "log": "03-smoke_canvas.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-hyz0zz", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:canvas:no-gpu", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 32, + "log": "04-smoke_canvas_no-gpu.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-ZyWDcm", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:canvas:no-webgpu", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 29, + "log": "05-smoke_canvas_no-webgpu.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-iJeAKW", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:kind-faults", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 89, + "log": "06-smoke_kind-faults.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-ja6W40", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:plugin-restart", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 82, + "log": "07-smoke_plugin-restart.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-ZIfeTv", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:plugin", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 64, + "log": "08-smoke_plugin.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-tTk02c", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ], + "visualReview": "Clock and complete board reviewed in both themes; clock geometry intact; dark styling is markedly dimmer. No theme changes made.", + "supplementalEvidence": "landing-frozen-20261030/plugin-author" + }, + { + "gate": "smoke:godview", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 158, + "log": "09-smoke_godview.log", + "processCensuses": [ + { + "mark": "vf-godview-profile-Vpuph7", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:terminal-door", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 41, + "log": "10-smoke_terminal-door.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-KV3WYH", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:terminal-direct", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 44, + "log": "11-smoke_terminal-direct.log", + "processCensuses": [ + { + "mark": "vf-godview-profile-iGhlGr", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:studio", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 77, + "log": "12-smoke_studio.log", + "processCensuses": [ + { + "mark": "vf-studio-nVHLKH", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:bundled", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 112, + "log": "13-smoke_bundled.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-hOsSEr", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + }, + { + "mark": "vf-smoke-profile-vzsrxn", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + }, + { + "mark": "vf-smoke-profile-sely6x", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + } + ], + "gatesPassed": 13, + "gatesFailed": 0 + }, + "finalDesktopIntegrity": { + "revisions": { + "ghosttea-0.11.2": { + "head": "f535d6f679affe274aa49997f63125c5ce9c5213", + "status": [] + }, + "vibe-field": { + "head": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "status": [ + " M apps/ios/VibeFieldKit/Package.swift" + ] + } + }, + "nativeHelpers": [ + { + "binary": "field-native", + "sha256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "matchesFinalDeviceAndDesktopTestedBinary": true + }, + { + "binary": "field-terminal-host", + "sha256": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1", + "matchesFinalDeviceAndDesktopTestedBinary": true + }, + { + "binary": "field-freeway-host", + "sha256": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6", + "matchesFinalDeviceAndDesktopTestedBinary": true + } + ], + "correctedCIArchiveSha256": "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd", + "ownedPathProcessCensus": [], + "smokeProcessCensuses": 14, + "nonemptySmokeProcessCensuses": 0, + "productionPinsPromoted": false, + "temporarySwiftOverlayKeptLocal": true, + "runtimeOutputRestorationEvidence": "restore-qualified-runtime-binaries-20261030.json", + "fullVerifyExitCodeUnchanged": 1 + }, + "finalDesktopArchive": { + "archive": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/qualification-evidence/desktop-final-20261030/desktop-results.tar.gz", + "sha256": "c8f770c7480e130578243bf4260f834eebd65a3b28344bbf508b4a46caaa6d7c", + "bytes": 7691046, + "files": 69, + "everyFileVerifiedAgainstOriginal": true, + "approvedTestKeyAbsent": true, + "manifest": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/qualification-evidence/desktop-final-20261030/manifest.json" + } +} diff --git a/docs/ghostty-1.4-qualification/restore-qualified-runtime-binaries-20261030.json b/docs/ghostty-1.4-qualification/restore-qualified-runtime-binaries-20261030.json new file mode 100644 index 00000000..c6f560dc --- /dev/null +++ b/docs/ghostty-1.4-qualification/restore-qualified-runtime-binaries-20261030.json @@ -0,0 +1,38 @@ +{ + "cause": "cargo test feature unification enables tokio/test-util; ordinary cargo build restores the runtime feature graph", + "testTokioFingerprint": "tokio-0d49f801af0a13dc", + "runtimeTokioFingerprint": "tokio-fa843326b191abb7", + "tokioOnlyFeatureDifference": "test-util", + "restoredWith": [ + "cargo", + "build", + "-p", + "field-native", + "--bins" + ], + "exitCode": 0, + "elapsedSeconds": 177.16, + "helpers": [ + { + "binary": "field-native", + "testFeatureBuildSha256": "40b65d39607c0783edf8140163b711e98b9458a898856406b0d0552a732e5ccc", + "qualifiedRuntimeSha256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb", + "restoredSha256": "f54ed5d54ebad4740ccdf9f9162351d0a46f1c6eaa311bd0be790c5fd28164fb" + }, + { + "binary": "field-terminal-host", + "testFeatureBuildSha256": "f01ab26bd9a5be3308b13e6469194840ad9df2f5211798a8daf76707e770b738", + "qualifiedRuntimeSha256": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1", + "restoredSha256": "3fde56bdc74abbb55dfaf185edee294ab7f66d8f1cc6f9773ad267b10b9b39d1" + }, + { + "binary": "field-freeway-host", + "testFeatureBuildSha256": "a474ad48706f4e3cacf89425968cd5defc8e13e9bfee1c09f6893cf89bc8d323", + "qualifiedRuntimeSha256": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6", + "restoredSha256": "a87677fc0db770faf878f1df0bbc7deed153db002f80fe5f593a982f73606fa6" + } + ], + "allThreeRestoredToExactQualifiedHashes": true, + "log": "restore-qualified-runtime-binaries-20261030.log", + "productCodeChanged": false +} diff --git a/docs/ghostty-1.4-qualification/rust-counts.json b/docs/ghostty-1.4-qualification/rust-counts.json new file mode 100644 index 00000000..9572a0ce --- /dev/null +++ b/docs/ghostty-1.4-qualification/rust-counts.json @@ -0,0 +1,26 @@ +{ + "ghosttea-baseline-rust": { + "passed": 414, + "failed": 0, + "ignored": 3, + "testBinaries": 24 + }, + "ghosttea-candidate-rust": { + "passed": 422, + "failed": 0, + "ignored": 3, + "testBinaries": 25 + }, + "vibefield-baseline-rust": { + "passed": 493, + "failed": 0, + "ignored": 14, + "testBinaries": 31 + }, + "vibefield-candidate-rust": { + "passed": 493, + "failed": 0, + "ignored": 14, + "testBinaries": 31 + } +} diff --git a/docs/ghostty-1.4-qualification/storage-relocation.json b/docs/ghostty-1.4-qualification/storage-relocation.json new file mode 100644 index 00000000..2ce77c32 --- /dev/null +++ b/docs/ghostty-1.4-qualification/storage-relocation.json @@ -0,0 +1,42 @@ +{ + "completedAt": "2026-10-30T00:13:21Z", + "allocatedGiBRelocated": 20.98, + "destination": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29", + "validation": "Each directory copied with modes, timestamps and hardlinks; all file contents checksum-compared before originals were removed; all original paths retained as symlinks.", + "sourceAndEvidence": "preserved internally", + "directories": [ + { + "source": "/private/tmp/vibefield-libghostty-migration-2026-10-29/target-candidate", + "destination": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/target-candidate", + "allocatedSourceBytes": 7341580288, + "status": "relocated-and-linked" + }, + { + "source": "/private/tmp/vibefield-libghostty-migration-2026-10-29/target-vibefield-candidate", + "destination": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/target-vibefield-candidate", + "allocatedSourceBytes": 9519702016, + "status": "relocated-and-linked" + }, + { + "source": "/private/tmp/vibefield-libghostty-migration-2026-10-29/ghosttea-0.11.2/native/build", + "destination": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/ghosttea-native-build", + "allocatedSourceBytes": 3014553600, + "status": "relocated-and-linked" + }, + { + "source": "/private/tmp/vibefield-libghostty-migration-2026-10-29/derived-device-candidate", + "destination": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/derived-device-candidate", + "allocatedSourceBytes": 1365946368, + "status": "relocated-and-linked" + }, + { + "source": "/private/tmp/vibefield-libghostty-migration-2026-10-29/derived-device-baseline", + "destination": "/Volumes/SamsungRed/codex-build-cache/vibefield-libghostty-migration-2026-10-29/derived-device-baseline", + "allocatedSourceBytes": 1280655360, + "status": "relocated-and-linked" + } + ], + "internalFreeAtCompletionGiB": 64.35, + "ssdFreeAtCompletionGiB": 47.55, + "note": "Free-space totals also vary with other work on this shared machine; they are not an attribution of all reclaimed space to this migration." +} diff --git a/docs/ghostty-1.4-qualification/swift-baseline-summary.json b/docs/ghostty-1.4-qualification/swift-baseline-summary.json new file mode 100644 index 00000000..fa66a80d --- /dev/null +++ b/docs/ghostty-1.4-qualification/swift-baseline-summary.json @@ -0,0 +1,9 @@ +{ + "title": "Test - VibeFieldKit-Package", + "result": "Passed", + "passedTests": 1306, + "failedTests": 0, + "skippedTests": 2, + "totalTestCount": 1308, + "testFailures": [] +} diff --git a/docs/ghostty-1.4-qualification/swift-candidate-summary.json b/docs/ghostty-1.4-qualification/swift-candidate-summary.json new file mode 100644 index 00000000..fa66a80d --- /dev/null +++ b/docs/ghostty-1.4-qualification/swift-candidate-summary.json @@ -0,0 +1,9 @@ +{ + "title": "Test - VibeFieldKit-Package", + "result": "Passed", + "passedTests": 1306, + "failedTests": 0, + "skippedTests": 2, + "totalTestCount": 1308, + "testFailures": [] +} diff --git a/docs/ghostty-1.4-qualification/swift-device-baseline-synchronized-summary.json b/docs/ghostty-1.4-qualification/swift-device-baseline-synchronized-summary.json new file mode 100644 index 00000000..64a7137e --- /dev/null +++ b/docs/ghostty-1.4-qualification/swift-device-baseline-synchronized-summary.json @@ -0,0 +1,9 @@ +{ + "title": "Test - VibeField", + "result": "Passed", + "passedTests": 826, + "failedTests": 0, + "skippedTests": 0, + "totalTestCount": 826, + "testFailures": [] +} diff --git a/docs/ghostty-1.4-qualification/swift-device-candidate-summary.json b/docs/ghostty-1.4-qualification/swift-device-candidate-summary.json new file mode 100644 index 00000000..7da43025 --- /dev/null +++ b/docs/ghostty-1.4-qualification/swift-device-candidate-summary.json @@ -0,0 +1,8 @@ +{ + "result": "Passed", + "passedTests": 826, + "failedTests": 0, + "skippedTests": 1, + "totalTestCount": 827, + "testFailures": [] +} diff --git a/docs/ghostty-1.4-qualification/swift-device-hosted-baseline-summary.json b/docs/ghostty-1.4-qualification/swift-device-hosted-baseline-summary.json new file mode 100644 index 00000000..06d5c945 --- /dev/null +++ b/docs/ghostty-1.4-qualification/swift-device-hosted-baseline-summary.json @@ -0,0 +1,18 @@ +{ + "title": "Test - VibeField", + "result": "Failed", + "passedTests": 825, + "failedTests": 1, + "skippedTests": 0, + "totalTestCount": 826, + "testFailures": [ + { + "failureText": "Issue recorded: timed out waiting for the demand to come back", + "targetName": "FieldTerminalTests", + "testIdentifier": 41, + "testIdentifierString": "LanesOverTheDoorTests/theCellASessionOpensOrComesBackOnIsSaidFocused()", + "testIdentifierURL": "test://com.apple.xcode/VibeField/FieldTerminalTests/LanesOverTheDoorTests/theCellASessionOpensOrComesBackOnIsSaidFocused()", + "testName": "theCellASessionOpensOrComesBackOnIsSaidFocused()" + } + ] +} diff --git a/docs/ghostty-1.4-qualification/vibefield-bundle-link-recheck-20261030.json b/docs/ghostty-1.4-qualification/vibefield-bundle-link-recheck-20261030.json new file mode 100644 index 00000000..7f45e247 --- /dev/null +++ b/docs/ghostty-1.4-qualification/vibefield-bundle-link-recheck-20261030.json @@ -0,0 +1,6 @@ +{ + "command": "pnpm bundle:assert", + "exitCode": 0, + "elapsedSeconds": 4.03, + "log": "vibefield-bundle-link-recheck-20261030.log" +} diff --git a/docs/ghostty-1.4-qualification/vibefield-final-landing-frozen-20261030.json b/docs/ghostty-1.4-qualification/vibefield-final-landing-frozen-20261030.json new file mode 100644 index 00000000..fd2df0a4 --- /dev/null +++ b/docs/ghostty-1.4-qualification/vibefield-final-landing-frozen-20261030.json @@ -0,0 +1,226 @@ +{ + "sourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ghostteaSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "command": [ + "node", + "scripts/gate-landing.mjs", + "--logs", + "/private/tmp/vibefield-libghostty-migration-2026-10-29/evidence/landing-frozen-20261030" + ], + "exitCode": 0, + "elapsedSeconds": 1742.53, + "log": "vibefield-final-landing-frozen-20261030.log", + "projectsSerial": true, + "maxVitestWorkers": 2, + "frozenDependencyGraphVerifiedBeforeAndAfter": true, + "automaticDependencyReinstallDisabled": true, + "buildOnlyOfflineFlagAbsent": true, + "timeoutsChanged": false, + "assertionsChanged": false, + "gates": [ + { + "gate": "still", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 40, + "log": "01-still.log", + "processCensuses": [] + }, + { + "gate": "smoke", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 41, + "log": "02-smoke.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-t9SHaL", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:canvas", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 933, + "log": "03-smoke_canvas.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-hyz0zz", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:canvas:no-gpu", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 32, + "log": "04-smoke_canvas_no-gpu.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-ZyWDcm", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:canvas:no-webgpu", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 29, + "log": "05-smoke_canvas_no-webgpu.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-iJeAKW", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:kind-faults", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 89, + "log": "06-smoke_kind-faults.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-ja6W40", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:plugin-restart", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 82, + "log": "07-smoke_plugin-restart.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-ZIfeTv", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:plugin", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 64, + "log": "08-smoke_plugin.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-tTk02c", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ], + "visualReview": "Clock and complete board reviewed in both themes; clock geometry intact; dark styling is markedly dimmer. No theme changes made.", + "supplementalEvidence": "landing-frozen-20261030/plugin-author" + }, + { + "gate": "smoke:godview", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 158, + "log": "09-smoke_godview.log", + "processCensuses": [ + { + "mark": "vf-godview-profile-Vpuph7", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:terminal-door", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 41, + "log": "10-smoke_terminal-door.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-KV3WYH", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:terminal-direct", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 44, + "log": "11-smoke_terminal-direct.log", + "processCensuses": [ + { + "mark": "vf-godview-profile-iGhlGr", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:studio", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 77, + "log": "12-smoke_studio.log", + "processCensuses": [ + { + "mark": "vf-studio-nVHLKH", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + }, + { + "gate": "smoke:bundled", + "verdict": "ok", + "exitCode": 0, + "elapsedSeconds": 112, + "log": "13-smoke_bundled.log", + "processCensuses": [ + { + "mark": "vf-smoke-profile-hOsSEr", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + }, + { + "mark": "vf-smoke-profile-vzsrxn", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + }, + { + "mark": "vf-smoke-profile-sely6x", + "left": [], + "reaped": [], + "helpersSettledMs": 0 + } + ] + } + ], + "gatesPassed": 13, + "gatesFailed": 0 +} diff --git a/docs/ghostty-1.4-qualification/vibefield-final-verify-20261030.json b/docs/ghostty-1.4-qualification/vibefield-final-verify-20261030.json new file mode 100644 index 00000000..a8cc008b --- /dev/null +++ b/docs/ghostty-1.4-qualification/vibefield-final-verify-20261030.json @@ -0,0 +1,11 @@ +{ + "command": "pnpm verify", + "exitCode": 1, + "elapsedSeconds": 13.67, + "log": "vibefield-final-verify-20261030.log", + "sourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ghostteaSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "projectsSerial": true, + "timeoutsChanged": false, + "assertionsChanged": false +} diff --git a/docs/ghostty-1.4-qualification/vibefield-final-verify-aligned-20261030.json b/docs/ghostty-1.4-qualification/vibefield-final-verify-aligned-20261030.json new file mode 100644 index 00000000..495de486 --- /dev/null +++ b/docs/ghostty-1.4-qualification/vibefield-final-verify-aligned-20261030.json @@ -0,0 +1,11 @@ +{ + "command": "pnpm verify", + "status": "interrupted after dependency drift was discovered; invalid as a pinned qualification run", + "observedFielddFailures": 5, + "lockedLoroVersion": "1.13.8", + "installedLoroVersion": "1.17.1", + "priorPassedStaticStages": true, + "landingGatesStarted": false, + "timeoutsChanged": false, + "assertionsChanged": false +} diff --git a/docs/ghostty-1.4-qualification/vibefield-final-verify-frozen-20261030.json b/docs/ghostty-1.4-qualification/vibefield-final-verify-frozen-20261030.json new file mode 100644 index 00000000..9caf92ad --- /dev/null +++ b/docs/ghostty-1.4-qualification/vibefield-final-verify-frozen-20261030.json @@ -0,0 +1,252 @@ +{ + "sourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ghostteaSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "command": [ + "pnpm", + "verify" + ], + "exitCode": 1, + "elapsedSeconds": 971.26, + "log": "vibefield-final-verify-frozen-20261030.log", + "projectsSerial": true, + "maxVitestWorkers": 2, + "frozenDependencyGraphVerifiedBeforeAndAfter": true, + "automaticDependencyReinstallDisabled": true, + "buildOnlyOfflineFlagAbsent": true, + "timeoutsChanged": false, + "assertionsChanged": false, + "projects": [ + { + "project": "@vibefield/contracts", + "summary": "Tests 1214 passed (1214)", + "passed": 1214, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/trust", + "summary": "Tests 3 passed (3)", + "passed": 3, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-build", + "summary": "Tests 77 passed (77)", + "passed": 77, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-sdk", + "summary": "Tests 80 passed (80)", + "passed": 80, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/logging", + "summary": "Tests 54 passed | 1 skipped (55)", + "passed": 54, + "failed": 0, + "skipped": 1, + "todo": 0 + }, + { + "project": "@vibefield/fieldd", + "summary": "Tests 1 failed | 2607 passed | 11 skipped (2619)", + "passed": 2607, + "failed": 1, + "skipped": 11, + "todo": 0 + }, + { + "project": "@vibefield/fieldd-client", + "summary": "Tests 47 passed (47)", + "passed": 47, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/users", + "summary": "Tests 40 passed | 1 skipped (41)", + "passed": 40, + "failed": 0, + "skipped": 1, + "todo": 0 + }, + { + "project": "@vibefield/plugin-example-kv", + "summary": "Tests 5 passed (5)", + "passed": 5, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-runtime", + "summary": "Tests 55 passed (55)", + "passed": 55, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/audit", + "summary": "Tests 6 passed | 1 skipped (7)", + "passed": 6, + "failed": 0, + "skipped": 1, + "todo": 0 + }, + { + "project": "@vibefield/test-env", + "summary": "Tests 5 passed (5)", + "passed": 5, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-browser-pane", + "summary": "Tests 151 passed (151)", + "passed": 151, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-example-desk-clock", + "summary": "Tests 36 passed (36)", + "passed": 36, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-comment-pad", + "summary": "Tests 37 passed (37)", + "passed": 37, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/live-surfaces", + "summary": "Tests 38 passed (38)", + "passed": 38, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-browser", + "summary": "Tests 9 passed (9)", + "passed": 9, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/plugin-behavior-conformance", + "summary": "Tests 4 passed (4)", + "passed": 4, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/field-app", + "summary": "Tests 2807 passed (2807)", + "passed": 2807, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/fieldd-supervisor", + "summary": "Tests 74 passed (74)", + "passed": 74, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/screenshot", + "summary": "Tests 38 passed (38)", + "passed": 38, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/terminal-perf", + "summary": "Tests 92 passed (92)", + "passed": 92, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/electron-shell", + "summary": "Tests 1983 passed | 3 skipped (1986)", + "passed": 1983, + "failed": 0, + "skipped": 3, + "todo": 0 + }, + { + "project": "@vibefield/plugin-playground", + "summary": "Tests 68 passed (68)", + "passed": 68, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/dev-runner", + "passed": 257, + "failed": 0, + "skipped": 0 + }, + { + "project": "@vibefield/plugin-cli", + "summary": "Tests 115 passed (115)", + "passed": 115, + "failed": 0, + "skipped": 0, + "todo": 0 + }, + { + "project": "@vibefield/create-plugin", + "summary": "Tests 80 passed (80)", + "passed": 80, + "failed": 0, + "skipped": 0, + "todo": 0 + } + ], + "failedProjects": [ + "@vibefield/fieldd" + ], + "singleFailure": { + "test": "agent-session.test.ts item 87 cleanup-pending warning", + "expectedPendingMsAtLeast": 2000, + "observedPendingMs": 1999, + "behaviorChecksBeforeTimingAssertionPassed": true + }, + "remainingChainStagesNotRun": [ + "test:rust", + "gen:check" + ], + "testTotals": { + "passed": 9982, + "failed": 1, + "skipped": 17 + } +} diff --git a/docs/ghostty-1.4-qualification/vibefield-final-verify-peer-aligned-20261030.json b/docs/ghostty-1.4-qualification/vibefield-final-verify-peer-aligned-20261030.json new file mode 100644 index 00000000..c90d2cd8 --- /dev/null +++ b/docs/ghostty-1.4-qualification/vibefield-final-verify-peer-aligned-20261030.json @@ -0,0 +1,12 @@ +{ + "command": "pnpm verify", + "exitCode": 1, + "elapsedSeconds": 243.78, + "log": "vibefield-final-verify-peer-aligned-20261030.log", + "sourceRevision": "363fdf08c9e4859f52a2671c06aa6327171376d5", + "ghostteaSourceRevision": "f535d6f679affe274aa49997f63125c5ce9c5213", + "projectsSerial": true, + "hostPeerLinksAligned": true, + "timeoutsChanged": false, + "assertionsChanged": false +} diff --git a/docs/ghostty-1.4-regression-history.md b/docs/ghostty-1.4-regression-history.md new file mode 100644 index 00000000..96507034 --- /dev/null +++ b/docs/ghostty-1.4-regression-history.md @@ -0,0 +1,602 @@ +# Ghostty 1.4 migration: regression history, causes, and resolutions + +Original investigation recorded through **2026-10-31, America/Los_Angeles**, on +`codex/libghostty-1.4-vibefield`. The **November 2 publication addendum** below +records the subsequent fixes, device installations and release preparation on +`release/0.14.0`. Sections 1–9 retain their original checkpoint dates. + +The migration exposed several independent problems in VibeField's Godview panes, +Swift terminal, Truffle connection path, and verification-build packaging. Some +were new engine integration problems; several already existed before the port. +All affect acceptance of the migration regardless of where they originated. + +**Product acceptance remains open.** The earlier +[migration qualification report](./ghostty-1.4-migration.md) records valid results +for its exact builds, but subsequent signed-in manual testing found failures that +those checks missed. This document is the later record; earlier “passed” entries +must not be read as acceptance of the final product or the latest fixes. + +At the October 31 documentation checkpoint, the source port and earlier fixes +were committed through `fc0740eb08c9393cbc5aa689286188bf6b1641fd`. Later fixes were +still local changes, and the final block-rendering/resource-bundle build had not +yet been installed. Those statements are historical: the November 2 addendum +records the later installed app and committed fixes. Neither the earlier tests +nor the later UI checks replace qualification of the final published artifacts. + +## 1. Versions and ownership + +| Component or checkpoint | Exact identity | +| --- | --- | +| Released Ghosttea baseline | 0.13.7, `3eee581c14b05850c90bea09536d4bc6b856223b` | +| Previous Ghostty VT pin | `f8041e849b36efbbb9736b6ecf0ccfcb01d94e69` | +| Candidate Ghostty VT | 1.4.0, `83fb95624b0439ee660c2679b43b38c8b63769cd` | +| Initial source/build port | Ghosttea `e92396a` | +| Committed Ghosttea qualification checkpoint | `fc0740eb08c9393cbc5aa689286188bf6b1641fd` | +| Earlier VibeField qualification base | `e259f1fe31d9b3a01dca70dc33a5dad99841f6f3` | +| October 30 manual VibeField base | `ac5dd87d34879c7f02dccc9838ddbd4434d23f25` | +| VibeField migration applied to that worktree without committing | `363fdf08c9e4859f52a2671c06aa6327171376d5` | +| Truffle versions intentionally retained | Desktop Rust/sidecar 0.7.24; Swift 0.7.23 | + +Ghostty provides the VT model. Ghosttea owns shaping, frame production, WebGPU and +Metal rendering, and parts of the routed client. VibeField owns product panes, +focus/geometry policy, Swift attachment, and host integration. Truffle carries +discovery and network traffic. Updating the VT pin does not automatically import +Ghostty's application UI, renderer, fonts, or settings behavior into these layers. + +The manual worktrees use local dependency overlays. Their published manifests +still say Ghosttea 0.13.7, and the directory named `ghosttea-0.11.2` is historical; +neither identifies the linked native engine. Likewise, app version `0.1`/build `1` +does not distinguish these builds. Source revisions, local diffs, native archive +hashes, dependency resolution, and installed binary identity are the evidence. + +## 2. History of the phone's rendering path + +### July–August: desktop cell graphics and a separately shaped phone replica + +The initial Ghosttea repository commit, +[`ab9bac6`](https://github.com/vibecook-dev/ghosttea/commit/ab9bac6bb725444720f04570903d2c936c6020ea) +(July 14), already drew solid Unicode block characters as fractions of terminal +cells in WebGPU. Full blocks, halves, eighths, and quadrants therefore did not +depend on a font's ink reaching the edge of its line box. + +The first Metal renderer, +[`72c84f2`](https://github.com/vibecook-dev/ghosttea/commit/72c84f2dd2b2a633d13128a070d2e0dfc68502ec) +(July 17), drew those characters using their font bitmaps and glyph offsets. It +did not contain the equivalent cell-rectangle path. Its recorded author and +committer are James Yong, with no agent/coauthor trailer. Review of 28 released +Metal revisions found no removal of an earlier phone block-geometry fix. +October's graphics-color classification work (`4342f2a`) did not add that geometry. + +VibeField's August 5 phone attachment (`15dfa4af9` and `72929d81a`) consumed logical +TSP1 rows, built a local `GhostteaRuntime`, and shaped/encoded them on the phone. +It loaded the bundled JetBrains Mono Nerd Font and used device presentation +authority. The default fixture used 13 pt text, 19 pt row pitch, and a 14 pt +baseline. This font path made the same missing geometry treatment less visible +than the later host-font path; it is not proof that every earlier build was seamless. + +### September: why the phone started consuming the host's frames + +VibeField commit +[`b6d4b9f85`](https://github.com/vibefield/vibefield/commit/b6d4b9f858a605af62edf01fc599a85060b8a246), +“TM-5: the phone attaches over the routed client, and the cell's own frame reaches +the glass,” was authored and committed on September 13 in Pacific time. Git records +James Yong as author/committer and **Claude Fable 5.1 as coauthor**. Those are the +recorded credits, not proof of who wrote an individual line. + +That change removed the phone's local logical-row replica and encoding step. +`RoutedTerminalFrameSink` instead passed the cell's TRF1 frames directly to Metal. +The host cell constructed its text engine through `TextEngine::discover`; the +tested Mac resolves that to Menlo. **The Ghosttea pin was 0.10.1 on both sides of +this change.** This was a change to the rendering inputs and ownership, not a +Ghosttea version upgrade. + +The maintained VibeField design record, `draft/thinking-terminal-mesh-cells.md` +(Option T and decisions TM-D1–TM-D7), explains the reasons: + +- The old phone path depended on an in-process terminal service. It did not work + alongside the isolated cell architecture and the new Godview deck's routing. +- The phone would use the same routed protocol as desktop viewers, with expiring, + revocable, per-client/session grants instead of a separate bearer-key path. +- Truffle's sidecar proxy could forward directly to a cell without putting the + main service into the terminal byte path or adding another transport hop. +- The cell would shape and encode once. Each viewer would decode binary damage + frames, with credits and catch-up, eliminating the phone's row replica and + repeated encoding of whole logical rows. + +The record dates the choice of Option T to September 14. It is a maintained +design record, not an immutable transcript of the TM-5 commit. TM-5's immediate +hardware witness was pending; later records include a real phone connection +qualification. It would be inaccurate to say this architecture was never tested +on a device. + +The architecture had a concrete purpose, but the transition needed a rendering +parity check using both the former bundled font and the actual host font. Frame +delivery, identity, and reconnect tests did not establish that visual parity. + +### Appearance and resize policy also changed, at different times + +TM-5 removed local runtime reconfiguration: glyphs and explicit RGB colors now +came from the host. Follow-up `13120801a` corrected Settings/README claims that +the phone independently reshaped and restyled remote content. Local presentation +controls must be distinguished from host-authored glyphs and explicit colors. + +Initially the phone received input/read rights without geometry rights, so its +viewport setter only recorded a size. **That policy was superseded on September +30:** the latest focused pane, including a phone pane, takes geometry, input, and +read control through the lease. The October focus work implemented that policy. +The original no-resize decision therefore does not explain away the current +resize failure or change the acceptance requirement. + +## 3. Problems resolved during the initial 1.4 port + +These problems were addressed during the qualification recorded at `fc0740e`. +The Ghosttea changes are committed at that checkpoint; the associated VibeField +integration, including its Freeway door correction, still needs coordinated +landing. The earlier report holds the detailed build and test records. + +| Problem and cause | Resolution and qualification | +| --- | --- | +| Source/build interfaces changed with Ghostty 1.4 and Zig 0.17. | Ported the ABI/build recipe from Zig 0.15.2, preserving the G15/G21 and MSVC contracts and all nine extension exports. Prepared-source reproduction and C ABI checks passed. | +| Unicode 18 classified nine new emoji as wide while Rust/browser tables still treated them differently. Following text, caches, and selection backdrops could use the wrong columns. | Shaping and cache identity follow host cell boundaries. WebGPU selection covers host glyph spans and trailing spaces. Ambiguous graphics mapping falls back to host glyphs. Missing emoji artwork remains a font limitation, with correct cell spans. | +| Legacy hosts can reserve four columns for a joined emoji; stretching its artwork to that span distorted it. | Keep joined artwork at its natural two-cell visual width while anchoring following text to actual host columns. An intermediate attempt to split the joined emoji was superseded. | +| Godview routed copy returned an empty string; the Swift home card had no wired selection callback. This was an existing product gap included at the user's request. | Added negotiated `selection-text` requests to the authenticated host, requiring read authority only. Request/activation/lease correlation, bounds, timeout, cancellation, and no replay prevent stale clipboard writes. Empty selections preserve the clipboard; oversized replies fail explicitly. Old hosts report unsupported copy. | +| The Truffle proxy forwarded `/f//`, while the Freeway door accepted only the form without a trailing slash. | Accept exactly one trailing slash with the same complete secret and identity checks. Wrong secrets, extra slashes, and subpaths remain refused. Both comparison hosts include this fix: that baseline is not a pristine released product. | +| Linux cross-builds skipped Darwin's runtime-symbol normalization, leaving duplicate `memset` definitions in the macOS archive. | Run the checksum-pinned upstream `libsystem_override.sh`, then link and execute actual consumers. CI run 40969422970 remains rejected; corrected run 40972570187 passed macOS and both Windows CRT consumers with provenance/inventory checks. | +| Zig's package cache was unwritable in a read-only source mount; Windows found a broken pkg-config shim; `-fno-autolink` removed CRT stamps. | Put packages in writable build output, select native pkg-config, and add three metadata-only CRT witnesses using each compile group's actual flags/macros. Both Windows runtime variants passed consumer tests. | +| Cargo wrote to the SSD-configured target directory, but packaging read `root/target` and found an older artifact manifest. | Resolve Cargo's actual target directory and compose from it; create package-gate outputs in a fresh directory. Checks cover default, space-containing configured paths, and environment overrides. | + +Eight bundled font files were verified, with two Noto updates. Intentional emoji +pixel changes were reviewed: the new engine with only the old fonts restored +reproduced the old golden exactly. Layout and unchanged rendering invariants were +retained; replacing a golden alone was not used as evidence of correctness. + +## 4. October 30: signed-in manual testing reopened acceptance + +The isolated apps were installed alongside the normal app at the user's request. +After login, the phone first reported “desktop not serving.” Once connected, the +user reported no typing in Godview, phone display gaps, and broken resize controls, +and asked that transparency, light mode, and themes be checked. Both apps were +stopped before investigation. The following defects were reproduced independently; +not every original user action could be reconstructed from logs. + +### Desktop discovery used the wrong DNS name + +The separate desktop installation received a canonical Tailscale DNS name with a +collision suffix, while its raw hostname remained unchanged. Truffle's backend +knew the canonical `dnsName`, but the Swift public peer snapshot dropped it. +VibeField consequently constructed and dialed the unsuffixed address even though +the desktop services were healthy. + +The local Truffle fix carries optional `dnsName` through discovery, metadata +updates, provisional-peer merging, and equality. VibeField uses it after case and +trailing-root-dot normalization, retaining the old fallback when absent. Account +checks and ATS settings were unchanged. The rebuilt app reached the actual +desktop, its ready product feed, and terminal service. That established discovery +and connection, not working terminal input or rendering. Truffle's 108 Swift +tests and FieldMesh's 293 tests passed, including collision-name coverage. + +### Godview could exhaust recovery and remain unable to type + +Recorded control legs repeatedly closed with `4004 LEG_TIMEOUT`. After five +retries, returning to or focusing the pane did not restart recovery. The local +Ghosttea routed-control/runtime fix allows a fresh bounded attempt on focus or +visibility restoration; protocol errors and definitive refusals remain final. + +A real Godview lab reproduced timeout, retry exhaustion, hide/show, recovery of +the input lease, and execution of a typed command. Background timer throttling is +consistent with the heartbeat failures, but the exact scheduling cause of the +original manual failure is not proven. + +### Parked and resuming views exposed input too early + +The phone logs contained **78 `no-view` input drops** while its host view was +detached. Client and host now close the input gate while parked, and a late +“allowed” lease update cannot reopen it. This fixes the permission error; the +exact product lifecycle action that left the user's pane detached is still unknown. + +After Internet access recovered, a real Truffle retry exposed another race: the +first post-resume command was refused as `pending`. A buffered frame and cached +allowed status had reopened the client gate before the host resumed the view. +Both Swift and Godview now require the current live-demand acknowledgement and a +newer cell status. Demand sequences are unique across sessions sharing a +connection, so another session's acknowledgement cannot satisfy the barrier. +Tests fail before and pass after the change. Dropped keystrokes are never replayed. + +### Coalesced reattachment omitted the full scene + +If `none` then `live` demand arrived before the pump ran, its notifications could +coalesce. Looking only at current live demand missed the detach/reattach, and a +new view received a delta referring to an older scene. The native pump now tracks +the engine's attachment epoch and sends a full catch-up on change while +preserving the activation sequence. A deterministic test reproduced the failure. + +### Rejected phone scenes had no effective repair path + +A physical-phone run completed mirroring, copy, resize, park/resume, and reconnect, +then stalled during idle refresh. The renderer could reject a scene without +successfully obtaining a complete replacement. + +The local integration adds optional `scene-repair` negotiation and a frames-leg +`RequestScene`. The pump supplies a full catch-up without detaching the view or +giving up geometry ownership. Current-leg, session, lease, read-right, expiry, +demand, and revocation checks fence the request. Swift coalesces requests and +fails after three rejected full scenes. Shared fixtures, native socket tests, +physical-phone tests, and two complete Truffle runs pass. Core wire versions are +unchanged; this capability still requires coordinated host/client integration. + +### Remote font changes scaled the grid but not the glyphs + +The phone increased cell width and row height while keeping host-shaped glyph +coordinates and ink at their original size. This produced spacing/alignment +errors independently of the default-font block seams discussed below. + +Remote scenes now declare source cell metrics. Metal scales glyph positions, +ink, color glyphs, and cursor overlays to the viewer grid. Local-runtime behavior +is retained. A failing-before rendered regression now matches uniform glyph +scaling byte for byte. Underline thickness intentionally follows display scale, +so it is checked separately from the uniformly scaled glyph image. + +Mounted SwiftUI/Metal checks also verify that keyboard movement withholds +intermediate sizes, reports the final smaller grid, restores the grid on +expansion, and updates rows/columns on font changes without replacing the scene. + +### Appearance checks were broadened, with limits retained + +Tests cover alpha composition, light adaptation, palette provenance, minimum +contrast, and cached/direct rendering parity. Physical-phone captures show a +pink backing through 40% background opacity, light theme, 18 pt scaled glyphs, +and 13 pt dark appearance. Copy-selection highlighting was cleared before the +final appearance captures. + +These checks verify specific rendering behavior. They do not restore the old +device-owned shaping architecture or prove that every theme preference can +override host-authored explicit RGB colors. Live settings behavior and ownership +still belong in the signed-in acceptance round. Menlo's standard-size block gaps +were visible in both released and 1.4 frames and required a separate fix. + +## 5. October 31: released-build comparison and missing icons + +At the user's request, a comparison used released Ghosttea rather than Ghostty +1.4. The user then questioned the age of the Swift app, reported missing icons, +and identified the artifact specifically as black horizontal gaps in Claude's +TUI mascot. Both comparison apps were subsequently closed. + +### The source-age audit was a snapshot, not proof of current-main freshness + +The manual apps used VibeField `ac5dd87d3`. At the audit, main was +`2f71beb2ef6ca657fe5dad7412ea109d85d8d640`, 462 commits ahead overall. Four +production Swift commits affected five files for person/home identity and Stop +decoding; terminal, input, swarm, and design sources were unchanged in that +comparison. `TerminalSettingsSheet.swift` had the identical blob +`3037962678e28ec592502f9314bac341c2f90efa` at the two checkpoints; its layout history +went back to August, with later copy/attribution changes. + +Thus the old-looking settings did not prove that a months-old Swift checkout had +been selected. It also did not make `ac5dd87d3` current: main continued to advance, +including settings work. A fresh integration must record a new main SHA and +rebuild it before claiming to include “all latest updates.” + +### Missing icons were introduced by the manual build configuration + +The verification build globally overrode Xcode's `PRODUCT_BUNDLE_IDENTIFIER` to +install alongside the normal app. That override also reached **all nine SwiftPM +resource bundles**, giving them the app's identifier. UIKit resolved FieldHome +asset names against the wrong bundle. This was our verification-build packaging +error, not evidence of a Ghostty regression or missing icon source files. + +In a 46-case asset/appearance probe, five FieldHome icons failed in both light and +dark mode: ten failures. Changing only resource-bundle identifiers made all 46 +pass, with compiled `Assets.car` bytes unchanged. The fix introduces app-only +`VIBEFIELD_APP_BUNDLE_IDENTIFIER` and `VIBEFIELD_APP_DISPLAY_NAME` settings, +referenced by the app target; comparison scripts override those settings instead. + +Both comparison configurations were rebuilt. The rebuilt resources pass 46/46 +checks, all nine resource identifiers are unique and differ from the app ID, and +strict signing verification passes. These corrected builds were not installed +during this pass; on-phone icon confirmation remains pending. + +## 6. Claude mascot gaps: confirmed cause, likely trigger, final fix + +### What the controlled comparison proves + +The pre-fix Metal renderer used font ink bounds to draw solid blocks. At 13 pt, +Menlo's full-block ink is 13.5 pt high in a 19 pt row, leaving 5.5 pt of background +between stacked rows. Desktop WebGPU already bypassed these margins with +cell-aligned geometry. That renderer difference explains the measured seams. + +| Font used for the controlled input | Full-block ink height | Exact background gap | Versions checked | +| --- | --- | --- | --- | +| Bundled JetBrains Mono Nerd Font | 17.5 pt | 1.5 pt | Ghosttea 0.10.1 and 0.13.7 | +| Host-discovered Menlo | 13.5 pt | 5.5 pt | Ghosttea 0.10.1 and 0.13.7 | + +Measurements use 13 pt text, 19 pt row pitch, and 2× rendering, sampling through +repeated full blocks. Antialiased pixels count as ink; only exact background +pixels count as the gap. This supersedes an earlier threshold-based 2.0 pt estimate +for the bundled font. Corresponding old/new frame cross-replays were +pixel-identical. The 0.13.7-versus-1.4 Menlo control also reproduced the same gaps. + +The September TM-5 shift from bundled phone shaping to host-shaped frames is +therefore the **strongest candidate for making the old Metal omission much more +visible**. It is not an established first-bad installed build. No known last-good +phone binary/session has been recovered, and the bundled-font control itself has +a smaller gap. We cannot claim that Claude removed a previously working block +fix, that every pre-TM-5 run was seamless, or that Ghostty 1.4 introduced this bug. + +### Why existing checks missed it + +The existing Metal golden exercised ordinary text, Unicode, styling, and emoji. +It did not contain a multirow block mascot or assert continuity across row +boundaries. Transport checks showed delivery and recovery; source/pin checks +showed which build ran. Neither tested the missing visual invariant. A new +host-font path exposed a renderer parity gap without changing the Ghosttea pin. + +### The final implementation + +The local candidate adds `GhostteaBlockElements.swift` and changes +`GhostteaMetalRenderer.swift` to draw unambiguous, single-cell alpha8 solid block +elements as cell-relative rectangles. It covers U+2580–U+2590 and U+2594–U+259F: +full/partial blocks and quadrants. Shared boundaries snap to device pixels; +selection foreground, cursor text, faint alpha, and colors are retained. +Shade characters U+2591–U+2593 remain font-rendered. + +Complex clusters, combining sequences, variation selectors, ZWJ content, and +width mismatches conservatively retain the font path. ZWJ detection uses +`unicodeScalars`, including in the graphics-color classifier: searching composed +`Character` values could miss a joiner inside a combined emoji and map a later +letter to a block's column. + +An earlier native-raster prototype was evaluated and reverted: it reduced the +large gaps but left filtering seams. Archived prototype images are not the final +implementation. The final block fix changes Swift/Metal rendering, with **no +native production, TRF1-format, or Truffle-protocol change for this fix**. + +### Evidence for the fix, and the remaining boundary + +- All 96 pixel-matrix cases failed before and pass after: four font sizes + (9/13/16/20 pt), three display scales (1×/2×/3×), two submission paths, and four + appearances (dark/light/transparent/selected). Additional assertions cover + quadrants, faint/cursor behavior, and conservative Unicode fallback. +- The frame/rendering suite passed 85 tests, with two optional image exports + skipped; Swift Testing reports 87 total. These are tests and matrix cases, + not 183 independent tests. +- The 72 final replay images have seam-free mascot bodies using the **same + published 0.13.7 frame bytes** as their before images. All 24 same-size ordinary + text comparison regions are pixel-identical. +- The Ghostty 1.4 iPhone candidate builds and passes strict signing checks, + including the final Unicode guard and resource-identity correction. + +The evidence supports applying the fix to the 1.4 build as well as released-frame +input. The rendered QA uses the shared production Metal renderer on macOS. The +latest corrected app has not yet been installed or exercised over real Truffle +on the phone; compile/sign success is not a substitute for that run. + +## 7. Verification history and environmental failures + +Counts below belong to separate checkpoints and suites. Do not add them together +or treat earlier physical-device results as qualification of later source edits. + +| Checkpoint | Observed result | Limit | +| --- | --- | --- | +| Committed initial qualification through `fc0740e` | Candidate: 424 Rust tests, 3 ignored; 623 JS tests; ABI 197 types/1,916 assertions; FFI and ASan passed. Final signed iPhone package: 833 production tests. Four old/new host/client pairings: eight real remote tests. All 13 desktop landing gates passed. | Predates the signed-in manual regressions. Physical Windows GPU/product UI not covered. | +| October 30 discovery fix | 108 Truffle Swift tests and 293 FieldMesh tests; actual product connection reached ready state. | Connection success does not qualify terminal behavior. | +| Latest October 30 lifecycle/repair fixes | 527 physical-phone tests (183 routed, 177 terminal, 167 input); 449 Ghosttea React tests; native 310 unit and 26 socket tests, with one existing benchmark ignored; 1,306 contract tests and 185 mirrored iOS fixtures. | Exact builds recorded in the retry identity; predates the final block/icon fixes. | +| October 30 live behavior | Two complete physical-phone Truffle runs: real PTY input, independent mirrors, Unicode copy, geometry handoff/resize, park/resume, redial, idle repair, live Metal. Godview normal and forced-exhaustion labs executed typed commands and passed copy, alternate screen, hidden output, split/zoom, and reattach checks. | Isolated production-path probes, not a replacement for the user's signed-in round. | +| Resize and appearance controls | All 15 0.13.7/1.4 PNG pairs identical across five grids and three appearances; each resize also matched a fresh full frame. Remote-scaling renderer suite: 82 tests passed. | Offscreen macOS checks; block seams were present in both versions before the final fix. | +| October 31 block and bundle fixes | 96 block matrix cases, 85 passing frame tests plus two optional skips, 72 final replay images, 24 unchanged text regions, 46/46 rebuilt icon checks; iPhone build/sign passed. | No physical-phone installation or new live-transport run after these final fixes. | + +Other failures were preserved and classified rather than hidden by rerunning: + +- A baseline device test raced its own socket acknowledgement. Waiting for both + legs fixed the test without changing production resume behavior. This is + distinct from the later, real resume input-barrier bug. +- A competing root Metal drawable loop blocked isolated XCTest teardown. The + QA host paused its background views; mounted SwiftUI layout checks required a + scene-connected window. A desktop harness initially mistook textarea grant + state for actual readiness; it now uses Godview's real input-allowed marker, + inert state, and focus before injecting keys. Failed harness runs remain saved. +- One real-network attempt failed with `NSURLErrorDomain -1009` before terminal + attachment. On retry, connectivity recovered and exposed the genuine resume + race. A cleanup-time TailscaleKit exception initially obscured a timeout; + subsequent probes preserve the original error before cleanup. +- Automatic pnpm overlay installation drifted Loro/React away from the committed + lockfile. Frozen installation and repaired isolated generated links restored + the graph; preventing automatic reinstallation did not disable dependency + guards. A build-only offline variable was also removed from the runtime test + environment. The drifted run is not a valid baseline/candidate comparison. +- Disk-full attempts led to the user-requested, checksum-verified cache move to + the portable SSD. Path-bound Xcode caches were rebuilt. The Cargo output-path + defect above was corrected rather than relying on an old `target` symlink. +- Full workspace verification remains red. The final run passed static/build + stages and 26/27 JavaScript projects (9,982 passed, one failed, 17 skipped); + an unchanged cleanup assertion observed 1,999 ms against a 2,000 ms bound. + Focused baseline/candidate controls passing do not change that full-run result. +- Latency remains explicitly unqualified, as the user chose to continue under + heavy system load. Sixteen paired workload processes completed at roughly + 120 Hz with frame p95 around 9.1–9.3 ms, but those measurements do not establish + input-to-photon latency or a performance sign-off. + +## 8. What remains before migration acceptance + +1. Review and commit the local fixes in their owning repositories, preserving the + intentional transport versions and coordinated capability/fixture changes. + This document commits none of that implementation. Saved patch snapshots are + checkpoints and may predate the final block and packaging edits. +2. Integrate against a newly recorded latest VibeField main revision. Preserve the + app-only bundle settings and verify native hashes, resource IDs, and resolved + dependencies. Do not label the existing `ac5dd87d3` build as current main. +3. Install that final candidate alongside the regular phone app and run the user's + signed-in desktop/phone round: actual Claude mascot, immediate typing after + focus/resume, split panes, selection/copy, scrollback, keyboard movement, + font changes, geometry ownership, themes/light mode/opacity, backgrounding, + reconnect, and simultaneous Truffle mirroring. +4. Retain the unknowns: the exact original parked-view trigger and the last-good/ + first-bad installed build for the mascot have not been established. A known + earlier binary/session would allow a stronger historical comparison. +5. Resolve or explicitly disposition the full workspace timing gate, physical + Windows product/GPU coverage, new-emoji artwork gaps, and the unqualified + latency measurement before assigning the corresponding release guarantees. + +The desktop and phone apps are closed at the user's request. No launch, +installation, release promotion, or source-fix push is implied by this record. + +## 9. Evidence index + +The committed [migration report](./ghostty-1.4-migration.md) and +[qualification evidence](./ghostty-1.4-qualification/) cover the initial port. +Later evidence remains in local QA archives, **not files added by this commit**. +Paths below are relative to the original `electron-ghostty` workspace: + +- `G14` = `artifacts/ghostty-1.4-manual-20261030/evidence/` +- `REL` = `artifacts/ghosttea-release-manual-20261031/evidence/` + +| Evidence | Contents | +| --- | --- | +| `G14/ios-dns-fix-build-identity.json`, `ios-dns-fix-live-observation.json` | Canonical discovery correction and installed-app observation. | +| `G14/regression-retry-build-identity.json` | Source/binary identity for the 527-test, real-Truffle and Godview recovery checkpoint. | +| `G14/desktop-resume-pane-ready/pane-regression.json`, `desktop-resume-recovery-final/pane-regression.json` | Real pane operations, first-ready input, forced heartbeat/retry exhaustion and recovery. | +| `G14/iphone-resume-gate-after-summary.json`, `iphone-remote-resume-fixed-summary.json`, `iphone-remote-appearance-final-summary.json` | Physical suites and complete remote runs, with appearance captures alongside them. | +| `G14/regression-coalesced-reattach-before.log`, `regression-scene-repair-native.log`, `regression-remote-font-before.log`, `regression-remote-font-final.log` | Reattachment reproduction, repair checks, and before/after scaling evidence. | +| `G14/regression-resize-ab.json`, `regression-render-build-identities.json` | Released/candidate resize controls and native archive identities. | +| `REL/swift-history-audit.json` | Exact VibeField comparison revisions, affected Swift files, and Settings blob identity. | +| `REL/icon-resource-collision-result.json` | Ten failing icon cases, identifier-only control, and rebuilt-resource verification. | +| `REL/terminal-history/audit.json`, `terminal-history/claude-regression-origin.json` | Font/frame controls, historical source excerpts, Git attribution, 28 Metal revisions, measured gaps, and limits of the attribution. | +| `G14/claude-blocks/audit.json`, `metal-regression-before.log`, `metal-regression-after.log`, `resource-identity.json` | Final block-fix scope, pixel tests, exact source hashes, build/sign status, and bundle identities. The last three paths are also under `claude-blocks/`. | + +The exact final block-fix source hashes recorded by that audit are: + +| File | SHA-256 | +| --- | --- | +| `apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaBlockElements.swift` | `04988531f69b8773ca85b448850718a6c0a0c79049d747bebe956e82e5acb66b` | +| `apple/GhostteaKit/Sources/GhostteaTerminal/GhostteaMetalRenderer.swift` | `49058e2f84fc18edec3eebde85e081b57fc6feebb1cffe59b14a6a92a50e2f85` | +| `apple/GhostteaKit/Tests/GhostteaFrameTests/GhostteaBlockElementTests.swift` | `771a6e411132c76d76951ba3bc5235dc20f2ff350e47c146f8e50aab7314fc3d` | + +These hashes identify the local implementation reviewed here; they do not mean +those files are included in this documentation commit. The screenshot gallery +and archived prototype material are supporting review aids. The retained final +images, source identities, and test logs determine which result a claim refers to. + +## 10. November 2: committed fixes and release preparation + +The migration was rebased onto the latest published Ghosttea main, 0.13.8. The +release branch is `release/0.14.0`; the synchronized npm/Cargo version and generated +iOS inventory are 0.14.0. This preserves the newer conditional terminal-release +and acknowledged attachment-disposal behavior rather than publishing an older +fork of main. + +The fixes that sections 6–8 described as uncommitted are now tracked: + +- Ghosttea `59c002f` contains remote source-frame text metrics and the cell-geometry + treatment of solid blocks and quadrants. `ce80daa` contains routed foreground + recovery and the input barrier after resumed demand. +- Vibefield `e65d31e88` adds authenticated selection copy; `3d7081ea0` adds scene + repair and input gating; `165045ba4` connects mirrored glyph scaling to the + terminal font settings; `75ef64d2e` dials the canonical mesh DNS name. +- Vibefield `0c4590d7a` and `836d8e700` refine the terminal overlays, gesture/focus + ownership and keypad. `c2c72be4e` fixes both icon paths. `b8771d46a` retains the + desktop and physical-device qualification tools. + +These consumer commits are reviewable in +[Vibefield PR #12](https://github.com/vibefield/vibefield/pull/12). The original +manual-testing worktree remains separate; its machine-specific dependency and QA +configuration are not the publication branch's dependency pins. + +### Clean publication build exposed the remaining Truffle dependency + +The first clean iOS build against the published dependency graph failed to +compile `FieldStoreDoors+Mesh.swift`: published `Truffle.Peer` has no `dnsName`. +The manual builds had used the local Truffle patch described in section 4. The +Vibefield source fix alone therefore cannot close the discovery issue in a +released app. + +The Truffle patch is now committed as `2d91013` on latest main in +[Truffle PR #247](https://github.com/vibecook-dev/truffle/pull/247). All 116 root +Swift package tests passed, including canonical-name updates/clearing, local +snapshots, stable references and provisional-peer merging. The patch shipped in +[Truffle 0.7.26](https://github.com/vibecook-dev/truffle/releases/tag/truffle-v0.7.26) +through [release PR #248](https://github.com/vibecook-dev/truffle/pull/248). +[Release run 41563588677](https://github.com/vibecook-dev/truffle/actions/runs/41563588677) +passed its complete publication sequence; the final SwiftPM `v0.7.26` tag names +`101889f676d32989e2c32295c3b608ccd5972c02`. All 13 npm packages and three crates +were independently checked at that version. Both Ghosttea Swift manifests, +their resolved/provenance locks and the iOS inventory move to this release. +Vibefield's Swift pin must match when its Ghosttea 0.14.0 pin lands. + +The Swift sources at the release tag are identical to the reviewed candidate. +The TailscaleKit binary, license/privacy hashes and Ghosttea native source digest +are unchanged. The intervening Truffle Swift update also reads peers' application +tags, retaining the tested hostname fallback for older peers. Desktop Truffle +Rust and sidecar versions remain 0.7.24; the DNS fix does not change the existing +login and transport checks. The original failed clean build is retained at +`G14/publication-20261102/keypad-profiles-swift.log`. + +With the verified Truffle 0.7.26 pin, all **307 Ghosttea Swift tests** passed +against the published native archive, including the Claude block-gap, +resize/appearance and routed attachment regressions. A fresh downstream package +linked all **18 public Swift library products**. The served-byte audit and +generated iOS BOM/resource checks passed; the three pre-existing release +blockers remain recorded. These results qualify the updated Ghosttea package, +not a final published-dependency Vibefield device run. + +### Subsequent phone UI issues + +The corrected VibeField G14 app was built, installed alongside the regular app, +launched on the physical iPhone, and used for the subsequent user-directed UI +refinements. This supersedes the October 31 statement that no corrected app had +been installed. It does not claim a new complete four-pairing Truffle matrix on +the final 0.14.0 registry release. + +The terminal now keeps the keyboard hidden during scrolling, extends behind a +fading header, and uses floating paste/keyboard controls with a context indicator. +Remote-driver takeover folds the keyboard; the device name fits the available +width and the takeover message is centered in the available terminal area. + +The replacement keypad has Navigate, Shortcuts and Fn pages. Switching between +native and custom keyboards preserves the measured composer geometry. Switching +between drafting and direct terminal input keeps the outgoing responder mounted +until the incoming responder owns focus. Removing it, or disabling its UIKit +interaction too early, caused the observed keyboard dismissal/reappearance. +Deferred stale focus requests are rejected, and the redundant prompt Enter key +was removed. + +Two separate icon defects required separate fixes: + +- The swarm's PNG was already byte-identical to the desktop atlas. A CGContext + transform flipped its rows during Metal upload, so valid tile coordinates read + the wrong icons. Removing that transform makes the actual R8 texture match + desktop pixels; the new test reproduces the old failure. +- The terminal header uses SVG assets, not that atlas. Xcode misread compact + numeric/arc arguments in the Codex path. Separating commands and arguments + preserves all 59 geometry operations and the desktop reference pixels while + fixing the compiled asset at 11, 24 and 96 pt in light and dark appearances. + +### Evidence and remaining limits + +The later checks are distinct checkpoints, not counts to sum: + +| Checkpoint | Result | Boundary | +| --- | --- | --- | +| Keypad layout | 185 simulator checks passed, including portrait/landscape UI; physical-phone fixture checks preserved composer position, terminal rows and takeover folding. | Local rendering/input checks, not a new live Truffle matrix. | +| Direct-terminal mode switch | 186 checks passed; zero keyboard-hide notifications when switching both directions with native and custom keyboards. | Selected page, unsent draft and composer frame were checked in the simulator. | +| Duplicate Enter removal | 184 checks passed; no mode-switch keyboard dismissal. | The corresponding G14 app was built, installed and launched. | +| Swarm atlas | 38 checks passed; all 20 icons reviewed in both appearances. | Test reads the uploaded Metal texture, not just the PNG source. | +| Header SVG | UI check passed; all three sizes reviewed in both appearances; reference geometry and 240 px pixels unchanged. | Product app built, installed and launched. | +| November 2 release preparation | All Ghosttea workspace type checks and 631 JavaScript tests passed; 428 Rust tests passed with three existing ignored cases against the promoted VT archive; Vibefield's committed product sources passed 1,509 contract and 32 routed-host tests. Rust formatting and 205 iOS fixture copies / 67 contract constants passed. | The Vibefield tests used the local Ghosttea candidate. Published dependency pins and the complete release/consumer gates remain separate. | + +The local evidence directories are `G14/keypad-implementation`, +`G14/keypad-mode-switch`, `G14/live-enter-removal`, `G14/swarm-icons` and +`G14/header-icon`, each suffixed `-20261102`, under the evidence root indexed in +section 9. Their result JSON records the tested source hashes. Large xcresult +bundles, device data and test credentials are not release source assets. + +One recorded keypad limit remains: if the custom keypad is open during the first +rotation into an orientation with no measured native keyboard height, the first +ABC switch may recalibrate once (40 pt on the tested iOS 26.5 simulator). Subsequent +switches use the measured height. Physical hardware-keyboard input was checked by +routing tests, not an attached keyboard. + +Latency remains unqualified at the user's direction. Physical Windows GPU/product +behavior, real Wi-Fi/cellular handoff and long suspension remain unclaimed. The +pre-existing SSH production-readiness blockers and the historical full-Vibefield +agent-cleanup timing failure are not waived by these UI checks. Product release +CI must verify the final native pin, and Vibefield must qualify the published +0.14.0 dependency graph before its integration PR becomes merge-ready. diff --git a/native/fonts.lock.json b/native/fonts.lock.json index 54a6bc89..0e79d659 100644 --- a/native/fonts.lock.json +++ b/native/fonts.lock.json @@ -2,7 +2,7 @@ "schemaVersion": 2, "source": { "repository": "https://github.com/ghostty-org/ghostty.git", - "commit": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69" + "commit": "83fb95624b0439ee660c2679b43b38c8b63769cd" }, "license": { "id": "OFL-1.1", @@ -40,7 +40,7 @@ { "role": "fallback-emoji", "path": "src/font/res/NotoColorEmoji.ttf", - "sha256": "bf2a8506b80614ba190a34c7b037af1269a7d614fe9f3b613cc15cdeec6f814b" + "sha256": "72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b" }, { "role": "fallback-symbols-math", @@ -68,7 +68,7 @@ { "role": "fallback-emoji-text", "path": "src/font/res/NotoEmoji-Regular.ttf", - "sha256": "84a38f2f02374efb72cac2b7eb5589102e06fffc33f7b531ed708f4a08236c91" + "sha256": "cd07743a4c93d8da929afdd28c1d368f11da478a1093146a13610081c2a58440" } ], "additionalNotices": [ diff --git a/native/ghosttea/Cargo.toml b/native/ghosttea/Cargo.toml index 222dd6be..a7483488 100644 --- a/native/ghosttea/Cargo.toml +++ b/native/ghosttea/Cargo.toml @@ -14,10 +14,10 @@ anyhow.workspace = true async-trait.workspace = true bytes.workspace = true portable-pty = "0.9" -ghosttea-vt = { version = "0.13.8", path = "crates/ghosttea-vt" } -ghosttea-config = { version = "0.13.8", path = "crates/ghosttea-config" } -ghosttea-core = { version = "0.13.8", path = "crates/ghosttea-core" } -ghosttea-text = { version = "0.13.8", path = "crates/ghosttea-text" } +ghosttea-vt = { version = "0.14.0", path = "crates/ghosttea-vt" } +ghosttea-config = { version = "0.14.0", path = "crates/ghosttea-config" } +ghosttea-core = { version = "0.14.0", path = "crates/ghosttea-core" } +ghosttea-text = { version = "0.14.0", path = "crates/ghosttea-text" } serde.workspace = true serde_json.workspace = true subtle.workspace = true diff --git a/native/ghosttea/crates/ghosttea-core/Cargo.toml b/native/ghosttea/crates/ghosttea-core/Cargo.toml index ea7e4fd7..f7e81150 100644 --- a/native/ghosttea/crates/ghosttea-core/Cargo.toml +++ b/native/ghosttea/crates/ghosttea-core/Cargo.toml @@ -11,8 +11,8 @@ readme = "README.md" [dependencies] anyhow.workspace = true -ghosttea-text = { version = "0.13.8", path = "../ghosttea-text" } -ghosttea-vt = { version = "0.13.8", path = "../ghosttea-vt" } +ghosttea-text = { version = "0.14.0", path = "../ghosttea-text" } +ghosttea-vt = { version = "0.14.0", path = "../ghosttea-vt" } serde.workspace = true smallvec.workspace = true diff --git a/native/ghosttea/crates/ghosttea-core/src/model.rs b/native/ghosttea/crates/ghosttea-core/src/model.rs index d3f75ba9..c1976751 100644 --- a/native/ghosttea/crates/ghosttea-core/src/model.rs +++ b/native/ghosttea/crates/ghosttea-core/src/model.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::{Context, Result}; -use ghosttea_text::{FontStyle, GlyphDefinition, ShapedRow, StyleSpan, TextEngine}; +use ghosttea_text::{FontStyle, GlyphDefinition, ShapedRow, TerminalCellSpan, TextEngine}; use ghosttea_vt::{GhosttyTerminalCore, TerminalSnapshot, TrackedTerminalSelection}; use crate::{ @@ -89,7 +89,7 @@ pub struct TerminalModelOptions { #[derive(Clone, PartialEq, Eq, Hash)] struct RowShapeKey { text: String, - spans: Vec<(usize, usize, FontStyle)>, + spans: Vec, } #[derive(Clone)] @@ -534,14 +534,16 @@ impl TerminalModel { } let byte_start = byte_offset; byte_offset = (byte_offset + cell.text.len()).min(row.len()); - Some(( + Some(TerminalCellSpan { byte_start, - byte_offset, - FontStyle { + byte_end: byte_offset, + column: cell.column, + span: cell.span, + style: FontStyle { bold: cell.style.bold, italic: cell.style.italic, }, - )) + }) }) .collect(); let key = RowShapeKey { @@ -558,15 +560,7 @@ impl TerminalModel { cache.rows[row_index as usize] = Some(Arc::clone(cached)); continue; } - let spans: Vec<_> = span_tuples - .into_iter() - .map(|(byte_start, byte_end, style)| StyleSpan { - byte_start, - byte_end, - style, - }) - .collect(); - missing.push((row_index, key, spans)); + missing.push((row_index, key, span_tuples)); } // Only actual shaping takes the shared engine lock. Cache lookup, // row movement and frame assembly remain local to the session. @@ -579,7 +573,7 @@ impl TerminalModel { let hold_started = Instant::now(); for (row_index, key, spans) in missing { let shaped = engine - .shape_styled_row(&key.text, &spans) + .shape_terminal_row(&key.text, &spans) .with_context(|| format!("shape terminal row {row_index}"))?; cache.rows[row_index as usize] = Some(Arc::new(CachedRow { key, shaped })); } @@ -716,6 +710,85 @@ fn logical_snapshot( mod tests { use super::*; + #[test] + fn emoji_rendering_preserves_the_hosts_grapheme_mode() { + let runtime = Arc::new(TerminalRuntime::discover().unwrap()); + for (mode, next_column) in [("\u{1b}[?2027l", 4), ("\u{1b}[?2027h", 2)] { + let mut model = TerminalModel::new( + runtime.clone(), + TerminalModelOptions { + session_handle: 1, + session_epoch: 1, + layout_epoch: 1, + cols: 10, + rows: 2, + scrollback_bytes: 4096, + }, + ) + .unwrap(); + model + .feed(format!("{mode}👩‍💻X").as_bytes(), RenderRequest::Full) + .unwrap(); + let row = model.render_cache.rows[0].as_ref().unwrap(); + assert!( + row.shaped.glyphs.iter().all(|glyph| glyph.width + <= 2.0 + * runtime + .text_engine() + .lock() + .unwrap() + .metrics() + .cell_width_px + + 1.0), + "an emoji must not stretch across distinct terminal cells" + ); + assert!( + row.shaped + .glyphs + .iter() + .any(|glyph| glyph.cell_start == next_column && glyph.cell_span == 1) + ); + let logical = model.latest_logical.as_ref().unwrap(); + assert!( + logical.rows[0] + .cells + .iter() + .any(|cell| cell.column == next_column && cell.text == "X") + ); + } + } + + #[test] + fn unicode_18_rendered_glyphs_follow_the_terminal_grid() { + let runtime = Arc::new(TerminalRuntime::discover().unwrap()); + for codepoint in [ + 0x1f6d9, 0x1fa8b, 0x1fa8c, 0x1fa8d, 0x1facc, 0x1fadd, 0x1faeb, 0x1faf9, 0x1fafa, + ] { + let mut model = TerminalModel::new( + runtime.clone(), + TerminalModelOptions { + session_handle: 1, + session_epoch: 1, + layout_epoch: 1, + cols: 10, + rows: 2, + scrollback_bytes: 4096, + }, + ) + .unwrap(); + let text = format!("{}X", char::from_u32(codepoint).unwrap()); + model.feed(text.as_bytes(), RenderRequest::Full).unwrap(); + let row = model.render_cache.rows[0].as_ref().unwrap(); + assert!( + row.shaped + .glyphs + .iter() + .any(|glyph| glyph.cell_start == 2 && glyph.cell_span == 1), + "X must occupy column 2 after U+{codepoint:X}, including when its artwork is unavailable" + ); + } + } + #[test] fn scrolling_reuses_each_models_rows_beyond_the_shared_cache_capacity() { let runtime = Arc::new(TerminalRuntime::discover().unwrap()); diff --git a/native/ghosttea/crates/ghosttea-core/src/replica.rs b/native/ghosttea/crates/ghosttea-core/src/replica.rs index 6a6ea0d6..86fac74b 100644 --- a/native/ghosttea/crates/ghosttea-core/src/replica.rs +++ b/native/ghosttea/crates/ghosttea-core/src/replica.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::{Context, Result, bail, ensure}; -use ghosttea_text::{FontStyle, GlyphDefinition, ShapedRow, StyleSpan}; +use ghosttea_text::{FontStyle, GlyphDefinition, ShapedRow, TerminalCellSpan}; use ghosttea_vt::{ CellStyle, TerminalPalette, TerminalScrollbar, TerminalSelection, resolved_palette, }; @@ -46,7 +46,7 @@ impl FrameCell for ReplicaFrameCell { struct ReplicaRenderCache { rows: Vec, cells: Vec>, - shape_spans: Vec>, + shape_spans: Vec>, shaped_rows: Vec, sent_glyphs: HashSet, } @@ -407,18 +407,10 @@ impl LogicalReplicaModel { let wait = wait_started.elapsed(); let hold_started = Instant::now(); for row in prepared.iter().filter(|row| row.shape_spans.is_some()) { - let spans = row - .shape_spans - .as_ref() - .unwrap() - .iter() - .map(|&(byte_start, byte_end, style)| StyleSpan { - byte_start, - byte_end, - style, - }) - .collect::>(); - shaped_updates.push((row.index, engine.shape_styled_row(&row.text, &spans)?)); + shaped_updates.push(( + row.index, + engine.shape_terminal_row(&row.text, row.shape_spans.as_ref().unwrap())?, + )); } drop(engine); self.text_engine_performance @@ -503,7 +495,7 @@ struct PreparedReplicaRow { index: usize, text: String, cells: Vec, - shape_spans: Option>, + shape_spans: Option>, } fn prepare_logical_row( @@ -544,7 +536,7 @@ fn prepare_logical_cells(row: &LogicalRow, palette: &TerminalPalette) -> Vec Vec<(usize, usize, FontStyle)> { +fn logical_shape_spans(row: &LogicalRow) -> Vec { let mut byte_offset = 0; row.cells .iter() @@ -554,19 +546,21 @@ fn logical_shape_spans(row: &LogicalRow) -> Vec<(usize, usize, FontStyle)> { } let byte_start = byte_offset; byte_offset = (byte_offset + cell.text.len()).min(row.text.len()); - Some(( + Some(TerminalCellSpan { byte_start, - byte_offset, - FontStyle { + byte_end: byte_offset, + column: cell.column, + span: cell.span, + style: FontStyle { bold: cell.style.bold, italic: cell.style.italic, }, - )) + }) }) .collect() } -fn shape_spans_match(row: &LogicalRow, cached: &[(usize, usize, FontStyle)]) -> bool { +fn shape_spans_match(row: &LogicalRow, cached: &[TerminalCellSpan]) -> bool { let mut byte_offset = 0; let mut span_index = 0; for cell in &row.cells { @@ -575,14 +569,16 @@ fn shape_spans_match(row: &LogicalRow, cached: &[(usize, usize, FontStyle)]) -> } let byte_start = byte_offset; byte_offset = (byte_offset + cell.text.len()).min(row.text.len()); - let span = ( + let span = TerminalCellSpan { byte_start, - byte_offset, - FontStyle { + byte_end: byte_offset, + column: cell.column, + span: cell.span, + style: FontStyle { bold: cell.style.bold, italic: cell.style.italic, }, - ); + }; if cached.get(span_index) != Some(&span) { return false; } @@ -656,6 +652,58 @@ mod tests { .expect("replica update must contain a frame") } + #[test] + fn mirrors_preserve_host_unicode_width_and_reshape_geometry_only_changes() { + let runtime = Arc::new(TerminalRuntime::discover().unwrap()); + let mut replica = LogicalReplicaModel::new(runtime, 42); + let row = |width| LogicalRow { + text: "\u{1f6d9}X".into(), + cells: vec![ + crate::LogicalCell { + column: 0, + span: width, + text: "\u{1f6d9}".into(), + style: LogicalCellStyle::default(), + }, + crate::LogicalCell { + column: width, + span: 1, + text: "X".into(), + style: LogicalCellStyle::default(), + }, + ], + }; + let mut initial = snapshot("X"); + initial.rows = vec![row(1)]; + replica.publish(initial).unwrap(); + for (sequence, width) in [(1, 2), (2, 1), (3, 2)] { + let current = replica.latest().unwrap(); + replica + .publish_patch(LogicalTerminalPatch { + session_epoch: current.session_epoch, + layout_epoch: current.layout_epoch, + patch_sequence: sequence, + terminal_revision: current.terminal_revision + 1, + row_replacements: vec![RowReplacement { + row_index: 0, + row_revision: current.terminal_revision + 1, + row: row(width), + }], + cursor: None, + mouse_tracking: None, + scrollbar: None, + }) + .unwrap(); + assert_eq!(replica.text_engine_performance().acquisition_count, 1); + assert!( + replica.render_cache.shaped_rows[0] + .glyphs + .iter() + .any(|glyph| glyph.cell_start == width && glyph.cell_span == 1) + ); + } + } + #[test] fn logical_snapshot_is_shaped_into_a_full_frame() { let runtime = Arc::new(TerminalRuntime::discover().unwrap()); diff --git a/native/ghosttea/crates/ghosttea-ffi/src/lib.rs b/native/ghosttea/crates/ghosttea-ffi/src/lib.rs index 3d7d806d..dc3d11ac 100644 --- a/native/ghosttea/crates/ghosttea-ffi/src/lib.rs +++ b/native/ghosttea/crates/ghosttea-ffi/src/lib.rs @@ -1978,14 +1978,19 @@ mod tests { ghosttea_terminal_compress_scrollback_full(pointer, &mut supported), GHOSTTEA_STATUS_OK ); - // Ghostty performs strict page reclamation only on 64-bit Linux and - // Darwin (`src/terminal/mem.zig`); elsewhere it reports unsupported. + // Ghostty 1.4 performs strict page reclamation on 64-bit Linux, + // Darwin, Windows, and FreeBSD (`src/terminal/mem.zig`). // The call still succeeds and must preserve logical content. assert_eq!( supported, cfg!(all( target_pointer_width = "64", - any(target_os = "linux", target_vendor = "apple") + any( + target_os = "linux", + target_vendor = "apple", + target_os = "windows", + target_os = "freebsd" + ) )) ); let after = terminal_operation(pointer, PanicScope::Terminal, |model| { diff --git a/native/ghosttea/crates/ghosttea-text/src/emoji_presentation.rs b/native/ghosttea/crates/ghosttea-text/src/emoji_presentation.rs index 41cbe494..5fe32044 100644 --- a/native/ghosttea/crates/ghosttea-text/src/emoji_presentation.rs +++ b/native/ghosttea/crates/ghosttea-text/src/emoji_presentation.rs @@ -1,9 +1,11 @@ -//! Unicode 17.0 `Emoji_Presentation` property used by the pinned Ghostty font stack. +//! Unicode 18.0 `Emoji_Presentation` property used by the pinned Ghostty font stack. //! //! The ranges are derived from the `emoji-data.txt` consumed by Ghostty's pinned //! `uucode` dependency. Keeping this compact table beside the Rust shaper makes //! default presentation deterministic on every host without relying on platform //! font discovery. +//! Source: uucode ea62149739404a73c202b48a33bf6dd2af4bd9b0, emoji/emoji-data.txt. +//! SHA-256: 80d00f8e616a0ef27fd6b8de3b758c06383b5d917e2977709578e68baf733bf1. const EMOJI_PRESENTATION_RANGES: &[(u32, u32)] = &[ (0x231A, 0x231B), @@ -71,7 +73,7 @@ const EMOJI_PRESENTATION_RANGES: &[(u32, u32)] = &[ (0x1F680, 0x1F6C5), (0x1F6CC, 0x1F6CC), (0x1F6D0, 0x1F6D2), - (0x1F6D5, 0x1F6D8), + (0x1F6D5, 0x1F6D9), (0x1F6DC, 0x1F6DF), (0x1F6EB, 0x1F6EC), (0x1F6F4, 0x1F6FC), @@ -81,12 +83,11 @@ const EMOJI_PRESENTATION_RANGES: &[(u32, u32)] = &[ (0x1F93C, 0x1F945), (0x1F947, 0x1F9FF), (0x1FA70, 0x1FA7C), - (0x1FA80, 0x1FA8A), - (0x1FA8E, 0x1FAC6), + (0x1FA80, 0x1FAC6), (0x1FAC8, 0x1FAC8), - (0x1FACD, 0x1FADC), - (0x1FADF, 0x1FAEA), - (0x1FAEF, 0x1FAF8), + (0x1FACC, 0x1FADD), + (0x1FADF, 0x1FAEB), + (0x1FAEF, 0x1FAFA), ]; pub(crate) fn has_default_emoji_presentation(character: char) -> bool { @@ -114,5 +115,12 @@ mod tests { assert!(!has_default_emoji_presentation('⚠')); assert!(has_default_emoji_presentation('😀')); assert!(has_default_emoji_presentation('⌚')); + for codepoint in [ + 0x1f6d9, 0x1fa8b, 0x1fa8c, 0x1fa8d, 0x1facc, 0x1fadd, 0x1faeb, 0x1faf9, 0x1fafa, + ] { + assert!(has_default_emoji_presentation( + char::from_u32(codepoint).unwrap() + )); + } } } diff --git a/native/ghosttea/crates/ghosttea-text/src/lib.rs b/native/ghosttea/crates/ghosttea-text/src/lib.rs index f76b662b..0f71d782 100644 --- a/native/ghosttea/crates/ghosttea-text/src/lib.rs +++ b/native/ghosttea/crates/ghosttea-text/src/lib.rs @@ -4,7 +4,7 @@ use std::{ sync::Arc, }; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result, bail, ensure}; use fontdb::{Database, Family, ID, Query, Source as FontSource, Stretch, Style, Weight}; use harfrust::{Buffer, ShapeOptions, ShaperFont, shape}; use swash::{ @@ -153,6 +153,18 @@ pub struct StyleSpan { pub style: FontStyle, } +/// A terminal host's authoritative text cluster and grid placement. +/// Keep these boundaries when rendering a local or mirrored terminal: the +/// host's Unicode version and grapheme mode may differ from this shaper's. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct TerminalCellSpan { + pub byte_start: usize, + pub byte_end: usize, + pub column: u16, + pub span: u16, + pub style: FontStyle, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u8)] pub enum GlyphFormat { @@ -229,11 +241,13 @@ struct Grapheme<'a> { cell_span: u16, face: ID, style: FontStyle, + presentation: FontPresentation, } struct CachedShapedRow { text: String, spans: Vec, + cells: Option>, shaped: ShapedRow, } @@ -502,19 +516,43 @@ impl TextEngine { } pub fn shape_styled_row(&mut self, text: &str, spans: &[StyleSpan]) -> Result { - let cache_key = shaped_row_hash(text, spans); + self.shape_row_layout(text, spans, None) + } + + /// Shapes using the terminal's cell boundaries, widths and columns. + /// Empty cell metadata retains the text-only API's layout for legacy + /// logical rows. Nonempty metadata must cover the complete UTF-8 text. + pub fn shape_terminal_row( + &mut self, + text: &str, + cells: &[TerminalCellSpan], + ) -> Result { + self.shape_row_layout(text, &[], (!cells.is_empty()).then_some(cells)) + } + + fn shape_row_layout( + &mut self, + text: &str, + spans: &[StyleSpan], + cells: Option<&[TerminalCellSpan]>, + ) -> Result { + let cache_key = shaped_row_hash(text, spans, cells); if let Some(shaped) = self .shaped_rows .get(&cache_key) .and_then(|rows| { - rows.iter() - .find(|row| row.text == text && row.spans == spans) + rows.iter().find(|row| { + row.text == text && row.spans == spans && row.cells.as_deref() == cells + }) }) .map(|row| row.shaped.clone()) { return Ok(shaped); } - let graphemes = self.resolve_graphemes(text, spans)?; + let graphemes = match cells { + Some(cells) => self.resolve_terminal_cells(text, cells)?, + None => self.resolve_graphemes(text, spans)?, + }; let mut output = ShapedRow::default(); let mut used_definitions = HashSet::new(); let mut run_start = 0; @@ -522,9 +560,19 @@ impl TextEngine { let face = graphemes[run_start].face; let mut run_end = run_start + 1; let style = graphemes[run_start].style; + let presentation = graphemes[run_start].presentation; while run_end < graphemes.len() && graphemes[run_end].face == face && graphemes[run_end].style == style + // Emoji need independent visual fitting while their grid + // advance still comes from the host. Keep normal text runs + // together so ordinary cross-cell ligatures are unchanged. + && (cells.is_none() + || graphemes[run_end].presentation == presentation) + && graphemes[run_end].cell_start + == graphemes[run_end - 1] + .cell_start + .saturating_add(graphemes[run_end - 1].cell_span) { run_end += 1; } @@ -533,6 +581,7 @@ impl TextEngine { &graphemes[run_start..run_end], face, style, + cells.is_some() && presentation == FontPresentation::Emoji, &mut output.glyphs, &mut used_definitions, )?; @@ -555,6 +604,7 @@ impl TextEngine { .push(CachedShapedRow { text: text.to_owned(), spans: spans.to_vec(), + cells: cells.map(<[_]>::to_vec), shaped: output.clone(), }); self.shaped_row_count += 1; @@ -567,6 +617,60 @@ impl TextEngine { Ok(output) } + fn resolve_terminal_cells<'a>( + &mut self, + text: &'a str, + cells: &[TerminalCellSpan], + ) -> Result>> { + let mut byte_end = 0; + let mut column_end = 0; + let mut output = Vec::with_capacity(cells.len()); + for cell in cells { + ensure!( + cell.byte_start == byte_end && cell.byte_end > cell.byte_start, + "terminal cells must cover the row without gaps or overlaps" + ); + ensure!( + cell.span > 0 && cell.column >= column_end, + "terminal cells must have positive, non-overlapping grid spans" + ); + let cluster = text + .get(cell.byte_start..cell.byte_end) + .context("terminal cell is not a valid UTF-8 range")?; + column_end = cell + .column + .checked_add(cell.span) + .context("terminal cell exceeds the grid")?; + byte_end = cell.byte_end; + let primary = *self + .styled_faces + .get(&cell.style) + .context("missing styled primary face")?; + let presentation = cluster_presentation(cluster); + let face = if self.face_supports(primary, cluster)? + && self.face_matches(primary, presentation) + { + primary + } else { + self.fallback_for(cluster, presentation).unwrap_or(primary) + }; + output.push(Grapheme { + text: cluster, + byte_start: cell.byte_start, + cell_start: cell.column, + cell_span: cell.span, + face, + style: cell.style, + presentation, + }); + } + ensure!( + byte_end == text.len(), + "terminal cells do not cover the complete row" + ); + Ok(output) + } + fn resolve_graphemes<'a>( &mut self, text: &'a str, @@ -600,6 +704,7 @@ impl TextEngine { cell_span: span, face, style, + presentation, }); cell = cell.saturating_add(span); } @@ -691,6 +796,7 @@ impl TextEngine { graphemes: &[Grapheme<'_>], face_id: ID, style: FontStyle, + grid_emoji: bool, output: &mut Vec, used_definitions: &mut HashSet, ) -> Result<()> { @@ -721,6 +827,13 @@ impl TextEngine { 1.0 }; let mut pen_x = first.cell_start as f32 * metrics.cell_width_px; + let mut emoji_positions = HashMap::::new(); + if grid_emoji { + for glyph in &shaped { + emoji_positions.entry(glyph.cluster).or_default().0 += + glyph.x_advance as f32 / 64.0; + } + } let mut cluster_starts: Vec = shaped.iter().map(|glyph| glyph.cluster).collect(); cluster_starts.sort_unstable(); @@ -746,10 +859,28 @@ impl TextEngine { .map(|grapheme| grapheme.cell_start) .unwrap_or_else(|| last.cell_start.saturating_add(last.cell_span)); let cluster_span = cluster_end_cell.saturating_sub(cluster_cell_start).max(1); + let (glyph_x, scale_x) = if grid_emoji { + // Legacy width mode may reserve four (or more) columns for + // one ZWJ emoji. Preserve its combined artwork at at most two + // columns, then anchor the next cluster at ITS host column. + // Breaking the ZWJ into separate shaping runs would change + // the symbol; scaling the whole run would stretch it. + let (advance, offset) = emoji_positions.get_mut(&glyph.cluster).unwrap(); + let scale = if advance.abs() > f32::EPSILON { + f32::from(cluster_span.min(2)) * metrics.cell_width_px / *advance + } else { + 1.0 + }; + let x = f32::from(cluster_cell_start) * metrics.cell_width_px + *offset * scale; + *offset += glyph.x_advance as f32 / 64.0; + (x, scale) + } else { + (pen_x, run_scale_x) + }; if let Some((glyph_id, definition)) = self.rasterize(&loaded, glyph.glyph_id, style)? { - let x = pen_x - + glyph.x_offset as f32 / 64.0 * run_scale_x - + definition.bearing_x as f32 / raster_scale * run_scale_x; + let x = glyph_x + + glyph.x_offset as f32 / 64.0 * scale_x + + definition.bearing_x as f32 / raster_scale * scale_x; let y = metrics.baseline_px - glyph.y_offset as f32 / 64.0 - definition.bearing_y as f32 / raster_scale; @@ -758,7 +889,7 @@ impl TextEngine { style_id: style_id(style), x, y, - width: definition.width as f32 / raster_scale * run_scale_x, + width: definition.width as f32 / raster_scale * scale_x, height: definition.height as f32 / raster_scale, cell_start: cluster_cell_start, cell_span: cluster_span, @@ -988,10 +1119,11 @@ fn style_id(style: FontStyle) -> u32 { u32::from(style.bold) | (u32::from(style.italic) << 1) } -fn shaped_row_hash(text: &str, spans: &[StyleSpan]) -> u64 { +fn shaped_row_hash(text: &str, spans: &[StyleSpan], cells: Option<&[TerminalCellSpan]>) -> u64 { let mut hasher = DefaultHasher::new(); text.hash(&mut hasher); spans.hash(&mut hasher); + cells.hash(&mut hasher); hasher.finish() } @@ -1020,7 +1152,7 @@ mod tests { FontResource::new("test-monospace", bytes).with_face_index(index as usize) } - fn presentation_test_engine() -> TextEngine { + pub(super) fn presentation_test_engine() -> TextEngine { let mut fonts = FontResources::new(FontResource::new( "JetBrainsMonoNerdFont-Regular.ttf", include_bytes!( @@ -1074,6 +1206,87 @@ mod tests { assert!(engine.fallback_for("a", FontPresentation::Text).is_some()); } + #[test] + fn terminal_layout_preserves_missing_glyph_width_and_separates_cached_host_layouts() { + let mut engine = presentation_test_engine(); + let text = "\u{1f6d9}X"; + // Unicode 17 and 18 hosts disagree about the new character's width. + // Neither bundled emoji font has artwork yet; the next cell still + // belongs at the position sent by that host, including on cache hits. + assert_eq!( + engine.fallback_for("\u{1f6d9}", FontPresentation::Emoji), + None + ); + for width in [1, 2, 1, 2, 2, 1] { + let cells = [ + TerminalCellSpan { + byte_start: 0, + byte_end: 4, + column: 0, + span: width, + style: FontStyle::default(), + }, + TerminalCellSpan { + byte_start: 4, + byte_end: 5, + column: width, + span: 1, + style: FontStyle::default(), + }, + ]; + let shaped = engine.shape_terminal_row(text, &cells).unwrap(); + assert!( + shaped + .glyphs + .iter() + .any(|glyph| glyph.cell_start == width && glyph.cell_span == 1) + ); + assert!( + shaped + .glyphs + .iter() + .any(|glyph| glyph.cell_start == 0 && glyph.cell_span == width) + ); + } + } + + #[test] + fn terminal_layout_keeps_grid_gaps_and_rejects_invalid_utf8_ranges() { + let mut engine = presentation_test_engine(); + let mut cells = [ + TerminalCellSpan { + byte_start: 0, + byte_end: 1, + column: 0, + span: 1, + style: FontStyle::default(), + }, + TerminalCellSpan { + byte_start: 1, + byte_end: 2, + column: 5, + span: 1, + style: FontStyle::default(), + }, + ]; + let shaped = engine.shape_terminal_row("XX", &cells).unwrap(); + assert_eq!( + shaped + .glyphs + .iter() + .map(|g| g.cell_start) + .collect::>(), + [0, 5] + ); + assert!(shaped.glyphs[1].x >= 5.0 * engine.metrics().cell_width_px); + assert!(engine.shape_terminal_row("é", &cells).is_err()); + cells[1].column = 0; + assert!(engine.shape_terminal_row("XX", &cells).is_err()); + cells[1].column = 1; + cells[1].span = 0; + assert!(engine.shape_terminal_row("XX", &cells).is_err()); + } + #[test] fn resolves_unicode_and_explicit_presentation_selectors() { assert_eq!(cluster_presentation("\u{23f8}"), FontPresentation::Text); @@ -1349,3 +1562,136 @@ mod tests { assert_eq!(engine.shaped_row_count, 1); } } + +#[cfg(test)] +mod terminal_emoji_boundaries { + use super::*; + + #[test] + fn emoji_joining_respects_host_cell_boundaries_without_stretching() { + let mut engine = super::tests::presentation_test_engine(); + let text = "👩\u{200d}💻X"; + let style = FontStyle::default(); + // Legacy width accounting keeps the two emoji in separate cells. + let split = [ + TerminalCellSpan { + byte_start: 0, + byte_end: 7, + column: 0, + span: 2, + style, + }, + TerminalCellSpan { + byte_start: 7, + byte_end: 11, + column: 2, + span: 2, + style, + }, + TerminalCellSpan { + byte_start: 11, + byte_end: 12, + column: 4, + span: 1, + style, + }, + ]; + let row = engine.shape_terminal_row(text, &split).unwrap(); + assert!( + row.glyphs + .iter() + .all(|glyph| glyph.width <= 2.0 * engine.metrics().cell_width_px + 1.0), + "a joined emoji must not stretch across distinct terminal cells: {:?}", + row.glyphs + ); + assert!( + row.glyphs + .iter() + .any(|glyph| glyph.cell_start == 4 && glyph.cell_span == 1) + ); + assert_eq!( + row.glyphs.len(), + 2, + "the ZWJ must remain one combined emoji plus X" + ); + let split_emoji = row.glyphs[0]; + // Mode 2027 hosts give the entire ZWJ sequence one two-column cell. + let joined = [ + TerminalCellSpan { + byte_start: 0, + byte_end: 11, + column: 0, + span: 2, + style, + }, + TerminalCellSpan { + byte_start: 11, + byte_end: 12, + column: 2, + span: 1, + style, + }, + ]; + let row = engine.shape_terminal_row(text, &joined).unwrap(); + assert!( + row.glyphs + .iter() + .any(|glyph| glyph.cell_start == 0 && glyph.cell_span == 2) + ); + assert!( + row.glyphs + .iter() + .any(|glyph| glyph.cell_start == 2 && glyph.cell_span == 1) + ); + assert_eq!(row.glyphs[0].glyph_id, split_emoji.glyph_id); + assert_eq!(row.glyphs[0].width, split_emoji.width); + assert_eq!(row.glyphs[0].height, split_emoji.height); + let adjacent = engine + .shape_terminal_row( + "👩‍💻😀X", + &[ + TerminalCellSpan { + byte_start: 0, + byte_end: 7, + column: 0, + span: 2, + style, + }, + TerminalCellSpan { + byte_start: 7, + byte_end: 11, + column: 2, + span: 2, + style, + }, + TerminalCellSpan { + byte_start: 11, + byte_end: 15, + column: 4, + span: 2, + style, + }, + TerminalCellSpan { + byte_start: 15, + byte_end: 16, + column: 6, + span: 1, + style, + }, + ], + ) + .unwrap(); + let second_emoji = adjacent + .glyphs + .iter() + .find(|glyph| glyph.cell_start == 4) + .unwrap(); + assert!((second_emoji.x - 4.0 * engine.metrics().cell_width_px).abs() < 1.0); + assert!( + adjacent + .glyphs + .iter() + .any(|glyph| glyph.cell_start == 6 && glyph.cell_span == 1) + ); + } +} diff --git a/native/ghosttea/crates/ghosttea-truffle/Cargo.toml b/native/ghosttea/crates/ghosttea-truffle/Cargo.toml index 4dc349fa..a9ae0818 100644 --- a/native/ghosttea/crates/ghosttea-truffle/Cargo.toml +++ b/native/ghosttea/crates/ghosttea-truffle/Cargo.toml @@ -22,8 +22,8 @@ required-features = ["interop-fixture"] [dependencies] anyhow.workspace = true async-trait.workspace = true -ghosttea = { version = "0.13.8", path = "../.." } -ghosttea-text = { version = "0.13.8", path = "../ghosttea-text" } +ghosttea = { version = "0.14.0", path = "../.." } +ghosttea-text = { version = "0.14.0", path = "../ghosttea-text" } rand.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/native/ghosttea/crates/ghosttea-vt-sys/artifacts.json b/native/ghosttea/crates/ghosttea-vt-sys/artifacts.json index 81822052..ec026c6a 100644 --- a/native/ghosttea/crates/ghosttea-vt-sys/artifacts.json +++ b/native/ghosttea/crates/ghosttea-vt-sys/artifacts.json @@ -2,40 +2,56 @@ "schemaVersion": 1, "source": { "repository": "https://github.com/ghostty-org/ghostty.git", - "commit": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69", + "commit": "83fb95624b0439ee660c2679b43b38c8b63769cd", "patches": [ { "path": "native/patches/ghostty-vt-g15-g21.patch", - "sha256": "c33a0d43191760d5a816f7b2553c45f5bae5f010dfa075c5db645a85b59c4842" + "sha256": "d5942d6ad16bf402f1a574a158ed1e081566dcc857b6557104daf874f4557a82" }, { "path": "native/patches/ghostty-vt-msvc-runtime.patch", - "sha256": "42235a49af44573401208c98fc145f2274bf1642f6b0ab55ffc93d8ee8376205" + "sha256": "ff22fe161179669320cb68a5c5e42c5549c7f0e47419cef6f458f43c1c61df93" } ], - "diffSha256": "107cd162e53350f3a9dede4565a8cc1cedb073d2670df2ec35b1a558cc832981", + "diffSha256": "63a9c60e8f5a1bf7707ea2a6f836d38cc1c6251883d748819fb0987a22c0a523", "buildRecipe": { "zig": { - "version": "0.15.2", - "linuxAarch64Url": "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz", - "linuxAarch64Sha256": "958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f", - "macosAarch64Url": "https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz", - "macosAarch64Sha256": "3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b", - "windowsX8664Url": "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip", - "windowsX8664Sha256": "3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" + "version": "0.17.0", + "linuxAarch64Url": "https://ziglang.org/download/0.17.0/zig-aarch64-linux-0.17.0.tar.xz", + "linuxAarch64Sha256": "9e8d11661d4ae3bd57702a3832781e23ad151dde5798e16a5ccd503f65234ff8", + "macosAarch64Url": "https://ziglang.org/download/0.17.0/zig-aarch64-macos-0.17.0.tar.xz", + "macosAarch64Sha256": "b607e9b9234790a008116ae5bdb71c6243b84b9fb42a53a9e70fde41c06c536a", + "windowsX8664Url": "https://ziglang.org/download/0.17.0/zig-x86_64-windows-0.17.0.zip", + "windowsX8664Sha256": "b5663f69581dcf391293fbf16c06cb80d81d806545ce618b4d0bab7f0eb8c428" }, "builder": { "image": "debian@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818", "platform": "linux/arm64", "zigBuildJobs": 1, - "zigBuildSeed": 0, "normalizer": { "runner": "macos-26", "developerDirectory": "/Applications/Xcode_26.6.app/Contents/Developer", "xcodeVersion": "26.6", "xcodeBuild": "17F113", - "stripFlags": ["-S"] - } + "stripFlags": [ + "-S" + ], + "libSystemOverride": { + "path": "native/normalizers/ghostty-libsystem-override.sh", + "sha256": "e4f84e42c29e90744e50af5bcfabc7514638ddc01bcdcf5afe3700f93e4dd60f" + } + }, + "zigOptimize": "fast", + "zigPackageDirectory": "packages", + "windowsPkgConfig": { + "package": "pkgconfiglite", + "version": "0.28" + }, + "msvcRuntimeWitnesses": [ + "ghosttea-msvc-simd.obj", + "ghosttea-msvc-simdutf.obj", + "ghosttea-msvc-highway.obj" + ] }, "targets": { "aarch64-apple-darwin": { @@ -65,47 +81,47 @@ } } }, - "digest": "1dceb4dd5089652bd3fd07974cd7663cddde0653074bc73d70c2b2e65c779616" + "digest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7" }, "targets": { "aarch64-apple-darwin": { - "sourceDigest": "1dceb4dd5089652bd3fd07974cd7663cddde0653074bc73d70c2b2e65c779616", - "release": "ghostty-vt-1dceb4dd5089", - "filename": "ghostty-vt-1dceb4dd5089-aarch64-apple-darwin.tar", - "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghostty-vt-1dceb4dd5089/ghostty-vt-1dceb4dd5089-aarch64-apple-darwin.tar", - "sha256": "dd29f5ea46b4a7ebd90012b9e1f9230c0b88529bdbf7f9ea54c70eb8b1fc5755", - "size": 2497024, + "sourceDigest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7", + "release": "ghostty-vt-b2a959db6a0d", + "filename": "ghostty-vt-b2a959db6a0d-aarch64-apple-darwin.tar", + "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghostty-vt-b2a959db6a0d/ghostty-vt-b2a959db6a0d-aarch64-apple-darwin.tar", + "sha256": "b7d95901b38bf490a4f9808c019b91df9731d43913e880cb5e4c4df7218b6b7b", + "size": 3679232, "libraryPath": "lib/libghostty-vt.a", - "librarySha256": "54525d46e0563d8adfc714dece62e9fd1e75f55597f549926f8cbfa10ae7851b", - "headersSha256": "e4de5535809e4fb95b857febf9805e767932f8e99f8705c89f7998a484366342", + "librarySha256": "0844e97d76ab520b560cd0a9ef7c33403f307dbdf50167d26ad09a43775fb4dd", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", "reproducible": false, - "candidateRunId": 40385660334 + "candidateRunId": 40972570187 }, "x86_64-pc-windows-msvc": { - "sourceDigest": "1dceb4dd5089652bd3fd07974cd7663cddde0653074bc73d70c2b2e65c779616", - "release": "ghostty-vt-1dceb4dd5089", - "filename": "ghostty-vt-1dceb4dd5089-x86_64-pc-windows-msvc.tar", - "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghostty-vt-1dceb4dd5089/ghostty-vt-1dceb4dd5089-x86_64-pc-windows-msvc.tar", - "sha256": "36f61e496fa43bb8012384fd50a9c3eefebb1b4cf0d89ec4d2ca9afca03dfc48", - "size": 9510400, + "sourceDigest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7", + "release": "ghostty-vt-b2a959db6a0d", + "filename": "ghostty-vt-b2a959db6a0d-x86_64-pc-windows-msvc.tar", + "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghostty-vt-b2a959db6a0d/ghostty-vt-b2a959db6a0d-x86_64-pc-windows-msvc.tar", + "sha256": "b8f364d60521f13f8fd8b3ec361b11585985e00542ca9bb2019cac6503402ea0", + "size": 13627392, "libraryPath": "lib/ghostty-vt-static.lib", - "librarySha256": "fbf782697f7ac12dd45415e518b4af1a375331cdf9ae9be1719649c9da75a7ba", - "headersSha256": "e4de5535809e4fb95b857febf9805e767932f8e99f8705c89f7998a484366342", + "librarySha256": "99da659f3a7f5d495700c1f527b8f7e8a5dc5b6e624fc102aff82c8b1420f834", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", "reproducible": false, - "candidateRunId": 40385660334 + "candidateRunId": 40972570187 }, "x86_64-pc-windows-msvc-crt-static": { - "sourceDigest": "1dceb4dd5089652bd3fd07974cd7663cddde0653074bc73d70c2b2e65c779616", - "release": "ghostty-vt-1dceb4dd5089", - "filename": "ghostty-vt-1dceb4dd5089-x86_64-pc-windows-msvc-crt-static.tar", - "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghostty-vt-1dceb4dd5089/ghostty-vt-1dceb4dd5089-x86_64-pc-windows-msvc-crt-static.tar", - "sha256": "3b9bd9cd484a261e0007170643730fe3c73ef8cf9c7a7916b17f3366e46cb05d", - "size": 9510400, + "sourceDigest": "b2a959db6a0db555b42ae84358a9680a8c6c5a3c4b9a9050eb0812baa215edb7", + "release": "ghostty-vt-b2a959db6a0d", + "filename": "ghostty-vt-b2a959db6a0d-x86_64-pc-windows-msvc-crt-static.tar", + "url": "https://github.com/vibecook-dev/ghosttea/releases/download/ghostty-vt-b2a959db6a0d/ghostty-vt-b2a959db6a0d-x86_64-pc-windows-msvc-crt-static.tar", + "sha256": "0249b96bec6f04b29bba09637c7cfdc375b4e05754e0162ee22128c4c5a475c7", + "size": 13628928, "libraryPath": "lib/ghostty-vt-static.lib", - "librarySha256": "089bd69e28f91e986f6c0b954ca47ce345e63f98f6c81e1ed8bc584b33596280", - "headersSha256": "e4de5535809e4fb95b857febf9805e767932f8e99f8705c89f7998a484366342", + "librarySha256": "94c149ed42fb8ea5d18c3717821b4c713af41dc77181d33fd167b58929d524ca", + "headersSha256": "0df7672281d665c37bddb962a12c49d93609dd531bea87adcf30b140991a785b", "reproducible": false, - "candidateRunId": 40385660334 + "candidateRunId": 40972570187 } } } diff --git a/native/ghosttea/crates/ghosttea-vt-sys/src/ghostty_shim.c b/native/ghosttea/crates/ghosttea-vt-sys/src/ghostty_shim.c index 2b8ecc4a..989e2c26 100644 --- a/native/ghosttea/crates/ghosttea-vt-sys/src/ghostty_shim.c +++ b/native/ghosttea/crates/ghosttea-vt-sys/src/ghostty_shim.c @@ -10,6 +10,17 @@ struct EgTrackedSelection { bool rectangle; }; +static GhosttyResult eg_mode_get(GhosttyTerminal terminal, + GhosttyMode mode, + bool* out_value) { + if (out_value == NULL) return GHOSTTY_INVALID_VALUE; + GhosttyTerminalModeConfig config = {.mode = mode, .value = false}; + GhosttyResult result = ghostty_terminal_get( + terminal, GHOSTTY_TERMINAL_DATA_MODE, &config); + if (result == GHOSTTY_SUCCESS) *out_value = config.value; + return result; +} + static double eg_srgb_to_linear(uint8_t value) { double v = value / 255.0; return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4); @@ -59,7 +70,7 @@ int eg_terminal_set_colors(EgTerminal* state, if (scheme == state->color_scheme) return GHOSTTY_SUCCESS; state->color_scheme = scheme; bool reporting = false; - if (ghostty_terminal_mode_get( + if (eg_mode_get( state->terminal, GHOSTTY_MODE_COLOR_SCHEME_REPORT, &reporting) != GHOSTTY_SUCCESS || !reporting) return GHOSTTY_SUCCESS; @@ -149,7 +160,7 @@ static void eg_pwd_changed(GhosttyTerminal terminal, void* userdata) { ((EgTerminal*)userdata)->effects |= EG_EFFECT_PWD; } -static GhosttyClipboardWriteResult eg_clipboard_write( +static GhosttyClipboardWriteResult eg_clipboard_write_result( GhosttyTerminal terminal, void* userdata, const GhosttyClipboardWrite* write) { @@ -160,6 +171,9 @@ static GhosttyClipboardWriteResult eg_clipboard_write( state->effects |= EG_EFFECT_CLIPBOARD; return GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS; } + // Ghosttea currently accepts one text representation. Do not acknowledge + // an atomic multi-format request after retaining only one representation. + if (write->contents_len != 1) return GHOSTTY_CLIPBOARD_WRITE_RESULT_UNSUPPORTED; const GhosttyClipboardContent* selected = NULL; for (size_t index = 0; index < write->contents_len; index++) { const GhosttyClipboardContent* content = &write->contents[index]; @@ -179,15 +193,24 @@ static GhosttyClipboardWriteResult eg_clipboard_write( return GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS; } +static void eg_clipboard_write(GhosttyTerminal terminal, + void* userdata, + const GhosttyClipboardWrite* write) { + if (write == NULL || + write->size < offsetof(GhosttyClipboardWrite, reply) + sizeof(write->reply) || + write->reply == NULL) return; + GhosttyClipboardWriteReply reply = GHOSTTY_INIT_SIZED(GhosttyClipboardWriteReply); + reply.result = eg_clipboard_write_result(terminal, userdata, write); + reply.remember = false; + write->reply(write, &reply); +} + EgTerminal* eg_terminal_new(uint16_t cols, uint16_t rows, size_t max_scrollback) { EgTerminal* state = calloc(1, sizeof(EgTerminal)); if (state == NULL) return NULL; - GhosttyTerminalOptions options = { - .cols = cols, - .rows = rows, - .max_scrollback = max_scrollback, - }; - if (ghostty_terminal_new(NULL, &state->terminal, options) != GHOSTTY_SUCCESS) goto fail; + if (ghostty_terminal_new(NULL, &state->terminal, cols, rows) != GHOSTTY_SUCCESS) goto fail; + if (ghostty_terminal_set(state->terminal, + GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES, &max_scrollback) != GHOSTTY_SUCCESS) goto fail; if (ghostty_render_state_new(NULL, &state->render) != GHOSTTY_SUCCESS) goto fail; if (ghostty_render_state_row_iterator_new(NULL, &state->rows) != GHOSTTY_SUCCESS) goto fail; if (ghostty_render_state_row_cells_new(NULL, &state->cells) != GHOSTTY_SUCCESS) goto fail; @@ -196,13 +219,20 @@ EgTerminal* eg_terminal_new(uint16_t cols, uint16_t rows, size_t max_scrollback) if (ghostty_mouse_encoder_new(NULL, &state->mouse_encoder) != GHOSTTY_SUCCESS) goto fail; if (ghostty_mouse_event_new(NULL, &state->mouse_event) != GHOSTTY_SUCCESS) goto fail; + // Check callback signatures before the generic option setter erases types. + GhosttyTerminalWritePtyFn write_pty = eg_write_pty; + GhosttyTerminalBellFn bell = eg_bell; + GhosttyTerminalTitleChangedFn title_changed = eg_title_changed; + GhosttyTerminalPwdChangedFn pwd_changed = eg_pwd_changed; + GhosttyTerminalClipboardWriteFn clipboard_write = eg_clipboard_write; + GhosttyTerminalColorSchemeFn color_scheme = eg_color_scheme; ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_USERDATA, state); - ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, (const void*)eg_write_pty); - ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_BELL, (const void*)eg_bell); - ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, (const void*)eg_title_changed); - ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_PWD_CHANGED, (const void*)eg_pwd_changed); - ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE, (const void*)eg_clipboard_write); - ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_COLOR_SCHEME, (const void*)eg_color_scheme); + ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, (const void*)write_pty); + ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_BELL, (const void*)bell); + ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, (const void*)title_changed); + ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_PWD_CHANGED, (const void*)pwd_changed); + ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE, (const void*)clipboard_write); + ghostty_terminal_set(state->terminal, GHOSTTY_TERMINAL_OPT_COLOR_SCHEME, (const void*)color_scheme); if (eg_terminal_set_colors( state, 255, 255, 255, @@ -220,7 +250,10 @@ EgTerminal* eg_terminal_new(uint16_t cols, uint16_t rows, size_t max_scrollback) bool default_cursor_blink = true; ghostty_terminal_set( state->terminal, GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_BLINK, &default_cursor_blink); - ghostty_terminal_mode_set(state->terminal, GHOSTTY_MODE_CURSOR_BLINKING, true); + GhosttyTerminalModeConfig cursor_blink = { + .mode = GHOSTTY_MODE_CURSOR_BLINKING, .value = true}; + if (ghostty_terminal_set(state->terminal, + GHOSTTY_TERMINAL_OPT_MODE, &cursor_blink) != GHOSTTY_SUCCESS) goto fail; return state; fail: @@ -261,7 +294,7 @@ int eg_terminal_encode_paste(EgTerminal* state, if (state == NULL || out_len == NULL || (data == NULL && data_len != 0) || (out == NULL && cap != 0)) return GHOSTTY_INVALID_VALUE; bool bracketed = false; - GhosttyResult result = ghostty_terminal_mode_get( + GhosttyResult result = eg_mode_get( state->terminal, GHOSTTY_MODE_BRACKETED_PASTE, &bracketed); if (result != GHOSTTY_SUCCESS) return result; char* copy = data_len == 0 ? NULL : malloc(data_len); @@ -334,7 +367,7 @@ bool eg_terminal_alternate_scroll(EgTerminal* state) { if (ghostty_terminal_get( state->terminal, GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN, &screen) != GHOSTTY_SUCCESS) return false; - if (ghostty_terminal_mode_get( + if (eg_mode_get( state->terminal, GHOSTTY_MODE_ALT_SCROLL, &alternate_scroll) != GHOSTTY_SUCCESS) return false; return screen == GHOSTTY_TERMINAL_SCREEN_ALTERNATE && alternate_scroll; @@ -346,7 +379,7 @@ int eg_terminal_mode_get_raw(EgTerminal* state, bool* out_value) { if (state == NULL || out_value == NULL || value > 0x7fff) return GHOSTTY_INVALID_VALUE; - return ghostty_terminal_mode_get( + return eg_mode_get( state->terminal, ghostty_mode_new(value, ansi), out_value); } @@ -1176,7 +1209,7 @@ int eg_terminal_encode_focus(EgTerminal* state, size_t* out_len) { if (state == NULL || out_len == NULL) return GHOSTTY_INVALID_VALUE; bool reporting = false; - if (ghostty_terminal_mode_get( + if (eg_mode_get( state->terminal, GHOSTTY_MODE_FOCUS_EVENT, &reporting) != GHOSTTY_SUCCESS || !reporting) { *out_len = 0; diff --git a/native/ghosttea/crates/ghosttea-vt/Cargo.toml b/native/ghosttea/crates/ghosttea-vt/Cargo.toml index ba3df533..7195aa4b 100644 --- a/native/ghosttea/crates/ghosttea-vt/Cargo.toml +++ b/native/ghosttea/crates/ghosttea-vt/Cargo.toml @@ -10,4 +10,4 @@ description = "Safe Rust adapter for Ghosttea's Ghostty VT core" readme = "README.md" [dependencies] -ghosttea-vt-sys = { version = "0.13.8", path = "../ghosttea-vt-sys" } +ghosttea-vt-sys = { version = "0.14.0", path = "../ghosttea-vt-sys" } diff --git a/native/ghosttea/crates/ghosttea-vt/src/lib.rs b/native/ghosttea/crates/ghosttea-vt/src/lib.rs index 04b17e39..88f33607 100644 --- a/native/ghosttea/crates/ghosttea-vt/src/lib.rs +++ b/native/ghosttea/crates/ghosttea-vt/src/lib.rs @@ -2557,12 +2557,17 @@ mod tests { assert_eq!(restored.scrollbar, bottom.scrollbar); } - /// Ghostty performs strict page reclamation only on 64-bit Linux and - /// Darwin (`src/terminal/mem.zig`). Elsewhere full compression reports + /// Ghostty performs strict page reclamation on 64-bit Linux, Darwin, + /// Windows and FreeBSD (`src/terminal/mem.zig`). Elsewhere full compression reports /// unsupported and leaves scrollback memory resident. const SCROLLBACK_RECLAIM_SUPPORTED: bool = cfg!(all( target_pointer_width = "64", - any(target_os = "linux", target_vendor = "apple") + any( + target_os = "linux", + target_vendor = "apple", + target_os = "windows", + target_os = "freebsd" + ) )); #[test] diff --git a/native/ghosttea/crates/ghosttea-vt/tests/ghostty_14.rs b/native/ghosttea/crates/ghosttea-vt/tests/ghostty_14.rs new file mode 100644 index 00000000..c10a9fe2 --- /dev/null +++ b/native/ghosttea/crates/ghosttea-vt/tests/ghostty_14.rs @@ -0,0 +1,43 @@ +use ghosttea_vt::GhosttyTerminalCore; +#[test] +fn kitty_clipboard_write_replies_synchronously() { + let mut t = GhosttyTerminalCore::new(80, 24, 1_000_000).unwrap(); + t.feed(b"\x1b]5522;type=write:id=q1\x1b\\\x1b]5522;type=wdata:mime=dGV4dC9wbGFpbg==;aGVsbG8=\x1b\\\x1b]5522;type=wdata\x1b\\"); + let snapshot = t.snapshot().unwrap(); + assert_eq!(snapshot.clipboard, Some(b"hello".to_vec())); + assert_eq!( + snapshot.pty_response, + b"\x1b]5522;type=write:status=DONE:id=q1\x1b\\" + ); +} +#[test] +fn unsupported_atomic_clipboard_request_is_not_partially_committed() { + let mut t = GhosttyTerminalCore::new(80, 24, 1_000_000).unwrap(); + t.feed(b"\x1b]5522;type=write:id=q2\x1b\\\x1b]5522;type=wdata:mime=dGV4dC9wbGFpbg==;aGVsbG8=\x1b\\\x1b]5522;type=wdata:mime=dGV4dC9odG1s;PGI+aGk8L2I+\x1b\\\x1b]5522;type=wdata\x1b\\"); + let snapshot = t.snapshot().unwrap(); + assert_eq!(snapshot.clipboard, None); + assert_eq!( + snapshot.pty_response, + b"\x1b]5522;type=write:status=ENOSYS:id=q2\x1b\\" + ); +} +#[test] +fn resize_repairs_live_pending_wrap_before_next_character() { + let mut t = GhosttyTerminalCore::new(10, 4, 1_000_000).unwrap(); + t.feed(b"123456789|"); + assert!(t.pending_wrap().unwrap()); + t.resize(8, 4).unwrap(); + t.feed(b"X"); + let snapshot = t.snapshot().unwrap(); + assert_eq!(snapshot.rows[0], "12345678"); + assert_eq!(snapshot.rows[1], "9|X"); + assert_eq!((snapshot.cursor.x, snapshot.cursor.y), (3, 1)); +} +#[test] +fn cancelled_osc_does_not_publish_a_title() { + let mut t = GhosttyTerminalCore::new(80, 24, 1_000_000).unwrap(); + t.feed(b"before\x1b]2;discard\x18after"); + let snapshot = t.snapshot().unwrap(); + assert_eq!(snapshot.title, None); + assert_eq!(snapshot.rows[0], "beforeafter"); +} diff --git a/native/ghosttea/fixtures/phase2/font-parity.json b/native/ghosttea/fixtures/phase2/font-parity.json index 8587b72a..4e5dfb3c 100644 --- a/native/ghosttea/fixtures/phase2/font-parity.json +++ b/native/ghosttea/fixtures/phase2/font-parity.json @@ -38,7 +38,7 @@ "role": "fallback-0", "family": "Noto Color Emoji", "faceIndex": 0, - "sha256": "bf2a8506b80614ba190a34c7b037af1269a7d614fe9f3b613cc15cdeec6f814b" + "sha256": "72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b" }, { "role": "fallback-1", @@ -56,7 +56,7 @@ "role": "fallback-3", "family": "Noto Emoji", "faceIndex": 0, - "sha256": "84a38f2f02374efb72cac2b7eb5589102e06fffc33f7b531ed708f4a08236c91" + "sha256": "cd07743a4c93d8da929afdd28c1d368f11da478a1093146a13610081c2a58440" } ], "cases": { @@ -83,12 +83,12 @@ "emoji-fallback": { "glyphCount": 9, "definitionCount": 9, - "sha256": "00247df6157df4b8e1f2c09380f68b66f990108964d2c89280bcbd6b147d1f61" + "sha256": "e7c0afd6aae4c49e0d58653427346f4088810bd8807b247021e58feabbb1b1d5" }, "emoji-presentation": { "glyphCount": 4, "definitionCount": 3, - "sha256": "9d1d5ba7323a77090d1ebbe26d71c5f712e4e21274f8a3a8217b50d10ede17ef" + "sha256": "8460b1ccad4691ee6363560ce62fbb1508c29c4e9a408c15213e5eb96108b3be" }, "italic": { "glyphCount": 14, diff --git a/native/ghosttead/Cargo.toml b/native/ghosttead/Cargo.toml index 580c9920..faf87f90 100644 --- a/native/ghosttead/Cargo.toml +++ b/native/ghosttead/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] anyhow.workspace = true dotenvy.workspace = true -ghosttea = { version = "0.13.8", path = "../ghosttea" } -ghosttea-truffle = { version = "0.13.8", path = "../ghosttea/crates/ghosttea-truffle" } +ghosttea = { version = "0.14.0", path = "../ghosttea" } +ghosttea-truffle = { version = "0.14.0", path = "../ghosttea/crates/ghosttea-truffle" } tokio.workspace = true truffle-core.workspace = true diff --git a/native/ghostty-config.lock.json b/native/ghostty-config.lock.json index 2fd6b46e..3e160a79 100644 --- a/native/ghostty-config.lock.json +++ b/native/ghostty-config.lock.json @@ -49,5 +49,9 @@ "background": [40, 44, 52], "paddingX": [2, 2], "paddingY": [2, 2] + }, + "uxGroundTruth": { + "cliVersion": "1.3.1", + "vendorCommit": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69" } } diff --git a/native/ghostty.lock.json b/native/ghostty.lock.json index 21d2c0b4..46eeafe1 100644 --- a/native/ghostty.lock.json +++ b/native/ghostty.lock.json @@ -1,42 +1,52 @@ { "ghostty": { "repository": "https://github.com/ghostty-org/ghostty.git", - "commit": "f8041e849b36efbbb9736b6ecf0ccfcb01d94e69" + "commit": "83fb95624b0439ee660c2679b43b38c8b63769cd" }, "vtPatchSet": { - "diffSha256": "107cd162e53350f3a9dede4565a8cc1cedb073d2670df2ec35b1a558cc832981", + "diffSha256": "63a9c60e8f5a1bf7707ea2a6f836d38cc1c6251883d748819fb0987a22c0a523", "patches": [ { "path": "native/patches/ghostty-vt-g15-g21.patch", - "sha256": "c33a0d43191760d5a816f7b2553c45f5bae5f010dfa075c5db645a85b59c4842" + "sha256": "d5942d6ad16bf402f1a574a158ed1e081566dcc857b6557104daf874f4557a82" }, { "path": "native/patches/ghostty-vt-msvc-runtime.patch", - "sha256": "42235a49af44573401208c98fc145f2274bf1642f6b0ab55ffc93d8ee8376205" + "sha256": "ff22fe161179669320cb68a5c5e42c5549c7f0e47419cef6f458f43c1c61df93" } ] }, "zig": { - "version": "0.15.2", - "linuxAarch64Url": "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz", - "linuxAarch64Sha256": "958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f", - "macosAarch64Url": "https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz", - "macosAarch64Sha256": "3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b", - "windowsX8664Url": "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip", - "windowsX8664Sha256": "3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" + "version": "0.17.0", + "linuxAarch64Url": "https://ziglang.org/download/0.17.0/zig-aarch64-linux-0.17.0.tar.xz", + "linuxAarch64Sha256": "9e8d11661d4ae3bd57702a3832781e23ad151dde5798e16a5ccd503f65234ff8", + "macosAarch64Url": "https://ziglang.org/download/0.17.0/zig-aarch64-macos-0.17.0.tar.xz", + "macosAarch64Sha256": "b607e9b9234790a008116ae5bdb71c6243b84b9fb42a53a9e70fde41c06c536a", + "windowsX8664Url": "https://ziglang.org/download/0.17.0/zig-x86_64-windows-0.17.0.zip", + "windowsX8664Sha256": "b5663f69581dcf391293fbf16c06cb80d81d806545ce618b4d0bab7f0eb8c428" }, "builder": { "image": "debian@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818", "platform": "linux/arm64", "zigBuildJobs": 1, - "zigBuildSeed": 0, "normalizer": { "runner": "macos-26", "developerDirectory": "/Applications/Xcode_26.6.app/Contents/Developer", "xcodeVersion": "26.6", "xcodeBuild": "17F113", - "stripFlags": ["-S"] - } + "stripFlags": ["-S"], + "libSystemOverride": { + "path": "native/normalizers/ghostty-libsystem-override.sh", + "sha256": "e4f84e42c29e90744e50af5bcfabc7514638ddc01bcdcf5afe3700f93e4dd60f" + } + }, + "zigOptimize": "fast", + "zigPackageDirectory": "packages", + "windowsPkgConfig": { + "package": "pkgconfiglite", + "version": "0.28" + }, + "msvcRuntimeWitnesses": ["ghosttea-msvc-simd.obj", "ghosttea-msvc-simdutf.obj", "ghosttea-msvc-highway.obj"] }, "targets": { "aarch64-apple-darwin": { @@ -71,10 +81,10 @@ "appleBuilder": { "architecture": "arm64", "minimumIosVersion": "17.0", - "xcodeVersion": "26.3", - "xcodeBuild": "17C529", - "macosSdkVersion": "26.2", - "iphoneosSdkVersion": "26.2", - "iphonesimulatorSdkVersion": "26.2" + "xcodeVersion": "26.6", + "xcodeBuild": "17F113", + "macosSdkVersion": "26.5", + "iphoneosSdkVersion": "26.5", + "iphonesimulatorSdkVersion": "26.5" } } diff --git a/native/normalizers/ghostty-libsystem-override.sh b/native/normalizers/ghostty-libsystem-override.sh new file mode 100644 index 00000000..14f30187 --- /dev/null +++ b/native/normalizers/ghostty-libsystem-override.sh @@ -0,0 +1,134 @@ +#!/bin/sh +# +# Darwin-only: rewrite a static archive so that consumers linking it bind +# libc/libm symbols to Apple's libSystem instead of the bundled Zig +# compiler-rt. This uses the Apple toolchain (xcrun, nmedit) and libSystem +# semantics, so it must run on a Darwin host for a Darwin target. +# +# Background: our static library bundles Zig's compiler-rt, which defines +# strong global implementations of libc/libm functions (memcpy, memmove, +# memset, cos, sin, ...). When a consumer (e.g. the macOS app via the +# XCFramework) links this archive, ld64 resolves those symbols from the +# archive instead of libSystem, silently replacing Apple's highly +# optimized implementations (_platform_memmove and friends, vectorized +# libm) with compiler-rt's generic ones. We measured Zig 0.16 (LLVM 21) +# compiler-rt memmove costing several percent of PTY throughput on +# scroll-region workloads versus libSystem's. +# +# The mechanism is to localize (make non-external) the libSystem-provided +# symbols defined by the compiler_rt.o archive member. This keeps +# compiler_rt.o functional for the intrinsics that libSystem does NOT +# provide (e.g. the f128 conversions ___extenddftf2/___extendxftf2, *q +# math, sincos*) while letting every other archive member bind the +# well-known libc/libm symbols to libSystem at final link. +# +# usage: libsystem_override.sh +set -eu + +in="$1" +out="$2" + +# Zig 0.17 passes generated paths relative to the build root. Preserve +# their meaning when we change into the temporary directory below. +case "$in" in +/*) ;; +*) in="$PWD/$in" ;; +esac +case "$out" in +/*) ;; +*) out="$PWD/$out" ;; +esac + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +cp -f "$in" "$out" +chmod u+w "$out" + +# The symbols to prefer from libSystem. Everything listed here is a +# stable macOS export (verified against libSystem's link surface). +# Notably NOT listed (not exported by libSystem): the *q (f128) +# variants, sincos/sincosf/sincosl, and all ___-prefixed compiler +# intrinsics except the fortify _chk wrappers below. +cat >"$tmp/localize.txt" <<'EOF' +_bcmp +_memcmp +_memcpy +_memmove +_memset +_strlen +___memcpy_chk +___memmove_chk +___memset_chk +___strcat_chk +___strcpy_chk +_ceil +_ceilf +_ceill +_cos +_cosf +_cosl +_exp +_exp2 +_exp2f +_exp2l +_expf +_expl +_fabs +_fabsf +_fabsl +_floor +_floorf +_floorl +_fma +_fmaf +_fmal +_fmax +_fmaxf +_fmaxl +_fmin +_fminf +_fminl +_fmod +_fmodf +_fmodl +_log +_log10 +_log10f +_log10l +_log2 +_log2f +_log2l +_logf +_logl +_round +_roundf +_roundl +_sin +_sinf +_sinl +_sqrt +_sqrtf +_sqrtl +_tan +_tanf +_tanl +_trunc +_truncf +_truncl +EOF + +cd "$tmp" + +xcrun ar x "$out" compiler_rt.o +chmod 644 compiler_rt.o + +# nmedit takes a keep-list (-s): all current global definitions except +# the ones we want to localize. +xcrun nm -g compiler_rt.o | awk '$2 ~ /^[A-TV-Z]$/ {print $3}' | sort -u >all.txt +sort -u localize.txt >loc.txt +comm -23 all.txt loc.txt >keep.txt +xcrun nmedit -s keep.txt compiler_rt.o + +xcrun ar r "$out" compiler_rt.o +xcrun ranlib "$out" 2>/dev/null || true diff --git a/native/patches/ghostty-vt-g15-g21.patch b/native/patches/ghostty-vt-g15-g21.patch index d632221d..bd07d652 100644 --- a/native/patches/ghostty-vt-g15-g21.patch +++ b/native/patches/ghostty-vt-g15-g21.patch @@ -1,8 +1,8 @@ diff --git a/include/ghostty/vt/formatter.h b/include/ghostty/vt/formatter.h -index 5cdcd11a3a7aed8aa73f9c2104ba10df0391a503..6da16fe6133966fbc3459f1dbf755bd3df03ff2d 100644 +index 938fc4b39cd8d2df87645d611191a656d447bf5d..f0e7c3489d16b96f6d37bbe342504fc80fea8265 100644 --- a/include/ghostty/vt/formatter.h +++ b/include/ghostty/vt/formatter.h -@@ -139,0 +140,25 @@ GHOSTTY_API GhosttyResult ghostty_formatter_terminal_new( +@@ -168,0 +169,25 @@ GHOSTTY_API GhosttyResult ghostty_formatter_format( +/** + * Create a formatter that emits terminal state without screen content. + * @@ -75,10 +75,10 @@ index c43791dc238cf09b8c8488ef10c044e67eb44440..c463d9bd610947739cbee464609082d6 + GhosttyHyperlinkIdentity *out_identity); + diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h -index 26ee0c0b8670b4a7c81a4840480063b03bb320a7..28a54fd337d03c552cd8723c323a6460e9953dc3 100644 +index b5865565971796090e412ad8d02e838b672f08f9..eb368beb196a954637319e379a5da2958998fbb4 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h -@@ -319,0 +320,96 @@ typedef enum GHOSTTY_ENUM_TYPED { +@@ -400,0 +401,96 @@ typedef enum GHOSTTY_ENUM_TYPED { +/** Character set designation stored by a terminal screen. */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_TERMINAL_CHARSET_UTF8 = 0, @@ -175,7 +175,7 @@ index 26ee0c0b8670b4a7c81a4840480063b03bb320a7..28a54fd337d03c552cd8723c323a6460 + GhosttyTerminalSemanticClick semantic_prompt_click; +} GhosttyTerminalScreenState; + -@@ -1425,0 +1522,86 @@ GHOSTTY_API GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, +@@ -3641,0 +3738,86 @@ GHOSTTY_API GhosttyResult ghostty_terminal_compress( +/** + * Read the DECSC saved-cursor slot belonging to a specific screen. + * @@ -263,10 +263,10 @@ index 26ee0c0b8670b4a7c81a4840480063b03bb320a7..28a54fd337d03c552cd8723c323a6460 + GhosttyHyperlinkIdentity* out_identity); + diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h -index 214d282296c5f66aa29b4c9b183655f9c6793ac5..5d0def15cf39a65f72dcbc0174c58399a52c275c 100644 +index 465976898755efc2308f5287a3eaf81079c9e0e7..c053836002e3660f39e8feefea90cf4e9aedb4f1 100644 --- a/include/ghostty/vt/types.h +++ b/include/ghostty/vt/types.h -@@ -159,0 +160,10 @@ typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells; +@@ -211,0 +212,10 @@ typedef struct GhosttySearchImpl* GhosttySearch; +/** + * Opaque iterator over every retained row of one selected terminal screen. + * @@ -278,29 +278,29 @@ index 214d282296c5f66aa29b4c9b183655f9c6793ac5..5d0def15cf39a65f72dcbc0174c58399 +typedef struct GhosttyTerminalScreenRowIteratorImpl* GhosttyTerminalScreenRowIterator; + diff --git a/src/lib_vt.zig b/src/lib_vt.zig -index e01cdbb892c36cf88d4088c45bb464a429654d57..b1b2b10086b74980b9b1074b95a52d5ae241da17 100644 +index 06252080a2facbfc7593cd13ace3b16d1f8375b3..82771cfdb1bd77a9f3be93bf2a3e96114c4e6651 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig -@@ -230,0 +231 @@ comptime { -+ @export(&c.formatter_terminal_state_new, .{ .name = "ghostty_formatter_terminal_state_new" }); -@@ -269,0 +271,7 @@ comptime { -+ @export(&c.terminal_saved_cursor, .{ .name = "ghostty_terminal_saved_cursor" }); -+ @export(&c.terminal_screen_get, .{ .name = "ghostty_terminal_screen_get" }); -+ @export(&c.terminal_screen_grid_ref, .{ .name = "ghostty_terminal_screen_grid_ref" }); -+ @export(&c.terminal_screen_row_iterator_new, .{ .name = "ghostty_terminal_screen_row_iterator_new" }); -+ @export(&c.terminal_screen_row_iterator_free, .{ .name = "ghostty_terminal_screen_row_iterator_free" }); -+ @export(&c.terminal_screen_row_iterator_next, .{ .name = "ghostty_terminal_screen_row_iterator_next" }); -+ @export(&c.terminal_screen_cursor_hyperlink, .{ .name = "ghostty_terminal_screen_cursor_hyperlink" }); -@@ -311,0 +320 @@ comptime { -+ @export(&c.grid_ref_hyperlink_identity, .{ .name = "ghostty_grid_ref_hyperlink_identity" }); +@@ -259,0 +260,9 @@ comptime { ++ @export(&c.terminal_saved_cursor, .{ .name = "ghostty_terminal_saved_cursor" }); ++ @export(&c.terminal_screen_get, .{ .name = "ghostty_terminal_screen_get" }); ++ @export(&c.terminal_screen_grid_ref, .{ .name = "ghostty_terminal_screen_grid_ref" }); ++ @export(&c.terminal_screen_row_iterator_new, .{ .name = "ghostty_terminal_screen_row_iterator_new" }); ++ @export(&c.terminal_screen_row_iterator_free, .{ .name = "ghostty_terminal_screen_row_iterator_free" }); ++ @export(&c.terminal_screen_row_iterator_next, .{ .name = "ghostty_terminal_screen_row_iterator_next" }); ++ @export(&c.terminal_screen_cursor_hyperlink, .{ .name = "ghostty_terminal_screen_cursor_hyperlink" }); ++ @export(&c.grid_ref_hyperlink_identity, .{ .name = "ghostty_grid_ref_hyperlink_identity" }); ++ +@@ -287,0 +297 @@ comptime { ++ @export(&c.formatter_terminal_state_new, .{ .name = "ghostty_formatter_terminal_state_new" }); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig -index 6c4db3237c84ace7079e2f2eaa7b379580667d45..e07a98dd3ea48cef129e0cd147091397b55c059c 100644 +index 0fd016774df4d314ab4a4f939dec30e25de2088c..5bccc34b47d3db0fd559530ccd7d09a8cb2a593e 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig -@@ -1315 +1315 @@ pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct { +@@ -1530 +1530 @@ pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct { - // This shouldn't happen because startHyperlink should handle + // This shouldn't happen because startHyperlink handles -@@ -1394,6 +1394,5 @@ inline fn cursorChangePin(self: *Screen, new: Pin) void { +@@ -1614,6 +1614,5 @@ inline fn cursorChangePin(self: *Screen, new: Pin) void { - // Re-add - self.startHyperlink(link.uri, switch (link.id) { - .explicit => |v| v, @@ -312,7 +312,7 @@ index 6c4db3237c84ace7079e2f2eaa7b379580667d45..e07a98dd3ea48cef129e0cd147091397 + // live OSC 8 run merely because it crossed a page boundary. + self.startHyperlinkValue(link.*) catch |err| { + // This shouldn't happen because startHyperlinkValue handles -@@ -2440 +2439,10 @@ pub fn startHyperlink( +@@ -2868 +2867,10 @@ pub fn startHyperlink( - // Loop until we have enough page memory to add the hyperlink + return self.startHyperlinkValue(link); +} @@ -325,10 +325,10 @@ index 6c4db3237c84ace7079e2f2eaa7b379580667d45..e07a98dd3ea48cef129e0cd147091397 +) PageList.IncreaseCapacityError!void { + // Loop until we have enough page memory to add the hyperlink. diff --git a/src/terminal/c/formatter.zig b/src/terminal/c/formatter.zig -index 5b4504c8c8f2dd7767e2b3d55aaf42bcc7055617..6139976f1a41f9445cf4f4f41dad9081c225df7f 100644 +index 00239d6373191aa4a1cc33935bae9137b2bcf56e..d5c33f51032b835ec6401dc2467c66707eca91a0 100644 --- a/src/terminal/c/formatter.zig +++ b/src/terminal/c/formatter.zig -@@ -115,0 +116,26 @@ pub fn terminal_new( +@@ -116,0 +117,26 @@ pub fn terminal_new( + false, + ) catch |err| { + result.* = null; @@ -355,9 +355,9 @@ index 5b4504c8c8f2dd7767e2b3d55aaf42bcc7055617..6139976f1a41f9445cf4f4f41dad9081 + terminal_, + opts, + true, -@@ -130,0 +157 @@ fn terminal_new_( +@@ -131,0 +158 @@ fn terminal_new_( + state_only: bool, -@@ -150,4 +177,8 @@ fn terminal_new_( +@@ -151,4 +178,8 @@ fn terminal_new_( - if (opts.selection) |sel| formatter.content = .{ - .selection = sel.toZig() orelse - return error.InvalidValue, @@ -370,14 +370,11 @@ index 5b4504c8c8f2dd7767e2b3d55aaf42bcc7055617..6139976f1a41f9445cf4f4f41dad9081 + return error.InvalidValue, + }; + } -@@ -405,0 +437,63 @@ test "format vt" { +@@ -571,0 +603,57 @@ test "format vt" { +test "terminal_state_new emits state without screen content" { + var source: terminal_c.Terminal = null; -+ try testing.expectEqual(Result.success, terminal_c.new( -+ &lib.alloc.test_allocator, -+ &source, -+ .{ .cols = 12, .rows = 3, .max_scrollback = 10_000 }, -+ )); ++ try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &source, 12, 3)); ++ try testing.expectEqual(Result.success, terminal_c.set(source, .scrollback_max_bytes, &@as(usize, 10_000))); + defer terminal_c.free(source); + + terminal_c.vt_write(source, "secret\x1b[?2004h", 14); @@ -420,11 +417,8 @@ index 5b4504c8c8f2dd7767e2b3d55aaf42bcc7055617..6139976f1a41f9445cf4f4f41dad9081 + try testing.expect(std.mem.indexOf(u8, buf[0..written], "secret") == null); + + var replacement: terminal_c.Terminal = null; -+ try testing.expectEqual(Result.success, terminal_c.new( -+ &lib.alloc.test_allocator, -+ &replacement, -+ .{ .cols = 12, .rows = 3, .max_scrollback = 10_000 }, -+ )); ++ try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &replacement, 12, 3)); ++ try testing.expectEqual(Result.success, terminal_c.set(replacement, .scrollback_max_bytes, &@as(usize, 10_000))); + defer terminal_c.free(replacement); + terminal_c.vt_write(replacement, buf[0..written].ptr, written); + @@ -435,7 +429,7 @@ index 5b4504c8c8f2dd7767e2b3d55aaf42bcc7055617..6139976f1a41f9445cf4f4f41dad9081 +} + diff --git a/src/terminal/c/grid_ref.zig b/src/terminal/c/grid_ref.zig -index 946ca8a1e36c35cbc305e6f18fc6325e7b744c43..a407153b73b0d93ab1773ca2a29df06b4091bde0 100644 +index 20f49be91ac959128db3eb7798749dbda8fc040e..b176946a648eb8a844c523699bd3c76d14c5b1e9 100644 --- a/src/terminal/c/grid_ref.zig +++ b/src/terminal/c/grid_ref.zig @@ -42,0 +43,14 @@ pub const CGridRef = extern struct { @@ -491,15 +485,12 @@ index 946ca8a1e36c35cbc305e6f18fc6325e7b744c43..a407153b73b0d93ab1773ca2a29df06b + return .success; +} + -@@ -255,0 +307,118 @@ test "grid_ref_hyperlink_uri with hyperlink" { +@@ -257,0 +309,112 @@ test "grid_ref_hyperlink_uri with hyperlink" { + +test "grid_ref_hyperlink_identity preserves explicit bytes" { + var terminal: terminal_c.Terminal = null; -+ try testing.expectEqual(Result.success, terminal_c.new( -+ &lib.alloc.test_allocator, -+ &terminal, -+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, -+ )); ++ try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &terminal, 80, 24)); ++ try testing.expectEqual(Result.success, terminal_c.set(terminal, .scrollback_max_bytes, &@as(usize, 10_000))); + defer terminal_c.free(terminal); + + const seq = @@ -538,11 +529,8 @@ index 946ca8a1e36c35cbc305e6f18fc6325e7b744c43..a407153b73b0d93ab1773ca2a29df06b + +test "grid_ref implicit hyperlink identity is stable across pages" { + var terminal: terminal_c.Terminal = null; -+ try testing.expectEqual(Result.success, terminal_c.new( -+ &lib.alloc.test_allocator, -+ &terminal, -+ .{ .cols = 215, .rows = 2, .max_scrollback = 20_000_000 }, -+ )); ++ try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &terminal, 215, 2)); ++ try testing.expectEqual(Result.success, terminal_c.set(terminal, .scrollback_max_bytes, &@as(usize, 20_000_000))); + defer terminal_c.free(terminal); + + const open = "\x1b]8;;https://example.com/implicit\x1b\\"; @@ -611,12 +599,12 @@ index 946ca8a1e36c35cbc305e6f18fc6325e7b744c43..a407153b73b0d93ab1773ca2a29df06b + ); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig -index 37dc57684aa5e6edea164acb3d4faa37955667b3..bd49b01f84ef1a67cf7dd6489563b616aac90299 100644 +index a0c0dc33cf3485d17db3a6ba1ee7351e2509b458..9411509642d7cc3e96650bd9b0847aab2379972a 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig -@@ -78,0 +79 @@ pub const formatter_terminal_new = formatter.terminal_new; +@@ -84,0 +85 @@ pub const formatter_format = formatter.format; +pub const formatter_terminal_state_new = formatter.terminal_state_new; -@@ -191,0 +193,7 @@ pub const terminal_get_multi = terminal.get_multi; +@@ -204,0 +206,7 @@ pub const terminal_continuation_alloc = terminal.continuation_alloc; +pub const terminal_saved_cursor = terminal.saved_cursor; +pub const terminal_screen_get = terminal.screen_get; +pub const terminal_screen_grid_ref = terminal.screen_grid_ref; @@ -624,15 +612,15 @@ index 37dc57684aa5e6edea164acb3d4faa37955667b3..bd49b01f84ef1a67cf7dd6489563b616 +pub const terminal_screen_row_iterator_free = terminal.screen_row_iterator_free; +pub const terminal_screen_row_iterator_next = terminal.screen_row_iterator_next; +pub const terminal_screen_cursor_hyperlink = terminal.screen_cursor_hyperlink; -@@ -225,0 +234 @@ pub const grid_ref_hyperlink_uri = grid_ref.grid_ref_hyperlink_uri; +@@ -259,0 +268 @@ pub const grid_ref_hyperlink_uri = grid_ref.grid_ref_hyperlink_uri; +pub const grid_ref_hyperlink_identity = grid_ref.grid_ref_hyperlink_identity; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig -index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0c4126606 100644 +index 5f047f489083060ec33ec2e7471e8d0e1c8db204..49555d665d0ce6100718a442ea30a174fc5ba4e8 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig -@@ -14,0 +15 @@ const modes = @import("../modes.zig"); +@@ -19,0 +20 @@ const mouse = @import("../mouse.zig"); +const charsets = @import("../charsets.zig"); -@@ -624,0 +626,9 @@ pub const TerminalCursorStyle = enum(c_int) { +@@ -1804,0 +1806,9 @@ pub const TerminalCursorStyle = enum(c_int) { + + fn fromZig(value: Screen.CursorStyle) TerminalCursorStyle { + return switch (value) { @@ -642,7 +630,7 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + .block_hollow => .block_hollow, + }; + } -@@ -720,0 +731,131 @@ pub const TerminalScreen = ScreenSet.Key; +@@ -1865,0 +1876,131 @@ pub const TerminalScreen = ScreenSet.Key; +const ScreenRowIteratorWrapper = struct { + alloc: std.mem.Allocator, + rows: PageList.RowIterator, @@ -675,8 +663,8 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 +/// C: GhosttyTerminalSemanticClick +pub const TerminalSemanticClick = enum(c_int) { + none = 0, -+ click_events_absolute = 1, -+ click_events_relative = 2, ++ events_absolute = 1, ++ events_relative = 2, + line = 3, + multiple = 4, + conservative_vertical = 5, @@ -686,8 +674,8 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + return switch (value) { + .none => .none, + .click_events => |click| switch (click) { -+ .absolute => .click_events_absolute, -+ .relative => .click_events_relative, ++ .absolute => .events_absolute, ++ .relative => .events_relative, + }, + .cl => |click| switch (click) { + .line => .line, @@ -774,7 +762,7 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + semantic_prompt_click: TerminalSemanticClick, +}; + -@@ -834,0 +976,188 @@ pub fn get_multi( +@@ -2062,0 +2204,188 @@ pub fn get_multi( +pub fn saved_cursor( + terminal_: Terminal, + screen_key: TerminalScreen, @@ -784,7 +772,7 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + const result = out orelse return .invalid_value; + if (result.size < @sizeOf(SavedCursor)) return .invalid_value; + if (comptime std.debug.runtime_safety) { -+ _ = std.meta.intToEnum(TerminalScreen, @intFromEnum(screen_key)) catch ++ _ = std.enums.fromInt(TerminalScreen, @backingInt(screen_key)) orelse + return .invalid_value; + } + const screen = t.screens.get(screen_key) orelse return .no_value; @@ -802,7 +790,7 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + const result = out orelse return .invalid_value; + if (result.size < @sizeOf(ScreenState)) return .invalid_value; + if (comptime std.debug.runtime_safety) { -+ _ = std.meta.intToEnum(TerminalScreen, @intFromEnum(screen_key)) catch ++ _ = std.enums.fromInt(TerminalScreen, @backingInt(screen_key)) orelse + return .invalid_value; + } + const screen = t.screens.get(screen_key) orelse return .no_value; @@ -826,8 +814,8 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + .charset = .fromZig(screen.charset), + .kitty_keyboard_flags = @as(u8, screen.kitty_keyboard.current().int()), + .viewport_offset = @intCast(scrollbar.offset), -+ .cursor_semantic_content = @enumFromInt( -+ @intFromEnum(screen.cursor.semantic_content), ++ .cursor_semantic_content = @fromBackingInt( ++ @backingInt(screen.cursor.semantic_content), + ), + .cursor_semantic_content_clear_eol = screen.cursor.semantic_content_clear_eol, + .hyperlink_implicit_id = @intCast(screen.cursor.hyperlink_implicit_id), @@ -848,7 +836,7 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 +) callconv(lib.calling_conv) Result { + const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; + if (comptime std.debug.runtime_safety) { -+ _ = std.meta.intToEnum(TerminalScreen, @intFromEnum(screen_key)) catch ++ _ = std.enums.fromInt(TerminalScreen, @backingInt(screen_key)) orelse + return .invalid_value; + } + const screen = t.screens.get(screen_key) orelse return .no_value; @@ -868,7 +856,7 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + result.* = null; + const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; + if (comptime std.debug.runtime_safety) { -+ _ = std.meta.intToEnum(TerminalScreen, @intFromEnum(screen_key)) catch ++ _ = std.enums.fromInt(TerminalScreen, @backingInt(screen_key)) orelse + return .invalid_value; + } + const screen = t.screens.get(screen_key) orelse return .no_value; @@ -931,7 +919,7 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + if (identity.size < @sizeOf(grid_ref_c.HyperlinkIdentity)) + return .invalid_value; + if (comptime std.debug.runtime_safety) { -+ _ = std.meta.intToEnum(TerminalScreen, @intFromEnum(screen_key)) catch ++ _ = std.enums.fromInt(TerminalScreen, @backingInt(screen_key)) orelse + return .invalid_value; + } + const screen = t.screens.get(screen_key) orelse return .no_value; @@ -963,18 +951,11 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + return .success; +} + -@@ -1472,0 +1802,243 @@ test "mode_set unknown mode" { +@@ -3420,0 +3750,233 @@ test "set and get mode validate configuration" { +test "saved cursor and screen reads are screen selected" { + var t: Terminal = null; -+ try testing.expectEqual(Result.success, new( -+ &lib.alloc.test_allocator, -+ &t, -+ .{ -+ .cols = 8, -+ .rows = 3, -+ .max_scrollback = 10_000, -+ }, -+ )); ++ try testing.expectEqual(Result.success, new(&lib.alloc.test_allocator, &t, 8, 3)); ++ try testing.expectEqual(Result.success, set(t, .scrollback_max_bytes, &@as(usize, 10_000))); + defer free(t); + + var saved: SavedCursor = undefined; @@ -1126,11 +1107,8 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 + +test "screen cursor hyperlink exposes URI and semantic identity" { + var t: Terminal = null; -+ try testing.expectEqual(Result.success, new( -+ &lib.alloc.test_allocator, -+ &t, -+ .{ .cols = 12, .rows = 3, .max_scrollback = 10_000 }, -+ )); ++ try testing.expectEqual(Result.success, new(&lib.alloc.test_allocator, &t, 12, 3)); ++ try testing.expectEqual(Result.success, set(t, .scrollback_max_bytes, &@as(usize, 10_000))); + defer free(t); + + var identity: grid_ref_c.HyperlinkIdentity = .{}; @@ -1208,29 +1186,38 @@ index 1c3b9f0c557aa603cf40eab920623eba07cdc7e8..7eafd01b2271c57342d8a3ea649ccbb0 +} + diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig -index f160f3852bf12aed376abf10a9b62ac1c336abe1..2434505631c6ffe038f753a2d06ee5ad4f04bf04 100644 +index db75bfddf2a0594edff74a84d295bef66d5d9e49..21dcc605cd617b5ad0c254e4b1d6346213dea26a 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig -@@ -51,0 +52 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: { -+ .{ "GhosttyHyperlinkIdentity", StructInfo.init(grid_ref.HyperlinkIdentity) }, -@@ -70,0 +72 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: { -+ .{ "GhosttyTerminalCharsetState", StructInfo.init(terminal.TerminalCharsetState) }, -@@ -71,0 +74,2 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: { -+ .{ "GhosttyTerminalSavedCursor", StructInfo.init(terminal.SavedCursor) }, -+ .{ "GhosttyTerminalScreenState", StructInfo.init(terminal.ScreenState) }, -@@ -79 +83 @@ pub const json: [:0]const u8 = json: { -- @setEvalBranchQuota(100000); -+ @setEvalBranchQuota(200_000); +@@ -173,0 +174,10 @@ const type_decls = [_]TypeDecl{ ++ .initStruct("GhosttyHyperlinkIdentity", grid_ref.HyperlinkIdentity), ++ .initStruct("GhosttyTerminalCharsetState", terminal.TerminalCharsetState), ++ .initStruct("GhosttyTerminalSavedCursor", terminal.SavedCursor), ++ .initStruct("GhosttyTerminalScreenState", terminal.ScreenState), ++ .initOpaque("GhosttyTerminalScreenRowIterator"), ++ .initEnum("GhosttyHyperlinkIdentityTag", grid_ref.HyperlinkIdentityTag, "GHOSTTY_HYPERLINK_IDENTITY_"), ++ .initEnum("GhosttyTerminalCharset", terminal.TerminalCharset, "GHOSTTY_TERMINAL_CHARSET_"), ++ .initEnum("GhosttyTerminalCharsetSlot", terminal.TerminalCharsetSlot, "GHOSTTY_TERMINAL_CHARSET_SLOT_"), ++ .initEnum("GhosttyTerminalProtectedMode", terminal.TerminalProtectedMode, "GHOSTTY_TERMINAL_PROTECTED_MODE_"), ++ .initEnum("GhosttyTerminalSemanticClick", terminal.TerminalSemanticClick, "GHOSTTY_TERMINAL_SEMANTIC_CLICK_"), diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig -index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc24543125660365 100644 +index f1ec6b7c13c014e51b22be331f9917944628f80e..8cf227df70e4d51624279ce640a1fd84ee346b52 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig -@@ -298,0 +299,58 @@ pub const TerminalFormatter = struct { +@@ -409,4 +409,4 @@ pub const TerminalFormatter = struct { +- // Emit terminal modes that differ from defaults. We probably have +- // some modes we want to emit before and some after, but for now for +- // simplicity we just emit them all before. If we make this more complex +- // later we should add test cases for it. + // DECCOLM (mode 3) is ignored unless mode 40 is already enabled, and a + // successful DECCOLM resize recreates the tabstop table. Restore this + // pair before custom tabstops and before the generated mode-order loop + // (where numeric mode 3 otherwise precedes mode 40). -+ if (self.opts.emit == .vt and self.extra.modes) { +@@ -414,4 +414,8 @@ pub const TerminalFormatter = struct { +- inline for (@typeInfo(modespkg.Mode).@"enum".field_names, @typeInfo(modespkg.Mode).@"enum".field_values) |field, field_value| { +- const mode: modespkg.Mode = @fromBackingInt(field_value); +- const current = self.terminal.modes.get(mode); +- const default_val = @field(self.terminal.modes.default, field); + const mode_40 = self.terminal.modes.get(.enable_mode_3); + const mode_40_default = self.terminal.modes.default.enable_mode_3; + if (mode_40 != mode_40_default) { @@ -1239,7 +1226,12 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + .{if (mode_40) "h" else "l"}, + ); + } -+ +@@ -419,5 +423,24 @@ pub const TerminalFormatter = struct { +- if (current != default_val) { +- const tag: modespkg.ModeTag = @bitCast(@backingInt(mode)); +- const prefix = if (tag.ansi) "" else "?"; +- const suffix = if (current) "h" else "l"; +- try writer.print("\x1b[{s}{d}{s}", .{ prefix, tag.value, suffix }); + const mode_3 = self.terminal.modes.get(.@"132_column"); + const mode_3_default = self.terminal.modes.default.@"132_column"; + if (mode_3 != mode_3_default) { @@ -1250,90 +1242,65 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + } + } + -+ // Tabstop restoration moves the cursor. Emit it after DECCOLM but -+ // before the remaining modes and screen content so origin mode and -+ // horizontal margins cannot shift the requested absolute columns. ++ // Emit tabstop positions before the screen contents because setting ++ // them moves the cursor. Screen formatting will restore the requested ++ // cursor position afterwards. + if (self.opts.emit == .vt and self.extra.tabstops) { + // Clear all tabs (CSI 3 g) + try writer.print("\x1b[3g", .{}); + -+ // Set each configured tabstop by moving cursor and using HTS. ++ // Set each configured tabstop by moving cursor and using HTS + for (0..self.terminal.cols) |col| { + if (self.terminal.tabstops.get(col)) { ++ // Move cursor to the column (1-indexed) + try writer.print("\x1b[{d}G", .{col + 1}); ++ // Set tab (HTS) + try writer.print("\x1bH", .{}); -+ } -+ } -+ // PageListFormatter begins from the terminal home position. -+ try writer.print("\x1b[H", .{}); -+ -+ if (self.pin_map) |*m| { -+ var discarding: std.Io.Writer.Discarding = .init(&.{}); -+ var extra_formatter: TerminalFormatter = self; -+ extra_formatter.content = .none; -+ extra_formatter.pin_map = null; -+ extra_formatter.extra = .none; -+ extra_formatter.extra.tabstops = true; -+ try extra_formatter.format(&discarding.writer); +@@ -426,0 +450,3 @@ pub const TerminalFormatter = struct { ++ // Screen contents are formatted relative to the top-left. ++ try writer.writeAll("\x1b[H"); + -+ m.map.appendNTimes( -+ m.alloc, -+ self.terminal.screens.active.pages.getTopLeft(.screen), -+ std.math.cast(usize, discarding.count) orelse return error.WriteFailed, -+ ) catch return error.WriteFailed; -+ } -+ } -+ -@@ -305,0 +364 @@ pub const TerminalFormatter = struct { +@@ -434 +460 @@ pub const TerminalFormatter = struct { +- extra_formatter.extra.modes = true; ++ extra_formatter.extra.tabstops = true; +@@ -447,6 +473,10 @@ pub const TerminalFormatter = struct { +- // Emit tabstop positions before the screen contents because setting +- // them moves the cursor. Screen formatting will restore the requested +- // cursor position afterwards. +- if (self.opts.emit == .vt and self.extra.tabstops) { +- // Clear all tabs (CSI 3 g) +- try writer.print("\x1b[3g", .{}); ++ // Emit terminal modes that differ from defaults. We probably have ++ // some modes we want to emit before and some after, but for now for ++ // simplicity we just emit them all before. If we make this more complex ++ // later we should add test cases for it. ++ if (self.opts.emit == .vt and self.extra.modes) { ++ inline for (@typeInfo(modespkg.Mode).@"enum".field_names, @typeInfo(modespkg.Mode).@"enum".field_values) |field, field_value| { ++ const mode: modespkg.Mode = @fromBackingInt(field_value); + if (mode == .enable_mode_3 or mode == .@"132_column") continue; -@@ -339,0 +399,3 @@ pub const TerminalFormatter = struct { -+ // Terminal-level extras below can reset or move the cursor. Delay CUP -+ // until all of them have been emitted. -+ if (self.opts.emit == .vt) screen_formatter.extra.cursor = false; -@@ -363,16 +424,0 @@ pub const TerminalFormatter = struct { -- // Emit tabstop positions -- if (self.extra.tabstops) { -- // Clear all tabs (CSI 3 g) -- try writer.print("\x1b[3g", .{}); ++ const current = self.terminal.modes.get(mode); ++ const default_val = @field(self.terminal.modes.default, field); +@@ -454,7 +484,5 @@ pub const TerminalFormatter = struct { +- // Set each configured tabstop by moving cursor and using HTS +- for (0..self.terminal.cols) |col| { +- if (self.terminal.tabstops.get(col)) { +- // Move cursor to the column (1-indexed) +- try writer.print("\x1b[{d}G", .{col + 1}); +- // Set tab (HTS) +- try writer.print("\x1bH", .{}); ++ if (current != default_val) { ++ const tag: modespkg.ModeTag = @bitCast(@backingInt(mode)); ++ const prefix = if (tag.ansi) "" else "?"; ++ const suffix = if (current) "h" else "l"; ++ try writer.print("\x1b[{s}{d}{s}", .{ prefix, tag.value, suffix }); +@@ -464,3 +491,0 @@ pub const TerminalFormatter = struct { +- // Screen contents are formatted relative to the top-left. +- try writer.writeAll("\x1b[H"); - -- // Set each configured tabstop by moving cursor and using HTS -- for (0..self.terminal.cols) |col| { -- if (self.terminal.tabstops.get(col)) { -- // Move cursor to the column (1-indexed) -- try writer.print("\x1b[{d}G", .{col + 1}); -- // Set tab (HTS) -- try writer.print("\x1bH", .{}); -- } -- } -- } -- -@@ -392,0 +439,21 @@ pub const TerminalFormatter = struct { -+ // Restore the cursor last. CUP is relative to the margins while -+ // origin mode is active, so translate Ghostty's absolute cursor -+ // coordinates only when this formatter emitted both pieces of -+ // state that make the target interpret CUP that way. -+ if (self.extra.screen.cursor) { -+ const cursor = &self.terminal.screens.active.cursor; -+ const origin = self.extra.modes and -+ self.terminal.modes.get(.origin) and -+ self.extra.scrolling_region; -+ const row = if (origin) -+ cursor.y -| self.terminal.scrolling_region.top -+ else -+ cursor.y; -+ const column = if (origin and -+ self.terminal.modes.get(.enable_left_and_right_margin)) -+ cursor.x -| self.terminal.scrolling_region.left -+ else -+ cursor.x; -+ try writer.print("\x1b[{d};{d}H", .{ row + 1, column + 1 }); -+ } -+ -@@ -401 +467,0 @@ pub const TerminalFormatter = struct { -- extra_formatter.extra.tabstops = self.extra.tabstops; -@@ -403,0 +470 @@ pub const TerminalFormatter = struct { -+ extra_formatter.extra.screen.cursor = self.extra.screen.cursor; -@@ -652,0 +720,12 @@ pub const ScreenFormatter = struct { +@@ -474 +499 @@ pub const TerminalFormatter = struct { +- extra_formatter.extra.tabstops = true; ++ extra_formatter.extra.modes = true; +@@ -864,0 +890,12 @@ pub const ScreenFormatter = struct { + + // SS2/SS3 applies to exactly the next printable character. It + // therefore has to be emitted after all other formatter output @@ -1346,16 +1313,16 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + }; + try writer.print("{s}", .{seq}); + } -@@ -4876 +4955 @@ test "Screen vt with kitty keyboard" { +@@ -6370 +6407 @@ test "Screen vt with kitty keyboard" { -test "Screen vt with charsets" { +test "Screen vt with charsets including pending single shift" { -@@ -4892,2 +4971,3 @@ test "Screen vt with charsets" { +@@ -6387,2 +6424,3 @@ test "Screen vt with charsets" { - // Set G0 to DEC special and shift to G1 - s.nextSlice("\x1b(0\x0ehello"); + // Set G0 and G2 to DEC special, shift GL to G1, and leave SS2 + // pending. The formatter must not consume that one-shot state itself. + s.nextSlice("\x1b(0\x0ehello\x1b*0\x1bN"); -@@ -4923,0 +5004,8 @@ test "Screen vt with charsets" { +@@ -6418,0 +6457,8 @@ test "Screen vt with charsets" { + try testing.expectEqual( + t.screens.active.charset.charsets.get(.G2), + t2.screens.active.charset.charsets.get(.G2), @@ -1364,7 +1331,7 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + t.screens.active.charset.single_shift, + t2.screens.active.charset.single_shift, + ); -@@ -5073,0 +5162,82 @@ test "Terminal vt with tabstops" { +@@ -6720,0 +6767,82 @@ test "Terminal vt with tabstops" { +test "Terminal vt restores cursor after margins and tabstops" { + const testing = std.testing; + const alloc = testing.allocator; @@ -1372,7 +1339,7 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + -+ var t = try Terminal.init(alloc, .{ ++ var t = try Terminal.init(testing.io, alloc, .{ + .cols = 20, + .rows = 6, + }); @@ -1391,7 +1358,7 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + try formatter.format(&builder.writer); + const output = builder.writer.buffered(); + -+ var t2 = try Terminal.init(alloc, .{ ++ var t2 = try Terminal.init(testing.io, alloc, .{ + .cols = 20, + .rows = 6, + }); @@ -1418,7 +1385,7 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + -+ var source = try Terminal.init(alloc, .{ .cols = 80, .rows = 6 }); ++ var source = try Terminal.init(testing.io, alloc, .{ .cols = 80, .rows = 6 }); + defer source.deinit(alloc); + var source_stream = source.vtStream(); + defer source_stream.deinit(); @@ -1434,7 +1401,7 @@ index 553ea28d0930ec68a387ae041a0ffaa338bca997..91d621b641755a45f76106d0dc245431 + formatter.extra.tabstops = true; + try formatter.format(&builder.writer); + -+ var replacement = try Terminal.init(alloc, .{ .cols = 80, .rows = 6 }); ++ var replacement = try Terminal.init(testing.io, alloc, .{ .cols = 80, .rows = 6 }); + defer replacement.deinit(alloc); + var replacement_stream = replacement.vtStream(); + defer replacement_stream.deinit(); diff --git a/native/patches/ghostty-vt-msvc-runtime.patch b/native/patches/ghostty-vt-msvc-runtime.patch index 10285a13..c643c4c9 100644 --- a/native/patches/ghostty-vt-msvc-runtime.patch +++ b/native/patches/ghostty-vt-msvc-runtime.patch @@ -1,82 +1,145 @@ diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig -index a02447285dde036836f638267ab4c5f394521299..0373d531f3d792939d257a1972dea24b5aefcb0e 100644 +index 18dbf92c2af0ed5e31c11532601be9863585ddf7..58d85cab3c009f6fb36d20e769b0e6decefad73d 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig -@@ -2,0 +3,2 @@ const std = @import("std"); +@@ -1,0 +2 @@ const std = @import("std"); +const MsvcRuntime = enum { static, dll }; -+ -@@ -5,0 +8 @@ pub fn build(b: *std.Build) !void { +@@ -5,0 +7 @@ pub fn build(b: *std.Build) !void { + const msvc_runtime = b.option(MsvcRuntime, "msvc-runtime", "The MSVC C runtime to compile for: static (/MT) or dll (/MD)") orelse .static; -@@ -104,0 +108,5 @@ pub fn build(b: *std.Build) !void { -+ // `static` is clang's default for msvc, so only `dll` adds a flag. -+ if (target.result.abi == .msvc and msvc_runtime == .dll) { -+ try flags.append(b.allocator, "-fms-runtime-lib=dll"); +@@ -115,0 +118,27 @@ pub fn build(b: *std.Build) !void { ++ if (target.result.abi == .msvc) { ++ try flags.append(b.allocator, if (msvc_runtime == .dll) "-fms-runtime-lib=dll" else "-fms-runtime-lib=static"); ++ } ++ ++ // Upstream disables autolink on the SIMD objects. Compile a metadata-only ++ // witness with the SAME flags, enabling autolink only for its pragma. It ++ // derives the stamp from compiler macros, not the requested option. ++ if (target.result.abi == .msvc) { ++ const witness = b.addWriteFiles().add("ghosttea-msvc-highway.cpp", ++ \\#ifndef _MT ++ \\#error MSVC runtime selection did not define _MT ++ \\#endif ++ \\#ifdef _DEBUG ++ \\#error Ghosttea release artifacts must not use a debug CRT ++ \\#endif ++ \\#ifdef _DLL ++ \\#pragma detect_mismatch("RuntimeLibrary", "MD_DynamicRelease") ++ \\#else ++ \\#pragma detect_mismatch("RuntimeLibrary", "MT_StaticRelease") ++ \\#endif ++ ); ++ const witness_flags = try b.allocator.alloc([]const u8, flags.items.len + 1); ++ @memcpy(witness_flags[0..flags.items.len], flags.items); ++ witness_flags[flags.items.len] = "-fautolink"; ++ lib.root_module.addCSourceFile(.{ .file = witness, .flags = witness_flags }); + } + diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig -index 0859edc26e779ca9601ff493588e94f742a36026..a086d2d7951df796ca34c0f797d90ee99bb2d53e 100644 +index 3efc0826344c30e406b6801f669852fcec500fab..e895df610df3dfb7fa99c34c1f50489d5e273bd7 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig -@@ -2,0 +3,2 @@ const std = @import("std"); +@@ -1,0 +2 @@ const std = @import("std"); +const MsvcRuntime = enum { static, dll }; -+ -@@ -6,0 +9 @@ pub fn build(b: *std.Build) !void { +@@ -6,0 +8 @@ pub fn build(b: *std.Build) !void { + const msvc_runtime = b.option(MsvcRuntime, "msvc-runtime", "The MSVC C runtime to compile for: static (/MT) or dll (/MD)") orelse .static; -@@ -70,0 +74,2 @@ pub fn build(b: *std.Build) !void { -+ // `static` is clang's default for msvc, so only `dll` adds a flag. -+ if (msvc_runtime == .dll) try flags.append(b.allocator, "-fms-runtime-lib=dll"); +@@ -71,0 +74 @@ pub fn build(b: *std.Build) !void { ++ try flags.append(b.allocator, if (msvc_runtime == .dll) "-fms-runtime-lib=dll" else "-fms-runtime-lib=static"); +@@ -81,0 +85,23 @@ pub fn build(b: *std.Build) !void { ++ // Upstream disables autolink on the SIMD objects. Compile a metadata-only ++ // witness with the SAME flags, enabling autolink only for its pragma. It ++ // derives the stamp from compiler macros, not the requested option. ++ if (target.result.abi == .msvc) { ++ const witness = b.addWriteFiles().add("ghosttea-msvc-simdutf.cpp", ++ \\#ifndef _MT ++ \\#error MSVC runtime selection did not define _MT ++ \\#endif ++ \\#ifdef _DEBUG ++ \\#error Ghosttea release artifacts must not use a debug CRT ++ \\#endif ++ \\#ifdef _DLL ++ \\#pragma detect_mismatch("RuntimeLibrary", "MD_DynamicRelease") ++ \\#else ++ \\#pragma detect_mismatch("RuntimeLibrary", "MT_StaticRelease") ++ \\#endif ++ ); ++ const witness_flags = try b.allocator.alloc([]const u8, flags.items.len + 1); ++ @memcpy(witness_flags[0..flags.items.len], flags.items); ++ witness_flags[flags.items.len] = "-fautolink"; ++ lib.root_module.addCSourceFile(.{ .file = witness, .flags = witness_flags }); ++ } ++ diff --git a/src/build/Config.zig b/src/build/Config.zig -index 0a99473174e27262f04169d0e765f2d2b85d53b7..daebee669875580a82e9c3f370e3ed6edebf694f 100644 +index 24157293b5cb349d519b06f09616c99edd6adc20..16d8238edc0dfecfcd3a1c07b4e3365660d45750 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig -@@ -45,0 +46 @@ strip: bool = false, +@@ -50,0 +51 @@ strip: bool = false, +msvc_runtime: MsvcRuntime = .static, -@@ -339,0 +341,6 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf +@@ -433,0 +435,6 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf + config.msvc_runtime = b.option( + MsvcRuntime, + "msvc-runtime", + "The MSVC C runtime that C and C++ objects are compiled for: static (/MT) or dll (/MD). Only msvc targets read it.", + ) orelse .static; + -@@ -692,0 +700,19 @@ pub const ExeEntrypoint = enum { -+/// The MSVC C runtime that C and C++ objects are compiled for, named as -+/// `-fms-runtime-lib` names it. Every object in one link must agree, and the -+/// consumer of a static library decides: a Rust target links `dll` (/MD) -+/// unless it enables `crt-static`. +@@ -877,0 +885,16 @@ pub const ExeEntrypoint = enum { ++/// The MSVC C runtime that C and C++ objects are compiled for. Every ++/// object in one link must agree: Rust links `dll` (/MD) unless its ++/// target enables `crt-static`. +pub const MsvcRuntime = enum { + static, + dll, + -+ /// The compiler flags that select this runtime. `static` is clang's own -+ /// default for msvc targets, so it adds none and leaves objects as they were. + pub fn cFlags(self: MsvcRuntime, target: std.Target) []const []const u8 { + if (target.abi != .msvc) return &.{}; + return switch (self) { -+ .static => &.{}, ++ .static => &.{"-fms-runtime-lib=static"}, + .dll => &.{"-fms-runtime-lib=dll"}, + }; + } +}; + diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig -index 44c300e15c569b3d00394894e760621ae4ff5f11..63b8fc31e2aa5619fc99e8dee44cf9310231ae14 100644 +index 9d987240bf8d09e5497487f71affc577191943ba..53b5c250fc439d9accc31b4de3a2663a2572972c 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig -@@ -142 +142 @@ fn initVt( +@@ -158 +158 @@ fn initVt( - try SharedDeps.addSimd(b, vt, simd_libs); + try SharedDeps.addSimd(b, vt, simd_libs, cfg.msvc_runtime); diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig -index b68be92d0112d20ec4859a2a79b06ee10d8cf654..633ef20f2d9db6a9fc57c3a87699580197855ac4 100644 +index 8f4e23954211c1517f0ae96143326cf74ba84937..e78966ec2b14b696d930f056768bacd32aff17b5 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig -@@ -383,0 +384 @@ pub fn add( +@@ -431,0 +432 @@ pub fn add( + self.config.msvc_runtime, -@@ -761,0 +763 @@ pub fn addSimd( +@@ -883,0 +885 @@ pub fn addSimd( + msvc_runtime: Config.MsvcRuntime, -@@ -774,0 +777 @@ pub fn addSimd( +@@ -896,0 +899 @@ pub fn addSimd( + .@"msvc-runtime" = msvc_runtime, -@@ -790,0 +794 @@ pub fn addSimd( +@@ -912,0 +916 @@ pub fn addSimd( + .@"msvc-runtime" = msvc_runtime, -@@ -856,0 +861,2 @@ pub fn addSimd( +@@ -983,0 +988,25 @@ pub fn addSimd( + try flags.appendSlice(b.allocator, msvc_runtime.cFlags(target.result)); + ++ // Upstream disables autolink on the SIMD objects. Compile a metadata-only ++ // witness with the SAME flags, enabling autolink only for its pragma. It ++ // derives the stamp from compiler macros, not the requested option. ++ if (target.result.abi == .msvc) { ++ const witness = b.addWriteFiles().add("ghosttea-msvc-simd.cpp", ++ \\#ifndef _MT ++ \\#error MSVC runtime selection did not define _MT ++ \\#endif ++ \\#ifdef _DEBUG ++ \\#error Ghosttea release artifacts must not use a debug CRT ++ \\#endif ++ \\#ifdef _DLL ++ \\#pragma detect_mismatch("RuntimeLibrary", "MD_DynamicRelease") ++ \\#else ++ \\#pragma detect_mismatch("RuntimeLibrary", "MT_StaticRelease") ++ \\#endif ++ ); ++ const witness_flags = try b.allocator.alloc([]const u8, flags.items.len + 1); ++ @memcpy(witness_flags[0..flags.items.len], flags.items); ++ witness_flags[flags.items.len] = "-fautolink"; ++ m.addCSourceFile(.{ .file = witness, .flags = witness_flags }); ++ } ++ diff --git a/package-lock.json b/package-lock.json index 7394eed5..fcc9e10c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghosttea", - "version": "0.13.8", + "version": "0.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghosttea", - "version": "0.13.8", + "version": "0.14.0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -28,14 +28,14 @@ }, "apps/desktop": { "name": "ghosttea-demo", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@vibecook/ghosttea": "0.13.8", - "@vibecook/ghosttea-electron": "0.13.8", - "@vibecook/ghosttea-frame": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8", - "@vibecook/ghosttea-react": "0.13.8", + "@vibecook/ghosttea": "0.14.0", + "@vibecook/ghosttea-electron": "0.14.0", + "@vibecook/ghosttea-frame": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0", + "@vibecook/ghosttea-react": "0.14.0", "react": "19.2.7", "react-dom": "19.2.7" }, @@ -55,14 +55,14 @@ }, "apps/desktop-experiment": { "name": "ghosttea-desktop-experiment", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@vibecook/ghosttea": "0.13.8", - "@vibecook/ghosttea-electron": "0.13.8", - "@vibecook/ghosttea-frame": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8", - "@vibecook/ghosttea-react": "0.13.8", + "@vibecook/ghosttea": "0.14.0", + "@vibecook/ghosttea-electron": "0.14.0", + "@vibecook/ghosttea-frame": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0", + "@vibecook/ghosttea-react": "0.14.0", "react": "19.2.7", "react-dom": "19.2.7" }, @@ -117,7 +117,7 @@ }, "apps/desktop-pipeline-lab": { "name": "ghosttea-desktop-pipeline-lab", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "dependencies": { "react": "19.2.7", @@ -7880,10 +7880,10 @@ }, "packages/ghosttea": { "name": "@vibecook/ghosttea", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea-protocol": "0.14.0" }, "devDependencies": { "typescript": "7.0.2", @@ -7892,10 +7892,10 @@ }, "packages/ghosttea-client": { "name": "@vibecook/ghosttea-client", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea-protocol": "0.14.0" }, "devDependencies": { "@types/node": "26.1.1", @@ -7943,11 +7943,11 @@ }, "packages/ghosttea-electron": { "name": "@vibecook/ghosttea-electron", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@vibecook/ghosttea-client": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea-client": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0" }, "devDependencies": { "@types/node": "26.1.1", @@ -7996,7 +7996,7 @@ }, "packages/ghosttea-frame": { "name": "@vibecook/ghosttea-frame", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "devDependencies": { "typescript": "7.0.2", @@ -8040,7 +8040,7 @@ }, "packages/ghosttea-native-tabs": { "name": "@vibecook/ghosttea-native-tabs", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "devDependencies": { "@types/node": "26.1.1", @@ -8085,7 +8085,7 @@ }, "packages/ghosttea-protocol": { "name": "@vibecook/ghosttea-protocol", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "devDependencies": { "typescript": "7.0.2", @@ -8129,12 +8129,12 @@ }, "packages/ghosttea-react": { "name": "@vibecook/ghosttea-react", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "dependencies": { - "@vibecook/ghosttea": "0.13.8", - "@vibecook/ghosttea-frame": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea": "0.14.0", + "@vibecook/ghosttea-frame": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0" }, "devDependencies": { "@types/react": "19.2.17", @@ -8220,7 +8220,7 @@ }, "packages/ghosttead": { "name": "@vibecook/ghosttead", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT", "devDependencies": { "@types/node": "26.1.1", @@ -8228,18 +8228,18 @@ "vitest": "4.1.10" }, "optionalDependencies": { - "@vibecook/ghosttead-darwin-arm64": "0.13.8", - "@vibecook/ghosttead-win32-x64": "0.13.8" + "@vibecook/ghosttead-darwin-arm64": "0.14.0", + "@vibecook/ghosttead-win32-x64": "0.14.0" } }, "packages/ghosttead-darwin-arm64": { "name": "@vibecook/ghosttead-darwin-arm64", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT" }, "packages/ghosttead-win32-x64": { "name": "@vibecook/ghosttead-win32-x64", - "version": "0.13.8", + "version": "0.14.0", "license": "MIT" }, "packages/ghosttead/node_modules/typescript": { diff --git a/package.json b/package.json index cedaf1b4..49775ab4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghosttea", "private": true, - "version": "0.13.8", + "version": "0.14.0", "description": "Ghostty-powered native terminal runtime for Electron applications.", "license": "MIT", "author": "James Yong", @@ -128,7 +128,7 @@ "test:publish-npm-packages": "node --test tests/publish-npm-packages.test.mjs", "test:release-channel": "node --test tests/release-channel.test.mjs", "test:apple-native-pin": "node --test tests/apple-native-pin.test.mjs", - "test:apple-native-inputs": "node --test tests/apple-native-inputs.test.mjs", + "test:apple-native-inputs": "node --test tests/apple-native-inputs.test.mjs tests/cargo-target-directory.test.mjs", "test:ghostty-vt-msvc-runtime": "node --test tests/ghostty-vt-msvc-runtime.test.mjs", "check:desktop": "npm run format:check && npm run lint && npm run check:workspace-names && npm run check:build-script-inputs && npm run build:sdk && npm run check --workspaces --if-present && cargo check --workspace --all-targets --all-features --locked && cargo clippy --workspace --all-targets --all-features --locked -- -D warnings", "test:desktop": "npm run build:sdk && npm run test --workspaces --if-present && cargo test --workspace --locked && npm run test:ghosttea-core:ffi && npm run test:release-notes && npm run test:publish-npm-packages && npm run test:release-channel && npm run test:apple-native-pin && npm run test:apple-native-inputs && npm run test:ghostty-vt-msvc-runtime && npm run test:integration:built && npm run test:bench:lib && npm run test:bench:render && npm run test:bench:truffle", diff --git a/packages/ghosttea-client/package.json b/packages/ghosttea-client/package.json index 4dabe4f1..fcd1eea8 100644 --- a/packages/ghosttea-client/package.json +++ b/packages/ghosttea-client/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttea-client", - "version": "0.13.8", + "version": "0.14.0", "description": "Electron-free Node control client for the Ghosttea terminal service.", "license": "MIT", "author": "James Yong", @@ -41,7 +41,7 @@ "node": ">=22" }, "dependencies": { - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea-protocol": "0.14.0" }, "scripts": { "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", diff --git a/packages/ghosttea-electron/package.json b/packages/ghosttea-electron/package.json index de792fc7..cece23b6 100644 --- a/packages/ghosttea-electron/package.json +++ b/packages/ghosttea-electron/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttea-electron", - "version": "0.13.8", + "version": "0.14.0", "description": "Electron lifecycle and direct renderer transport for Ghosttea.", "license": "MIT", "author": "James Yong", @@ -64,8 +64,8 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@vibecook/ghosttea-client": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea-client": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0" }, "peerDependencies": { "electron": ">=35" diff --git a/packages/ghosttea-frame/package.json b/packages/ghosttea-frame/package.json index 09056f59..f966a1cc 100644 --- a/packages/ghosttea-frame/package.json +++ b/packages/ghosttea-frame/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttea-frame", - "version": "0.13.8", + "version": "0.14.0", "description": "Binary frame decoder and shared rendering types for Ghosttea.", "license": "MIT", "author": "James Yong", diff --git a/packages/ghosttea-native-tabs/package.json b/packages/ghosttea-native-tabs/package.json index 9433d399..8936e1a5 100644 --- a/packages/ghosttea-native-tabs/package.json +++ b/packages/ghosttea-native-tabs/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttea-native-tabs", - "version": "0.13.8", + "version": "0.14.0", "description": "Prebuilt macOS native window-tab ordering addon for Electron applications.", "license": "MIT", "author": "James Yong", diff --git a/packages/ghosttea-protocol/package.json b/packages/ghosttea-protocol/package.json index ac5372b4..0f1e3ec0 100644 --- a/packages/ghosttea-protocol/package.json +++ b/packages/ghosttea-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttea-protocol", - "version": "0.13.8", + "version": "0.14.0", "description": "Typed control protocol for Ghosttea terminal clients and bridges.", "license": "MIT", "author": "James Yong", diff --git a/packages/ghosttea-protocol/src/routed.test.ts b/packages/ghosttea-protocol/src/routed.test.ts index 4482e689..5422f655 100644 --- a/packages/ghosttea-protocol/src/routed.test.ts +++ b/packages/ghosttea-protocol/src/routed.test.ts @@ -118,3 +118,25 @@ describe("TPv3 routed protocol", () => { expect(routedCrc32c(new TextEncoder().encode("123456789"))).toBe(0xe306_9283); }); }); + +describe("selection-text wire bounds", () => { + const identity = { requestId: "copy-1", sessionId: "session-a", activationId: "activation-a", leaseEpoch: 4 }; + it("accepts absolute history selections and read results only on their proper leg", () => { + const body = { ...identity, startColumn: 0, startRow: 160, endColumn: 79, endRow: 170, selectAll: false }; + const encoded = encodeRoutedMessage("SelectionTextRequest", body); + expect(decodeRoutedMessage(encoded, ROUTED_LEG_INBOUND.control).ok).toBe(true); + expect(decodeRoutedMessage(encoded, ROUTED_LEG_INBOUND.frames).ok).toBe(false); + for (const patch of [{ startRow: -1 }, { endColumn: 65536 }, { endRow: 4294967296 }, { leaseEpoch: 1.5 }]) + expect(decodeRoutedMessage(JSON.stringify({ type: "SelectionTextRequest", ...body, ...patch })).ok).toBe(false); + }); + it("requires exactly one success or refusal and preserves Unicode", () => { + for (const fields of [{ text: "中 é 👩‍💻 🛙X" }, { text: "" }, { error: "UNAVAILABLE" }, { error: "TOO_LARGE" }]) + expect(decodeRoutedMessage(JSON.stringify({ type: "SelectionTextResult", ...identity, ...fields })).ok).toBe( + true, + ); + for (const fields of [{}, { text: "private", error: "UNAVAILABLE" }, { text: null }, { error: "unknown" }]) + expect(decodeRoutedMessage(JSON.stringify({ type: "SelectionTextResult", ...identity, ...fields })).ok).toBe( + false, + ); + }); +}); diff --git a/packages/ghosttea-protocol/src/routed.ts b/packages/ghosttea-protocol/src/routed.ts index db5f4ea6..19f79c08 100644 --- a/packages/ghosttea-protocol/src/routed.ts +++ b/packages/ghosttea-protocol/src/routed.ts @@ -455,6 +455,31 @@ export interface RoutedCalibrationPing { [key: string]: unknown; } +/** Optional, negotiated read on the authenticated control leg. Coordinates + * use the terminal's absolute history rows; the cell selects the engine view. */ +export interface RoutedSelectionTextRequest { + requestId: string; + sessionId: string; + activationId: string; + leaseEpoch: number; + startColumn: number; + startRow: number; + endColumn: number; + endRow: number; + selectAll: boolean; +} + +export interface RoutedSelectionTextResult { + requestId: string; + sessionId: string; + activationId: string; + leaseEpoch: number; + text?: string; + error?: "UNAVAILABLE" | "TOO_LARGE"; +} + +export const ROUTED_SELECTION_TEXT_CAPABILITY = "selection-text"; + export type RoutedMessageBodyMap = { ConnectionHello: RoutedConnectionHello; ConnectionAccepted: RoutedConnectionAccepted; @@ -477,6 +502,8 @@ export type RoutedMessageBodyMap = { TransportCredit: RoutedTransportCredit; SceneApplied: RoutedSceneApplied; CalibrationPing: RoutedCalibrationPing; + SelectionTextRequest: RoutedSelectionTextRequest; + SelectionTextResult: RoutedSelectionTextResult; }; export type RoutedMessageType = keyof RoutedMessageBodyMap; @@ -506,6 +533,8 @@ export const ROUTED_MESSAGE_TYPES = [ "TransportCredit", "SceneApplied", "CalibrationPing", + "SelectionTextRequest", + "SelectionTextResult", ] as const satisfies readonly RoutedMessageType[]; export const ROUTED_LEG_INBOUND = { @@ -517,6 +546,7 @@ export const ROUTED_LEG_INBOUND = { "ClaimGeometry", "ReleaseGeometry", "TransferGeometry", + "SelectionTextRequest", ], frames: ["ConnectionHello", "LegHeartbeat", "AttachFramesLeg", "TransportCredit", "SceneApplied", "CalibrationPing"], } as const satisfies Readonly>; @@ -532,6 +562,7 @@ export const ROUTED_LEG_OUTBOUND = { "CellActivationStatus", "GeometryCommitted", "GeometryRefused", + "SelectionTextResult", ], frames: ["ConnectionAccepted", "ConnectionRefused", "LegHeartbeatAck", "FramesLegAttached", "AttachRefused"], } as const satisfies Readonly>; @@ -742,6 +773,29 @@ function hasActivation(record: Record): boolean { function validateRoutedBody(type: RoutedMessageType, body: Record): boolean { switch (type) { + case "SelectionTextRequest": + case "SelectionTextResult": { + if ( + !isNonEmptyString(body.requestId) || + body.requestId.length > 64 || + !isNonEmptyString(body.sessionId) || + body.sessionId.length > 256 || + !isNonEmptyString(body.activationId) || + body.activationId.length > 256 || + !isCounter(body.leaseEpoch) + ) + return false; + if (type === "SelectionTextResult") + return ( + (typeof body.text === "string" && body.error === undefined) || + (body.text === undefined && (body.error === "UNAVAILABLE" || body.error === "TOO_LARGE")) + ); + return ( + [body.startColumn, body.endColumn].every((value) => isCounter(value) && value <= 65_535) && + [body.startRow, body.endRow].every((value) => isCounter(value) && value <= 4_294_967_295) && + typeof body.selectAll === "boolean" + ); + } case "ConnectionHello": return ( isCounter(body.protocolMajor) && diff --git a/packages/ghosttea-react/package.json b/packages/ghosttea-react/package.json index 911cba16..b05c4716 100644 --- a/packages/ghosttea-react/package.json +++ b/packages/ghosttea-react/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttea-react", - "version": "0.13.8", + "version": "0.14.0", "description": "React terminal surface and worker renderer for Ghosttea.", "license": "MIT", "author": "James Yong", @@ -58,9 +58,9 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@vibecook/ghosttea": "0.13.8", - "@vibecook/ghosttea-frame": "0.13.8", - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea": "0.14.0", + "@vibecook/ghosttea-frame": "0.14.0", + "@vibecook/ghosttea-protocol": "0.14.0" }, "peerDependencies": { "react": ">=19" diff --git a/packages/ghosttea-react/src/TerminalSurface.tsx b/packages/ghosttea-react/src/TerminalSurface.tsx index 7c61815e..af3fd426 100644 --- a/packages/ghosttea-react/src/TerminalSurface.tsx +++ b/packages/ghosttea-react/src/TerminalSurface.tsx @@ -470,6 +470,15 @@ function TerminalSurfaceSession({ const focusedRef = useRef(false); const [viewId] = useState(() => crypto.randomUUID()); const [inputFocused, setInputFocused] = useState(false); + const [copyError, setCopyError] = useState(); + useEffect(() => { + if (!copyError) return; + const timer = setTimeout(() => setCopyError(undefined), 5_000); + return () => clearTimeout(timer); + }, [copyError]); + const reportCopyError = useCallback((error: unknown) => { + setCopyError(error instanceof Error ? error.message : "Terminal copy failed. Try again."); + }, []); const selectionAnchorRef = useRef(null); const selectionRef = useRef<{ anchor: CellPoint; focus: CellPoint } | null>(null); const selectionAllRef = useRef(false); @@ -565,9 +574,10 @@ function TerminalSurfaceSession({ if (availability !== "performed") return availability; if (effect.type === "copy" && selection) { + setCopyError(undefined); void terminalRuntime .copySelection(session.id, viewId, selection, selectionAllRef.current) - .catch((error: unknown) => console.error("[terminal-runtime] clipboard copy failed", error)); + .catch(reportCopyError); return "performed"; } if (effect.type === "select_all") { @@ -656,7 +666,16 @@ function TerminalSurfaceSession({ } return "unperformed"; }, - [interactive, readClipboard, session.handle, session.id, setLocalSelection, terminalRuntime, viewId], + [ + interactive, + readClipboard, + reportCopyError, + session.handle, + session.id, + setLocalSelection, + terminalRuntime, + viewId, + ], ); // Text input is typing: it ends the local selection, as a forwarded key does. @@ -1047,9 +1066,10 @@ function TerminalSurfaceSession({ setLocalSelection(null); terminalRuntime.setSelection(session.handle, null, viewId); } else { + setCopyError(undefined); void terminalRuntime .copySelection(session.id, viewId, selectionRef.current, selectionAllRef.current) - .catch((error: unknown) => console.error("[terminal-runtime] clipboard copy failed", error)); + .catch(reportCopyError); } } pointerModeRef.current = null; @@ -1137,6 +1157,11 @@ function TerminalSurfaceSession({ return (
+ {copyError ? ( +
+ {copyError} +
+ ) : null} { it("uses terminal columns for mixed ASCII, block, and box cells", () => { @@ -16,6 +16,54 @@ describe("WebGPU special-cell classification", () => { expect([...cells.block]).toEqual([[2, "▀"]]); expect([...cells.box]).toEqual([[4, "╭"]]); }); + + it("does not replace letters with blocks after a host-width ZWJ cluster", () => { + // Browser widths put the block at column 4; the host puts the letter a + // there. The native font glyphs remain the exact fallback for this row. + const cells = specialDrawingCells("👩‍💻ab▀X", [ + { cellStart: 0, cellSpan: 4 }, + { cellStart: 4, cellSpan: 1 }, + { cellStart: 5, cellSpan: 1 }, + { cellStart: 6, cellSpan: 1 }, + { cellStart: 7, cellSpan: 1 }, + ]); + expect(cells.block.size).toBe(0); + expect(cells.box.size).toBe(0); + }); + + it("preserves procedural drawing when the host grid agrees", () => { + const cells = specialDrawingCells("日▀e\u0301╭", [ + { cellStart: 0, cellSpan: 2 }, + { cellStart: 2, cellSpan: 1 }, + { cellStart: 3, cellSpan: 1 }, + { cellStart: 4, cellSpan: 1 }, + ]); + expect([...cells.block]).toEqual([[2, "▀"]]); + expect([...cells.box]).toEqual([[4, "╭"]]); + }); +}); + +describe("native selection backdrop", () => { + it("covers following text after a host's four-column joined emoji", () => { + expect( + selectionRowCellLength("👩‍💻X", [ + { cellStart: 0, cellSpan: 4 }, + { cellStart: 4, cellSpan: 1 }, + ]), + ).toBe(5); + expect( + selectionRowCellLength("🛙X", [ + { cellStart: 0, cellSpan: 2 }, + { cellStart: 2, cellSpan: 1 }, + ]), + ).toBe(3); + }); + + it("keeps trailing spaces and text-only fallback selection", () => { + expect(selectionRowCellLength("X ", [{ cellStart: 0, cellSpan: 1 }])).toBe(4); + expect(selectionRowCellLength("👩‍💻X ", [{ cellStart: 4, cellSpan: 1 }])).toBe(8); + expect(selectionRowCellLength("日e\u0301", [])).toBe(3); + }); }); describe("WebGPU effect targets", () => { diff --git a/packages/ghosttea-react/src/renderers/webgpu-renderer.ts b/packages/ghosttea-react/src/renderers/webgpu-renderer.ts index 0c81cdc5..747f557b 100644 --- a/packages/ghosttea-react/src/renderers/webgpu-renderer.ts +++ b/packages/ghosttea-react/src/renderers/webgpu-renderer.ts @@ -1,6 +1,12 @@ /// -import { CursorStyle, GlyphFormat, type GlyphDefinition, type StyleDefinition } from "@vibecook/ghosttea-frame"; +import { + CursorStyle, + GlyphFormat, + type GlyphDefinition, + type GlyphInstance, + type StyleDefinition, +} from "@vibecook/ghosttea-frame"; import { emptyRenderMetrics, type TerminalRenderMetrics } from "../performance.js"; import { @@ -562,6 +568,19 @@ function cellLength(text: string): number { return splitGraphemes(text).reduce((total, grapheme) => total + graphemeCellWidth(grapheme), 0); } +type NativeCellSpan = Pick; + +function nativeCellEnd(glyphs: readonly NativeCellSpan[]): number { + return glyphs.reduce((end, glyph) => Math.max(end, glyph.cellStart + glyph.cellSpan), 0); +} + +/** Every selected native glyph needs a selection backdrop, even when the host + * uses a different Unicode version or legacy ZWJ width. Keep trailing spaces. */ +export function selectionRowCellLength(text: string, glyphs: readonly NativeCellSpan[]): number { + const trailing = text.slice(text.trimEnd().length); + return Math.max(cellLength(text), nativeCellEnd(glyphs) + cellLength(trailing)); +} + const BOX_DIRECTIONS = new Map([ ["─", "lr"], ["━", "lr"], @@ -629,7 +648,7 @@ export interface SpecialDrawingCells { block: Map; } -export function specialDrawingCells(text: string): SpecialDrawingCells { +export function specialDrawingCells(text: string, glyphs?: readonly NativeCellSpan[]): SpecialDrawingCells { const box = new Map(); const block = new Map(); let column = 0; @@ -651,6 +670,13 @@ export function specialDrawingCells(text: string): SpecialDrawingCells { } if (simple) return { box, block }; + // TRF1 provides glyph grid positions but no text-to-cell mapping. When widths + // disagree, or a ZWJ makes that mapping ambiguous, draw the host's actual + // glyphs. Guessing a column can turn an unrelated letter into a block. + if (glyphs?.length && (text.includes("\u200d") || nativeCellEnd(glyphs) !== cellLength(text.trimEnd()))) { + return { box, block }; + } + box.clear(); block.clear(); column = 0; @@ -1648,7 +1674,10 @@ export class WebGpuTerminalRenderer implements TerminalRenderer { for (const row of renderRows) { if (row < start.row || row > end.row) continue; const first = row === start.row ? start.column : 0; - const last = row === end.row ? end.column : Math.max(0, cellLength(view.rows[row] ?? "") - 1); + const last = + row === end.row + ? end.column + : Math.max(0, selectionRowCellLength(view.rows[row] ?? "", view.nativeRows[row] ?? []) - 1); pushRectangle( rectangleVertices, (ORIGIN_X + first * CELL_WIDTH) * scale, @@ -1666,7 +1695,7 @@ export class WebGpuTerminalRenderer implements TerminalRenderer { if (hasNativeRows) { for (const row of renderRows) { - const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? ""); + const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "", view.nativeRows[row]); for (const instance of view.nativeRows[row] ?? []) { const definition = view.glyphDefinitions.get(instance.glyphId); if (!definition) continue; @@ -1885,7 +1914,10 @@ export class WebGpuTerminalRenderer implements TerminalRenderer { for (const row of renderRows) { if (row < start.row || row > end.row) continue; const first = row === start.row ? start.column : 0; - const last = row === end.row ? end.column : Math.max(0, cellLength(view.rows[row] ?? "") - 1); + const last = + row === end.row + ? end.column + : Math.max(0, selectionRowCellLength(view.rows[row] ?? "", view.nativeRows[row] ?? []) - 1); pushRectangle( rectangleVertices, (ORIGIN_X + first * CELL_WIDTH) * scale, @@ -1904,7 +1936,7 @@ export class WebGpuTerminalRenderer implements TerminalRenderer { const hasNativeRows = view.nativeRows.some((row) => row.length > 0); if (hasNativeRows) { for (const row of renderRows) { - const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? ""); + const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "", view.nativeRows[row]); for (const instance of view.nativeRows[row] ?? []) { const definition = view.glyphDefinitions.get(instance.glyphId); if (!definition) continue; diff --git a/packages/ghosttea-react/src/routed-activation.ts b/packages/ghosttea-react/src/routed-activation.ts index cbc6a95e..a46b238d 100644 --- a/packages/ghosttea-react/src/routed-activation.ts +++ b/packages/ghosttea-react/src/routed-activation.ts @@ -28,6 +28,9 @@ export interface RoutedActivationState { activationId: string; phase: RoutedActivationPhase; inputPolicy: RoutedInputPolicy; + sourceDemand: "live" | "none"; + resumeDemandSequence?: number; + inputStatusAfterDemand?: number; unavailableReason?: string; replacesActivationId?: string; controlAttached: boolean; @@ -84,6 +87,8 @@ export type RoutedActivationEvent = | { type: "renew-failed" } | { type: "renewed"; grantGeneration: number; rights: readonly RoutedSessionRight[] } | { type: "input-policy"; policy: RoutedInputPolicy } + | { type: "demand-declared"; mode: "live" | "none"; sequence?: number } + | { type: "demand-accepted"; sequence: number } | { type: "retry" } | { type: "detach" | "replaced" | "runtime-destroyed" }; @@ -97,6 +102,7 @@ export function initialRoutedActivation( activationId, phase: "unresolved", inputPolicy, + sourceDemand: "live", controlAttached: false, framesAttached: false, rights: [], @@ -130,6 +136,9 @@ function withReadiness(state: RoutedActivationState): RoutedActivationState { sceneCoversAccepted; const inputAllowed = presentationReady && + state.sourceDemand === "live" && + state.resumeDemandSequence === undefined && + (state.inputStatusAfterDemand === undefined || state.lastCellStatusSequence > state.inputStatusAfterDemand) && state.inputPolicy === "read-write" && state.grantInputValid && state.rights.includes("input") && @@ -180,6 +189,25 @@ export function reduceRoutedActivation( if (event.type === "input-policy") return withReadiness({ ...state, inputPolicy: event.policy }); switch (event.type) { + case "demand-declared": { + const next = { ...state, sourceDemand: event.mode }; + if (event.mode === "none") { + delete next.resumeDemandSequence; + delete next.inputStatusAfterDemand; + } else if (state.sourceDemand === "none" || state.resumeDemandSequence !== undefined) { + // A cached worker frame and old allowed lease cannot authorize input + // into a view the cell is still reattaching after a visibility change. + if (event.sequence !== undefined) next.resumeDemandSequence = event.sequence; + next.inputStatusAfterDemand = state.lastCellStatusSequence; + } + return withReadiness(next); + } + case "demand-accepted": { + if (state.resumeDemandSequence !== event.sequence) return state; + const next = { ...state, inputStatusAfterDemand: state.lastCellStatusSequence }; + delete next.resumeDemandSequence; + return withReadiness(next); + } case "ticket-minted": if (!["unresolved", "recovering", "unavailable"].includes(state.phase)) return state; if (!event.endpointsPresent) { @@ -213,6 +241,8 @@ export function reduceRoutedActivation( delete reset.trfIdentity; delete reset.cellStatus; delete reset.presentationStatus; + delete reset.resumeDemandSequence; + delete reset.inputStatusAfterDemand; return { ...reset, phase: "attaching", diff --git a/packages/ghosttea-react/src/routed-control.test.ts b/packages/ghosttea-react/src/routed-control.test.ts index b19763e0..2250f78f 100644 --- a/packages/ghosttea-react/src/routed-control.test.ts +++ b/packages/ghosttea-react/src/routed-control.test.ts @@ -145,6 +145,34 @@ describe("main-thread routed control transport", () => { return { transport, events, open, connect, accepted, attaches }; } + it("correlates demand acknowledgements across sessions sharing a control connection", () => { + const h = replacementFixture(); + try { + h.open("activation-a"); + const socket = h.connect(); + h.accepted(socket, "activation-a", 1); + h.transport.attach({ + cellBootId, + controlUrl: "ws://127.0.0.1/control", + transportGrant, + attachGrant: { ...attachGrant, claims: { ...attachGrant.claims, sessionId: "session-b" } }, + activationId: "activation-b", + initialDemand: { mode: "live" }, + }); + h.transport.declareDemand("activation-a", { mode: "none" }); + const a = h.transport.declareDemand("activation-a", { mode: "live" })!; + h.transport.declareDemand("activation-b", { mode: "none" }); + const b = h.transport.declareDemand("activation-b", { mode: "live" })!; + expect(a).not.toBe(b); + socket.receive(encodeRoutedMessage("DemandAccepted", { demandSequence: b })); + expect(h.events.at(-1)).toEqual({ type: "demand-accepted", activationId: "activation-b", demandSequence: b }); + socket.receive(encodeRoutedMessage("DemandAccepted", { demandSequence: a })); + expect(h.events.at(-1)).toEqual({ type: "demand-accepted", activationId: "activation-a", demandSequence: a }); + } finally { + h.transport.dispose(); + } + }); + it("replaces the native attachment after the last local view detached", () => { const h = replacementFixture(); try { diff --git a/packages/ghosttea-react/src/routed-control.ts b/packages/ghosttea-react/src/routed-control.ts index 4a7bf1f1..458e9df5 100644 --- a/packages/ghosttea-react/src/routed-control.ts +++ b/packages/ghosttea-react/src/routed-control.ts @@ -3,6 +3,8 @@ import { ROUTED_DOOR_LIMITS, ROUTED_LEG_OUTBOUND, ROUTED_PROTOCOL_VERSION, + ROUTED_SELECTION_TEXT_CAPABILITY, + type RoutedSelectionTextRequest, decodeRoutedMessage, encodeRoutedMessage, type RoutedCellActivationStatus, @@ -106,6 +108,15 @@ interface ControlConnection { accepted?: RoutedConnectionAccepted; activationIds: Set; pendingGeometry: PendingGeometryReply[]; + pendingSelections: Map< + string, + { + request: RoutedSelectionTextRequest; + resolve(text: string): void; + reject(error: Error): void; + timer: ReturnType; + } + >; replacements: Map; heartbeatSequence: number; heartbeatAckSequence: number; @@ -132,6 +143,9 @@ export class RoutedControlTransport { readonly #acceptExtensionMessages: boolean; readonly #connections = new Map(); readonly #activations = new Map(); + // DemandAccepted names no activation. Globally unique sequences preserve + // per-activation monotonicity without confusing two sessions on one leg. + #nextDemandSequence = 0; #disposed = false; constructor(options: { @@ -187,7 +201,7 @@ export class RoutedControlTransport { replacement?.admitted !== activationId ) return undefined; - activation.demandSequence += 1; + activation.demandSequence = ++this.#nextDemandSequence; const body: RoutedDeclareDemand = { sessionId: activation.request.attachGrant.claims.sessionId, activationId, @@ -238,6 +252,56 @@ export class RoutedControlTransport { }); } + /** A read, independent of the input lease. Never send it to an older host. + * Bound outstanding work and reject on detach, timeout or connection loss. */ + selectionText( + activationId: string, + selection: Omit, + ): Promise { + const activation = this.#activations.get(activationId); + const connection = activation ? this.#connections.get(activation.request.cellBootId) : undefined; + if ( + !activation || + !connection?.accepted?.capabilities.includes(ROUTED_SELECTION_TEXT_CAPABILITY) || + connection.socket.readyState !== socketOpen || + !activation.request.attachGrant.claims.rights.includes("read") + ) { + return Promise.reject(new Error("Selection copy is unavailable on this terminal host.")); + } + if (connection.pendingSelections.size >= 16) return Promise.reject(new Error("Too many pending selection copies.")); + const request: RoutedSelectionTextRequest = { + ...selection, + requestId: crypto.randomUUID(), + sessionId: activation.request.attachGrant.claims.sessionId, + activationId, + leaseEpoch: activation.request.attachGrant.claims.leaseEpoch ?? 0, + }; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + connection.pendingSelections.delete(request.requestId); + reject(new Error("Selection copy timed out.")); + }, 5_000); + connection.pendingSelections.set(request.requestId, { request, resolve, reject, timer }); + try { + if (!this.#sendActivation(activationId, encodeRoutedMessage("SelectionTextRequest", request))) + throw new Error("Selection copy is unavailable on this terminal host."); + } catch (error) { + clearTimeout(timer); + connection.pendingSelections.delete(request.requestId); + reject(error); + } + }); + } + + #cancelSelections(connection: ControlConnection, activationId?: string): void { + for (const [id, pending] of connection.pendingSelections) { + if (activationId !== undefined && pending.request.activationId !== activationId) continue; + clearTimeout(pending.timer); + connection.pendingSelections.delete(id); + pending.reject(new Error("Terminal detached before selection copy completed.")); + } + } + /** * Extension seam for a future terminal-input wire verb. The current T1 * contract publishes no such tag, so Ghosttea never invents one itself. @@ -254,6 +318,7 @@ export class RoutedControlTransport { this.#activations.delete(activationId); const connection = this.#connections.get(activation.request.cellBootId); connection?.activationIds.delete(activationId); + if (connection) this.#cancelSelections(connection, activationId); const replacement = connection?.replacements.get(activation.request.attachGrant.claims.sessionId); if (replacement?.queued === activation) delete replacement.queued; } @@ -263,6 +328,7 @@ export class RoutedControlTransport { this.#disposed = true; for (const activationId of [...this.#activations.keys()]) this.detach(activationId); for (const connection of this.#connections.values()) { + this.#cancelSelections(connection); if (connection.heartbeatTimer !== undefined) clearTimeout(connection.heartbeatTimer); if (connection.heartbeatDeadlineTimer !== undefined) clearTimeout(connection.heartbeatDeadlineTimer); if (connection.socket.readyState < socketClosing) connection.socket.close(1000, "runtime-destroyed"); @@ -290,6 +356,7 @@ export class RoutedControlTransport { socket, activationIds: new Set(), pendingGeometry: [], + pendingSelections: new Map(), replacements: new Map(), heartbeatSequence: 0, heartbeatAckSequence: 0, @@ -375,6 +442,35 @@ export class RoutedControlTransport { this.#closeConnection(connection, 4003, "message before ConnectionAccepted", true); return; } + if (message.type === "SelectionTextResult") { + if (!connection.accepted.capabilities.includes(ROUTED_SELECTION_TEXT_CAPABILITY)) { + this.#closeConnection(connection, ROUTED_CLOSE_CODES.PROTOCOL, "unnegotiated selection result", true); + return; + } + const pending = connection.pendingSelections.get(message.requestId); + if (!pending) return; // A late result from a timed-out/detached request. + const request = pending.request; + if ( + message.sessionId !== request.sessionId || + message.activationId !== request.activationId || + message.leaseEpoch !== request.leaseEpoch + ) { + this.#closeConnection(connection, ROUTED_CLOSE_CODES.PROTOCOL, "selection result binding mismatch", true); + return; + } + clearTimeout(pending.timer); + connection.pendingSelections.delete(message.requestId); + if (message.error !== undefined) + pending.reject( + new Error( + message.error === "TOO_LARGE" + ? "Selection is too large to copy. Select a smaller range." + : "Selection copy is unavailable on this terminal host.", + ), + ); + else pending.resolve(message.text!); + return; + } if (message.type === "LegHeartbeatAck") { if (message.sequence > connection.heartbeatSequence) { this.#closeConnection(connection, ROUTED_CLOSE_CODES.PROTOCOL, "heartbeat ack is ahead", true); @@ -642,6 +738,7 @@ export class RoutedControlTransport { #closed(connection: ControlConnection, code: number, reason: string): void { if (this.#connections.get(connection.cellBootId) !== connection) return; this.#connections.delete(connection.cellBootId); + this.#cancelSelections(connection); if (connection.heartbeatTimer !== undefined) clearTimeout(connection.heartbeatTimer); if (connection.heartbeatDeadlineTimer !== undefined) clearTimeout(connection.heartbeatDeadlineTimer); this.#emit({ diff --git a/packages/ghosttea-react/src/routed-runtime.test.ts b/packages/ghosttea-react/src/routed-runtime.test.ts index c0b349c8..c820ae73 100644 --- a/packages/ghosttea-react/src/routed-runtime.test.ts +++ b/packages/ghosttea-react/src/routed-runtime.test.ts @@ -157,16 +157,23 @@ interface RoutedSeatHarness { /** One routed session ("session-a", handle "11", client "window-1") with its control leg attached. */ async function routedSeat( - options: { views?: string[]; rights?: string[]; attach?: boolean } = {}, + options: { + views?: string[]; + rights?: string[]; + attach?: boolean; + capabilities?: string[]; + copy?: (text: string) => void; + } = {}, ): Promise { vi.stubGlobal("window", globalThis); const worker = new FakeWorker(); const socket = new FakeSocket(); const runtime = new GhostteaTerminalRuntime({ transport: "routed", + capabilities: options.capabilities ?? ["resume"], host: { openTicket: async () => ticket("cell-a", "session-a", "set-a") }, platform: { - writeClipboard: () => undefined, + writeClipboard: options.copy ?? (() => undefined), forceCanvasFallback: () => false, setForceCanvasFallback: () => undefined, reload: () => undefined, @@ -187,7 +194,7 @@ async function routedSeat( legGeneration: 1, heartbeatTtlMs: 15_000, protocolLimits: DEFAULT_ROUTED_PROTOCOL_LIMITS, - capabilities: ["resume"], + capabilities: options.capabilities ?? ["resume"], }), ); const attach = JSON.parse(socket.sent.at(-1)!) as SentMessage & { activationId: string }; @@ -346,6 +353,47 @@ describe("routed terminal runtime", () => { return { runtime, worker, socket, first, mount, attaches, framesAttaches, connect, accept, present, dispose }; } + it("holds input while hidden and until the resumed demand has a fresh cell status", async () => { + const h = inputFixture(); + try { + h.mount("view-a"); + await h.connect(); + const activationId = h.attaches().at(-1)!.activationId; + h.accept(activationId, 1); + h.present(activationId); + const status = { ...h.runtime.routedActivation(h.first.id)!.cellStatus! }; + delete status.type; + expect(h.runtime.routedViewInputAllowed("view-a")).toBe(true); + h.runtime.setVisible(h.first.handle, false, "view-a"); + expect.soft(h.runtime.routedViewInputAllowed("view-a")).toBe(false); + h.runtime.sendText(h.first.id, "view-a", "hidden"); + h.runtime.setVisible(h.first.handle, true, "view-a"); + h.socket.receive(encodeRoutedMessage("CellActivationStatus", { ...status, cellStatusSequence: 2 })); + expect.soft(h.runtime.routedViewInputAllowed("view-a")).toBe(false); + h.runtime.sendText(h.first.id, "view-a", "before demand acknowledgement"); + const demands = h.socket.sent.map((line) => JSON.parse(line)).filter((m) => m.type === "DeclareDemand"); + h.socket.receive(encodeRoutedMessage("DemandAccepted", { demandSequence: demands.at(-2)!.demandSequence })); + expect.soft(h.runtime.routedViewInputAllowed("view-a")).toBe(false); + h.socket.receive(encodeRoutedMessage("DemandAccepted", { demandSequence: demands.at(-1)!.demandSequence })); + h.socket.receive(encodeRoutedMessage("CellActivationStatus", { ...status, cellStatusSequence: 2 })); + expect.soft(h.runtime.routedViewInputAllowed("view-a")).toBe(false); + h.socket.receive( + encodeRoutedMessage("CellActivationStatus", { + ...status, + cellStatusSequence: 3, + input: { state: "suspended", reason: "pending" }, + }), + ); + expect(h.runtime.routedViewInputAllowed("view-a")).toBe(false); + h.socket.receive(encodeRoutedMessage("CellActivationStatus", { ...status, cellStatusSequence: 4 })); + expect(h.runtime.routedViewInputAllowed("view-a")).toBe(true); + h.runtime.sendText(h.first.id, "view-a", "after confirmation"); + expect(h.socket.sent.map((line) => JSON.parse(line)).filter((m) => m.type === "TerminalInput")).toHaveLength(1); + } finally { + h.dispose(); + } + }); + it("restores input after repeated last-pane detach and reattach with a fresh presentation lease", async () => { const h = inputFixture(); try { @@ -1152,6 +1200,75 @@ describe("routed terminal runtime", () => { runtime.dispose(); }); + it.each(["visible", "focused"])( + "retries exhausted control recovery when its pane becomes %s again", + async (trigger) => { + vi.useFakeTimers(); + vi.stubGlobal("window", globalThis); + const worker = new FakeWorker(); + const sockets: FakeSocket[] = []; + const openTicket = vi.fn(async () => ticket("cell-a", "session-a", "set-a")); + const runtime = new GhostteaTerminalRuntime({ + transport: "routed", + host: { openTicket }, + platform: { + writeClipboard: () => undefined, + forceCanvasFallback: () => false, + setForceCanvasFallback: () => undefined, + reload: () => undefined, + }, + websocketFactory: () => { + const socket = new FakeSocket(); + sockets.push(socket); + return socket as unknown as WebSocket; + }, + workerFactory: () => worker as unknown as Worker, + }); + try { + runtime.registerSession(session("session-a", "11")); + runtime.mount("session-a", "11", "view-a", canvas()); + await flush(); + if (trigger === "visible") runtime.setVisible("11", false, "view-a"); + else runtime.setFocused("11", "view-a", false, 80, 24); + // A covered renderer can repeatedly miss its heartbeat before a new + // scene is ready. Exhaust the actual bounded retry path, not its reducer. + for (let attempt = 0; attempt < 6; attempt++) { + const socket = sockets.at(-1)!; + socket.open(); + socket.receive( + encodeRoutedMessage("ConnectionAccepted", { + selectedProtocolVersion: { major: 1, minor: 0 }, + connectionSetId: "set-a", + channel: "control", + legGeneration: attempt + 1, + heartbeatTtlMs: 15_000, + protocolLimits: DEFAULT_ROUTED_PROTOCOL_LIMITS, + capabilities: ["resume"], + }), + ); + socket.close(4004, "heartbeat timeout"); + await vi.advanceTimersByTimeAsync(2_100); + } + expect(runtime.routedActivation("session-a")).toMatchObject({ + phase: "unavailable", + unavailableReason: "recovery-exhausted", + inputAllowed: false, + }); + expect(openTicket).toHaveBeenCalledTimes(6); + if (trigger === "visible") runtime.setVisible("11", true, "view-a"); + else runtime.setFocused("11", "view-a", true, 80, 24); + await vi.advanceTimersByTimeAsync(150); + expect(openTicket).toHaveBeenCalledTimes(7); + expect(runtime.routedActivation("session-a")?.phase).toBe("attaching"); + expect(runtime.routedViewInputAllowed("view-a")).toBe(false); + } finally { + runtime.dispose(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + } + }, + ); + it("re-dials one protocol failure, then makes a persistent failure unavailable", async () => { vi.stubGlobal("window", globalThis); const worker = new FakeWorker(); @@ -1220,6 +1337,12 @@ describe("routed terminal runtime", () => { activationId: secondActivationId, }); + runtime.setVisible(first.handle, false, "view-a"); + runtime.setVisible(first.handle, true, "view-a"); + runtime.setFocused(first.handle, "view-a", true, 80, 24); + expect(runtime.routedActivation(first.id)?.phase).toBe("unavailable"); + expect(openTicket).toHaveBeenCalledTimes(2); + runtime.dispose(); }); @@ -2118,3 +2241,80 @@ describe("focus and the routed geometry seat (TP-R4)", () => { } }); }); + +describe("routed selection clipboard ownership", () => { + it("discards an older refusal after a newer copy succeeds", async () => { + const copied: string[] = []; + const f = await routedSeat({ capabilities: ["selection-text"], copy: (text) => copied.push(text) }); + const selection = { anchor: { column: 0, row: 0 }, focus: { column: 4, row: 0 } }; + const first = f.runtime.copySelection("session-a", "view-a", selection); + await flush(); + const request1 = JSON.parse(f.socket.sent.at(-1)!) as Record; + const second = f.runtime.copySelection("session-a", "view-a", selection); + await flush(); + const request2 = JSON.parse(f.socket.sent.at(-1)!) as Record; + f.socket.receive(JSON.stringify({ ...request2, type: "SelectionTextResult", text: "newest" })); + await expect(second).resolves.toBe("newest"); + f.socket.receive(JSON.stringify({ ...request1, type: "SelectionTextResult", error: "UNAVAILABLE" })); + await expect(first).resolves.toBe(""); + expect(copied).toEqual(["newest"]); + f.runtime.dispose(); + }); + it("keeps the newest copy and preserves the clipboard for empty selections", async () => { + const copied: string[] = []; + const f = await routedSeat({ capabilities: ["selection-text"], copy: (text) => copied.push(text) }); + const selection = { anchor: { column: 0, row: 0 }, focus: { column: 4, row: 0 } }; + const first = f.runtime.copySelection("session-a", "view-a", selection); + await flush(); + const request1 = JSON.parse(f.socket.sent.at(-1)!) as Record; + const second = f.runtime.copySelection("session-a", "view-a", selection); + await flush(); + const request2 = JSON.parse(f.socket.sent.at(-1)!) as Record; + f.socket.receive(JSON.stringify({ ...request2, type: "SelectionTextResult", text: "newest" })); + await expect(second).resolves.toBe("newest"); + f.socket.receive(JSON.stringify({ ...request1, type: "SelectionTextResult", text: "older" })); + await expect(first).resolves.toBe(""); + const empty = f.runtime.copySelection("session-a", "view-a", selection); + await flush(); + const request3 = JSON.parse(f.socket.sent.at(-1)!) as Record; + f.socket.receive(JSON.stringify({ ...request3, type: "SelectionTextResult", text: "" })); + await expect(empty).resolves.toBe(""); + expect(copied).toEqual(["newest"]); + f.runtime.dispose(); + }); + it("copies a read-only pane without acquiring input or geometry", async () => { + const copied: string[] = []; + const f = await routedSeat({ + rights: ["read"], + capabilities: ["selection-text"], + copy: (text) => copied.push(text), + }); + const pending = f.runtime.copySelection( + "session-a", + "view-a", + { anchor: { column: 0, row: 0 }, focus: { column: 79, row: 159 } }, + true, + ); + await flush(); + const request = JSON.parse(f.socket.sent.at(-1)!) as Record; + expect(request.type).toBe("SelectionTextRequest"); + f.socket.receive(JSON.stringify({ ...request, type: "SelectionTextResult", text: "中 é 👩‍💻 🛙X" })); + await expect(pending).resolves.toBe("中 é 👩‍💻 🛙X"); + expect(copied).toEqual(["中 é 👩‍💻 🛙X"]); + expect(f.verbs()).toEqual([]); + f.runtime.dispose(); + }); + it("does not overwrite the clipboard when the runtime closes before a response", async () => { + const copied: string[] = []; + const f = await routedSeat({ capabilities: ["selection-text"], copy: (text) => copied.push(text) }); + const pending = expect( + f.runtime.copySelection("session-a", "view-a", { anchor: { column: 0, row: 0 }, focus: { column: 4, row: 0 } }), + ).rejects.toThrow(); + await flush(); + const request = JSON.parse(f.socket.sent.at(-1)!) as Record; + f.runtime.dispose(); + f.socket.receive(JSON.stringify({ ...request, type: "SelectionTextResult", text: "late" })); + await pending; + expect(copied).toEqual([]); + }); +}); diff --git a/packages/ghosttea-react/src/routed-selection.test.ts b/packages/ghosttea-react/src/routed-selection.test.ts new file mode 100644 index 00000000..4bf75d16 --- /dev/null +++ b/packages/ghosttea-react/src/routed-selection.test.ts @@ -0,0 +1,182 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + DEFAULT_ROUTED_PROTOCOL_LIMITS, + encodeRoutedMessage, + type RoutedCellTransportGrant, + type RoutedSessionAttachGrant, +} from "@vibecook/ghosttea-protocol"; +import { RoutedControlTransport } from "./routed-control"; + +class FakeSocket extends EventTarget { + readyState = 0; + readonly sent: string[] = []; + + send(data: string): void { + this.sent.push(data); + } + + open(): void { + this.readyState = 1; + this.dispatchEvent(new Event("open")); + } + + receive(data: string): void { + this.dispatchEvent(new MessageEvent("message", { data })); + } + + close(code = 1000, reason = ""): void { + if (this.readyState === 3) return; + this.readyState = 3; + const event = new Event("close") as Event & { code: number; reason: string }; + Object.assign(event, { code, reason }); + this.dispatchEvent(event); + } +} + +const cellBootId = "cell-a"; +const transportGrant: RoutedCellTransportGrant = { + protected: { + v: 1, + typ: "CellTransportGrant", + iss: "fieldd", + alg: "HS256", + kid: { cellBootId, keyGeneration: 1 }, + }, + claims: { + audienceCellBootId: cellBootId, + clientId: "client-a", + connectionSetId: "set-a", + allowedChannels: ["control", "frames"], + transportGrantGeneration: 1, + issuedAt: 1, + expiresAt: 2, + nonce: "nonce-a", + }, + mac: "opaque", +}; +const attachGrant: RoutedSessionAttachGrant = { + protected: { + v: 1, + typ: "SessionAttachGrant", + iss: "fieldd", + alg: "HS256", + kid: { cellBootId, keyGeneration: 1 }, + }, + claims: { + audienceCellBootId: cellBootId, + clientId: "client-a", + sessionId: "session-a", + leaseEpoch: 1, + routeRevision: 1, + grantGeneration: 1, + rights: ["input", "read"], + issuedAt: 1, + expiresAt: 2, + }, + mac: "opaque", +}; + +const selection = { startColumn: 0, startRow: 0, endColumn: 79, endRow: 159, selectAll: true }; +const transports: RoutedControlTransport[] = []; +afterEach(() => { + for (const t of transports.splice(0)) t.dispose(); + vi.useRealTimers(); +}); +function fixture(negotiated = true) { + const socket = new FakeSocket(); + const transport = new RoutedControlTransport({ + socketFactory: () => socket as unknown as WebSocket, + emit: () => undefined, + }); + transports.push(transport); + transport.attach({ + cellBootId, + controlUrl: "ws://localhost/control", + transportGrant, + attachGrant: { ...attachGrant, claims: { ...attachGrant.claims, rights: ["read"] } }, + activationId: "activation-a", + initialDemand: { mode: "live" }, + capabilities: ["selection-text"], + }); + socket.open(); + socket.receive( + encodeRoutedMessage("ConnectionAccepted", { + selectedProtocolVersion: { major: 1, minor: 0 }, + connectionSetId: "set-a", + channel: "control", + legGeneration: 1, + heartbeatTtlMs: 15_000, + protocolLimits: DEFAULT_ROUTED_PROTOCOL_LIMITS, + capabilities: negotiated ? ["selection-text"] : [], + }), + ); + const requests = () => + socket.sent + .map((text) => JSON.parse(text) as { type: string; requestId: string }) + .filter((message) => message.type === "SelectionTextRequest"); + const answer = (requestId: string, fields: Record) => + socket.receive( + JSON.stringify({ + type: "SelectionTextResult", + requestId, + sessionId: "session-a", + activationId: "activation-a", + leaseEpoch: 1, + ...fields, + }), + ); + return { transport, socket, requests, answer }; +} +describe("routed selection reads", () => { + it("does not send an unknown verb to an older host", async () => { + const f = fixture(false); + await expect(f.transport.selectionText("activation-a", selection)).rejects.toThrow("unavailable"); + expect(f.requests()).toHaveLength(0); + expect(f.socket.readyState).toBe(1); + }); + it("copies Unicode for a read-only grant and correlates out-of-order replies", async () => { + const f = fixture(); + const first = f.transport.selectionText("activation-a", selection); + const second = f.transport.selectionText("activation-a", { ...selection, selectAll: false }); + const requests = f.requests(); + f.answer(requests[1]!.requestId, { text: "" }); + f.answer(requests[0]!.requestId, { text: "中 é 👩‍💻 🛙X\nrow 160" }); + await expect(first).resolves.toBe("中 é 👩‍💻 🛙X\nrow 160"); + await expect(second).resolves.toBe(""); + }); + it("rejects a reply bound to a different session and closes the offending leg", async () => { + const f = fixture(); + const answer = expect(f.transport.selectionText("activation-a", selection)).rejects.toThrow("detached"); + f.answer(f.requests()[0]!.requestId, { sessionId: "other-session", text: "private" }); + await answer; + expect(f.socket.readyState).toBe(3); + }); + it("bounds pending reads, cancels on detach, and ignores late replies", async () => { + const f = fixture(); + const pending = Array.from({ length: 16 }, () => + expect(f.transport.selectionText("activation-a", selection)).rejects.toThrow("detached"), + ); + await expect(f.transport.selectionText("activation-a", selection)).rejects.toThrow("Too many"); + f.transport.detach("activation-a"); + await Promise.all(pending); + f.answer(f.requests()[0]!.requestId, { text: "late" }); + expect(f.socket.readyState).toBe(1); + }); + it("times out and cancels reads on connection loss without replay", async () => { + vi.useFakeTimers(); + const f = fixture(); + const timed = expect(f.transport.selectionText("activation-a", selection)).rejects.toThrow("timed out"); + await vi.advanceTimersByTimeAsync(5_001); + await timed; + const closed = expect(f.transport.selectionText("activation-a", selection)).rejects.toThrow("detached"); + f.socket.close(); + await closed; + }); + it("reports oversized copies without truncating text or disconnecting", async () => { + const f = fixture(); + const pending = expect(f.transport.selectionText("activation-a", selection)).rejects.toThrow("too large"); + f.answer(f.requests()[0]!.requestId, { error: "TOO_LARGE" }); + await pending; + expect(f.socket.readyState).toBe(1); + }); +}); diff --git a/packages/ghosttea-react/src/runtime.ts b/packages/ghosttea-react/src/runtime.ts index 05637768..ef2343d9 100644 --- a/packages/ghosttea-react/src/runtime.ts +++ b/packages/ghosttea-react/src/runtime.ts @@ -188,6 +188,7 @@ interface ViewRuntimeState { visible: boolean; inputSequence: number; resizeSequence: number; + copySequence?: number; controlEpoch: number | undefined; desiredCols: number | undefined; desiredRows: number | undefined; @@ -1802,6 +1803,7 @@ export class GhostteaTerminalRuntime extends EventTarget { entry.ticket = ticket; entry.clientId = ticket.attachGrant.claims.clientId; this.#transitionRouted(entry, { type: "transport-ready" }); + this.#transitionRouted(entry, { type: "demand-declared", mode: this.#routedDemand(entry).mode }); this.#routedControl!.attach({ cellBootId: ticket.route.cellBootId, controlUrl: ticket.endpoints.controlUrl, @@ -1985,6 +1987,11 @@ export class GhostteaTerminalRuntime extends EventTarget { } return; } + if (event.type === "demand-accepted") { + const entry = event.activationId ? this.#routedByActivation.get(event.activationId) : undefined; + if (entry) this.#transitionRouted(entry, { type: "demand-accepted", sequence: event.demandSequence }); + return; + } if (event.type === "cell-status") { const entry = this.#routedByActivation.get(event.status.activationId); if (!entry || event.status.sessionId !== entry.sessionId) return; @@ -2438,7 +2445,23 @@ export class GhostteaTerminalRuntime extends EventTarget { } #declareRoutedDemand(entry: RoutedActivationRuntime): void { - this.#routedControl?.declareDemand(entry.state.activationId, this.#routedDemand(entry)); + const demand = this.#routedDemand(entry); + const sequence = this.#routedControl?.declareDemand(entry.state.activationId, demand); + this.#transitionRouted(entry, { + type: "demand-declared", + mode: demand.mode, + ...(sequence === undefined ? {} : { sequence }), + }); + } + + /** A new foreground interaction gets a fresh bounded recovery attempt. + * Background timer throttling can exhaust retries while nobody can use the + * pane. Keep protocol/refusal failures final, and never replay dropped input. */ + #retryExhaustedRoutedActivation(entry: RoutedActivationRuntime): void { + if (entry.state.phase !== "unavailable" || entry.state.unavailableReason !== "recovery-exhausted") return; + entry.recoveryAttempts = 0; + this.#transitionRouted(entry, { type: "retry" }); + this.#recoverRoutedActivation(entry, "retry"); } #releaseRoutedView(sessionId: string, viewId: string): void { @@ -3044,6 +3067,8 @@ export class GhostteaTerminalRuntime extends EventTarget { if (this.#routedHost) { const session = this.#sessionByHandle.get(sessionHandle); if (!session) return; + const entry = this.#routedBySession.get(session.id); + const wasLive = entry !== undefined && this.#routedDemand(entry).mode === "live"; if (surfaceId) { const view = this.#views.get(surfaceId); if (view) view.visible = visible; @@ -3052,8 +3077,10 @@ export class GhostteaTerminalRuntime extends EventTarget { if (view.sessionId === session.id) view.visible = visible; } } - const entry = this.#routedBySession.get(session.id); - if (entry) this.#declareRoutedDemand(entry); + if (entry) { + if (visible && !wasLive) this.#retryExhaustedRoutedActivation(entry); + this.#declareRoutedDemand(entry); + } } } @@ -3327,6 +3354,7 @@ export class GhostteaTerminalRuntime extends EventTarget { if (this.#routedHost) { const entry = this.#routedBySession.get(session.id); if (!entry) return; + if (focused) this.#retryExhaustedRoutedActivation(entry); // The focus that asked for a take still waiting on the leg has ended. if (!focused && entry.queuedTake?.viewId === viewId) delete entry.queuedTake; this.#declareRoutedDemand(entry); @@ -3350,7 +3378,39 @@ export class GhostteaTerminalRuntime extends EventTarget { async copySelection(sessionId: string, viewId: string, selection: CellSelection, selectAll = false): Promise { await this.connect(); - if (this.#routedHost) return ""; + if (this.#routedHost) { + const view = this.#views.get(viewId); + const entry = this.#routedBySession.get(sessionId); + if (!view || view.sessionId !== sessionId || !entry?.state.controlAttached || !entry.viewIds.has(viewId)) + return ""; + const activationId = entry.state.activationId; + const copySequence = (view.copySequence ?? 0) + 1; + view.copySequence = copySequence; + const text = await this.#routedControl!.selectionText(activationId, { + startColumn: selection.anchor.column, + startRow: selection.anchor.row, + endColumn: selection.focus.column, + endRow: selection.focus.row, + selectAll, + }).catch((error: unknown) => { + // A superseded refusal must not replace feedback from a newer copy. + if (view.copySequence !== copySequence) return ""; + throw error; + }); + // A delayed answer must not overwrite the clipboard after its pane was + // closed, moved, or attached to a replacement activation. + if ( + this.#disposed || + this.#views.get(viewId) !== view || + view.copySequence !== copySequence || + !entry.viewIds.has(viewId) || + this.#routedBySession.get(sessionId) !== entry || + entry.state.activationId !== activationId + ) + return ""; + if (text) this.#platform.writeClipboard(text); + return text; + } const view = this.#views.get(viewId); if (!view || view.sessionId !== sessionId) return ""; // A frozen replica stays copyable: offline the daemon answers from the diff --git a/packages/ghosttea/package.json b/packages/ghosttea/package.json index badfd127..d5adc7be 100644 --- a/packages/ghosttea/package.json +++ b/packages/ghosttea/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttea", - "version": "0.13.8", + "version": "0.14.0", "description": "Typed browser client for the Ghosttea terminal service.", "license": "MIT", "author": "James Yong", @@ -38,7 +38,7 @@ "provenance": true }, "dependencies": { - "@vibecook/ghosttea-protocol": "0.13.8" + "@vibecook/ghosttea-protocol": "0.14.0" }, "scripts": { "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", diff --git a/packages/ghosttead-darwin-arm64/package.json b/packages/ghosttead-darwin-arm64/package.json index 0fe488b9..692dfd6e 100644 --- a/packages/ghosttead-darwin-arm64/package.json +++ b/packages/ghosttead-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttead-darwin-arm64", - "version": "0.13.8", + "version": "0.14.0", "description": "Ghosttea terminal daemon binary for macOS on Apple silicon.", "license": "MIT", "author": "James Yong", diff --git a/packages/ghosttead-win32-x64/package.json b/packages/ghosttead-win32-x64/package.json index fc476a29..1e92924a 100644 --- a/packages/ghosttead-win32-x64/package.json +++ b/packages/ghosttead-win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttead-win32-x64", - "version": "0.13.8", + "version": "0.14.0", "description": "Ghosttea terminal daemon binary for Windows on x64.", "license": "MIT", "author": "James Yong", diff --git a/packages/ghosttead/package.json b/packages/ghosttead/package.json index 46d94fa3..b96b3a56 100644 --- a/packages/ghosttead/package.json +++ b/packages/ghosttead/package.json @@ -1,6 +1,6 @@ { "name": "@vibecook/ghosttead", - "version": "0.13.8", + "version": "0.14.0", "description": "Prebuilt Ghosttea terminal daemon, resolved to the binary for the current platform.", "license": "MIT", "author": "James Yong", @@ -48,8 +48,8 @@ "prepublishOnly": "npm run build" }, "optionalDependencies": { - "@vibecook/ghosttead-darwin-arm64": "0.13.8", - "@vibecook/ghosttead-win32-x64": "0.13.8" + "@vibecook/ghosttead-darwin-arm64": "0.14.0", + "@vibecook/ghosttead-win32-x64": "0.14.0" }, "devDependencies": { "@types/node": "26.1.1", diff --git a/scripts/build-ghosttea-apple-native.mjs b/scripts/build-ghosttea-apple-native.mjs index 62a9fd47..f3c8a7fc 100644 --- a/scripts/build-ghosttea-apple-native.mjs +++ b/scripts/build-ghosttea-apple-native.mjs @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; +import { cargoTargetDirectory } from "./cargo-target-directory.mjs"; const root = resolve(import.meta.dirname, ".."); const packageVersion = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version; @@ -184,6 +185,7 @@ execute(process.execPath, [join(root, "scripts/sync-ghosttea-fonts.mjs")]); const ghosttyInfo = readXCFramework(ghosttyArtifact); const sshInfo = readXCFramework(sshArtifact); +const cargoTarget = cargoTargetDirectory(root, environment); rmSync(work, { recursive: true, force: true }); rmSync(output, { recursive: true, force: true }); mkdirSync(prefixRoot, { recursive: true }); @@ -231,11 +233,25 @@ function stageGhosttyPrefix(platform) { for (const platform of platforms) { const prefix = stageGhosttyPrefix(platform); - execute("cargo", ["build", "--locked", "--release", "-p", "ghosttea-apple-ffi", "--target", platform.target], { - environment: { - GHOSTTY_VT_PREFIX: prefix, + execute( + "cargo", + [ + "build", + "--locked", + "--release", + "-p", + "ghosttea-apple-ffi", + "--target", + platform.target, + "--target-dir", + cargoTarget, + ], + { + environment: { + GHOSTTY_VT_PREFIX: prefix, + }, }, - }); + ); } const libraries = []; @@ -278,7 +294,7 @@ for (const platform of platforms) { ].join("\n"), ); - const rustLibrary = join(root, "target", platform.target, "release", "libghosttea_apple_ffi.a"); + const rustLibrary = join(cargoTarget, platform.target, "release", "libghosttea_apple_ffi.a"); if (!existsSync(rustLibrary)) throw new Error(`Missing unified Rust static library ${rustLibrary}.`); requireExactCAbi(rustLibrary); requireUniqueMembers(rustLibrary); diff --git a/scripts/build-ghostty-vt-apple.mjs b/scripts/build-ghostty-vt-apple.mjs index 94751bfe..6847869f 100644 --- a/scripts/build-ghostty-vt-apple.mjs +++ b/scripts/build-ghostty-vt-apple.mjs @@ -11,7 +11,11 @@ const xcframework = join(install, "lib/ghostty-vt.xcframework"); const packageArtifact = join(root, "apple/GhostteaKit/Artifacts/ghostty-vt.xcframework"); const developerDir = process.env.GHOSTTY_DEVELOPER_DIR ?? `/Applications/Xcode_${lock.appleBuilder.xcodeVersion}.app/Contents/Developer`; -const commandEnvironment = { ...process.env, DEVELOPER_DIR: developerDir }; +const commandEnvironment = { + ...process.env, + DEVELOPER_DIR: developerDir, + ZIG_GLOBAL_CACHE_DIR: join(output, "global"), +}; function capture(command, args, cwd = root) { const result = spawnSync(command, args, { cwd, env: commandEnvironment, encoding: "utf8" }); @@ -66,15 +70,16 @@ mkdirSync(output, { recursive: true }); const args = [ "build", + `-j${lock.builder.zigBuildJobs}`, "--cache-dir", join(output, "cache"), - "--global-cache-dir", - join(output, "global"), + "--pkg-dir", + join(output, lock.builder.zigPackageDirectory), "--prefix", install, "-Demit-lib-vt", "-Demit-xcframework", - "-Doptimize=ReleaseFast", + `-Doptimize=${lock.builder.zigOptimize}`, ]; const result = spawnSync(zig, args, { cwd: vendor, env: commandEnvironment, stdio: "inherit" }); if (result.error) throw result.error; diff --git a/scripts/build-ghostty-vt.mjs b/scripts/build-ghostty-vt.mjs index a4164cfc..54c4b9d1 100644 --- a/scripts/build-ghostty-vt.mjs +++ b/scripts/build-ghostty-vt.mjs @@ -51,16 +51,15 @@ function zigArguments(resolve) { return [ "build", `-j${lock.builder.zigBuildJobs}`, - "--seed", - String(lock.builder.zigBuildSeed), "--cache-dir", resolve("cache"), - "--global-cache-dir", - resolve("global"), + "--pkg-dir", + resolve(lock.builder.zigPackageDirectory), "--prefix", resolve("install"), "-Demit-lib-vt", - "-Doptimize=ReleaseFast", + "-Demit-xcframework=false", + `-Doptimize=${lock.builder.zigOptimize}`, `-Dtarget=${config.zigTarget}`, `-Dcpu=${config.zigCpu}`, ...msvcRuntimeArguments(), @@ -97,6 +96,8 @@ function containerBuild() { lock.builder.platform, "--user", `${process.getuid?.() ?? 1000}:${process.getgid?.() ?? 1000}`, + "--env", + "ZIG_GLOBAL_CACHE_DIR=/out/global", "-v", `${vendor}:/src:ro`, "-v", @@ -125,7 +126,7 @@ function nativeBuild() { return spawnSync( zig, zigArguments((part) => join(output, part)), - { cwd: vendor, stdio: "inherit" }, + { cwd: vendor, stdio: "inherit", env: { ...process.env, ZIG_GLOBAL_CACHE_DIR: join(output, "global") } }, ); } diff --git a/scripts/cargo-target-directory.mjs b/scripts/cargo-target-directory.mjs new file mode 100644 index 00000000..1dc240ea --- /dev/null +++ b/scripts/cargo-target-directory.mjs @@ -0,0 +1,21 @@ +import { execFileSync } from "node:child_process"; +import { isAbsolute } from "node:path"; + +// A checkout's target/ symlink does not override Cargo's user/workspace config. +// Ask Cargo so producers and archive consumers use the same directory, including +// when build caches live on an external drive. +export function cargoTargetDirectory(root, env = process.env) { + const metadata = JSON.parse( + execFileSync("cargo", ["metadata", "--format-version", "1", "--no-deps", "--offline", "--locked"], { + cwd: root, + env, + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + stdio: ["ignore", "pipe", "inherit"], + }), + ); + if (typeof metadata.target_directory !== "string" || !isAbsolute(metadata.target_directory)) { + throw new Error("Cargo metadata did not report an absolute target directory."); + } + return metadata.target_directory; +} diff --git a/scripts/check-ghostty-upgrade-procedure.mjs b/scripts/check-ghostty-upgrade-procedure.mjs index 764e276d..5d4093d2 100644 --- a/scripts/check-ghostty-upgrade-procedure.mjs +++ b/scripts/check-ghostty-upgrade-procedure.mjs @@ -35,10 +35,15 @@ const configCommit = configLock.ghostty?.commit; if (!/^[0-9a-f]{40}$/.test(configCommit ?? "")) { throw new Error("native/ghostty-config.lock.json must pin a full lowercase 40-character commit."); } -requireEqual(groundTruthCommit, commit, "Ghostty UX ground-truth commit"); +const uxCommit = configLock.uxGroundTruth?.vendorCommit; +if (!/^[0-9a-f]{40}$/.test(uxCommit ?? "")) { + throw new Error("native/ghostty-config.lock.json must pin the reviewed UX source commit."); +} +requireEqual(groundTruthCommit, uxCommit, "Ghostty UX ground-truth commit"); const version = groundTruthVersion.match(/^\s*-\s+version:\s+(\S+)\s*$/m)?.[1]; if (!version) throw new Error("Ghostty UX ground truth does not report a version."); requireEqual(version, configLock.ghostty?.version, "Ghostty config ground-truth version"); +requireEqual(version, configLock.uxGroundTruth?.cliVersion, "Ghostty UX CLI version"); requireEqual(configLock.ghostty?.tag, `v${version}`, "Ghostty config release tag"); if ( !configCompatibilitySource.includes(`GHOSTTY_CONFIG_COMPAT_COMMIT: &str = "${configCommit}"`) || @@ -99,7 +104,13 @@ requireEqual( requireEqual(artifacts.source, sourceIdentity, "native artifact source-and-recipe identity"); requireEqual(fonts.source, lock.ghostty, "font source pin"); requireEqual(lock.builder?.zigBuildJobs, 1, "Ghostty VT Zig build concurrency"); -requireEqual(lock.builder?.zigBuildSeed, 0, "Ghostty VT Zig build seed"); +requireEqual(lock.builder?.zigOptimize, "fast", "Ghostty VT optimization mode"); +requireEqual(lock.builder?.zigPackageDirectory, "packages", "Ghostty VT writable package directory"); +requireEqual( + lock.builder?.msvcRuntimeWitnesses, + ["ghosttea-msvc-simd.obj", "ghosttea-msvc-simdutf.obj", "ghosttea-msvc-highway.obj"], + "Ghostty VT MSVC runtime witnesses", +); requireEqual(lock.builder?.normalizer?.runner, "macos-26", "Ghostty VT normalizer runner"); for (const field of ["developerDirectory", "xcodeVersion", "xcodeBuild"]) { if (typeof lock.builder?.normalizer?.[field] !== "string" || lock.builder.normalizer[field] === "") { @@ -272,7 +283,10 @@ const desktopBuild = read("scripts/build-ghostty-vt.mjs"); for (const text of [ "-Dcpu=${config.zigCpu}", "-j${lock.builder.zigBuildJobs}", - "lock.builder.zigBuildSeed", + "-Doptimize=${lock.builder.zigOptimize}", + "-Demit-xcframework=false", + "ZIG_GLOBAL_CACHE_DIR", + "lock.builder.zigPackageDirectory", "-Dmsvc-runtime=${config.msvcRuntime}", ]) { if (!desktopBuild.includes(text)) { diff --git a/scripts/check-packages.mjs b/scripts/check-packages.mjs index 972ff31f..ab87e8b5 100644 --- a/scripts/check-packages.mjs +++ b/scripts/check-packages.mjs @@ -256,8 +256,13 @@ try { const rustCrates = join(fixture, "rust-crates"); mkdirSync(rustCrates); + // Fresh archives must belong to this fixture, even when Cargo's configured + // target is shared by other checkouts or lives on an external drive. + const cargoTarget = join(fixture, "rust-package-target"); run("cargo", [ "package", + "--target-dir", + cargoTarget, "--workspace", "--exclude", "ghosttead", @@ -273,7 +278,7 @@ try { "--no-verify", ]); for (const crate of rustPackages) { - run(tar, ["-xzf", join(root, `target/package/${crate}-${version}.crate`), "-C", rustCrates]); + run(tar, ["-xzf", join(cargoTarget, `package/${crate}-${version}.crate`), "-C", rustCrates]); } const embeddingConsumer = join(fixture, "rust-embedding-consumer"); diff --git a/scripts/extract-ghostty-ux.mjs b/scripts/extract-ghostty-ux.mjs index e0d0ff38..79af65c5 100644 --- a/scripts/extract-ghostty-ux.mjs +++ b/scripts/extract-ghostty-ux.mjs @@ -7,9 +7,10 @@ * 1. GHOSTTY env / CLI binary * 2. /Applications/Ghostty.app/Contents/MacOS/ghostty * - * Locked vendor commit (native/ghostty.lock.json) is recorded alongside dumps. - * When CLI dumps disagree with the locked source, the locked source wins — - * re-check by building Ghostty from vendor or re-running after upgrade. + * The application UX baseline is pinned separately from VT in + * native/ghostty-config.lock.json. Both the CLI version and reviewed source + * checkout must match before any fixture is overwritten. GHOSTTY_UX_SOURCE + * selects that checkout when the VT vendor has moved ahead. */ import { execFileSync } from "node:child_process"; @@ -60,10 +61,25 @@ function main() { process.exit(1); } + const configLock = JSON.parse(readFileSync(join(root, "native/ghostty-config.lock.json"), "utf8")); + const expected = configLock.uxGroundTruth; + const version = run(ghostty, ["+version"]); + const cliVersion = version.match(/version:\s*(\S+)/)?.[1]; + if (!expected || cliVersion !== expected.cliVersion || cliVersion !== configLock.ghostty?.version) { + throw new Error(`Ghostty CLI ${cliVersion ?? "unknown"} does not match the reviewed config/UX baseline.`); + } + const vendorGit = process.env.GHOSTTY_UX_SOURCE ?? join(root, "native/vendor/ghostty"); + const vendorCommit = execFileSync("git", ["-C", vendorGit, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + if (vendorCommit !== expected.vendorCommit) { + throw new Error(`UX source must be ${expected.vendorCommit}; set GHOSTTY_UX_SOURCE to that reviewed checkout.`); + } + const vendorNote = + `${vendorCommit}\n` + + execFileSync("git", ["-C", vendorGit, "log", "-1", "--format=%H%n%s%n%ci"], { encoding: "utf8" }); + mkdirSync(outDir, { recursive: true }); mkdirSync(fixtureDir, { recursive: true }); - const version = run(ghostty, ["+version"]); write(join(outDir, "ghostty-version.txt"), version); const keybindsTxt = run(ghostty, ["+list-keybinds", "--default", "--plain"]); @@ -136,24 +152,8 @@ function main() { .filter(Boolean); write(join(outDir, "actions-from-source.json"), JSON.stringify(actions, null, 2)); - // Vendor commit record - const lockPath = join(root, "native/ghostty.lock.json"); - let vendorNote = ""; - if (existsSync(lockPath)) { - const lock = JSON.parse(readFileSync(lockPath, "utf8")); - vendorNote += `${lock.ghostty?.commit ?? "unknown"}\n`; - } - const vendorGit = join(root, "native/vendor/ghostty"); - if (existsSync(join(vendorGit, ".git"))) { - try { - vendorNote += execFileSync("git", ["-C", vendorGit, "log", "-1", "--format=%H%n%s%n%ci"], { - encoding: "utf8", - }); - } catch { - // ignore - } - } - write(join(outDir, "vendor-commit.txt"), vendorNote || "unknown\n"); + // The independently reviewed application UX source, not the current VT source. + write(join(outDir, "vendor-commit.txt"), vendorNote); // Intentional Ghosttea extensions (never overwrite note casually) const extensions = { diff --git a/scripts/ghosttea-apple-native-artifact.mjs b/scripts/ghosttea-apple-native-artifact.mjs index 5143c611..f1b3bd49 100644 --- a/scripts/ghosttea-apple-native-artifact.mjs +++ b/scripts/ghosttea-apple-native-artifact.mjs @@ -197,6 +197,7 @@ export const nativeSourceInputs = [ "Cargo.toml", "native/ghosttea/Cargo.toml", "scripts/build-ghosttea-apple-native.mjs", + "scripts/cargo-target-directory.mjs", "scripts/build-ghostty-vt-apple.mjs", "scripts/ghostty-vt-target.mjs", "scripts/build-ssh-candidate-apple.mjs", diff --git a/scripts/ghostty-vt-msvc-runtime.mjs b/scripts/ghostty-vt-msvc-runtime.mjs index 5d0a01d2..bd3f629b 100644 --- a/scripts/ghostty-vt-msvc-runtime.mjs +++ b/scripts/ghostty-vt-msvc-runtime.mjs @@ -1,12 +1,15 @@ // Reads the C runtime an MSVC static library was compiled for, from the linker // directives its objects carry. // -// Every object compiled against the MSVC C++ standard headers records its C +// With autolink enabled, MSVC C++ standard headers record their C // runtime as `/FAILIFMISMATCH:"RuntimeLibrary=..."` in its `.drectve` section, // and the linker refuses two different values in one link (LNK2038). A Rust // target links the dynamic runtime unless it enables `crt-static`, so each // Windows Ghostty VT target is built for one runtime, and packaging refuses a -// library whose objects say otherwise. +// library whose objects say otherwise. Ghostty 1.4 disables autolink on its +// SIMD code, so its patch adds one metadata-only witness per C++ compile group. +// Witnesses use that group's exact compiler flags plus -fautolink and derive +// the stamp from _MT/_DLL; packaging requires all three, not just any one. const ARCHIVE_MAGIC = "!\n"; const MEMBER_HEADER_BYTES = 60; @@ -103,16 +106,18 @@ function defaultLibraryOf(directive) { * "static"): an object stamped for the other runtime, a default library of the * other runtime, or no stamp at all, which would mean this reader saw nothing. */ -export function msvcRuntimeProblems(archive, runtime) { +export function msvcRuntimeProblems(archive, runtime, requiredWitnesses = []) { const expected = RUNTIME_STAMPS[runtime]; if (!expected) throw new Error(`unknown MSVC runtime ${runtime}`); const problems = []; let stamped = 0; + const witnesses = new Set(); for (const member of archiveDirectives(archive)) { for (const directive of member.directives) { const stamp = stampOf(directive); if (stamp !== null) { stamped += 1; + witnesses.add(member.name.replaceAll("\\", "/").split("/").at(-1)); if (stamp !== expected) problems.push(`${member.name} is stamped RuntimeLibrary=${stamp}, not ${expected}`); } const library = defaultLibraryOf(directive); @@ -122,5 +127,8 @@ export function msvcRuntimeProblems(archive, runtime) { } } if (stamped === 0) problems.push(`no object records a RuntimeLibrary stamp; expected ${expected}`); + for (const witness of requiredWitnesses) { + if (!witnesses.has(witness)) problems.push(`missing stamped runtime witness ${witness}`); + } return problems; } diff --git a/scripts/ghostty-vt-target.mjs b/scripts/ghostty-vt-target.mjs index a6311855..4208fdf8 100644 --- a/scripts/ghostty-vt-target.mjs +++ b/scripts/ghostty-vt-target.mjs @@ -97,6 +97,7 @@ function assertNoIndexOrUntrackedChanges(vendor) { /** The immutable source description recorded in bundles and lock manifests. */ export function ghosttySourceIdentity() { + ghosttyNormalizerFiles(); const patches = patchInputs().map(({ path, sha256: checksum }) => ({ path, sha256: checksum })); return { repository: lock.ghostty.repository, @@ -121,6 +122,17 @@ export function ghosttySourceIdentity() { }; } +/** Reviewed Darwin postprocessing that the Linux cross-builder cannot run. */ +export function ghosttyNormalizerFiles() { + const input = lock.builder.normalizer?.libSystemOverride; + if (!input) return []; + const absolutePath = join(root, input.path); + if (sha256(readFileSync(absolutePath)) !== input.sha256) { + throw new Error(`Ghostty normalizer checksum mismatch: ${input.path}`); + } + return [{ ...input, absolutePath }]; +} + /** Verified patch files for provenance packaging. */ export function ghosttyPatchFiles() { return patchInputs().map(({ path, sha256: checksum, absolutePath }) => ({ diff --git a/scripts/normalize-ghostty-vt.mjs b/scripts/normalize-ghostty-vt.mjs index 6ad7b6b4..117b4c9b 100644 --- a/scripts/normalize-ghostty-vt.mjs +++ b/scripts/normalize-ghostty-vt.mjs @@ -1,8 +1,15 @@ import { createHash } from "node:crypto"; -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; -import { installPrefix, libraryPath, lock, resolveTarget, targetConfig } from "./ghostty-vt-target.mjs"; +import { + ghosttyNormalizerFiles, + installPrefix, + libraryPath, + lock, + resolveTarget, + targetConfig, +} from "./ghostty-vt-target.mjs"; const target = resolveTarget(); const config = targetConfig(target); @@ -51,6 +58,20 @@ for (const expected of [`Xcode ${normalizer.xcodeVersion}`, `Build version ${nor } } +// Upstream applies this step only on a Darwin build host. Our Linux cross-build +// reaches Darwin here: apply the same reviewed compiler-rt/libSystem policy +// before stripping, or its two memset definitions collide in Rust consumers. +const [override] = ghosttyNormalizerFiles(); +if (!override) throw new Error("The Darwin normalizer must pin the upstream libSystem override."); +const normalized = `${library}.libsystem`; +const postprocess = spawnSync("/bin/sh", [override.absolutePath, library, normalized], { + env: commandEnvironment, + stdio: "inherit", +}); +if (postprocess.error) throw postprocess.error; +if (postprocess.status !== 0) process.exit(postprocess.status ?? 1); +renameSync(normalized, library); + const strip = spawnSync("xcrun", ["strip", ...normalizer.stripFlags, library], { env: commandEnvironment, stdio: "inherit", diff --git a/scripts/package-ghostty-vt.mjs b/scripts/package-ghostty-vt.mjs index 50558fb5..8ed4a86a 100644 --- a/scripts/package-ghostty-vt.mjs +++ b/scripts/package-ghostty-vt.mjs @@ -5,6 +5,7 @@ import { release as osRelease } from "node:os"; import { join, relative } from "node:path"; import { artifactNames, + ghosttyNormalizerFiles, ghosttyPatchFiles, ghosttySourceDigest, ghosttySourceIdentity, @@ -109,6 +110,10 @@ const inputs = [ bundlePath: `SOURCE-PATCHES/${patch.path}`, sourcePath: patch.absolutePath, })), + ...ghosttyNormalizerFiles().map((normalizer) => ({ + bundlePath: `SOURCE-NORMALIZERS/${normalizer.path}`, + sourcePath: normalizer.absolutePath, + })), ].map((file) => ({ ...file, contents: readFileSync(file.sourcePath) })); // An MSVC library is built for one C runtime, and every object in a consumer's @@ -116,8 +121,11 @@ const inputs = [ // whatever `--allow-mismatch` says: that flag tolerates a manifest that has not // caught up, never a library its consumers cannot link. if (config.zigTarget.endsWith("-msvc")) { + if (!Array.isArray(lock.builder.msvcRuntimeWitnesses) || lock.builder.msvcRuntimeWitnesses.length !== 3) { + throw new Error("The Windows build recipe must require all three SIMD runtime witnesses."); + } const archive = inputs.find((file) => file.bundlePath === library).contents; - const problems = msvcRuntimeProblems(archive, config.msvcRuntime); + const problems = msvcRuntimeProblems(archive, config.msvcRuntime, lock.builder.msvcRuntimeWitnesses); if (problems.length > 0) { throw new Error(`${target} is not built for the ${config.msvcRuntime} C runtime:\n- ${problems.join("\n- ")}`); } @@ -152,7 +160,8 @@ const artifact = { image: lock.builder.image, platform: lock.builder.platform, zigBuildJobs: lock.builder.zigBuildJobs, - zigBuildSeed: lock.builder.zigBuildSeed, + zigOptimize: lock.builder.zigOptimize, + zigPackageDirectory: lock.builder.zigPackageDirectory, zigVersion: lock.zig.version, zigTarget: config.zigTarget, zigCpu: config.zigCpu, @@ -168,7 +177,8 @@ const artifact = { mode: "native", hostPlatform: config.hostPlatform, zigBuildJobs: lock.builder.zigBuildJobs, - zigBuildSeed: lock.builder.zigBuildSeed, + zigOptimize: lock.builder.zigOptimize, + zigPackageDirectory: lock.builder.zigPackageDirectory, zigVersion: lock.zig.version, zigTarget: config.zigTarget, zigCpu: config.zigCpu, diff --git a/tests/cargo-target-directory.test.mjs b/tests/cargo-target-directory.test.mjs new file mode 100644 index 00000000..928569b8 --- /dev/null +++ b/tests/cargo-target-directory.test.mjs @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { cargoTargetDirectory } from "../scripts/cargo-target-directory.mjs"; + +test("archive consumers follow Cargo's configured directory and environment override", (t) => { + const fixture = realpathSync(mkdtempSync(join(tmpdir(), "ghosttea-cargo-target-"))); + t.after(() => rmSync(fixture, { recursive: true, force: true })); + const root = join(fixture, "workspace"); + mkdirSync(join(root, "src"), { recursive: true }); + mkdirSync(join(root, ".cargo")); + const cargoHome = join(fixture, "cargo-home"); + mkdirSync(cargoHome); + writeFileSync(join(root, "Cargo.toml"), '[package]\nname = "target-probe"\nversion = "0.0.0"\nedition = "2024"\n'); + writeFileSync(join(root, "src/lib.rs"), ""); + writeFileSync(join(root, "Cargo.lock"), 'version = 4\n\n[[package]]\nname = "target-probe"\nversion = "0.0.0"\n'); + const env = { ...process.env, CARGO_HOME: cargoHome }; + delete env.CARGO_TARGET_DIR; + delete env.CARGO_BUILD_TARGET_DIR; + assert.equal(cargoTargetDirectory(root, env), join(root, "target")); + + const external = join(fixture, "portable SSD", "build cache"); + writeFileSync(join(root, ".cargo/config.toml"), `[build]\ntarget-dir = ${JSON.stringify(external)}\n`); + assert.equal(cargoTargetDirectory(root, env), external); + + const override = join(fixture, "isolated candidate"); + assert.equal(cargoTargetDirectory(root, { ...env, CARGO_TARGET_DIR: override }), override); +}); diff --git a/tests/ghostty-vt-msvc-runtime.test.mjs b/tests/ghostty-vt-msvc-runtime.test.mjs index 7779772d..ee5d0383 100644 --- a/tests/ghostty-vt-msvc-runtime.test.mjs +++ b/tests/ghostty-vt-msvc-runtime.test.mjs @@ -95,3 +95,16 @@ test("refuses a library in which no object records a stamp, since that reads as "no object records a RuntimeLibrary stamp; expected MD_DynamicRelease", ]); }); + +test("requires a stamped witness for every independently compiled C++ group", () => { + const required = ["ghosttea-msvc-simd.obj", "ghosttea-msvc-simdutf.obj", "ghosttea-msvc-highway.obj"]; + const complete = required.map((name) => [`D:\\build\\${name}`, object(DYNAMIC)]); + assert.deepEqual(msvcRuntimeProblems(archive(complete), "dll", required), []); + assert.deepEqual(msvcRuntimeProblems(archive(complete.slice(0, 2)), "dll", required), [ + "missing stamped runtime witness ghosttea-msvc-highway.obj", + ]); + complete[2] = [required[2], object(STATIC)]; + assert.ok( + msvcRuntimeProblems(archive(complete), "dll", required).some((problem) => problem.includes("MT_StaticRelease")), + ); +});