git: 05e578be6139 - main - misc/opencode: update to 2.2.5

From: Oleksii Samorukov <samm_at_FreeBSD.org>
Date: Sun, 01 Nov 2026 17:11:51 UTC
The branch main has been updated by samm:

URL: https://cgit.FreeBSD.org/ports/commit/?id=05e578be6139f0a0f385993528923699ebf96474

commit 05e578be6139f0a0f385993528923699ebf96474
Author:     Oleksii Samorukov <samm@FreeBSD.org>
AuthorDate: 2026-11-01 17:11:41 +0000
Commit:     Oleksii Samorukov <samm@FreeBSD.org>
CommitDate: 2026-11-01 17:11:41 +0000

    misc/opencode: update to 2.2.5
---
 misc/opencode/Makefile                             |  53 ++++----
 misc/opencode/Makefile.crates                      |  25 +---
 misc/opencode/distinfo                             |  52 +-------
 misc/opencode/files/patch-packages_app_vite.js     |  19 +++
 .../files/patch-packages_cli_script_build.ts       |  32 ++---
 .../patch-packages_cli_script_opencode-pty.ts      |   2 +-
 .../patch-packages_core_src_config_discovery.ts    |  19 +++
 ...packages_core_src_filesystem_watcher-binding.ts |  24 ++++
 .../patch-packages_core_src_filesystem_watcher.ts  | 148 +++++++++++++++++++++
 misc/opencode/files/setup-opentui-freebsd.py       |  15 +--
 misc/opencode/files/webui-wasm-package.json        |   3 +-
 11 files changed, 263 insertions(+), 129 deletions(-)

diff --git a/misc/opencode/Makefile b/misc/opencode/Makefile
index 260b1f4303bc..277178c23e76 100644
--- a/misc/opencode/Makefile
+++ b/misc/opencode/Makefile
@@ -1,7 +1,6 @@
 PORTNAME=	opencode
 DISTVERSIONPREFIX=	v
-DISTVERSION=	2.1.0
-PORTREVISION=	1
+DISTVERSION=	2.2.5
 CATEGORIES=	misc
 
 # Upstream ships no FreeBSD builds of OpenTUI's Zig core, fff's Rust cdylib, the
@@ -13,15 +12,13 @@ CATEGORIES=	misc
 # OPENTUI_VERSION and FFF_VERSION must match the pinned @opentui/core and
 # @ff-labs/fff-bun releases (the root package.json catalog and
 # packages/core/package.json) or the native FFI ABI breaks.
-OPENTUI_VERSION=	0.6.6
+OPENTUI_VERSION=	0.8.0
 FFF_VERSION=	0.10.5
 #
-# opencode-pty is the persistent terminal daemon the client spawns.  Its version
-# must match the pinned @opencode-ai/pty release and speak the same wire protocol
-# as the bundled client (packages/cli/package.json, packages/core/package.json),
-# and GHOSTTY_REVISION must match its ghostty-revision file, or the client
-# rejects the daemon as a protocol mismatch.
-OPENCODE_PTY_VERSION=	0.3.1
+# opencode-pty is the persistent terminal daemon the client spawns.  It is built
+# from the same source tree as the client (packages/process-daemon), and
+# GHOSTTY_REVISION must match its ghostty-revision file, or the client rejects
+# the daemon as a protocol mismatch.
 GHOSTTY_REVISION=	ab0b9da9e88fcb4b0533a1854e84628f663930af
 
 MASTER_SITES=	https://registry.npmjs.org/@ff-labs/fff-bun/-/:fffbun
@@ -34,7 +31,6 @@ GH_PROJECT=	opencode
 GH_TAGNAME=	v${DISTVERSION}
 GH_TUPLE=	anomalyco:opentui:v${OPENTUI_VERSION}:opentui \
 		dmtrKovalenko:fff:v${FFF_VERSION}:fff \
-		anomalyco:opencode-pty:v${OPENCODE_PTY_VERSION}:opencodepty \
 		ghostty-org:ghostty:${GHOSTTY_REVISION}:ghostty
 
 MAINTAINER=	samm@FreeBSD.org
@@ -59,7 +55,9 @@ CARGO_TEST=	no
 # rolldown has no native binding and its WASI fallback SIGSEGVs under node26.
 BUILD_DEPENDS=	${LOCALBASE}/bin/bun:lang/bun \
 		${LOCALBASE}/bin/zig:lang/zig \
-		${LOCALBASE}/bin/node:www/node24
+		${LOCALBASE}/bin/node:www/node24 \
+		npm-node24>0:www/npm-node24 \
+		gmake:devel/gmake
 # post-fetch warms the offline bun and Ghostty Zig caches, so both are needed.
 # Zig 0.16 hardcodes /etc/ssl/cert.pem on FreeBSD; ca_root_nss provides it.
 FETCH_DEPENDS=	${LOCALBASE}/bin/bun:lang/bun \
@@ -73,7 +71,7 @@ RUN_DEPENDS=	rg:textproc/ripgrep
 
 OPENTUI_WRKSRC=	${WRKDIR}/opentui-${OPENTUI_VERSION}
 FFF_WRKSRC=	${WRKDIR}/fff-${FFF_VERSION}
-OPENCODE_PTY_WRKSRC=	${WRKDIR}/opencode-pty-${OPENCODE_PTY_VERSION}
+OPENCODE_PTY_WRKSRC=	${WRKSRC}/packages/process-daemon
 GHOSTTY_WRKSRC=		${WRKDIR}/ghostty-${GHOSTTY_REVISION}
 
 # Build only the fff C-ABI cdylib (not the nvim/python/mcp crates).
@@ -82,6 +80,8 @@ FFF_SO=			${CARGO_TARGET_DIR}/release/libfff_c.so
 
 BUN=			${LOCALBASE}/bin/bun
 ZIG=			${LOCALBASE}/bin/zig
+NODE=			${LOCALBASE}/bin/node
+NODE_GYP=		${LOCALBASE}/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
 BUN_INSTALL_CACHE_DIR=	${WRKDIR}/bun-cache
 WEBUI_WASM_DIR=	${WRKDIR}/gen-webui
 WEBUI_WASM_CACHE=	${DISTDIR}/${DIST_SUBDIR}/opencode-webui-wasm-${DISTVERSION}.tar.gz
@@ -89,7 +89,8 @@ BUN_CACHE_TARBALL=	${DISTDIR}/${DIST_SUBDIR}/opencode-bun-cache-${DISTVERSION}.t
 ZIG_PTY_CACHE_DIR=	${WRKDIR}/zig-pty-cache
 ZIG_PTY_CACHE=		${DISTDIR}/${DIST_SUBDIR}/opencode-pty-zig-cache-${DISTVERSION}.tar.gz
 
