Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
49acd5a
feat(web): render terminals with libghostty-vt
StiensWout Jul 29, 2026
c4cb761
fix(web): harden Ghostty terminal interactions
StiensWout Jul 29, 2026
6c2a882
fix(web): preserve terminal-native shortcuts
StiensWout Jul 29, 2026
50d7145
fix(web): keep terminal selections stable
StiensWout Jul 29, 2026
36e3129
fix(web): polish Ghostty terminal rendering
StiensWout Jul 29, 2026
c8db7a7
fix(web): forward terminal input events
StiensWout Jul 29, 2026
9f552d3
fix(web): report terminal copy failures
StiensWout Jul 29, 2026
d72e2ca
fix(web): commit terminal composition once
StiensWout Jul 29, 2026
5de75ee
fix(web): retain terminal composition guard
StiensWout Jul 29, 2026
126cd30
fix(web): distinguish terminal composition input
StiensWout Jul 29, 2026
92bd4ec
feat(web): finish Ghostty terminal interactions
StiensWout Jul 29, 2026
8f08f6a
fix(web): harden Ghostty terminal lifecycle
StiensWout Jul 29, 2026
43cb0e0
fix(web): retain Ghostty key text through encoding
StiensWout Jul 29, 2026
8fd64a1
fix(web): handle Ghostty terminal edge cases
StiensWout Jul 30, 2026
4e35993
fix(web): encode shifted Kitty keys
StiensWout Jul 30, 2026
e383d44
fix(web): respect active keyboard layouts
StiensWout Jul 30, 2026
a5ce471
fix(web): guard keyboard fallback by shift
StiensWout Jul 30, 2026
6d748b6
test(web): cover remapped Kitty control keys
StiensWout Jul 30, 2026
85c30c9
fix(web): keep Ghostty terminal input readable
StiensWout Jul 30, 2026
fa94040
fix(web): align Ghostty cursor with text
StiensWout Jul 30, 2026
69ec0a2
fix(web): preserve terminal mouse defaults
StiensWout Jul 30, 2026
374e1b4
fix(web): avoid terminal flicker while resizing
StiensWout Jul 30, 2026
ed1dc84
feat(web): add Ghostty scrollback scrollbar
StiensWout Jul 30, 2026
c0cf2be
fix(web): keep terminal text inside its cells
StiensWout Jul 30, 2026
e5af532
fix(web): clear stale Ghostty cursors
StiensWout Jul 30, 2026
33cfc29
fix(web): refresh Ghostty scroll state before input
StiensWout Jul 30, 2026
7930110
Merge branch 'main' into agent/ghostty-web-terminal
StiensWout Jul 30, 2026
3dacf68
fix(web): report hover mouse motion only in any-event tracking
StiensWout Jul 31, 2026
73a5323
fix(web): fall back to Nerd Font faces in the terminal canvas
StiensWout Jul 31, 2026
6e17ad9
fix(web): run exit handling when attaching to exited terminals
StiensWout Jul 31, 2026
91de531
Merge branch 'main' into agent/ghostty-web-terminal
StiensWout Jul 31, 2026
da91992
fix(web): polish Ghostty terminal scrolling, selection, and input
StiensWout Jul 31, 2026
94e34c7
fix(web): polish terminal drawer lifecycle around the async surface
StiensWout Jul 31, 2026
748d3a9
fix(web): swallow key releases for presses the terminal never saw
StiensWout Jul 31, 2026
6fa11a7
fix(web): notify the PTY on first fit and keep keyup suppression acro…
StiensWout Jul 31, 2026
da2dbba
fix(web): always install the canvas DPR transform on first fit
StiensWout Jul 31, 2026
0054869
Merge branch 'main' into agent/ghostty-web-terminal
StiensWout Jul 31, 2026
dcc5b37
fix(web): make the paste shortcut deterministic and refuse truncated …
StiensWout Jul 31, 2026
69b11b1
fix(web): converge cached Ghostty source on the pinned revision
StiensWout Jul 31, 2026
2bf617a
fix(web): use Ghostty's soft-wrap flag for links and keep native past…
StiensWout Jul 31, 2026
16b4553
fix(web): only claim the paste race after the native paste delivers text
StiensWout Jul 31, 2026
ad3ec79
fix(web): suppress the browser's default paste insertion unconditionally
StiensWout Jul 31, 2026
3dc5604
refactor(web): single-source the Ghostty pin through embedded build info
StiensWout Jul 31, 2026
6a31d5e
feat(web): groundwork for configurable terminal fonts
StiensWout Jul 31, 2026
b72cc87
fix(web): survive WebKit table quirks, selection run splits, late fonts
StiensWout Jul 31, 2026
c94e6bf
fix(web): anchor the terminal grid to the bottom and serialize setFont
StiensWout Jul 31, 2026
3cf380c
fix(web): repaint resizes synchronously and settle PTY dims after drags
StiensWout Jul 31, 2026
7997cd1
feat(web): bundle the symbols-only Nerd Font for default glyph coverage
StiensWout Jul 31, 2026
e610992
fix(web): move the grid origin only together with a full repaint
StiensWout Jul 31, 2026
124718c
fix(web): flush the pending PTY resize when the surface disposes
StiensWout Jul 31, 2026
5da6b1f
fix(web): keep the resize debounce intact; flush only on dispose
StiensWout Jul 31, 2026
2d2e6c5
fix(web): stop drawer and panel terminals from allocating the same id
StiensWout Jul 31, 2026
bba6211
fix(web): recreate terminal allocators when any allocation source cha…
StiensWout Jul 31, 2026
0b3c1fc
fix(web): anchor to the bottom only once scrollback exists
StiensWout Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/desktop/src/electron/ElectronProtocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("ElectronProtocol", () => {
assert.equal(yield* Effect.promise(() => response.text()), "ok");
assert.include(
response.headers.get("content-security-policy") ?? "",
"script-src 'self' 'unsafe-inline' https://clerk.t3.codes https://challenges.cloudflare.com",
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://clerk.t3.codes https://challenges.cloudflare.com",
);
assert.include(
response.headers.get("content-security-policy") ?? "",
Expand Down Expand Up @@ -212,6 +212,7 @@ describe("ElectronProtocol", () => {
assert.deepEqual(directives["script-src"], [
"'self'",
"'unsafe-inline'",
"'wasm-unsafe-eval'",
"https://clerk.t3.codes",
"https://challenges.cloudflare.com",
]);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/electron/ElectronProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrat
const scriptSources = [
"'self'",
"'unsafe-inline'",
"'wasm-unsafe-eval'",
...(clerkOrigin ? [clerkOrigin] : []),
"https://challenges.cloudflare.com",
];
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ Ghostty's MIT license applies to the vendored framework. Keep this notice in syn
## Ghostty / libghostty-vt

The Android terminal renderer vendors upstream `libghostty-vt` shared libraries and C headers.
The web terminal vendors a WebAssembly build from the same revision and uses the same C ABI.

- Upstream project: https://github.com/ghostty-org/ghostty
- Vendored revision: `9f62873bf195e4d8a762d768a1405a5f2f7b1697`
- License: MIT

Ghostty's MIT license applies to the vendored Android libraries. Keep this notice in sync when
updating `Vendor/libghostty-vt`.
Ghostty's MIT license applies to the vendored Android and web libraries. Keep this notice and both
artifacts in sync when updating `Vendor/libghostty-vt`.

## MesloLGS NF (Android terminal font)

Expand Down
3 changes: 1 addition & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"build:ghostty-wasm": "bash scripts/build-libghostty-wasm.sh",
"dev": "vp dev",
"build": "vp build",
"preview": "vp preview",
Expand Down Expand Up @@ -31,8 +32,6 @@
"@t3tools/shared": "workspace:*",
"@tanstack/react-pacer": "^0.19.4",
"@tanstack/react-router": "^1.160.2",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"class-variance-authority": "^0.7.1",
"effect": "catalog:",
"jose": "catalog:",
Expand Down
123 changes: 123 additions & 0 deletions apps/web/scripts/build-libghostty-wasm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WEB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
REPO_DIR="$(cd "${WEB_DIR}/../.." && pwd)"
MOBILE_VENDOR_DIR="${REPO_DIR}/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt"
VENDOR_DIR="${WEB_DIR}/src/terminal/ghostty/vendor"

GHOSTTY_REVISION="$(tr -d '[:space:]' < "${MOBILE_VENDOR_DIR}/VERSION")"
GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/.cache/t3code/ghostty-${GHOSTTY_REVISION:0:8}}"
GHOSTTY_ZIG_VERSION="${GHOSTTY_ZIG_VERSION:-0.15.2}"
GHOSTTY_ZIG="${GHOSTTY_ZIG:-}"

log() {
printf '[libghostty-vt-wasm] %s\n' "$*"
}

die() {
printf '[libghostty-vt-wasm] error: %s\n' "$*" >&2
exit 1
}

require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

ensure_zig() {
if [[ -n "${GHOSTTY_ZIG}" ]]; then
[[ -x "${GHOSTTY_ZIG}" ]] || die "GHOSTTY_ZIG is not executable: ${GHOSTTY_ZIG}"
return
fi
if command -v zig >/dev/null 2>&1 && [[ "$(zig version)" == "${GHOSTTY_ZIG_VERSION}" ]]; then
GHOSTTY_ZIG="$(command -v zig)"
return
fi

local host_os host_arch cache_dir
host_os="$(uname -s | tr '[:upper:]' '[:lower:]')"
host_arch="$(uname -m)"
case "${host_os}" in
darwin) host_os="macos" ;;
linux) ;;
*) die "unsupported host OS for Zig download: ${host_os}" ;;
esac
case "${host_arch}" in
arm64) host_arch="aarch64" ;;
aarch64 | x86_64) ;;
*) die "unsupported host architecture: ${host_arch}" ;;
esac

