From 282acc84b0ffc7e1a0b232bd3813075e9f868d69 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Tue, 13 Oct 2026 22:18:26 -0700 Subject: [PATCH 1/3] chore(deps): bump @opentui/core packages to 0.5.17 --- bun.lock | 50 +++++++++++++++++++++++++------------------------- package.json | 18 +++++++++--------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/bun.lock b/bun.lock index 38f79da0f..18fcc534a 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ "@intx/types": "workspace:*", "@intx/workflow-host": "0.3.0", "@modelcontextprotocol/sdk": "^1.29.0", - "@opentui/core": "0.5.12", + "@opentui/core": "0.5.17", "arktype": "catalog:", "isomorphic-git": "catalog:", }, @@ -44,14 +44,14 @@ "ws": "^8.21.0", }, "optionalDependencies": { - "@opentui/core-darwin-arm64": "0.5.12", - "@opentui/core-darwin-x64": "0.5.12", - "@opentui/core-linux-arm64": "0.5.12", - "@opentui/core-linux-arm64-musl": "0.5.12", - "@opentui/core-linux-x64": "0.5.12", - "@opentui/core-linux-x64-musl": "0.5.12", - "@opentui/core-win32-arm64": "0.5.12", - "@opentui/core-win32-x64": "0.5.12", + "@opentui/core-darwin-arm64": "0.5.17", + "@opentui/core-darwin-x64": "0.5.17", + "@opentui/core-linux-arm64": "0.5.17", + "@opentui/core-linux-arm64-musl": "0.5.17", + "@opentui/core-linux-x64": "0.5.17", + "@opentui/core-linux-x64-musl": "0.5.17", + "@opentui/core-win32-arm64": "0.5.17", + "@opentui/core-win32-x64": "0.5.17", }, }, "agents/artist": { @@ -373,7 +373,7 @@ "@intx/workflow": ["@intx/workflow@0.3.0", "", { "dependencies": { "@intx/agent": "0.3.0", "@intx/inference": "0.3.0", "@intx/types": "0.3.0", "arktype": "^2.1.29" } }, "sha512-vaIqTohNNdCVhvZ4btWbIbpjp3140e5939tQP7t+rSkucTvnn6US+bXho1qUJyNQ577mf3YXHGgBCW8ppsnscA=="], - "@intx/workflow-deploy": ["@intx/workflow-deploy@0.3.0", "", { "dependencies": { "@intx/agent": "0.3.0", "@intx/tool-packaging": "0.3.0", "@intx/types": "0.3.0", "@intx/workflow": "0.3.0" } }, "sha512-eiCFS+e6DV9UTWFX5eINQCbXHi2w5Wn9SXtAOHZa4PIXOzxXKVJL/uXE/c6TfCVqhrsDdzYwrtt9koi5qmYXCg=="], + "@intx/workflow-deploy": ["@intx/workflow-deploy@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/tool-packaging": "0.4.0", "@intx/types": "0.4.0", "@intx/workflow": "0.4.0" } }, "sha512-TSLg9lPZvJ+leg2n0I/SWxZjcNQtzsH2oKLD04ptEsBZcbIuMINsQeZBmYV5Brdovy0J/SrWZyrOJ8lc+k/Fdw=="], "@intx/workflow-host": ["@intx/workflow-host@0.3.0", "", { "dependencies": { "@intx/agent": "0.3.0", "@intx/crypto": "0.3.0", "@intx/hub-sessions": "0.3.0", "@intx/inference": "0.3.0", "@intx/log": "0.3.0", "@intx/mail-memory": "0.3.0", "@intx/mime": "0.3.0", "@intx/storage-isogit": "0.3.0", "@intx/types": "0.3.0", "@intx/workflow": "0.3.0", "arktype": "^2.1.29" } }, "sha512-vhujPnhCrf2RensenRPSH63SQZlAvtgwLyZeNWSUHlNJ94g72xzaYAa/eYcc9OHGGdGHVo63OFQ1oS1j3/kORg=="], @@ -407,23 +407,23 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], - "@opentui/core": ["@opentui/core@0.5.12", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.12", "@opentui/core-darwin-x64": "0.5.12", "@opentui/core-linux-arm64": "0.5.12", "@opentui/core-linux-arm64-musl": "0.5.12", "@opentui/core-linux-x64": "0.5.12", "@opentui/core-linux-x64-musl": "0.5.12", "@opentui/core-win32-arm64": "0.5.12", "@opentui/core-win32-x64": "0.5.12" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ZXBE5gmvdovmV8zJQrOQf6E44v1tJRDEgrM2MYhEglzgXZ+smIUp95O8zeRYGsuIzQIiMPMgQqKtTJuzvAb7BQ=="], + "@opentui/core": ["@opentui/core@0.5.17", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.17", "@opentui/core-darwin-x64": "0.5.17", "@opentui/core-linux-arm64": "0.5.17", "@opentui/core-linux-arm64-musl": "0.5.17", "@opentui/core-linux-x64": "0.5.17", "@opentui/core-linux-x64-musl": "0.5.17", "@opentui/core-win32-arm64": "0.5.17", "@opentui/core-win32-x64": "0.5.17" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-75vMaKPKc6tnnUWEultPxiiSU0kqNSdAjbcsFGQecjCF4Z1Jrr4NZBiRo3SoMciai/nq24JCiYcVqPjaKtl/6Q=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YdVnP0tAyerBNl0mIcmQEOotPeZzW1VnSXKBl5cyZ5e6nDd2Y+ui/8eRPpn1oqcamf1NCnzS4ohMgejOvna8Zg=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ptobzW+1zM29nHvadwy+8uW9jvV8NJr63vcQXWe2rQNtpwN/6k7lEXWiS5LndeBs9AMV5tTsXMEMhttgts4BDQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-uRrQJdHmLUSj3PV23QPi3WSimYTTxcXnVouxF6U4xMXlOv4N3SxnHfVwMRQkPqbGOfvVWHeLE6FdK4C+ubU0sQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-6yLbnElIZPxhZSrZSyS4mB5+k0YFlpRO0Q8uX4sLFTmvOy8tIeDPFcTGRQqZQ1Rk938tiAhf7iyAX3P1wL4Liw=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-XeKhuIaEtgipvuPHbl4qPOBj+Ut+2zObmsxMVM1jDcjz/FatG9PGeGQPx1G1SnvH2AgpT4K+eCu7DUF0+yIqoQ=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HO3dYgzQBGJhe58uJauVClIJg3vToYl/LZa7ZzqxQ+3vZ//cxaJ7tgXhHWR2uJE02550yI69e5nqBpSOTT9E8Q=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-VZ2sNMw1d/r1SLPjUbOP9LKscKz1CQjID8adTL6gG8Lrrq+mYcIUxutyB+P/eG0J/7oRZLPR6OMt7dUOap6RTg=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-QMRdDbmR0LwpmX1HXVbN+V3A44/QsbWzMsF3V7cEz7MaDq6RNBs3CUFqfkPorKgPtI75T0AxM7FdJnjt9C4d0Q=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-eZiCjEzwbb6qClPPfk32Nha9xmr9obt69Xj0+9SKsXxWLBKkjQEGOMRoh/R9ObaQF4aq8If1xV3VEY0sD9W9vg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.17", "", { "os": "linux", "cpu": "x64" }, "sha512-lNmtQlcwsio4IVo2trc0QoMyun31UsN0mzmSc2BtYMOeOC5e0PyWaEAZWJb/g6AinOs9xwJGL02JAN5J3QPWsw=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-WWW0hVBoSYZ3D6AgZ4u2Y5/u/IyIq2pDb+4yI3WgJ70Wyt6ofHy+6kRGRgbXFn1p+rPInAHjCXD2v6C7iEKSrA=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M2RatQaeET/werHFSH+cg7OkQk/Wn0VoVXZLLEL7DiCDTuhZyMfi3uFDz0+R1IGiiUITqHWpZdZ1LdBY8NCjwQ=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-aLbm6870Ybls6CYL4zMOCImTBPLZHZMUXJFGqMI44lIWxitkAtT6zg5lYA4oRqFRzzryDclxr29+hDgT3p3Blw=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-MyVVpZ0CyGDQ+g4eM0zURqEd/9jDrBIPcZdLyZOmWbdoVu36PLM9KAkqwl/n5ASn/rOrQd+d14jUAsZJvFTqzw=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.12", "", { "os": "win32", "cpu": "x64" }, "sha512-KTwtwpfd2zF9opVh3SyRJYDd1o3Xv4XL8OZb8Zi+CqWUel6Y2IDCiVivCv8fGJt3J7wOIXXtuZI9ZUkLyKJCiQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.17", "", { "os": "win32", "cpu": "x64" }, "sha512-etkuzyfclRX9wel4ivCOk90dUdZNtjK7U6vr7ZHmwkZyOik34ec3Gse0B/jyk192Xdx0l50Tl2xgC1h68XRiEQ=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q=="], @@ -1009,12 +1009,16 @@ "@intx/hub-api/@intx/hub-sessions": ["@intx/hub-sessions@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/crypto": "0.4.0", "@intx/db": "0.4.0", "@intx/hub-common": "0.4.0", "@intx/log": "0.4.0", "@intx/mime": "0.4.0", "@intx/pack-transport": "0.4.0", "@intx/storage-isogit": "0.4.0", "@intx/tool-packaging": "0.4.0", "@intx/types": "0.4.0", "@intx/workflow": "0.4.0", "@intx/workflow-deploy": "0.4.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "isomorphic-git": "^1.27.2", "tar": "^7.5.1" } }, "sha512-/3nd/B/gENy3lq+MA2AR95tBu3Rkkh176XM+A8/mNMGnolPBGw+1JBUGGJ8gvdvAfo64dwRcEs3BI/VD7zFlPQ=="], - "@intx/hub-api/@intx/workflow-deploy": ["@intx/workflow-deploy@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/tool-packaging": "0.4.0", "@intx/types": "0.4.0", "@intx/workflow": "0.4.0" } }, "sha512-TSLg9lPZvJ+leg2n0I/SWxZjcNQtzsH2oKLD04ptEsBZcbIuMINsQeZBmYV5Brdovy0J/SrWZyrOJ8lc+k/Fdw=="], - "@intx/hub-sessions/@intx/db": ["@intx/db@0.3.0", "", { "dependencies": { "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", "@intx/log": "0.3.0", "@intx/types": "0.3.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "postgres": "^3.4.8" } }, "sha512-MXNeUVJTftQuSHj5zSUBQsUcFN3wUBs8t6yT6lqjbaNxpT1C7kGx8qXb95WuadZln+krlLhyoUwGMF7EmYvlnw=="], "@intx/hub-sessions/@intx/hub-common": ["@intx/hub-common@0.3.0", "", { "dependencies": { "@intx/types": "0.3.0" } }, "sha512-dA8pTmBTu1JHGd3rGJrMsRWrai4GBBNnPxpZFFtXwQPzgL290lx2R+9BpRo2apdKbmgd+KOVshLvOdcUhAqghw=="], + "@intx/hub-sessions/@intx/workflow-deploy": ["@intx/workflow-deploy@0.3.0", "", { "dependencies": { "@intx/agent": "0.3.0", "@intx/tool-packaging": "0.3.0", "@intx/types": "0.3.0", "@intx/workflow": "0.3.0" } }, "sha512-eiCFS+e6DV9UTWFX5eINQCbXHi2w5Wn9SXtAOHZa4PIXOzxXKVJL/uXE/c6TfCVqhrsDdzYwrtt9koi5qmYXCg=="], + + "@intx/workflow-deploy/@intx/tool-packaging": ["@intx/tool-packaging@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/log": "0.4.0", "@intx/storage-isogit": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29", "isomorphic-git": "^1.27.2", "npm-package-arg": "^12.0.2", "npm-pick-manifest": "^10.0.0", "npm-registry-fetch": "^19.0.0", "semver": "^7.7.2", "ssri": "^12.0.0", "tar": "^7.5.1" } }, "sha512-eBZIbGUcEBp0ppMCceA1PPd75LvXpUyWitqTIu7AsLxRNlU9gbE1hymWPXnhUKnt3LgSlHUi959Ehd6jG6mR7g=="], + + "@intx/workflow-deploy/@intx/workflow": ["@intx/workflow@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/inference": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-F5Is/lyKb4/WguAVYMDXwtI4ddUvqXbg34rcM6hPUOqupJHpbbD9+ZK/6TDfvbCiEgginbTVwMmNj6Fyg8vU+Q=="], + "@npmcli/agent/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], @@ -1055,10 +1059,6 @@ "@intx/hub-api/@intx/hub-sessions/@intx/workflow": ["@intx/workflow@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/inference": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-F5Is/lyKb4/WguAVYMDXwtI4ddUvqXbg34rcM6hPUOqupJHpbbD9+ZK/6TDfvbCiEgginbTVwMmNj6Fyg8vU+Q=="], - "@intx/hub-api/@intx/workflow-deploy/@intx/tool-packaging": ["@intx/tool-packaging@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/log": "0.4.0", "@intx/storage-isogit": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29", "isomorphic-git": "^1.27.2", "npm-package-arg": "^12.0.2", "npm-pick-manifest": "^10.0.0", "npm-registry-fetch": "^19.0.0", "semver": "^7.7.2", "ssri": "^12.0.0", "tar": "^7.5.1" } }, "sha512-eBZIbGUcEBp0ppMCceA1PPd75LvXpUyWitqTIu7AsLxRNlU9gbE1hymWPXnhUKnt3LgSlHUi959Ehd6jG6mR7g=="], - - "@intx/hub-api/@intx/workflow-deploy/@intx/workflow": ["@intx/workflow@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/inference": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-F5Is/lyKb4/WguAVYMDXwtI4ddUvqXbg34rcM6hPUOqupJHpbbD9+ZK/6TDfvbCiEgginbTVwMmNj6Fyg8vU+Q=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "glob/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], diff --git a/package.json b/package.json index f857ae955..d840751e3 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@intx/types": "workspace:*", "@intx/workflow-host": "0.3.0", "@modelcontextprotocol/sdk": "^1.29.0", - "@opentui/core": "0.5.12", + "@opentui/core": "0.5.17", "arktype": "catalog:", "isomorphic-git": "catalog:", "@corbits/code-agent-designer": "workspace:*", @@ -125,13 +125,13 @@ "ws": "^8.21.0" }, "optionalDependencies": { - "@opentui/core-darwin-arm64": "0.5.12", - "@opentui/core-darwin-x64": "0.5.12", - "@opentui/core-linux-arm64": "0.5.12", - "@opentui/core-linux-arm64-musl": "0.5.12", - "@opentui/core-linux-x64": "0.5.12", - "@opentui/core-linux-x64-musl": "0.5.12", - "@opentui/core-win32-arm64": "0.5.12", - "@opentui/core-win32-x64": "0.5.12" + "@opentui/core-darwin-arm64": "0.5.17", + "@opentui/core-darwin-x64": "0.5.17", + "@opentui/core-linux-arm64": "0.5.17", + "@opentui/core-linux-arm64-musl": "0.5.17", + "@opentui/core-linux-x64": "0.5.17", + "@opentui/core-linux-x64-musl": "0.5.17", + "@opentui/core-win32-arm64": "0.5.17", + "@opentui/core-win32-x64": "0.5.17" } } From 4958d68061e34e7ce77f006495566f0384cdc805 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Tue, 13 Oct 2026 22:50:01 -0700 Subject: [PATCH 2/3] fix(tui): bump @opentui/core packages to 0.6.4 --- bun.lock | 36 +++++++++++----------- package.json | 18 +++++------ src/tui/runner/settings-theme.test.ts | 43 +++++++++++++++++++-------- src/tui/shell/internals.ts | 3 ++ src/tui/shell/prompt.ts | 41 ++++++++++++++----------- src/tui/shell/transcript.ts | 14 +++++---- src/tui/slash-popup-gate.test.ts | 21 +++++++++++++ src/tui/stream.test.ts | 11 ++++--- src/tui/stream.ts | 36 ++++++++++++++++------ src/tui/syntax-highlight.test.ts | 26 ++++++++++++---- src/tui/url-links.ts | 17 ++++++++++- src/tui/width-contract.ts | 39 +++++++++++++++++++----- 12 files changed, 216 insertions(+), 89 deletions(-) diff --git a/bun.lock b/bun.lock index 18fcc534a..a2a39c07e 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ "@intx/types": "workspace:*", "@intx/workflow-host": "0.3.0", "@modelcontextprotocol/sdk": "^1.29.0", - "@opentui/core": "0.5.17", + "@opentui/core": "0.6.4", "arktype": "catalog:", "isomorphic-git": "catalog:", }, @@ -44,14 +44,14 @@ "ws": "^8.21.0", }, "optionalDependencies": { - "@opentui/core-darwin-arm64": "0.5.17", - "@opentui/core-darwin-x64": "0.5.17", - "@opentui/core-linux-arm64": "0.5.17", - "@opentui/core-linux-arm64-musl": "0.5.17", - "@opentui/core-linux-x64": "0.5.17", - "@opentui/core-linux-x64-musl": "0.5.17", - "@opentui/core-win32-arm64": "0.5.17", - "@opentui/core-win32-x64": "0.5.17", + "@opentui/core-darwin-arm64": "0.6.4", + "@opentui/core-darwin-x64": "0.6.4", + "@opentui/core-linux-arm64": "0.6.4", + "@opentui/core-linux-arm64-musl": "0.6.4", + "@opentui/core-linux-x64": "0.6.4", + "@opentui/core-linux-x64-musl": "0.6.4", + "@opentui/core-win32-arm64": "0.6.4", + "@opentui/core-win32-x64": "0.6.4", }, }, "agents/artist": { @@ -407,23 +407,23 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], - "@opentui/core": ["@opentui/core@0.5.17", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.17", "@opentui/core-darwin-x64": "0.5.17", "@opentui/core-linux-arm64": "0.5.17", "@opentui/core-linux-arm64-musl": "0.5.17", "@opentui/core-linux-x64": "0.5.17", "@opentui/core-linux-x64-musl": "0.5.17", "@opentui/core-win32-arm64": "0.5.17", "@opentui/core-win32-x64": "0.5.17" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-75vMaKPKc6tnnUWEultPxiiSU0kqNSdAjbcsFGQecjCF4Z1Jrr4NZBiRo3SoMciai/nq24JCiYcVqPjaKtl/6Q=="], + "@opentui/core": ["@opentui/core@0.6.4", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.6.4", "@opentui/core-darwin-x64": "0.6.4", "@opentui/core-linux-arm64": "0.6.4", "@opentui/core-linux-arm64-musl": "0.6.4", "@opentui/core-linux-x64": "0.6.4", "@opentui/core-linux-x64-musl": "0.6.4", "@opentui/core-win32-arm64": "0.6.4", "@opentui/core-win32-x64": "0.6.4" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ZCywvSi9A8H5IoDyrw3sKpPkzimsJ+CoUuQWg0bSKLVLh/tYJDmOsFFOnlRzUZPnhGRV8rtLlR613NoEIytig=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ptobzW+1zM29nHvadwy+8uW9jvV8NJr63vcQXWe2rQNtpwN/6k7lEXWiS5LndeBs9AMV5tTsXMEMhttgts4BDQ=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.6.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mZRj5dwR9+wvota0WDgGwYe3xiNzr4ZTpXcSDB2/vPCjuT5npvO9Y1KQWnNBtI85aOtszBHKirXtVqMk0/kjPQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-6yLbnElIZPxhZSrZSyS4mB5+k0YFlpRO0Q8uX4sLFTmvOy8tIeDPFcTGRQqZQ1Rk938tiAhf7iyAX3P1wL4Liw=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.6.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-9Oa2QPLwg0AFy5EAXCQnIxOphPyCo/g476PBfyl32hUKbSxscKJeq1fvepZBdD3kAzSZFGzIroUZ2FhLg2b2kw=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HO3dYgzQBGJhe58uJauVClIJg3vToYl/LZa7ZzqxQ+3vZ//cxaJ7tgXhHWR2uJE02550yI69e5nqBpSOTT9E8Q=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.6.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pstmXbojEysbDiP6SSfDxk7nD6Sy/xFy+AhEfZstV8SKUHGZAbrLep4GnggL5Q5/soy5gQtQTcaaFhK4LtQNxw=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-QMRdDbmR0LwpmX1HXVbN+V3A44/QsbWzMsF3V7cEz7MaDq6RNBs3CUFqfkPorKgPtI75T0AxM7FdJnjt9C4d0Q=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.6.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-eKRQ13r146JPCuL2NeHcj9i86arZOk9XGJkFc63x7y1iH7YFxgA5sx/KevsqmLNl3F8LE3j+RhX3HPAlmR/KrQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.17", "", { "os": "linux", "cpu": "x64" }, "sha512-lNmtQlcwsio4IVo2trc0QoMyun31UsN0mzmSc2BtYMOeOC5e0PyWaEAZWJb/g6AinOs9xwJGL02JAN5J3QPWsw=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.6.4", "", { "os": "linux", "cpu": "x64" }, "sha512-QVEzBYhjq0T2kzrH2qpvn4trUA+XPJyp5ASEsDbcnihyMFtStdj6wggJ8Ufp+EIeEuf1bi7STv7Ui9MtvgQ2+w=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M2RatQaeET/werHFSH+cg7OkQk/Wn0VoVXZLLEL7DiCDTuhZyMfi3uFDz0+R1IGiiUITqHWpZdZ1LdBY8NCjwQ=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.6.4", "", { "os": "linux", "cpu": "x64" }, "sha512-1DW/t/7hJc9HpDzmkrZ6pKbXH2uEjOY3Uwo1FlHGZSRy6XMjireF2iU+ucJKO+Krsu07c8hVAOxFQb+sqkLscQ=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-MyVVpZ0CyGDQ+g4eM0zURqEd/9jDrBIPcZdLyZOmWbdoVu36PLM9KAkqwl/n5ASn/rOrQd+d14jUAsZJvFTqzw=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.6.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-KmFGqYoQ+ZbDbZrx7GNXxu/mt06zyBOwIDXKdsiUFgC6sZODz69QGN4xpLhF+popDVCjDyLtFxKW0vMfIOXSXA=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.17", "", { "os": "win32", "cpu": "x64" }, "sha512-etkuzyfclRX9wel4ivCOk90dUdZNtjK7U6vr7ZHmwkZyOik34ec3Gse0B/jyk192Xdx0l50Tl2xgC1h68XRiEQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.6.4", "", { "os": "win32", "cpu": "x64" }, "sha512-NzgXKe+AIrQGXMESc+ScyL6T55h4m7IEA09r8BpxgjwfzMSRzGvI9//0Ps8bQNnGXIDURH8zuS11SWneKhbL3A=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q=="], diff --git a/package.json b/package.json index d840751e3..05622e059 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@intx/types": "workspace:*", "@intx/workflow-host": "0.3.0", "@modelcontextprotocol/sdk": "^1.29.0", - "@opentui/core": "0.5.17", + "@opentui/core": "0.6.4", "arktype": "catalog:", "isomorphic-git": "catalog:", "@corbits/code-agent-designer": "workspace:*", @@ -125,13 +125,13 @@ "ws": "^8.21.0" }, "optionalDependencies": { - "@opentui/core-darwin-arm64": "0.5.17", - "@opentui/core-darwin-x64": "0.5.17", - "@opentui/core-linux-arm64": "0.5.17", - "@opentui/core-linux-arm64-musl": "0.5.17", - "@opentui/core-linux-x64": "0.5.17", - "@opentui/core-linux-x64-musl": "0.5.17", - "@opentui/core-win32-arm64": "0.5.17", - "@opentui/core-win32-x64": "0.5.17" + "@opentui/core-darwin-arm64": "0.6.4", + "@opentui/core-darwin-x64": "0.6.4", + "@opentui/core-linux-arm64": "0.6.4", + "@opentui/core-linux-arm64-musl": "0.6.4", + "@opentui/core-linux-x64": "0.6.4", + "@opentui/core-linux-x64-musl": "0.6.4", + "@opentui/core-win32-arm64": "0.6.4", + "@opentui/core-win32-x64": "0.6.4" } } diff --git a/src/tui/runner/settings-theme.test.ts b/src/tui/runner/settings-theme.test.ts index 550b1965b..28131d903 100644 --- a/src/tui/runner/settings-theme.test.ts +++ b/src/tui/runner/settings-theme.test.ts @@ -25,23 +25,38 @@ afterEach(() => { }); /** Live heading colour in the transcript registry. */ -function registryHeadingFg(): RGBA | undefined { - return transcriptSyntaxStyle().getAllStyles().get("markup.heading.1")?.fg; +function registryHeadingFg(h: Harness): RGBA | undefined { + return transcriptSyntaxStyle(h.renderer) + .getAllStyles() + .get("markup.heading.1")?.fg; } function painted(spans: CapturedSpan[], text: string, fg: RGBA): boolean { return spans.some((s) => s.text.includes(text) && s.fg.equals(fg)); } +/** 0.6.x leases a session buffer around each presented frame; a capture + * landing inside that window throws OutputBusy. The poll retries on the + * next round anyway, so a losing capture keeps polling instead of + * throwing — the ready predicate, never the trace, decides the outcome. */ +function readSpans(h: Harness): CapturedSpan[] { + try { + return h.captureSpans().lines.flatMap((line) => line.spans); + } catch (err) { + if (err instanceof Error && err.message.includes("OutputBusy")) return []; + throw err; + } +} + async function settleSpans( h: Harness, ready: (spans: CapturedSpan[]) => boolean, ): Promise { - let spans = h.captureSpans().lines.flatMap((line) => line.spans); + let spans = readSpans(h); for (let i = 0; i < 400 && !ready(spans); i++) { await h.renderOnce(); await new Promise((resolve) => setTimeout(resolve, 25)); - spans = h.captureSpans().lines.flatMap((line) => line.spans); + spans = readSpans(h); } return spans; } @@ -64,7 +79,7 @@ describe("applyThemePinLive", () => { painted(spans, "pin cycle", darkHeading), ); expect(painted(before, "pin cycle", darkHeading)).toBe(true); - expect(registryHeadingFg()?.equals(darkHeading)).toBe(true); + expect(registryHeadingFg(h)?.equals(darkHeading)).toBe(true); // No cost movement anywhere in this cycle: the meter never moves, // so only an unconditional repaint repaints anything. @@ -74,7 +89,7 @@ describe("applyThemePinLive", () => { expect(applied).toBe("corbits-light"); expect(UI.name).toBe("corbits-light"); expect(UI.ground).not.toBe(darkGround); - expect(registryHeadingFg()?.equals(lightHeading)).toBe(true); + expect(registryHeadingFg(h)?.equals(lightHeading)).toBe(true); const after = await settleSpans(h, (spans) => painted(spans, "pin cycle", lightHeading), @@ -106,14 +121,14 @@ describe("applyThemePinLive", () => { painted(spans, "pin cycle", lightHeading), ); expect(painted(before, "pin cycle", lightHeading)).toBe(true); - expect(registryHeadingFg()?.equals(lightHeading)).toBe(true); + expect(registryHeadingFg(h)?.equals(lightHeading)).toBe(true); const applied = applyThemePinLive("dark", () => repaintShellForTheme(shell), ); expect(applied).toBe("corbits-dark"); expect(UI.name).toBe("corbits-dark"); - expect(registryHeadingFg()?.equals(darkHeading)).toBe(true); + expect(registryHeadingFg(h)?.equals(darkHeading)).toBe(true); const after = await settleSpans(h, (spans) => painted(spans, "pin cycle", darkHeading), @@ -129,11 +144,13 @@ describe("applyThemePinLive", () => { ); }, 30000); - test("auto re-resolves through the startup path and rebuilds the registry", () => { + test("auto re-resolves through the startup path and rebuilds the registry", async () => { setTheme("corbits-dark"); - const applied = applyThemePinLive("auto", () => undefined); - expect(["corbits-dark", "corbits-light"]).toContain(applied); - expect(UI.name).toBe(applied); - expect(registryHeadingFg()?.equals(RGBA.fromHex(UI.heading))).toBe(true); + await withTestRenderer(async (h) => { + const applied = applyThemePinLive("auto", () => undefined); + expect(["corbits-dark", "corbits-light"]).toContain(applied); + expect(UI.name).toBe(applied); + expect(registryHeadingFg(h)?.equals(RGBA.fromHex(UI.heading))).toBe(true); + }); }); }); diff --git a/src/tui/shell/internals.ts b/src/tui/shell/internals.ts index ea352a6ee..64352d44d 100644 --- a/src/tui/shell/internals.ts +++ b/src/tui/shell/internals.ts @@ -284,6 +284,9 @@ export type ShellRenderer = Pick< | "off" | "isDestroyed" | "clearSelection" + // 0.6.x native style construction needs the owning Context: the renderer's + // native scene. + | "nativeScene" >; export interface AppShellOptions { diff --git a/src/tui/shell/prompt.ts b/src/tui/shell/prompt.ts index 0e6d4b9e0..dce497cc9 100644 --- a/src/tui/shell/prompt.ts +++ b/src/tui/shell/prompt.ts @@ -45,6 +45,7 @@ import { shellInternals, shellPromptImageSource, shellRecognitionSource, + type ShellRenderer, } from "./internals.js"; import { appendStreamRow, @@ -177,17 +178,19 @@ export function setPromptCostContext( paintPromptBorder(shell); } -let cachedPromptSyntaxStyle: SyntaxStyle | null = null; - -let cachedPromptRecognizedStyleId: number | null = null; +/** 0.6.x native styles must name their owning Context — the live renderer's + * native scene — so the style is keyed per renderer instead of a module + * global; `promptStyleGeneration` invalidates it like the old cache did. */ +const promptStylesByRenderer = new WeakMap< + ShellRenderer, + { generation: number; style: SyntaxStyle } +>(); let promptStyleGeneration = 0; const promptHighlightGenerations = new WeakMap(); onThemeChange(() => { - cachedPromptSyntaxStyle = null; - cachedPromptRecognizedStyleId = null; promptStyleGeneration += 1; }); @@ -195,17 +198,20 @@ onThemeChange(() => { * Lazy like `transcriptSyntaxStyle`: construction reaches into the native * render lib. */ -function promptRecognizedStyleId(): number { - if (cachedPromptSyntaxStyle === null) { - cachedPromptSyntaxStyle = SyntaxStyle.fromStyles({ - recognized: { fg: UI.action }, - }); - } - if (cachedPromptRecognizedStyleId === null) { - cachedPromptRecognizedStyleId = - cachedPromptSyntaxStyle.resolveStyleId("recognized") ?? 0; +function promptStyleFor(renderer: ShellRenderer): SyntaxStyle { + const cached = promptStylesByRenderer.get(renderer); + if (cached !== undefined && cached.generation === promptStyleGeneration) { + return cached.style; } - return cachedPromptRecognizedStyleId; + const style = SyntaxStyle.fromStyles( + { recognized: { fg: UI.action } }, + renderer.nativeScene, + ); + promptStylesByRenderer.set(renderer, { + generation: promptStyleGeneration, + style, + }); + return style; } const promptHighlightedValue = new WeakMap(); @@ -226,8 +232,9 @@ export function syncPromptHighlights(shell: AppShell): void { promptHighlightedValue.set(shell, value); promptHighlightGenerations.set(shell, promptStyleGeneration); - const styleId = promptRecognizedStyleId(); - shell.prompt.syntaxStyle = cachedPromptSyntaxStyle; + const style = promptStyleFor(shell.renderer); + const styleId = style.resolveStyleId("recognized") ?? 0; + shell.prompt.syntaxStyle = style; shell.prompt.clearAllHighlights(); const matcher = resolvePromptRecognitionMatcher(source); for (const span of resolvePromptHighlightSpans(value, matcher)) { diff --git a/src/tui/shell/transcript.ts b/src/tui/shell/transcript.ts index 9db2ae7b9..d5af6024a 100644 --- a/src/tui/shell/transcript.ts +++ b/src/tui/shell/transcript.ts @@ -355,9 +355,13 @@ export function buildRowNode( } /** Shared construction options for a transcript markdown body's renderer. */ -function markdownBodyOptions(gutter: PaintedStreamLine, width: number) { +function markdownBodyOptions( + ctx: CliRenderer, + gutter: PaintedStreamLine, + width: number, +) { return { - syntaxStyle: transcriptSyntaxStyle(), + syntaxStyle: transcriptSyntaxStyle(ctx), fg: gutter.fg, width, flexShrink: 0, @@ -430,7 +434,7 @@ function createMarkdownBody( if (split === null) { // Native incremental block stability: only the trailing block is unstable. return new MarkdownRenderable(ctx, { - ...markdownBodyOptions(gutter, width), + ...markdownBodyOptions(ctx, gutter, width), content, streaming: row.streaming === true, }); @@ -438,14 +442,14 @@ function createMarkdownBody( const column = new BoxRenderable(ctx, { flexDirection: "column", width }); column.add( new MarkdownRenderable(ctx, { - ...markdownBodyOptions(gutter, width), + ...markdownBodyOptions(ctx, gutter, width), content: split.frozen, streaming: false, }), ); column.add( new MarkdownRenderable(ctx, { - ...markdownBodyOptions(gutter, width), + ...markdownBodyOptions(ctx, gutter, width), content: split.live, streaming: row.streaming === true, marginTop: split.gapRows, diff --git a/src/tui/slash-popup-gate.test.ts b/src/tui/slash-popup-gate.test.ts index 38ac34eac..4b0838aa7 100644 --- a/src/tui/slash-popup-gate.test.ts +++ b/src/tui/slash-popup-gate.test.ts @@ -1072,4 +1072,25 @@ describe("overlay host occupancy and opt-in deferral", () => { expect(isOverlayHostIdle(shell)).toBe(true); }); }); + + test("a `/` right after the Ctrl+C arm still opens the popup", async () => { + // Smoke suspicion (2026-10-14 TUI QA): a `/` typed immediately after the + // first Ctrl+C press seemed suppressed, letting the slash text submit to + // the agent. Both path stages share the key wiring, so the arm must not + // eat the key or leave the popup host unusable — this pins the sequence + // end to end through the real keybinding dispatch (idle shell, empty + // prompt, no live workers — the arming press, never the quit). + await withShell(async ({ shell, press, render }) => { + press("Ctrl+C"); + await render(); + expect(isSlashPopupOpen(shell)).toBe(false); + + press("/"); + await render(); + expect(shell.prompt.value).toBe("/"); + expect(isSlashPopupOpen(shell)).toBe(true); + // The unfiltered catalog stays listed (popup opens; nothing filtered). + expect(shell.paletteCommands[0]?.id).toBe("help"); + }); + }); }); diff --git a/src/tui/stream.test.ts b/src/tui/stream.test.ts index d5d7f59c5..202666f3f 100644 --- a/src/tui/stream.test.ts +++ b/src/tui/stream.test.ts @@ -19,6 +19,7 @@ import { toolCallRow } from "./diff"; import { toolResultRow } from "./mcp-view"; import { mergeToolRows } from "./tool-rows"; import { UI } from "./theme"; +import { withTestRenderer } from "./harness"; const SOLO: RowLayout = { width: 56, multiAgent: false }; const CREW: RowLayout = { width: 56, multiAgent: true }; @@ -577,9 +578,11 @@ describe("sub-agent dispatch row marks", () => { // underline-absence on the link scopes so a future hover style cannot // sneak one in. describe("transcriptSyntaxStyle markdown links", () => { - test("link cells carry no underline", () => { - const styles = transcriptSyntaxStyle().getAllStyles(); - expect(styles.get("markup.link")?.underline).not.toBe(true); - expect(styles.get("markup.link.url")?.underline).not.toBe(true); + test("link cells carry no underline", async () => { + await withTestRenderer(async (h) => { + const styles = transcriptSyntaxStyle(h.renderer).getAllStyles(); + expect(styles.get("markup.link")?.underline).not.toBe(true); + expect(styles.get("markup.link.url")?.underline).not.toBe(true); + }); }); }); diff --git a/src/tui/stream.ts b/src/tui/stream.ts index c8d30a592..f8fda49b5 100644 --- a/src/tui/stream.ts +++ b/src/tui/stream.ts @@ -1,7 +1,7 @@ /** Transcript stream row model — product-skin styling for plain and * markdown renderables. */ -import { SyntaxStyle } from "@opentui/core"; +import { SyntaxStyle, type CliRenderer } from "@opentui/core"; import { stringWidth, wrapLines } from "./view/height.js"; import type { DiffView } from "./diff.js"; @@ -773,17 +773,35 @@ function markdownStyles() { }; } -let cachedSyntaxStyle: SyntaxStyle | null = null; +/** 0.6.x native styles must name the Context that owns them; the owner is the + * live renderer, so the shared style is keyed per renderer instead of held in + * a module global. A theme pin bumps the generation; the next reader rebuilds + * from the new `UI` colors, matching the old invalidation. */ +const transcriptStylesByRenderer = new WeakMap< + CliRenderer, + { generation: number; style: SyntaxStyle } +>(); -/** Shared transcript SyntaxStyle, built lazily — the native render lib is - * unreachable until a renderer exists. */ -export function transcriptSyntaxStyle(): SyntaxStyle { - if (cachedSyntaxStyle === null) { - cachedSyntaxStyle = SyntaxStyle.fromStyles({ ...markdownStyles() }); +let transcriptStyleGeneration = 0; + +/** Shared transcript SyntaxStyle for `ctx`, built lazily — the native render + * lib is unreachable until a renderer exists. */ +export function transcriptSyntaxStyle(ctx: CliRenderer): SyntaxStyle { + const cached = transcriptStylesByRenderer.get(ctx); + if (cached !== undefined && cached.generation === transcriptStyleGeneration) { + return cached.style; } - return cachedSyntaxStyle; + const style = SyntaxStyle.fromStyles( + { ...markdownStyles() }, + ctx.nativeScene, + ); + transcriptStylesByRenderer.set(ctx, { + generation: transcriptStyleGeneration, + style, + }); + return style; } onThemeChange(() => { - cachedSyntaxStyle = null; + transcriptStyleGeneration += 1; }); diff --git a/src/tui/syntax-highlight.test.ts b/src/tui/syntax-highlight.test.ts index 72b8becf0..8dcd7eff7 100644 --- a/src/tui/syntax-highlight.test.ts +++ b/src/tui/syntax-highlight.test.ts @@ -1,6 +1,7 @@ /** * Fenced-code highlighting: colour lives in the native renderer - * (transcriptSyntaxStyle() over the bundled tree-sitter grammars), while the + * (the per-renderer SyntaxStyle built by transcriptSyntaxStyle(h.renderer) + * over the bundled tree-sitter grammars), while the * synchronous StyledSegment model keeps geometry only. These tests pin both * halves: the plain sync fallback and the language parity the native renderer * applies. @@ -41,7 +42,7 @@ const SYNTAX_FGS = [KEYWORD_FG, STRING_FG, COMMENT_FG, FUNCTION_FG]; function renderFence(h: Harness, fence: string): void { h.root.add( new MarkdownRenderable(h.renderer, { - syntaxStyle: transcriptSyntaxStyle(), + syntaxStyle: transcriptSyntaxStyle(h.renderer), fg: UI.text, width: 80, flexShrink: 0, @@ -51,17 +52,30 @@ function renderFence(h: Harness, fence: string): void { ); } +function readSpans(h: Harness): CapturedSpan[] { + try { + return h.captureSpans().lines.flatMap((line) => line.spans); + } catch (err) { + // 0.6.x leases a session buffer around each presented frame; a capture + // landing inside that window throws OutputBusy. The poll retries on the + // next round anyway, so a losing capture keeps polling instead of + // throwing — the ready predicate, never the trace, decides the outcome. + if (err instanceof Error && err.message.includes("OutputBusy")) return []; + throw err; + } +} + async function settleSpans( h: Harness, ready: (spans: CapturedSpan[]) => boolean, ): Promise { - let spans = h.captureSpans().lines.flatMap((line) => line.spans); - for (let i = 0; i < 400 && !ready(spans); i++) { + for (let i = 0; i < 400; i += 1) { + const spans = readSpans(h); + if (ready(spans)) return spans; await h.renderOnce(); await new Promise((resolve) => setTimeout(resolve, 25)); - spans = h.captureSpans().lines.flatMap((line) => line.spans); } - return spans; + return readSpans(h); } function coloured(spans: CapturedSpan[], text: string, fg: RGBA): boolean { diff --git a/src/tui/url-links.ts b/src/tui/url-links.ts index 2ff13a209..b4291a57b 100644 --- a/src/tui/url-links.ts +++ b/src/tui/url-links.ts @@ -398,7 +398,22 @@ export function armMarkdownLinks( if (start === null || !isUrlOpenClick(event)) return; if (markdownLinkAt(renderer, event.x, event.y) === start) openUrl(start); }; - target.onMouseOut = () => { + target.onMouseOut = (event) => { + // 0.6.x refreshes over/out lazily, so an out earned by an earlier + // pointer position can land between a press and its release although + // the pointer never moved. An out whose coordinates still resolve to a + // descendant of target means the pointer stayed inside the transcript — + // only an actual departure clears the press. + try { + let current: Renderable | null | undefined = + Renderable.renderablesByNumber.get(renderer.hitTest(event.x, event.y)); + while (current) { + if (current === target) return; + current = current.parent; + } + } catch { + // Unresolvable hit: treat as departed, like pre-0.6 did implicitly. + } press = null; }; } diff --git a/src/tui/width-contract.ts b/src/tui/width-contract.ts index e07331246..937919832 100644 --- a/src/tui/width-contract.ts +++ b/src/tui/width-contract.ts @@ -10,7 +10,11 @@ * is the failure mode this check removes. */ -import { resolveRenderLib, type WidthMethod } from "@opentui/core"; +import { + OptimizedBuffer, + ResourceContext, + type WidthMethod, +} from "@opentui/core"; import { stringWidth, WIDTH_PROBE } from "./view/height.js"; @@ -22,18 +26,39 @@ export interface WidthContractReport { readonly widthMethod: WidthMethod; } -/** Width OpenTUI's own table assigns `text`, or null when it cannot encode it. */ +/** Width OpenTUI's own table assigns `text`, or null when it cannot encode it. + * + * 0.6.x moves `encodeUnicode` off the render lib onto a Context-owned buffer, + * so the measure builds its own throwaway Context. `objectCapacity: 1, + * renderCellsMax: 1` is not OpenTUI's internal shape (ImageOwner keeps a live + * 4096/1 context); this call is one-shot and self-destroying, and 1/1 is the + * tightest capacity the probe verifies as sufficient to encode and free. An + * encode failure still reads as null — this check reports, never throws. */ export function measureRendererWidth( text: string, widthMethod: WidthMethod, ): number | null { - const lib = resolveRenderLib(); - const encoded = lib.encodeUnicode(text, widthMethod); - if (encoded === null) return null; + const owner = new ResourceContext({ objectCapacity: 1, renderCellsMax: 1 }); + const buffer = OptimizedBuffer.create(1, 1, widthMethod, { owner }); try { - return encoded.data.reduce((n, cell) => n + cell.width, 0); + let encoded; + try { + encoded = buffer.encodeUnicode(text); + } catch { + return null; + } + if (encoded === null) return null; + try { + return encoded.data.reduce((n, cell) => n + cell.width, 0); + } finally { + buffer.freeUnicode(encoded); + } } finally { - lib.freeUnicode(encoded); + try { + buffer.destroy(); + } finally { + owner.destroy(); + } } } From 0c8b2818d9ed15137b3469df489d9a56659fe2da Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 15 Oct 2026 12:52:33 -0700 Subject: [PATCH 3/3] fix(tui): free syntax styles after theme pins drop them SyntaxStyle.destroy frees the native style and nothing finalizes it. Destroying on cache overwrite use-after-frees a style the rows or prompt still paint, so the previous style stays queued until they drop it. --- src/tui/runner/settings-theme.test.ts | 140 +++++++++++++++++++++----- src/tui/shell/chrome.ts | 2 + src/tui/shell/prompt.ts | 36 ++++--- src/tui/shell/transcript.ts | 23 +++++ src/tui/stream.ts | 42 +++++--- src/tui/syntax-highlight.test.ts | 27 +---- src/tui/syntax-style-cache.ts | 51 ++++++++++ src/tui/test-helpers.ts | 28 ++++++ 8 files changed, 274 insertions(+), 75 deletions(-) create mode 100644 src/tui/syntax-style-cache.ts diff --git a/src/tui/runner/settings-theme.test.ts b/src/tui/runner/settings-theme.test.ts index 28131d903..ff309c635 100644 --- a/src/tui/runner/settings-theme.test.ts +++ b/src/tui/runner/settings-theme.test.ts @@ -11,13 +11,16 @@ */ import { afterEach, describe, expect, test } from "bun:test"; -import { RGBA, type CapturedSpan } from "@opentui/core"; +import { RGBA, SyntaxStyle, type CapturedSpan } from "@opentui/core"; import { corbitsDark, corbitsLight, setTheme, UI } from "../theme.js"; import { transcriptSyntaxStyle } from "../stream.js"; import { withTestRenderer, type Harness } from "../harness.js"; +import { settleSpans } from "../test-helpers.js"; import { appendStreamRow } from "../shell/chrome.js"; import { createAppShell } from "../shell/index.js"; +import { setPromptRecognitionSource } from "../shell/internals.js"; +import { syncPromptHighlights } from "../shell/prompt.js"; import { applyThemePinLive, repaintShellForTheme } from "./settings.js"; afterEach(() => { @@ -31,34 +34,20 @@ function registryHeadingFg(h: Harness): RGBA | undefined { .get("markup.heading.1")?.fg; } -function painted(spans: CapturedSpan[], text: string, fg: RGBA): boolean { - return spans.some((s) => s.text.includes(text) && s.fg.equals(fg)); -} - -/** 0.6.x leases a session buffer around each presented frame; a capture - * landing inside that window throws OutputBusy. The poll retries on the - * next round anyway, so a losing capture keeps polling instead of - * throwing — the ready predicate, never the trace, decides the outcome. */ -function readSpans(h: Harness): CapturedSpan[] { +/** `destroy()` frees the native style and there is no finalizer; a later + * read throws instead of returning a palette. */ +function syntaxStyleAlive(style: SyntaxStyle): boolean { try { - return h.captureSpans().lines.flatMap((line) => line.spans); + style.getStyleCount(); + return true; } catch (err) { - if (err instanceof Error && err.message.includes("OutputBusy")) return []; + if (err instanceof Error && err.message.includes("destroyed")) return false; throw err; } } -async function settleSpans( - h: Harness, - ready: (spans: CapturedSpan[]) => boolean, -): Promise { - let spans = readSpans(h); - for (let i = 0; i < 400 && !ready(spans); i++) { - await h.renderOnce(); - await new Promise((resolve) => setTimeout(resolve, 25)); - spans = readSpans(h); - } - return spans; +function painted(spans: CapturedSpan[], text: string, fg: RGBA): boolean { + return spans.some((s) => s.text.includes(text) && s.fg.equals(fg)); } describe("applyThemePinLive", () => { @@ -153,4 +142,109 @@ describe("applyThemePinLive", () => { expect(registryHeadingFg(h)?.equals(RGBA.fromHex(UI.heading))).toBe(true); }); }); + + test("a second pin destroys the transcript style the rows dropped", async () => { + await withTestRenderer( + async (h) => { + setTheme("corbits-dark"); + const shell = createAppShell(h.renderer, { + terminal: { columns: 80, rows: 40 }, + wireKeys: false, + }); + try { + appendStreamRow(shell, { role: "assistant", text: "# pin cycle" }); + const first = transcriptSyntaxStyle(h.renderer); + applyThemePinLive("light", () => repaintShellForTheme(shell)); + const second = transcriptSyntaxStyle(h.renderer); + expect(second).not.toBe(first); + expect(syntaxStyleAlive(first)).toBe(false); + expect(syntaxStyleAlive(second)).toBe(true); + + applyThemePinLive("dark", () => repaintShellForTheme(shell)); + const third = transcriptSyntaxStyle(h.renderer); + expect(third).not.toBe(second); + expect(syntaxStyleAlive(second)).toBe(false); + expect(syntaxStyleAlive(third)).toBe(true); + } finally { + shell.dispose(); + setTheme("corbits-dark"); + } + }, + { width: 80, height: 40 }, + ); + }); + + test("refreshing the transcript cache leaves a style rows still paint", async () => { + await withTestRenderer( + async (h) => { + setTheme("corbits-dark"); + const shell = createAppShell(h.renderer, { + terminal: { columns: 80, rows: 40 }, + wireKeys: false, + }); + try { + appendStreamRow(shell, { role: "assistant", text: "# pin cycle" }); + const painted = transcriptSyntaxStyle(h.renderer); + setTheme("corbits-light"); + const rebuilt = transcriptSyntaxStyle(h.renderer); + expect(rebuilt).not.toBe(painted); + expect(syntaxStyleAlive(painted)).toBe(true); + repaintShellForTheme(shell); + expect(syntaxStyleAlive(painted)).toBe(false); + expect(syntaxStyleAlive(transcriptSyntaxStyle(h.renderer))).toBe( + true, + ); + } finally { + shell.dispose(); + setTheme("corbits-dark"); + } + }, + { width: 80, height: 40 }, + ); + }); + + test("a pin destroys the prompt style only after the prompt drops it", async () => { + await withTestRenderer( + async (h) => { + setTheme("corbits-dark"); + const shell = createAppShell(h.renderer, { + terminal: { columns: 80, rows: 40 }, + wireKeys: false, + }); + setPromptRecognitionSource(shell, () => ({ + commandNames: ["implement"], + })); + const originalDestroy = SyntaxStyle.prototype.destroy; + let destroyedWhileHeld = false; + try { + shell.prompt.value = "/implement"; + syncPromptHighlights(shell); + const painted = shell.prompt.syntaxStyle; + expect(painted).not.toBeNull(); + if (painted === null) return; + SyntaxStyle.prototype.destroy = function destroy(this: SyntaxStyle) { + if (this === painted && shell.prompt.syntaxStyle === painted) { + destroyedWhileHeld = true; + } + return originalDestroy.call(this); + }; + applyThemePinLive("light", () => repaintShellForTheme(shell)); + expect(shell.prompt.syntaxStyle).toBe(painted); + expect(syntaxStyleAlive(painted)).toBe(true); + syncPromptHighlights(shell); + expect(destroyedWhileHeld).toBe(false); + expect(shell.prompt.syntaxStyle).not.toBe(painted); + expect(syntaxStyleAlive(painted)).toBe(false); + const current = shell.prompt.syntaxStyle; + expect(current).not.toBeNull(); + if (current !== null) expect(syntaxStyleAlive(current)).toBe(true); + } finally { + SyntaxStyle.prototype.destroy = originalDestroy; + shell.dispose(); + setTheme("corbits-dark"); + } + }, + { width: 80, height: 40 }, + ); + }); }); diff --git a/src/tui/shell/chrome.ts b/src/tui/shell/chrome.ts index 743a9f1c5..d0a2e3438 100644 --- a/src/tui/shell/chrome.ts +++ b/src/tui/shell/chrome.ts @@ -143,6 +143,7 @@ import { transcriptRowChildren, transcriptRowLayout, transcriptRowOffset, + releaseRetiredTranscriptSyntaxStyles, } from "./transcript.js"; function syncPending(shell: AppShell): void { @@ -1411,6 +1412,7 @@ export function repaintTranscriptWindow(shell: AppShell): void { ), ); }); + releaseRetiredTranscriptSyntaxStyles(shell); } /** Tear the landing down on the first transcript row. The prompt box jumps diff --git a/src/tui/shell/prompt.ts b/src/tui/shell/prompt.ts index dce497cc9..b6d8f382a 100644 --- a/src/tui/shell/prompt.ts +++ b/src/tui/shell/prompt.ts @@ -36,6 +36,10 @@ import { interrupt, } from "../delivery-queue.js"; import { UI, onThemeChange } from "../theme.js"; +import { + destroyUnpaintedSyntaxStyles, + syntaxStyleForGeneration, +} from "../syntax-style-cache.js"; import { type AppShell, @@ -180,12 +184,15 @@ export function setPromptCostContext( /** 0.6.x native styles must name their owning Context — the live renderer's * native scene — so the style is keyed per renderer instead of a module - * global; `promptStyleGeneration` invalidates it like the old cache did. */ + * global; `promptStyleGeneration` invalidates it like the old cache did. + * The previous style stays queued until the prompt assignment drops it. */ const promptStylesByRenderer = new WeakMap< ShellRenderer, { generation: number; style: SyntaxStyle } >(); +const retiredPromptStyles = new WeakMap(); + let promptStyleGeneration = 0; const promptHighlightGenerations = new WeakMap(); @@ -199,19 +206,17 @@ onThemeChange(() => { * render lib. */ function promptStyleFor(renderer: ShellRenderer): SyntaxStyle { - const cached = promptStylesByRenderer.get(renderer); - if (cached !== undefined && cached.generation === promptStyleGeneration) { - return cached.style; - } - const style = SyntaxStyle.fromStyles( - { recognized: { fg: UI.action } }, - renderer.nativeScene, + return syntaxStyleForGeneration( + promptStylesByRenderer, + retiredPromptStyles, + renderer, + promptStyleGeneration, + () => + SyntaxStyle.fromStyles( + { recognized: { fg: UI.action } }, + renderer.nativeScene, + ), ); - promptStylesByRenderer.set(renderer, { - generation: promptStyleGeneration, - style, - }); - return style; } const promptHighlightedValue = new WeakMap(); @@ -235,6 +240,11 @@ export function syncPromptHighlights(shell: AppShell): void { const style = promptStyleFor(shell.renderer); const styleId = style.resolveStyleId("recognized") ?? 0; shell.prompt.syntaxStyle = style; + destroyUnpaintedSyntaxStyles( + retiredPromptStyles, + shell.renderer, + (pending) => shell.prompt.syntaxStyle === pending, + ); shell.prompt.clearAllHighlights(); const matcher = resolvePromptRecognitionMatcher(source); for (const span of resolvePromptHighlightSpans(value, matcher)) { diff --git a/src/tui/shell/transcript.ts b/src/tui/shell/transcript.ts index d5af6024a..9ef3eedbf 100644 --- a/src/tui/shell/transcript.ts +++ b/src/tui/shell/transcript.ts @@ -9,6 +9,7 @@ import { StyledText, type BaseRenderable, type CliRenderer, + type SyntaxStyle, } from "@opentui/core"; import { stringWidth } from "../view/height.js"; import { viewToTableContent, type McpStructuredView } from "../mcp-view.js"; @@ -37,6 +38,7 @@ import { toolRowLines, toolSentenceLines, transcriptSyntaxStyle, + dropRetiredTranscriptSyntaxStyles, type PaintedStreamLine, type RowLayout, type StreamRow, @@ -464,6 +466,27 @@ function createMarkdownBody( return column; } +/** Markdown bodies still holding a syntax style. A theme pin must not free + * a style that one of these nodes is still painting. */ +function paintedMarkdownSyntaxStyles(node: BaseRenderable): Set { + const painted = new Set(); + const walk = (current: BaseRenderable): void => { + if (current instanceof MarkdownRenderable) { + painted.add(current.syntaxStyle); + } + for (const child of current.getChildren()) walk(child); + }; + walk(node); + return painted; +} + +/** After the transcript rows have been rebuilt, free styles they dropped. */ +export function releaseRetiredTranscriptSyntaxStyles(shell: AppShell): void { + dropRetiredTranscriptSyntaxStyles(shell.renderer as CliRenderer, () => + paintedMarkdownSyntaxStyles(shell.renderer.root as BaseRenderable), + ); +} + /** Gutter + one pre-coloured text line per body row (a diff, expanded tool * arguments); wraps land under the body. */ function createStyledLinesRowRenderable( diff --git a/src/tui/stream.ts b/src/tui/stream.ts index f8fda49b5..df14607a8 100644 --- a/src/tui/stream.ts +++ b/src/tui/stream.ts @@ -12,6 +12,10 @@ import { type Thought, } from "./thinking.js"; import { UI, onThemeChange } from "./theme.js"; +import { + syntaxStyleForGeneration, + destroyUnpaintedSyntaxStyles, +} from "./syntax-style-cache.js"; import { pastTenseToolLabel } from "./tool-formatter.js"; /** One pre-coloured body line (an expanded call's structured args). */ @@ -776,30 +780,42 @@ function markdownStyles() { /** 0.6.x native styles must name the Context that owns them; the owner is the * live renderer, so the shared style is keyed per renderer instead of held in * a module global. A theme pin bumps the generation; the next reader rebuilds - * from the new `UI` colors, matching the old invalidation. */ + * from the new `UI` colors, matching the old invalidation. The previous + * style stays alive until rows drop it (`dropRetiredTranscriptSyntaxStyles`). */ const transcriptStylesByRenderer = new WeakMap< CliRenderer, { generation: number; style: SyntaxStyle } >(); +const retiredTranscriptStyles = new WeakMap(); + let transcriptStyleGeneration = 0; /** Shared transcript SyntaxStyle for `ctx`, built lazily — the native render * lib is unreachable until a renderer exists. */ export function transcriptSyntaxStyle(ctx: CliRenderer): SyntaxStyle { - const cached = transcriptStylesByRenderer.get(ctx); - if (cached !== undefined && cached.generation === transcriptStyleGeneration) { - return cached.style; - } - const style = SyntaxStyle.fromStyles( - { ...markdownStyles() }, - ctx.nativeScene, + return syntaxStyleForGeneration( + transcriptStylesByRenderer, + retiredTranscriptStyles, + ctx, + transcriptStyleGeneration, + () => SyntaxStyle.fromStyles({ ...markdownStyles() }, ctx.nativeScene), + ); +} + +/** Destroy transcript styles this renderer no longer paints. `painted` runs + * only when a previous style is queued, and only after rows have been + * rebuilt — a style the thunk still reports stays queued. */ +export function dropRetiredTranscriptSyntaxStyles( + ctx: CliRenderer, + painted: () => ReadonlySet, +): void { + const pending = retiredTranscriptStyles.get(ctx); + if (pending === undefined || pending.length === 0) return; + const held = painted(); + destroyUnpaintedSyntaxStyles(retiredTranscriptStyles, ctx, (style) => + held.has(style), ); - transcriptStylesByRenderer.set(ctx, { - generation: transcriptStyleGeneration, - style, - }); - return style; } onThemeChange(() => { diff --git a/src/tui/syntax-highlight.test.ts b/src/tui/syntax-highlight.test.ts index 8dcd7eff7..5a398f56c 100644 --- a/src/tui/syntax-highlight.test.ts +++ b/src/tui/syntax-highlight.test.ts @@ -10,6 +10,7 @@ import { describe, expect, test } from "bun:test"; import { MarkdownRenderable, RGBA, type CapturedSpan } from "@opentui/core"; import { withTestRenderer, type Harness } from "./harness"; +import { settleSpans } from "./test-helpers"; import { appendStreamRow } from "./shell/chrome.js"; import { createAppShell } from "./shell/index.js"; import { transcriptSyntaxStyle } from "./stream"; @@ -52,32 +53,6 @@ function renderFence(h: Harness, fence: string): void { ); } -function readSpans(h: Harness): CapturedSpan[] { - try { - return h.captureSpans().lines.flatMap((line) => line.spans); - } catch (err) { - // 0.6.x leases a session buffer around each presented frame; a capture - // landing inside that window throws OutputBusy. The poll retries on the - // next round anyway, so a losing capture keeps polling instead of - // throwing — the ready predicate, never the trace, decides the outcome. - if (err instanceof Error && err.message.includes("OutputBusy")) return []; - throw err; - } -} - -async function settleSpans( - h: Harness, - ready: (spans: CapturedSpan[]) => boolean, -): Promise { - for (let i = 0; i < 400; i += 1) { - const spans = readSpans(h); - if (ready(spans)) return spans; - await h.renderOnce(); - await new Promise((resolve) => setTimeout(resolve, 25)); - } - return readSpans(h); -} - function coloured(spans: CapturedSpan[], text: string, fg: RGBA): boolean { return spans.some((s) => s.text.includes(text) && s.fg.equals(fg)); } diff --git a/src/tui/syntax-style-cache.ts b/src/tui/syntax-style-cache.ts new file mode 100644 index 000000000..6907c380a --- /dev/null +++ b/src/tui/syntax-style-cache.ts @@ -0,0 +1,51 @@ +import type { SyntaxStyle } from "@opentui/core"; + +interface GenerationStyle { + readonly generation: number; + readonly style: SyntaxStyle; +} + +/** + * Theme pins bump `generation` while rows and the prompt still hold the + * previous native style. `SyntaxStyle.destroy()` frees that style and nothing + * finalizes it, so the previous one is queued here until the caller has + * dropped it — overwriting the cache must not free a style that is still + * painted. + */ +export function syntaxStyleForGeneration( + cache: WeakMap, + retired: WeakMap, + key: K, + generation: number, + build: () => SyntaxStyle, +): SyntaxStyle { + const cached = cache.get(key); + if (cached !== undefined && cached.generation === generation) { + return cached.style; + } + const style = build(); + if (cached !== undefined) { + const pending = retired.get(key); + if (pending === undefined) retired.set(key, [cached.style]); + else pending.push(cached.style); + } + cache.set(key, { generation, style }); + return style; +} + +/** Free queued styles `stillPainted` rejects. Held styles stay queued. */ +export function destroyUnpaintedSyntaxStyles( + retired: WeakMap, + key: K, + stillPainted: (style: SyntaxStyle) => boolean, +): void { + const pending = retired.get(key); + if (pending === undefined || pending.length === 0) return; + const keep: SyntaxStyle[] = []; + for (const style of pending) { + if (stillPainted(style)) keep.push(style); + else style.destroy(); + } + if (keep.length === 0) retired.delete(key); + else retired.set(key, keep); +} diff --git a/src/tui/test-helpers.ts b/src/tui/test-helpers.ts index 011141a7a..2cf7f4e0d 100644 --- a/src/tui/test-helpers.ts +++ b/src/tui/test-helpers.ts @@ -3,6 +3,8 @@ * that is always disposed. Defaults match the 80×24 / wireKeys-off scaffold * the suite used to copy into every file. */ +import type { CapturedSpan } from "@opentui/core"; + import { withTestRenderer, type Harness } from "./harness.js"; import { createAppShell } from "./shell/index.js"; import type { AppShell, AppShellOptions } from "./shell/internals.js"; @@ -39,3 +41,29 @@ export async function withAppShell( { width, height }, ); } + +/** 0.6.x leases a session buffer around each presented frame; a capture + * landing inside that window throws OutputBusy. The poll retries on the + * next round anyway, so a losing capture keeps polling instead of + * throwing — the ready predicate, never the trace, decides the outcome. */ +function readSpans(h: Harness): CapturedSpan[] { + try { + return h.captureSpans().lines.flatMap((line) => line.spans); + } catch (err) { + if (err instanceof Error && err.message.includes("OutputBusy")) return []; + throw err; + } +} + +export async function settleSpans( + h: Harness, + ready: (spans: CapturedSpan[]) => boolean, +): Promise { + let spans = readSpans(h); + for (let i = 0; i < 400 && !ready(spans); i++) { + await h.renderOnce(); + await new Promise((resolve) => setTimeout(resolve, 25)); + spans = readSpans(h); + } + return spans; +}