-PLIST_FILES=	bin/${PORTNAME}
+PLIST_FILES=	bin/${PORTNAME} \
+		lib/opencode/watcher.node
 
 .include <bsd.port.pre.mk>
 
@@ -149,25 +150,23 @@ post-extract:
 	@${MKDIR} ${ZIG_PTY_CACHE_DIR}
 	@${TAR} xf ${ZIG_PTY_CACHE} -C ${ZIG_PTY_CACHE_DIR}
 
-# OpenTUI, opencode-pty and Ghostty are extra source trees, so their patches are
-# applied by hand.
+# OpenTUI and Ghostty are extra source trees, and opencode-pty lives in
+# packages/process-daemon, so their patches are applied by hand.
 post-patch:
 	@${PATCH} -d ${OPENTUI_WRKSRC} -p0 --batch < ${FILESDIR}/opentui-build.zig.patch
 	@${PATCH} -d ${OPENTUI_WRKSRC} -p0 --batch < ${FILESDIR}/opentui-ghostty-vt-freebsd.patch
 	@${PATCH} -d ${OPENCODE_PTY_WRKSRC} -p1 --batch < ${FILESDIR}/opencode-pty-build.rs.patch
 	@${PATCH} -d ${GHOSTTY_WRKSRC} -p1 --batch < ${FILESDIR}/ghostty-zig-0.16-std.patch
 
-# Effect 4.0.1 rejects 64-bit st_dev values, which ZFS on FreeBSD reports.
-# Upstream fixed this in Effect 4.0.5, but OpenCode still pins 4.0.1.
 do-build:
 	@${ECHO_MSG} "===> Installing OpenCode dependencies (offline)"
 	@cd ${WRKSRC} && ${SETENV} BUN_INSTALL_CACHE_DIR=${BUN_INSTALL_CACHE_DIR} \
 		${BUN} install --frozen-lockfile
-	@${ECHO_MSG} "===> Allowing 64-bit ZFS device IDs in @effect/platform-node-shared"
-	@for f in ${WRKSRC}/node_modules/.bun/@effect+platform-node-shared@*/node_modules/@effect/platform-node-shared/dist/NodeFileSystem.js \
-		${WRKSRC}/node_modules/@effect/platform-node-shared/dist/NodeFileSystem.js; do \
-		[ -f "$${f}" ] || continue; \
-		${SED} -i '' 's/dev: bigintToNumber(stat.dev, "dev")/dev: Number(stat.dev)/' "$${f}"; \
+	@${ECHO_MSG} "===> Building the @parcel/watcher kqueue backend"
+	@for d in ${WRKSRC}/node_modules/.bun/@parcel+watcher@*/node_modules/@parcel/watcher; do \
+		[ -d "$${d}" ] || continue; \
+		(cd "$${d}" && ${SETENV} HOME=${WRKDIR} ${MAKE_ENV} \
+			${NODE} ${NODE_GYP} rebuild --nodedir=${LOCALBASE} --python=${PYTHON_CMD}); \
 	done
 	@${ECHO_MSG} "===> Installing @ff-labs/fff-bun (upstream ships no FreeBSD package)"
 	@${MKDIR} ${WRKSRC}/node_modules/@ff-labs
@@ -212,10 +211,10 @@ do-build:
 	@cd ${WRKSRC}/packages/cli && ${SETENV} \
 		HOME=${WRKDIR} \
 		OPENCODE_VERSION=${DISTVERSION} \
+		OPENCODE_TARGET=freebsd-${OPENTUI_ARCH} \
 		BUN_INSTALL_CACHE_DIR=${BUN_INSTALL_CACHE_DIR} \
 		OPENCODE_PTY_BIN=${OPENCODE_PTY_BIN} \
 		${BUN} run script/build.ts \
-			--target=opencode-freebsd-${OPENTUI_ARCH} \
 			--skip-install \
 			--outdir=dist
 	@test -x ${OPENCODE_BIN} || (${ECHO_MSG} "Missing ${OPENCODE_BIN}"; ${FALSE})
@@ -225,6 +224,11 @@ do-install:
 	# it (the default ${INSTALL_PROGRAM}) discards that payload, leaving a binary
 	# that runs as plain bun, so install it unstripped.
 	${INSTALL} -m 555 ${OPENCODE_BIN} ${STAGEDIR}${PREFIX}/bin/opencode
+	@${MKDIR} ${STAGEDIR}${PREFIX}/lib/opencode
+	@for f in ${WRKSRC}/node_modules/.bun/@parcel+watcher@*/node_modules/@parcel/watcher/build/Release/watcher.node; do \
+		[ -f "$${f}" ] || continue; \
+		${INSTALL} -m 555 "$${f}" ${STAGEDIR}${PREFIX}/lib/opencode/watcher.node; \
+	done
 
 do-test:
 	@${ECHO_MSG} "==> Running OpenCode smoke test..."
@@ -239,5 +243,8 @@ do-test:
 		${ECHO_MSG} "ERROR: the opencode-pty terminal daemon is not embedded in the binary"; \
 		${FALSE}; \
 	fi
+	@${ECHO_MSG} "==> Checking the @parcel/watcher native binding is installed..."
+	@test -f ${STAGEDIR}${PREFIX}/lib/opencode/watcher.node || \
+		(${ECHO_MSG} "ERROR: watcher.node is missing"; ${FALSE})
 
 .include <bsd.port.post.mk>
diff --git a/misc/opencode/Makefile.crates b/misc/opencode/Makefile.crates
index 0055b2040d21..d151f41ddfb9 100644
--- a/misc/opencode/Makefile.crates
+++ b/misc/opencode/Makefile.crates
@@ -1,5 +1,6 @@
-# Crates.io dependencies of the fff and opencode-pty Cargo.locks (see Makefile).
-# Re-create after bumping FFF_VERSION or OPENCODE_PTY_VERSION with:
+# Crates.io dependencies of the fff and opencode-pty (packages/process-daemon)
+# Cargo.locks (see Makefile).  Re-create after bumping FFF_VERSION or updating
+# packages/process-daemon with:
 #   make cargo-crates-merge               # needs ports-mgmt/portfmt
 # or:
 #   make cargo-crates > Makefile.crates && make makesum
@@ -12,7 +13,6 @@ CARGO_CRATES=ahash-0.8.12 \
 		anstyle-parse-1.0.0 \
 		anstyle-query-1.1.5 \
 		anstyle-wincon-3.0.11 \
-		anyhow-1.0.103 \
 		anyhow-1.0.104 \
 		arrayref-0.3.9 \
 		arrayvec-0.7.6 \
@@ -22,7 +22,6 @@ CARGO_CRATES=ahash-0.8.12 \
 		bincode-1.3.3 \
 		bindgen-0.72.1 \
 		bitflags-1.3.2 \
-		bitflags-2.11.0 \
 		bitflags-2.13.1 \
 		blake3-1.8.3 \
 		block-buffer-0.10.4 \