cache_dir="${HOME}/.cache/t3code/zig-${GHOSTTY_ZIG_VERSION}"
GHOSTTY_ZIG="${cache_dir}/zig"
if [[ -x "${GHOSTTY_ZIG}" ]]; then
return
fi

require_cmd curl
require_cmd tar
mkdir -p "${cache_dir}"
log "downloading Zig ${GHOSTTY_ZIG_VERSION}"
curl -fsSL \
"https://ziglang.org/download/${GHOSTTY_ZIG_VERSION}/zig-${host_arch}-${host_os}-${GHOSTTY_ZIG_VERSION}.tar.xz" \
| tar -xJ --strip-components=1 -C "${cache_dir}"
}

ensure_ghostty_source() {
require_cmd git
if [[ ! -d "${GHOSTTY_SOURCE_DIR}/.git" ]]; then
mkdir -p "$(dirname "${GHOSTTY_SOURCE_DIR}")"
log "cloning Ghostty ${GHOSTTY_REVISION}"
git clone --filter=blob:none --no-checkout https://github.com/ghostty-org/ghostty.git \
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
"${GHOSTTY_SOURCE_DIR}"
fi

# A cached checkout may still be on a previously pinned revision; converge on
# the pinned one instead of failing the rebuild.
local actual_revision
actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD 2>/dev/null || echo none)"
if [[ "${actual_revision}" != "${GHOSTTY_REVISION}" ]]; then
log "checking out Ghostty ${GHOSTTY_REVISION}"
git -C "${GHOSTTY_SOURCE_DIR}" fetch --depth=1 origin "${GHOSTTY_REVISION}"
git -C "${GHOSTTY_SOURCE_DIR}" checkout --detach "${GHOSTTY_REVISION}"
fi

actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD)"
[[ "${actual_revision}" == "${GHOSTTY_REVISION}" ]] || \
die "expected Ghostty ${GHOSTTY_REVISION}, found ${actual_revision}"
Comment thread
cursor[bot] marked this conversation as resolved.
}

ensure_zig
ensure_ghostty_source

build_root="$(mktemp -d)"
trap 'rm -rf "${build_root}"' EXIT

log "building ${GHOSTTY_REVISION} for wasm32-freestanding"
(
cd "${GHOSTTY_SOURCE_DIR}"
# The pinned revision rides along as semver build metadata so the artifact
# identifies its own provenance through ghostty_build_info(); mobile's
# VERSION file stays the single source of truth for the pin.
"${GHOSTTY_ZIG}" build \
-Demit-lib-vt \
-Dtarget=wasm32-freestanding \
-Doptimize=ReleaseSmall \
-Dstrip=true \
-Dlib-version-string="0.1.0-dev+${GHOSTTY_REVISION}" \
-p "${build_root}"
)

mkdir -p "${VENDOR_DIR}"
cp "${build_root}/bin/ghostty-vt.wasm" "${VENDOR_DIR}/ghostty-vt.wasm"
"${GHOSTTY_ZIG}" build-exe \
"${SCRIPT_DIR}/ghostty-write-pty.zig" \
-target wasm32-freestanding \
-O ReleaseSmall \
-fno-entry \
-rdynamic \
-femit-bin="${VENDOR_DIR}/ghostty-write-pty.wasm"
chmod 0644 "${VENDOR_DIR}/ghostty-write-pty.wasm"
log "wrote ${VENDOR_DIR}/ghostty-vt.wasm"
5 changes: 5 additions & 0 deletions apps/web/scripts/ghostty-write-pty.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extern "env" fn t3_write_pty(terminal: u32, userdata: u32, data: u32, len: u32) void;

export fn ghostty_write_pty(terminal: u32, userdata: u32, data: u32, len: u32) void {
t3_write_pty(terminal, userdata, data, len);
}
Loading
Loading