@@ -52,12 +51,10 @@ CARGO_CRATES=ahash-0.8.12 \
 		cpufeatures-0.2.17 \
 		criterion-0.5.1 \
 		criterion-plot-0.5.0 \
-		crossbeam-channel-0.5.15 \
 		crossbeam-channel-0.5.16 \
 		crossbeam-deque-0.8.6 \
 		crossbeam-epoch-0.9.20 \
 		crossbeam-queue-0.3.12 \
-		crossbeam-utils-0.8.21 \
 		crossbeam-utils-0.8.22 \
 		crunchy-0.2.4 \
 		crypto-common-0.1.7 \
@@ -136,7 +133,6 @@ CARGO_CRATES=ahash-0.8.12 \
 		is_terminal_polyfill-1.70.2 \
 		itertools-0.10.5 \
 		itertools-0.13.0 \
-		itoa-1.0.17 \
 		itoa-1.0.18 \
 		jobserver-0.1.34 \
 		js-sys-0.3.91 \
@@ -144,7 +140,6 @@ CARGO_CRATES=ahash-0.8.12 \
 		kqueue-sys-1.0.4 \
 		lazy_static-1.5.0 \
 		leb128fmt-0.1.0 \
-		libc-0.2.183 \
 		libc-0.2.189 \
 		libgit2-sys-0.18.7+1.9.6 \
 		libloading-0.8.9 \
@@ -155,10 +150,8 @@ CARGO_CRATES=ahash-0.8.12 \
 		litemap-0.8.1 \
 		lmdb-master-sys-0.2.5 \
 		lock_api-0.4.14 \
-		log-0.4.29 \
 		log-0.4.33 \
 		matchers-0.2.0 \
-		memchr-2.8.0 \
 		memchr-2.8.3 \
 		memmap2-0.9.11 \
 		memoffset-0.9.1 \
@@ -206,7 +199,6 @@ CARGO_CRATES=ahash-0.8.12 \
 		powerfmt-0.2.0 \
 		ppv-lite86-0.2.21 \
 		prettyplease-0.2.37 \
-		proc-macro2-1.0.106 \
 		proc-macro2-1.0.107 \
 		proptest-1.11.0 \
 		pyo3-0.24.2 \
@@ -215,12 +207,10 @@ CARGO_CRATES=ahash-0.8.12 \
 		pyo3-macros-0.24.2 \
 		pyo3-macros-backend-0.24.2 \
 		quick-error-1.2.3 \
-		quote-1.0.45 \
 		quote-1.0.47 \
 		r-efi-5.3.0 \
 		r-efi-6.0.0 \
 		rand-0.8.5 \
-		rand-0.9.4 \
 		rand-0.9.5 \
 		rand_chacha-0.3.1 \
 		rand_chacha-0.9.0 \
@@ -247,14 +237,10 @@ CARGO_CRATES=ahash-0.8.12 \
 		schemars_derive-1.2.1 \
 		scopeguard-1.2.0 \
 		semver-1.0.27 \
-		serde-1.0.228 \
 		serde-1.0.229 \
-		serde_core-1.0.228 \
 		serde_core-1.0.229 \
-		serde_derive-1.0.228 \
 		serde_derive-1.0.229 \
 		serde_derive_internals-0.29.1 \
-		serde_json-1.0.149 \
 		serde_json-1.0.151 \
 		serial2-0.2.38 \
 		sha2-0.10.9 \
@@ -269,7 +255,6 @@ CARGO_CRATES=ahash-0.8.12 \
 		socket2-0.6.3 \
 		stable_deref_trait-1.2.1 \
 		strsim-0.11.1 \
-		syn-2.0.117 \
 		syn-2.0.119 \
 		syn-3.0.3 \
 		synchronoise-1.0.1 \
@@ -308,7 +293,6 @@ CARGO_CRATES=ahash-0.8.12 \
 		version_check-0.9.5 \
 		walkdir-2.5.0 \
 		wasi-0.11.1+wasi-snapshot-preview1 \
-		wasip2-1.0.2+wasi-0.2.9 \
 		wasip2-1.0.4+wasi-0.2.12 \
 		wasip3-0.4.0+wasi-0.3.0-rc-2026-01-06 \
 		wasm-bindgen-0.2.114 \
@@ -361,9 +345,7 @@ CARGO_CRATES=ahash-0.8.12 \
 		xxhash-rust-0.8.15 \
 		yoke-0.8.1 \
 		yoke-derive-0.8.1 \
-		zerocopy-0.8.47 \
 		zerocopy-0.8.56 \
-		zerocopy-derive-0.8.47 \
 		zerocopy-derive-0.8.56 \
 		zerofrom-0.1.6 \
 		zerofrom-derive-0.1.6 \
@@ -371,5 +353,4 @@ CARGO_CRATES=ahash-0.8.12 \
 		zerovec-0.11.5 \
 		zerovec-derive-0.11.2 \
 		zlob-1.6.3 \
-		zmij-1.0.21 \
 		zmij-1.0.23
diff --git a/misc/opencode/distinfo b/misc/opencode/distinfo
index c049e364fe40..9ad5ba322651 100644
--- a/misc/opencode/distinfo
+++ b/misc/opencode/distinfo
@@ -1,4 +1,4 @@
-TIMESTAMP = 1792491194
+TIMESTAMP = 1793518348
 SHA256 (opencode/fff-bun-0.10.5.tgz) = 16afa70bcb078c9b28378545396298f73a67fc10e7ff7bba4f9ff1d0462c27f0
 SIZE (opencode/fff-bun-0.10.5.tgz) = 83121
 SHA256 (opencode/rust/crates/ahash-0.8.12.crate) = 5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75
@@ -19,8 +19,6 @@ SHA256 (opencode/rust/crates/anstyle-query-1.1.5.crate) = 40c48f72fd53cd289104fc
 SIZE (opencode/rust/crates/anstyle-query-1.1.5.crate) = 10264
 SHA256 (opencode/rust/crates/anstyle-wincon-3.0.11.crate) = 291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d
 SIZE (opencode/rust/crates/anstyle-wincon-3.0.11.crate) = 12638
-SHA256 (opencode/rust/crates/anyhow-1.0.103.crate) = 2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3
-SIZE (opencode/rust/crates/anyhow-1.0.103.crate) = 48746
 SHA256 (opencode/rust/crates/anyhow-1.0.104.crate) = 330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470
 SIZE (opencode/rust/crates/anyhow-1.0.104.crate) = 48819
 SHA256 (opencode/rust/crates/arrayref-0.3.9.crate) = 76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb
@@ -39,8 +37,6 @@ SHA256 (opencode/rust/crates/bindgen-0.72.1.crate) = 993776b509cfb49c750f11b8f07
 SIZE (opencode/rust/crates/bindgen-0.72.1.crate) = 246071
 SHA256 (opencode/rust/crates/bitflags-1.3.2.crate) = bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a
 SIZE (opencode/rust/crates/bitflags-1.3.2.crate) = 23021
-SHA256 (opencode/rust/crates/bitflags-2.11.0.crate) = 843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af
-SIZE (opencode/rust/crates/bitflags-2.11.0.crate) = 48906
 SHA256 (opencode/rust/crates/bitflags-2.13.1.crate) = b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da
 SIZE (opencode/rust/crates/bitflags-2.13.1.crate) = 51395
 SHA256 (opencode/rust/crates/blake3-1.8.3.crate) = 2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d
@@ -99,8 +95,6 @@ SHA256 (opencode/rust/crates/criterion-0.5.1.crate) = f2b12d017a929603d80db1831c
 SIZE (opencode/rust/crates/criterion-0.5.1.crate) = 110088
 SHA256 (opencode/rust/crates/criterion-plot-0.5.0.crate) = 6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1
 SIZE (opencode/rust/crates/criterion-plot-0.5.0.crate) = 22706
-SHA256 (opencode/rust/crates/crossbeam-channel-0.5.15.crate) = 82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2
-SIZE (opencode/rust/crates/crossbeam-channel-0.5.15.crate) = 92716
 SHA256 (opencode/rust/crates/crossbeam-channel-0.5.16.crate) = d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e
 SIZE (opencode/rust/crates/crossbeam-channel-0.5.16.crate) = 93189
 SHA256 (opencode/rust/crates/crossbeam-deque-0.8.6.crate) = 9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51
@@ -109,8 +103,6 @@ SHA256 (opencode/rust/crates/crossbeam-epoch-0.9.20.crate) = 2d6914041f254d6e917
 SIZE (opencode/rust/crates/crossbeam-epoch-0.9.20.crate) = 47545
 SHA256 (opencode/rust/crates/crossbeam-queue-0.3.12.crate) = 0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115
 SIZE (opencode/rust/crates/crossbeam-queue-0.3.12.crate) = 16270
-SHA256 (opencode/rust/crates/crossbeam-utils-0.8.21.crate) = d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28
-SIZE (opencode/rust/crates/crossbeam-utils-0.8.21.crate) = 42691
 SHA256 (opencode/rust/crates/crossbeam-utils-0.8.22.crate) = 61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17
 SIZE (opencode/rust/crates/crossbeam-utils-0.8.22.crate) = 46874
 SHA256 (opencode/rust/crates/crunchy-0.2.4.crate) = 460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5
@@ -267,8 +259,6 @@ SHA256 (opencode/rust/crates/itertools-0.10.5.crate) = b0fd2260e829bddf4cb6ea802
 SIZE (opencode/rust/crates/itertools-0.10.5.crate) = 115354
 SHA256 (opencode/rust/crates/itertools-0.13.0.crate) = 413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186
 SIZE (opencode/rust/crates/itertools-0.13.0.crate) = 146261
-SHA256 (opencode/rust/crates/itoa-1.0.17.crate) = 92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2
-SIZE (opencode/rust/crates/itoa-1.0.17.crate) = 15905
 SHA256 (opencode/rust/crates/itoa-1.0.18.crate) = 8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682
 SIZE (opencode/rust/crates/itoa-1.0.18.crate) = 15935
 SHA256 (opencode/rust/crates/jobserver-0.1.34.crate) = 9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33
@@ -283,8 +273,6 @@ SHA256 (opencode/rust/crates/lazy_static-1.5.0.crate) = bbd2bcb4c963f2ddae06a2ef
 SIZE (opencode/rust/crates/lazy_static-1.5.0.crate) = 14025
 SHA256 (opencode/rust/crates/leb128fmt-0.1.0.crate) = 09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2
 SIZE (opencode/rust/crates/leb128fmt-0.1.0.crate) = 14138
-SHA256 (opencode/rust/crates/libc-0.2.183.crate) = b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d
-SIZE (opencode/rust/crates/libc-0.2.183.crate) = 816918
 SHA256 (opencode/rust/crates/libc-0.2.189.crate) = 3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2
 SIZE (opencode/rust/crates/libc-0.2.189.crate) = 851502
 SHA256 (opencode/rust/crates/libgit2-sys-0.18.7+1.9.6.crate) = 23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89
@@ -305,14 +293,10 @@ SHA256 (opencode/rust/crates/lmdb-master-sys-0.2.5.crate) = 864808e0b19fb6dd3b70
 SIZE (opencode/rust/crates/lmdb-master-sys-0.2.5.crate) = 178812
 SHA256 (opencode/rust/crates/lock_api-0.4.14.crate) = 224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965
 SIZE (opencode/rust/crates/lock_api-0.4.14.crate) = 29249
-SHA256 (opencode/rust/crates/log-0.4.29.crate) = 5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897
-SIZE (opencode/rust/crates/log-0.4.29.crate) = 51515
 SHA256 (opencode/rust/crates/log-0.4.33.crate) = 0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad
 SIZE (opencode/rust/crates/log-0.4.33.crate) = 52997
 SHA256 (opencode/rust/crates/matchers-0.2.0.crate) = d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9
 SIZE (opencode/rust/crates/matchers-0.2.0.crate) = 7075
-SHA256 (opencode/rust/crates/memchr-2.8.0.crate) = f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79
-SIZE (opencode/rust/crates/memchr-2.8.0.crate) = 97780
 SHA256 (opencode/rust/crates/memchr-2.8.3.crate) = cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98
 SIZE (opencode/rust/crates/memchr-2.8.3.crate) = 99165
 SHA256 (opencode/rust/crates/memmap2-0.9.11.crate) = d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0
@@ -407,8 +391,6 @@ SHA256 (opencode/rust/crates/ppv-lite86-0.2.21.crate) = 85eae3c4ed2f50dcfe72643d
 SIZE (opencode/rust/crates/ppv-lite86-0.2.21.crate) = 22522
 SHA256 (opencode/rust/crates/prettyplease-0.2.37.crate) = 479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b
 SIZE (opencode/rust/crates/prettyplease-0.2.37.crate) = 72033
-SHA256 (opencode/rust/crates/proc-macro2-1.0.106.crate) = 8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934
-SIZE (opencode/rust/crates/proc-macro2-1.0.106.crate) = 59765
 SHA256 (opencode/rust/crates/proc-macro2-1.0.107.crate) = 985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9
 SIZE (opencode/rust/crates/proc-macro2-1.0.107.crate) = 59588
 SHA256 (opencode/rust/crates/proptest-1.11.0.crate) = 4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744
@@ -425,8 +407,6 @@ SHA256 (opencode/rust/crates/pyo3-macros-backend-0.24.2.crate) = 822ece1c7e10127
 SIZE (opencode/rust/crates/pyo3-macros-backend-0.24.2.crate) = 72654
 SHA256 (opencode/rust/crates/quick-error-1.2.3.crate) = a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0
 SIZE (opencode/rust/crates/quick-error-1.2.3.crate) = 15066
-SHA256 (opencode/rust/crates/quote-1.0.45.crate) = 41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924
-SIZE (opencode/rust/crates/quote-1.0.45.crate) = 31564
 SHA256 (opencode/rust/crates/quote-1.0.47.crate) = 1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001
 SIZE (opencode/rust/crates/quote-1.0.47.crate) = 31622
 SHA256 (opencode/rust/crates/r-efi-5.3.0.crate) = 69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f
@@ -435,8 +415,6 @@ SHA256 (opencode/rust/crates/r-efi-6.0.0.crate) = f8dcc9c7d52a811697d2151c701e0d
 SIZE (opencode/rust/crates/r-efi-6.0.0.crate) = 65303
 SHA256 (opencode/rust/crates/rand-0.8.5.crate) = 34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404
 SIZE (opencode/rust/crates/rand-0.8.5.crate) = 87113
-SHA256 (opencode/rust/crates/rand-0.9.4.crate) = 44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea
-SIZE (opencode/rust/crates/rand-0.9.4.crate) = 99786
 SHA256 (opencode/rust/crates/rand-0.9.5.crate) = b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41
 SIZE (opencode/rust/crates/rand-0.9.5.crate) = 100216
 SHA256 (opencode/rust/crates/rand_chacha-0.3.1.crate) = e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88
@@ -489,22 +467,14 @@ SHA256 (opencode/rust/crates/scopeguard-1.2.0.crate) = 94143f37725109f92c262ed2c
 SIZE (opencode/rust/crates/scopeguard-1.2.0.crate) = 11619
 SHA256 (opencode/rust/crates/semver-1.0.27.crate) = d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2
 SIZE (opencode/rust/crates/semver-1.0.27.crate) = 30081
-SHA256 (opencode/rust/crates/serde-1.0.228.crate) = 9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e
-SIZE (opencode/rust/crates/serde-1.0.228.crate) = 83652
 SHA256 (opencode/rust/crates/serde-1.0.229.crate) = 4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba
 SIZE (opencode/rust/crates/serde-1.0.229.crate) = 83669
-SHA256 (opencode/rust/crates/serde_core-1.0.228.crate) = 41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad
-SIZE (opencode/rust/crates/serde_core-1.0.228.crate) = 63111
 SHA256 (opencode/rust/crates/serde_core-1.0.229.crate) = 67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48
 SIZE (opencode/rust/crates/serde_core-1.0.229.crate) = 63100
-SHA256 (opencode/rust/crates/serde_derive-1.0.228.crate) = d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79
-SIZE (opencode/rust/crates/serde_derive-1.0.228.crate) = 59605
 SHA256 (opencode/rust/crates/serde_derive-1.0.229.crate) = e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348
 SIZE (opencode/rust/crates/serde_derive-1.0.229.crate) = 59864
 SHA256 (opencode/rust/crates/serde_derive_internals-0.29.1.crate) = 18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711
 SIZE (opencode/rust/crates/serde_derive_internals-0.29.1.crate) = 26189
-SHA256 (opencode/rust/crates/serde_json-1.0.149.crate) = 83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86
-SIZE (opencode/rust/crates/serde_json-1.0.149.crate) = 155994
 SHA256 (opencode/rust/crates/serde_json-1.0.151.crate) = c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14
 SIZE (opencode/rust/crates/serde_json-1.0.151.crate) = 156556
 SHA256 (opencode/rust/crates/serial2-0.2.38.crate) = b16809bc35793b19ce4e0c53924bc0dce3937f15487997cfdaed936004180730
@@ -533,8 +503,6 @@ SHA256 (opencode/rust/crates/stable_deref_trait-1.2.1.crate) = 6ce2be8dc25455e1f
 SIZE (opencode/rust/crates/stable_deref_trait-1.2.1.crate) = 8186
 SHA256 (opencode/rust/crates/strsim-0.11.1.crate) = 7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f
 SIZE (opencode/rust/crates/strsim-0.11.1.crate) = 14266
-SHA256 (opencode/rust/crates/syn-2.0.117.crate) = e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99
-SIZE (opencode/rust/crates/syn-2.0.117.crate) = 306011
 SHA256 (opencode/rust/crates/syn-2.0.119.crate) = 872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297
 SIZE (opencode/rust/crates/syn-2.0.119.crate) = 307407
 SHA256 (opencode/rust/crates/syn-3.0.3.crate) = 53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3
@@ -611,8 +579,6 @@ SHA256 (opencode/rust/crates/walkdir-2.5.0.crate) = 29790946404f91d9c5d06f9874ef
 SIZE (opencode/rust/crates/walkdir-2.5.0.crate) = 23951
 SHA256 (opencode/rust/crates/wasi-0.11.1+wasi-snapshot-preview1.crate) = ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b
 SIZE (opencode/rust/crates/wasi-0.11.1+wasi-snapshot-preview1.crate) = 28477
-SHA256 (opencode/rust/crates/wasip2-1.0.2+wasi-0.2.9.crate) = 9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5
-SIZE (opencode/rust/crates/wasip2-1.0.2+wasi-0.2.9.crate) = 129778
 SHA256 (opencode/rust/crates/wasip2-1.0.4+wasi-0.2.12.crate) = b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487
 SIZE (opencode/rust/crates/wasip2-1.0.4+wasi-0.2.12.crate) = 135311
 SHA256 (opencode/rust/crates/wasip3-0.4.0+wasi-0.3.0-rc-2026-01-06.crate) = 5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5
@@ -717,12 +683,8 @@ SHA256 (opencode/rust/crates/yoke-0.8.1.crate) = 72d6e5c6afb84d73944e5cedb052c46
 SIZE (opencode/rust/crates/yoke-0.8.1.crate) = 32016
 SHA256 (opencode/rust/crates/yoke-derive-0.8.1.crate) = b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d
 SIZE (opencode/rust/crates/yoke-derive-0.8.1.crate) = 7593
-SHA256 (opencode/rust/crates/zerocopy-0.8.47.crate) = efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87
-SIZE (opencode/rust/crates/zerocopy-0.8.47.crate) = 281496
 SHA256 (opencode/rust/crates/zerocopy-0.8.56.crate) = 556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb
 SIZE (opencode/rust/crates/zerocopy-0.8.56.crate) = 284907
-SHA256 (opencode/rust/crates/zerocopy-derive-0.8.47.crate) = 0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89
-SIZE (opencode/rust/crates/zerocopy-derive-0.8.47.crate) = 97648
 SHA256 (opencode/rust/crates/zerocopy-derive-0.8.56.crate) = f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1
 SIZE (opencode/rust/crates/zerocopy-derive-0.8.56.crate) = 105007
 SHA256 (opencode/rust/crates/zerofrom-0.1.6.crate) = 50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5
@@ -737,17 +699,13 @@ SHA256 (opencode/rust/crates/zerovec-derive-0.11.2.crate) = eadce39539ca5cb39855
 SIZE (opencode/rust/crates/zerovec-derive-0.11.2.crate) = 21421
 SHA256 (opencode/rust/crates/zlob-1.6.3.crate) = f0f010d9b10495cc0037cb47cedc143c39c7fa2d59f752dca19036fd0234a210
 SIZE (opencode/rust/crates/zlob-1.6.3.crate) = 172523
-SHA256 (opencode/rust/crates/zmij-1.0.21.crate) = b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa
-SIZE (opencode/rust/crates/zmij-1.0.21.crate) = 26665
 SHA256 (opencode/rust/crates/zmij-1.0.23.crate) = 29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b
 SIZE (opencode/rust/crates/zmij-1.0.23.crate) = 28612
-SHA256 (opencode/anomalyco-opencode-v2.1.0_GH0.tar.gz) = c2e3f3b1684c93e61efb5bf94ea3dec62f6cfaf9b4930e5103ab49b37d3b2a39
-SIZE (opencode/anomalyco-opencode-v2.1.0_GH0.tar.gz) = 79690631
-SHA256 (opencode/anomalyco-opentui-v0.6.6_GH0.tar.gz) = 9c7539a47c73f11022fcb81840fb7856342804a3d133a8e2a0a8cbe0c66a4fed
-SIZE (opencode/anomalyco-opentui-v0.6.6_GH0.tar.gz) = 31020853
+SHA256 (opencode/anomalyco-opencode-v2.2.5_GH0.tar.gz) = 04621edf6c47c2c7a36982ef39914d482c16977708fbd7b779f49f928bdc4fba
+SIZE (opencode/anomalyco-opencode-v2.2.5_GH0.tar.gz) = 83460564
+SHA256 (opencode/anomalyco-opentui-v0.8.0_GH0.tar.gz) = 20f55c5dea47b6db8d88166a767c9f3a588511662d909b567576b6d45aa37ef2
+SIZE (opencode/anomalyco-opentui-v0.8.0_GH0.tar.gz) = 31706403
 SHA256 (opencode/dmtrKovalenko-fff-v0.10.5_GH0.tar.gz) = db50fa8aaabb77211f8dbeb4d77dc8852e0ee52070e40bfd9ef5af44ac50accc
 SIZE (opencode/dmtrKovalenko-fff-v0.10.5_GH0.tar.gz) = 2798793
-SHA256 (opencode/anomalyco-opencode-pty-v0.3.1_GH0.tar.gz) = 101efcfde719cd9ead649d1fc1e1c31a31e89c854bf63b9cd2e977a7661f18a0
-SIZE (opencode/anomalyco-opencode-pty-v0.3.1_GH0.tar.gz) = 108956
 SHA256 (opencode/ghostty-org-ghostty-ab0b9da9e88fcb4b0533a1854e84628f663930af_GH0.tar.gz) = 651ffa67e98060bc41aac04fcab45f71fb9998eb265957d85a89218ecf94d50b
 SIZE (opencode/ghostty-org-ghostty-ab0b9da9e88fcb4b0533a1854e84628f663930af_GH0.tar.gz) = 38024504
diff --git a/misc/opencode/files/patch-packages_app_vite.js b/misc/opencode/files/patch-packages_app_vite.js
new file mode 100644
index 000000000000..5bb1af4e8e7f
--- /dev/null
+++ b/misc/opencode/files/patch-packages_app_vite.js
@@ -0,0 +1,19 @@
+Disable Solid's template validation in the production web UI build.
+
+babel-plugin-jsx-dom-expressions validates every generated template with
+parse5 when config.validate is enabled.  On FreeBSD/aarch64 this
+intermittently gets stuck in postprocess/isInvalidMarkup and the web UI build
+spins indefinitely.  The validation only warns about malformed template HTML,
+so disable it for the release build.
+
+--- packages/app/vite.js.orig
++++ packages/app/vite.js
+@@ -131,7 +131,7 @@
+     },
+   },
+   ...tailwind,
+-  solidPlugin(),
++  solidPlugin({ solid: { validate: false } }),
+ ]
+ 
+ export function inlineThemePreload(html) {
diff --git a/misc/opencode/files/patch-packages_cli_script_build.ts b/misc/opencode/files/patch-packages_cli_script_build.ts
index e5bfa1373c37..c35100bfb8a7 100644
--- a/misc/opencode/files/patch-packages_cli_script_build.ts
+++ b/misc/opencode/files/patch-packages_cli_script_build.ts
@@ -1,16 +1,14 @@
 Teach OpenCode's CLI build script about FreeBSD.
 
 Upstream's target list only covers linux, darwin and win32, and its per-target
-plugins assume a native @parcel/watcher package and bytecode support, neither of
-which exists on FreeBSD:
+plugins assume a native @parcel/watcher package and bytecode support, neither
+of which exists on FreeBSD:
 
-  * add the freebsd arm64/x64 targets so `--target=opencode-freebsd-<arch>`
+  * add the freebsd arm64/x64 targets so `OPENCODE_TARGET=freebsd-<arch>`
     resolves and `Bun.build({ compile: { target: "bun-freebsd-<arch>" } })`
     produces a FreeBSD executable;
-  * degrade the parcel-watcher binding to a stub on FreeBSD (there is no
-    @parcel/watcher-freebsd-* package); the watcher service already falls back
-    to node:fs for file/entries watches and logs "backend not supported" for
-    recursive directories;
+  * keep the real watcher binding on FreeBSD, since the port builds the native
+    kqueue addon and installs it next to the executable;
   * disable bytecode on FreeBSD, where bun does not emit it.
 
 --- packages/cli/script/build.ts.orig
@@ -22,24 +20,16 @@ which exists on FreeBSD:
 +  { os: "freebsd", arch: "arm64" },
 +  { os: "freebsd", arch: "x64" },
  ]
-
+ 
  const targets =
-@@ -143,10 +145,11 @@
+@@ -143,6 +145,7 @@
    const parcelWatcherPlugin: BunPlugin = {
      name: "parcel-watcher-binding",
      setup(build) {
--      build.onLoad({ filter: /filesystem[/\\]watcher-binding\.ts$/ }, () => ({
--        contents: `export default () => require(${JSON.stringify(parcelWatcherPackage)})`,
--        loader: "js",
--      }))
-+      build.onLoad({ filter: /filesystem[/\\]watcher-binding\.ts$/ }, () =>
-+        item.os === "freebsd"
-+          ? { contents: `export default () => { throw new Error("parcel watcher is unavailable on FreeBSD") }`, loader: "js" }
-+          : { contents: `export default () => require(${JSON.stringify(parcelWatcherPackage)})`, loader: "js" },
-+      )
-     },
-   }
-   const target = targetName(item)
++      if (item.os === "freebsd") return
+       build.onLoad({ filter: /filesystem[/\\]watcher-binding\.ts$/ }, () => ({
+         contents: `export default () => require(${JSON.stringify(parcelWatcherPackage)})`,
+         loader: "js",
 @@ -167,7 +170,7 @@
      external: ["node-gyp"],
      format: "esm",
diff --git a/misc/opencode/files/patch-packages_cli_script_opencode-pty.ts b/misc/opencode/files/patch-packages_cli_script_opencode-pty.ts
index a9446042c039..f2571fb7522b 100644
--- a/misc/opencode/files/patch-packages_cli_script_opencode-pty.ts
+++ b/misc/opencode/files/patch-packages_cli_script_opencode-pty.ts
@@ -8,7 +8,7 @@ binary passed via OPENCODE_PTY_BIN is embedded into the executable.
 --- packages/cli/script/opencode-pty.ts.orig
 +++ packages/cli/script/opencode-pty.ts
 @@ -19,7 +19,13 @@
- const pty = createRequire(core.resolve("@opencode-ai/pty/package.json"))
+ const pty = createRequire(core.resolve("@opencode/pty/package.json"))
  
  export async function resolveOpencodePty(target: Target): Promise<OpencodePtyAsset | undefined> {
 -  if (target.platform !== "darwin" && target.platform !== "linux" && target.platform !== "win32") return undefined
diff --git a/misc/opencode/files/patch-packages_core_src_config_discovery.ts b/misc/opencode/files/patch-packages_core_src_config_discovery.ts
new file mode 100644
index 000000000000..ed3a9e37ef90
--- /dev/null
+++ b/misc/opencode/files/patch-packages_core_src_config_discovery.ts
@@ -0,0 +1,19 @@
+Honor FreeBSD's hier(7) layout for the system-wide configuration directory.
+
+Upstream falls back to /etc/opencode on every platform that is neither Darwin
+nor Windows.  FreeBSD ports install into LOCALBASE, so use
+/usr/local/etc/opencode there instead, alongside the rest of the package's
+configuration.
+
+Upstream PR: https://github.com/anomalyco/opencode/pull/58347
+
+--- packages/core/src/config/discovery.ts.orig
++++ packages/core/src/config/discovery.ts
+@@ -28,6 +28,7 @@
+ export function systemDirectory(platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env) {
+   if (platform === "darwin") return AbsolutePath.make("/Library/Application Support/opencode")
+   if (platform === "win32") return AbsolutePath.make(path.join(env.ProgramData || "C:\\ProgramData", "opencode"))
++  if (platform === "freebsd") return AbsolutePath.make("/usr/local/etc/opencode")
+   return AbsolutePath.make("/etc/opencode")
+ }
+ 
diff --git a/misc/opencode/files/patch-packages_core_src_filesystem_watcher-binding.ts b/misc/opencode/files/patch-packages_core_src_filesystem_watcher-binding.ts
new file mode 100644
index 000000000000..f1618791f35c
--- /dev/null
+++ b/misc/opencode/files/patch-packages_core_src_filesystem_watcher-binding.ts
@@ -0,0 +1,24 @@
+Load the FreeBSD @parcel/watcher binding from the installed addon.
+
+The FreeBSD port builds the native kqueue addon itself and installs it at
+PREFIX/lib/opencode/watcher.node; load that file instead of a non-existent
+@parcel/watcher-freebsd-* package.
+
+--- packages/core/src/filesystem/watcher-binding.ts.orig
++++ packages/core/src/filesystem/watcher-binding.ts
+@@ -1,4 +1,5 @@
+ import { createRequire } from "node:module"
++import path from "node:path"
+ 
+ declare const OPENCODE_LIBC: string | undefined
+ 
+@@ -9,6 +10,8 @@
+   const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
+   return require(
+     process.env.OPENCODE_PARCEL_WATCHER_PATH ??
+-      `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
++      (process.platform === "freebsd"
++        ? path.join(path.dirname(process.execPath), "..", "lib", "opencode", "watcher.node")
++        : `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`),
+   )
+ }
diff --git a/misc/opencode/files/patch-packages_core_src_filesystem_watcher.ts b/misc/opencode/files/patch-packages_core_src_filesystem_watcher.ts
new file mode 100644
index 000000000000..ab14e61e0372
--- /dev/null
+++ b/misc/opencode/files/patch-packages_core_src_filesystem_watcher.ts
@@ -0,0 +1,148 @@
+Fix the FreeBSD watcher without triggering Bun's N-API crash.
+
+Bun 1.4.x segfaults or hangs when @parcel/watcher's kqueue subscription
+delivers events from its callback thread through an N-API thread-safe
+function, and its fs.watch does not report missing entries on FreeBSD.  Use
+the addon's native snapshot API for directory watches and a small JS poller
+for immediate file/entry watches instead.  Await the initial snapshot before
+reporting the subscription ready so config reloads cannot miss the first write.
+
+--- packages/core/src/filesystem/watcher.ts.orig
++++ packages/core/src/filesystem/watcher.ts
+@@ -7,7 +7,8 @@
+ import { FSUtil } from "@opencode/util/fs-util"
+ import { Cause, Context, Effect, Layer, PubSub, RcMap, Stream } from "effect"
+ import { lazy } from "../util/lazy.js"
+-import { watch } from "node:fs"
++import { readdirSync, rmSync, statSync, watch } from "node:fs"
++import { tmpdir } from "node:os"
+ import path from "path"
+ import loadBinding from "./watcher-binding.js"
+ 
+@@ -198,13 +199,126 @@
+   }),
+ )
+ 
++function poll(directory: string, names: ReadonlySet<string>, publish: (update: Update) => void): Subscription {
++  const scan = () => {
++    const next = new Map<string, string>()
++    let entries
++    try {
++      entries = readdirSync(directory, { withFileTypes: true })
++    } catch {
++      return next
++    }
++    for (const entry of entries) {
++      if (!names.has(entry.name)) continue
++      const file = path.join(directory, entry.name)
++      if (entry.isDirectory()) {
++        next.set(file, "directory")
++        continue
++      }
++      try {
++        const stat = statSync(file)
++        next.set(file, `${stat.mtimeMs}:${stat.size}`)
++      } catch {
++        next.set(file, "file")
++      }
++    }
++    return next
++  }
++  let previous = scan()
++  const timer = setInterval(() => {
++    const next = scan()
++    for (const [file, signature] of next) {
++      const prior = previous.get(file)
++      if (prior === undefined) publish({ path: file, type: "create" })
++      else if (prior !== signature) publish({ path: file, type: "update" })
++    }
++    for (const file of previous.keys()) if (!next.has(file)) publish({ path: file, type: "delete" })
++    previous = next
++  }, 100)
++  return {
++    unsubscribe: () => {
++      clearInterval(timer)
++      return Promise.resolve()
++    },
++    backend: "poll",
++  }
++}
++
++function snapshotPoller(
++  native: typeof ParcelWatcher,
++  directory: string,
++  ignore: readonly string[],
++  publish: (update: Update) => void,
++): Effect.Effect<Subscription> {
++  return Effect.promise(async () => {
++    const snapshot = path.join(tmpdir(), `opencode-watch-${process.pid}-${Math.random().toString(36).slice(2)}`)
++    const options = { backend: "brute-force" as ParcelWatcher.BackendType, ignore: [...ignore] }
++    let active = true
++    let ready = false
++    let running = false
++    const tick = async () => {
++      if (!active || running) return
++      running = true
++      try {
++        if (!ready) {
++          await native.writeSnapshot(directory, snapshot, options)
++          ready = true
++          return
++        }
++        const updates = await native.getEventsSince(directory, snapshot, options)
++        await native.writeSnapshot(directory, snapshot, options)
++        for (const update of updates) publish(update)
++      } catch {
++        ready = false
++      } finally {
++        running = false
++      }
++    }
++    // Establish the baseline before reporting the subscription as ready, so
++    // files created by the consumer right after subscribe are observed.
++    try {
++      await native.writeSnapshot(directory, snapshot, options)
++      ready = true
++    } catch {}
++    const timer = setInterval(tick, 250)
++    return {
++      unsubscribe: () => {
++        active = false
++        clearInterval(timer)
++        rmSync(snapshot, { force: true })
++        return Promise.resolve()
++      },
++      backend: "brute-force",
++    }
++  })
++}
++
++
+ export const nativeLayer = Layer.succeed(
+   Native,
+   Native.of({
+     subscribe: (input) => {
++      // Bun's parcel subscription path crashes on FreeBSD (N-API thread-safe
++      // functions). Use the native snapshot API instead, which does not spawn
++      // a callback thread, and poll the immediate entries from JS.
++      if (process.platform === "freebsd" && input.type === "directory") {
++        const native = watcher()
++        if (!native) {
++          return Effect.logError("watcher backend not supported", {
++            directory: input.target,
++            platform: process.platform,
++          }).pipe(Effect.as(undefined))
++        }
++        return snapshotPoller(native, input.target, input.ignore, input.publish)
++      }
+       if (input.type === "file" || input.type === "entries") {
+         const directory = input.type === "file" ? path.dirname(input.target) : input.target
+         const names = new Set(input.type === "file" ? [path.basename(input.target)] : input.names)
++        // Bun's fs.watch does not deliver events for missing entries on FreeBSD.
++        // Poll the containing directory instead of using a native addon.
++        if (process.platform === "freebsd") {
++          return Effect.sync(() => poll(directory, names, input.publish))
++        }
+         return Effect.try(() =>
+           watch(directory, { recursive: false }, (_event, file) => {
+             if (file && !names.has(file)) return
diff --git a/misc/opencode/files/setup-opentui-freebsd.py b/misc/opencode/files/setup-opentui-freebsd.py
index 0ae4a29a705d..5a505ff1e2d6 100644
--- a/misc/opencode/files/setup-opentui-freebsd.py
+++ b/misc/opencode/files/setup-opentui-freebsd.py
@@ -15,7 +15,6 @@ Where <arch> is "x64" or "arm64" (the Node/Bun arch tag), and
 containing node_modules/).
 """
 import os
-import re
 import shutil
 import sys
 
@@ -44,13 +43,6 @@ BRANCH = """  if (process.platform === "freebsd") {
   }
 """
 
-# Upstream disables the native renderer thread on Linux because it is
-# unreliable; on FreeBSD it deadlocks (the TUI freezes after the first render),
-# so force it off there too.  The bundle keeps the guard and the assignment on
-# separate lines, so match across the whitespace between them.
-RENDERER_THREAD_RE = re.compile(r'(if \(process\.platform === "linux")\)(\s*config\.useThread = false)')
-RENDERER_THREAD_NEW = r'\1 || process.platform === "freebsd")\2'
-
 INDEX_BUN = """const module = await import("./libopentui.so", { with: { type: "file" } })
 
 export default module.default
@@ -111,11 +103,6 @@ def patch_bundles(core: str) -> None:
         if 'process.platform === "freebsd"' not in src and THROW in src:
             src = src.replace(THROW, BRANCH + THROW, 1)
             changed = True
-        # Disable the native renderer thread on FreeBSD (see RENDERER_THREAD_RE).
-        out, n = RENDERER_THREAD_RE.subn(RENDERER_THREAD_NEW, src)
-        if n:
-            src = out
-            changed = True
         if changed:
             open(path, "w", encoding="utf-8").write(src)
             print(f"  - patched {name}")
@@ -137,7 +124,7 @@ def create_platform_package(root: str, lib: str, version: str, arch: str) -> str
 def main() -> int:
     root = sys.argv[1]
     lib = sys.argv[2]
-    version = sys.argv[3] if len(sys.argv) > 3 else "0.6.6"
+    version = sys.argv[3] if len(sys.argv) > 3 else "0.8.0"
     arch = sys.argv[4] if len(sys.argv) > 4 else "x64"
     if arch not in ("x64", "arm64"):
         raise SystemExit(f"unsupported arch tag: {arch}")
diff --git a/misc/opencode/files/webui-wasm-package.json b/misc/opencode/files/webui-wasm-package.json
index e83aa7dba2db..fdaaa0c62a9d 100644
--- a/misc/opencode/files/webui-wasm-package.json
+++ b/misc/opencode/files/webui-wasm-package.json
@@ -1,8 +1,9 @@
 {
   "name": "opencode-webui-wasm",
   "private": true,
-  "comment": "WASM fallbacks for the web UI's native Rust bindings, which have no FreeBSD/arm64 builds. Versions must match rolldown / lightningcss / @tailwindcss-oxide in the OpenCode tree.",
+  "comment": "WASM fallbacks for the web UI's native Rust bindings, which have no FreeBSD/arm64 builds. Versions must match rolldown / lightningcss / @tailwindcss-oxide in the OpenCode tree. @napi-rs/wasm-runtime is pinned because 1.2.6 requires an @emnapi version that does not match the one pinned by @rolldown/binding-wasm32-wasi.",
   "dependencies": {
+    "@napi-rs/wasm-runtime": "1.2.5",
     "@rolldown/binding-wasm32-wasi": "1.2.5",
     "lightningcss-wasm": "1.32.0",
     "@tailwindcss/oxide-wasm32-wasi": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz"