mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-29 04:56:20 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91802603ea | ||
|
|
8089091e82 |
@@ -1073,9 +1073,9 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.5.9",
|
||||
"@opentui/keymap": "0.5.9",
|
||||
"@opentui/solid": "0.5.9",
|
||||
"@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@dd9fe495b1be9c7d2558a08216c83d5c985ba643",
|
||||
"@opentui/keymap": "https://pkg.pr.new/anomalyco/opentui/@opentui/keymap@dd9fe495b1be9c7d2558a08216c83d5c985ba643",
|
||||
"@opentui/solid": "https://pkg.pr.new/anomalyco/opentui/@opentui/solid@dd9fe495b1be9c7d2558a08216c83d5c985ba643",
|
||||
"@pierre/diffs": "1.2.10",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.71.0",
|
||||
@@ -2212,27 +2212,27 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.5.9", "", { "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.9", "@opentui/core-darwin-x64": "0.5.9", "@opentui/core-linux-arm64": "0.5.9", "@opentui/core-linux-arm64-musl": "0.5.9", "@opentui/core-linux-x64": "0.5.9", "@opentui/core-linux-x64-musl": "0.5.9", "@opentui/core-win32-arm64": "0.5.9", "@opentui/core-win32-x64": "0.5.9" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-d0EWYyp6djitu1N1R0o75NrLl4TxY3oJEmRNrX9vSKKC5/jriGQdSV6lJmwiB77O0cxtBnPzWztAV79vA1J2fA=="],
|
||||
"@opentui/core": ["@opentui/core@https://pkg.pr.new/anomalyco/opentui/@opentui/core@dd9fe495b1be9c7d2558a08216c83d5c985ba643", { "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": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-arm64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "@opentui/core-darwin-x64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-x64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "@opentui/core-linux-arm64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "@opentui/core-linux-arm64-musl": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64-musl@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "@opentui/core-linux-x64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "@opentui/core-linux-x64-musl": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64-musl@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "@opentui/core-win32-arm64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-arm64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "@opentui/core-win32-x64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-x64@dd9fe495b1be9c7d2558a08216c83d5c985ba643" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-iJKwj+aq0w0TYZu0xafb5Nj5eBObZcKc4+qHCpj5EAQBRCid3Z8nROmiwdO+aDDeqwTYzfG53u+MU3urEtiweQ=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcpWGD8GwoO3UYYw0kLDI4qof3ElSwWW5M3fNG+Kw87BSjCa5frZqak5xebdZ/XU5Xsk+BjgbBsxbzo9yrIjZQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-arm64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-8Wo6B6VhFXu5Lq/HDXQCkxCJF4svJ3OPU+WcotQJuLT9/pXzik40WjTKVn3HECKiqQ3ryQS4ZxpnJIxlNDcpmg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgdZwgcwlDCqi0WPCS1d0GVSd+oKDFX+UVE1ZPei8u8W3pirXGSExxVHNku7QbEYCRvpCeMtoKIa6xN9cVTsoQ=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-x64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-Tt+qGxp5VJZKmw54XdzebE6a+0QVA5Sk8KWUDHfWXn4ley6tvpMZowrG2/O6TpKHP6XdA/bHm+Ewetyk7BO2Zg=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-vHBiJp69wMaL+fRMDpnXCCIpPyQRPZEWCWBatnblUUGIWUseqeQlaPow6ljHSbgAd/GjygAf4tFCyTqNT2ezZA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-p62IQHNFFQUzet57HMQndNQZtunDc0peSHudBoatCTV68QYKsD5gYjAK/BW1XY0zUoJM9Jk7BJnPBJPv1s5EYQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-A7GW4NCTInj2vy3X2gVO2Sm8jllgdLxwPdOklPqyl+xqivQtyG9qwS5YXoCk9fBJt10ORhYKETEjYEnIE5xvMQ=="],
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64-musl@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-qvQgkAdLSa1+SeMhMNBZ7zx7OlVi8yMsV+DDGTDrcc9sWXDzGJEKUq/R9XqqAag64Sloc12YA5oUYDKKj4E2VA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-rdw7QVopVHwj6fMrJc7hPGVaYg2V5NBlW3MUfkrQBlUlo/87OC4aYDAh5oRIL1CIFD9LjDdh7hwmUNbn/K9Pcw=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-JIkwBP6yLhh4ViTR53kx/NCY5w1ocTcKMdWidpObKLVLIkBat97y085teSvgkh+RFuJnUaJK7114XgdEONCjSg=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-J4wQs1OMPZ4hR93Op1C/BLFmIta2mUJm4M7djevlgcWcal4RSNO4V8QvUXfogWGci+ADZqEmX2osQQDY5NVJpw=="],
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64-musl@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-M1BPZZ7eLE/nfnS7uypyIxg5JKfkkZSkehszISrQOf8zQl3+j3kOPy17RqSCJKZboGFQwmg8rwlaxGNxA6sfVQ=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-OnmEsGLWwsHrm6sTsWSSesu4LGjSV1JNWqCsEItbuitWaqH6rXdSahSc5jMFsS57bZxUyaRz01yAqiZzUSXnVQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-arm64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-GFQ1GCA4FQg/MVpO1jt53X4fTaYsQTfW1EkOpQqmt/Rvls/oSklpElxh4ww5/giHEVnNxvcqX8+RG9U5DCmLpg=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/CnAIfKL7+ZeGLyZoXV5zS71Nd8Zn97RUir2DAIY05MJozmfg5s7XOAdUNYuK1cg5mbwlAuqeAsfbuWUAEe15g=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-x64@dd9fe495b1be9c7d2558a08216c83d5c985ba643", {}, "sha512-NKFUOLwBfd59koYiPgeqrP4hZERnG6B7+RnH2DNO3dAnIAl/7dISuOwWw/VacYBck4ToG0ZrbhBMTeBB2i5DqQ=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.5.9", "", { "dependencies": { "@opentui/core": "0.5.9" }, "peerDependencies": { "@opentui/react": "0.5.9", "@opentui/solid": "0.5.9", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-ZcRNeuCDv+LJ89BS5xBw90e0S1Etn6j41jflg6LZlWj4bnqjCU55dq0+zERUdkkC4rqD4SikM8q7V3ZfkwzyyQ=="],
|
||||
"@opentui/keymap": ["@opentui/keymap@https://pkg.pr.new/anomalyco/opentui/@opentui/keymap@dd9fe495b1be9c7d2558a08216c83d5c985ba643", { "dependencies": { "@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@dd9fe495b1be9c7d2558a08216c83d5c985ba643" }, "peerDependencies": { "@opentui/react": "0.5.9", "@opentui/solid": "0.5.9", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-Lw+wxniAxIqnZz9vFPwl8EsH33HZGnyukQTU/4dI01BwNKbx2hcxuFMjrHFP/MkqhvwmFXcvGSGfJrG8VLwFoA=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.5.9", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.9", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-zGSP/ia9ww+TTMvQMZjJW2+h7fg9YkVRIM723EgvP+v7ZBZ/0vMzPNh7FJ0jET3ysw0h5hsbvcYISs4sUcf+UA=="],
|
||||
"@opentui/solid": ["@opentui/solid@https://pkg.pr.new/anomalyco/opentui/@opentui/solid@dd9fe495b1be9c7d2558a08216c83d5c985ba643", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@dd9fe495b1be9c7d2558a08216c83d5c985ba643", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-wtq+offTD/Hxd9KXW5eVjeBgtBiJiXakkk1Jo/8ULX2fvG+trtdaAHr0DrlHsRv/xyUMfxPBaHRwPe4Gk8+8sw=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -6564,6 +6564,10 @@
|
||||
|
||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core": ["@opentui/core@0.5.9", "", { "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.9", "@opentui/core-darwin-x64": "0.5.9", "@opentui/core-linux-arm64": "0.5.9", "@opentui/core-linux-arm64-musl": "0.5.9", "@opentui/core-linux-x64": "0.5.9", "@opentui/core-linux-x64-musl": "0.5.9", "@opentui/core-win32-arm64": "0.5.9", "@opentui/core-win32-x64": "0.5.9" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-d0EWYyp6djitu1N1R0o75NrLl4TxY3oJEmRNrX9vSKKC5/jriGQdSV6lJmwiB77O0cxtBnPzWztAV79vA1J2fA=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.5.9", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.9", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-zGSP/ia9ww+TTMvQMZjJW2+h7fg9YkVRIM723EgvP+v7ZBZ/0vMzPNh7FJ0jET3ysw0h5hsbvcYISs4sUcf+UA=="],
|
||||
|
||||
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
||||
|
||||
"p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
@@ -7450,6 +7454,30 @@
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcpWGD8GwoO3UYYw0kLDI4qof3ElSwWW5M3fNG+Kw87BSjCa5frZqak5xebdZ/XU5Xsk+BjgbBsxbzo9yrIjZQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgdZwgcwlDCqi0WPCS1d0GVSd+oKDFX+UVE1ZPei8u8W3pirXGSExxVHNku7QbEYCRvpCeMtoKIa6xN9cVTsoQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-vHBiJp69wMaL+fRMDpnXCCIpPyQRPZEWCWBatnblUUGIWUseqeQlaPow6ljHSbgAd/GjygAf4tFCyTqNT2ezZA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-A7GW4NCTInj2vy3X2gVO2Sm8jllgdLxwPdOklPqyl+xqivQtyG9qwS5YXoCk9fBJt10ORhYKETEjYEnIE5xvMQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-rdw7QVopVHwj6fMrJc7hPGVaYg2V5NBlW3MUfkrQBlUlo/87OC4aYDAh5oRIL1CIFD9LjDdh7hwmUNbn/K9Pcw=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-J4wQs1OMPZ4hR93Op1C/BLFmIta2mUJm4M7djevlgcWcal4RSNO4V8QvUXfogWGci+ADZqEmX2osQQDY5NVJpw=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-OnmEsGLWwsHrm6sTsWSSesu4LGjSV1JNWqCsEItbuitWaqH6rXdSahSc5jMFsS57bZxUyaRz01yAqiZzUSXnVQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/CnAIfKL7+ZeGLyZoXV5zS71Nd8Zn97RUir2DAIY05MJozmfg5s7XOAdUNYuK1cg5mbwlAuqeAsfbuWUAEe15g=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
|
||||
|
||||
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
@@ -8248,6 +8276,8 @@
|
||||
|
||||
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||
|
||||
+3
-3
@@ -49,9 +49,9 @@
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.5.9",
|
||||
"@opentui/keymap": "0.5.9",
|
||||
"@opentui/solid": "0.5.9",
|
||||
"@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@dd9fe495b1be9c7d2558a08216c83d5c985ba643",
|
||||
"@opentui/keymap": "https://pkg.pr.new/anomalyco/opentui/@opentui/keymap@dd9fe495b1be9c7d2558a08216c83d5c985ba643",
|
||||
"@opentui/solid": "https://pkg.pr.new/anomalyco/opentui/@opentui/solid@dd9fe495b1be9c7d2558a08216c83d5c985ba643",
|
||||
"@tanstack/solid-virtual": "3.13.37",
|
||||
"@shikijs/stream": "4.4.3",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "bun test --timeout 30000 --only-failures",
|
||||
"bench:diff": "bun run --conditions=browser script/bench-diff.tsx",
|
||||
"typecheck": "tsgo -b"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Diff Rendering
|
||||
|
||||
## Goal
|
||||
|
||||
Keep complete large diffs usable without hiding patches or requiring loading consent. Measure initial rendering and scrolling independently, and preserve first/last-line access, wrapping, gutters, navigation, syntax, and selection.
|
||||
|
||||
## Benchmark
|
||||
|
||||
Run from `packages/tui`:
|
||||
|
||||
```sh
|
||||
bun run bench:diff hunks 1000
|
||||
bun run bench:diff lines 50000
|
||||
DIFF_BENCH_WIDTH=80 bun run bench:diff lines 50000
|
||||
bun run bench:diff long 20000
|
||||
DIFF_BENCH_FILES=40 bun run bench:diff hunks 25
|
||||
```
|
||||
|
||||
The benchmark renders the production `PatchDiff`, with one containing box per file as in the viewer. It bypasses size guards, verifies access to the complete patch's last line, warms up once, and measures seven runs by default. `DIFF_BENCH_RUNS` overrides the measured run count; `DIFF_BENCH_SYNTAX=1` enables JSON highlighting. Report first-frame median/spread, scrolling p95/max, and process RSS separately.
|
||||
|
||||
Do not render hunks directly under the benchmark's scrollbox: that gives them direct-child viewport culling that the viewer's file cards do not have.
|
||||
|
||||
## Hypotheses
|
||||
|
||||
1. Per-hunk gutter-sync scheduling repeats whole-file work. Coalescing callbacks should remove that quadratic mount cost without changing rendered content.
|
||||
2. Gutter framebuffers and background painting scale with content height. Viewport-bounded painting should reduce giant-hunk scroll cost and native allocation without windowing text content.
|
||||
3. Repeated `lineInfo` reads decode full-document arrays. Correctly invalidated caching or range reads should reduce metadata allocation while preserving wrapped/source-line mappings.
|
||||
|
||||
## Results
|
||||
|
||||
Measured on the local machine with one warmup and three measured runs:
|
||||
|
||||
```sh
|
||||
DIFF_BENCH_RUNS=3 bun run bench:diff hunks 1000
|
||||
```
|
||||
|
||||
| Experiment | Ready Median | Ready Range | Scroll p95 | Scroll Max | Process RSS Max |
|
||||
| ---------------------------------------------- | ------------ | ------------ | ---------- | ---------- | --------------- |
|
||||
| Baseline, complete file-card subtree | 8762 ms | 7845-9027 ms | 33.36 ms | 70.61 ms | 1616 MB |
|
||||
| Coalesce whole-file gutter callbacks | 324 ms | 306-377 ms | 31.58 ms | 34.27 ms | 967 MB |
|
||||
| Read widths without resetting number/sign maps | 307 ms | 301-318 ms | 29.61 ms | 34.86 ms | 967 MB |
|
||||
|
||||
Keep callback coalescing: initial rendering improves substantially with the same complete content and tail access. It does not solve steady-state painting; scrolling remains about 32 ms p95. RSS includes JavaScript/native allocation and process high-water behavior across repeated mounts, not just gutter storage.
|
||||
|
||||
The narrower width pass removes redundant map replacement and unbounded argument-list maxima. Its timing difference overlaps the earlier spread; keep it for fewer data mutations, not as an independently proven speedup.
|
||||
|
||||
Before the renderer patch, the same component with a 50,000-line modified hunk at 160 columns measured 225 ms median ready time (215-239 ms), 22.55 ms scroll p95, and 25.52 ms scroll maximum across three runs. A 100,000-line modified hunk at 80 columns measured 530 ms ready time and 60.51 ms scroll p95 in a single measured probe; that single-run probe is diagnostic, not a stable benchmark comparison.
|
||||
|
||||
The benchmark's initial setup is not a performance result; production-component and full-viewer Drive verification are complementary. Record subsequent isolated experiments here before keeping them.
|
||||
|
||||
## Final Matrix
|
||||
|
||||
The unguarded V2 base `0d42e76006` and the final consumer patch use the same harness and fixtures. Each case warms up once and measures seven runs; all cases run sequentially without concurrent build/test activity. Syntax is disabled in this rendering benchmark; the production viewer tests and Drive fixtures exercise JSON highlighting separately.
|
||||
|
||||
| Fixture | Ready Median Before / After | Scroll p95 Before / After | Process RSS Max Before / After |
|
||||
| ------------------------------------- | --------------------------- | ------------------------- | ------------------------------ |
|
||||
| 1,000 original hunks, 160 columns | 9083 / 264 ms | 53.11 / 7.20 ms | 1600 / 871 MB |
|
||||
| 50,000 modified lines, split | 231 / 160 ms | 38.57 / 0.34 ms | 784 / 677 MB |
|
||||
| 50,000 modified lines, unified | 236 / 164 ms | 21.67 / 0.29 ms | 1029 / 674 MB |
|
||||
| 20,000-character modified line, split | 59 / 77 ms | 0.33 / 0.27 ms | 240 / 234 MB |
|
||||
| 40 files with 25 hunks each | 343 / 161 ms | 1.48 / 1.28 ms | 790 / 568 MB |
|
||||
| 100,000 modified lines, unified | 552 / 331 ms | 53.42 / 0.36 ms | 1532 / 1084 MB |
|
||||
|
||||
All cases expose their complete tails. Long markers can cross wrap rows, so that case reconstructs the visible final Code pane without interleaving the other side; the earlier contiguous-screen-marker check falsely rejected both revisions and was corrected identically before accepting these results.
|
||||
|
||||
The long-line opening adds about one frame (18 ms) to settle actual pane-width alignment. That is a measured correctness tradeoff, not a speedup. The many-hunk base also shows substantial spread (7879-17307 ms ready, 309 ms maximum scroll frame); the final ready range is 259-301 ms with a 19 ms maximum scroll frame. Measurements include process/compiler/allocator behavior and should not be presented as hard latency or memory guarantees.
|
||||
|
||||
The dependency patch retains complete native text/layout and selection owners. It bounds decoration raster allocation, paint visits, and source-ID readback, not source loading or full content-node virtualization. Native `0.5.9` platform binaries remain unchanged.
|
||||
@@ -0,0 +1,175 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { CodeRenderable, type Renderable, type ScrollBoxRenderable } from "@opentui/core"
|
||||
import { testRender, useKeyboard } from "@opentui/solid"
|
||||
import { For } from "solid-js"
|
||||
import { PatchDiff } from "../src/component/patch-diff"
|
||||
import { ConfigProvider } from "../src/config"
|
||||
import { ThemeProvider, useThemes } from "../src/context/theme"
|
||||
import { TestTuiContexts } from "../test/fixture/tui-environment"
|
||||
import { emptyThemeSource } from "../test/fixture/fixture"
|
||||
import { largeDiffFixture } from "../test/fixture/large-diff"
|
||||
import { createTuiResolvedConfig } from "../test/fixture/tui-runtime"
|
||||
|
||||
const kind = process.argv[2] ?? "hunks"
|
||||
if (kind !== "hunks" && kind !== "lines" && kind !== "long") throw new Error("Use hunks, lines, or long")
|
||||
const count = Number(process.argv[3] ?? (kind === "hunks" ? 1000 : 20000))
|
||||
const width = Number(process.env.DIFF_BENCH_WIDTH ?? 160)
|
||||
const files = Number(process.env.DIFF_BENCH_FILES ?? 1)
|
||||
const iterations = Number(process.env.DIFF_BENCH_RUNS ?? 7)
|
||||
const fixture = largeDiffFixture(kind, count)
|
||||
const results: { ready: number; scroll: number[]; rss: number }[] = []
|
||||
|
||||
for (let iteration = 0; iteration <= iterations; iteration++) {
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
function Content() {
|
||||
const themes = useThemes()
|
||||
const theme = themes.current
|
||||
useKeyboard((key) => {
|
||||
if (key.name === "j") scroll?.scrollBy(1)
|
||||
})
|
||||
return (
|
||||
<scrollbox
|
||||
ref={(node: ScrollBoxRenderable) => (scroll = node)}
|
||||
width="100%"
|
||||
height="100%"
|
||||
verticalScrollbarOptions={{ visible: false }}
|
||||
>
|
||||
<For each={Array.from({ length: files }, (_, index) => index)}>
|
||||
{() => (
|
||||
<box>
|
||||
<PatchDiff
|
||||
diff={fixture.patch}
|
||||
view={width >= 100 ? "split" : "unified"}
|
||||
width="100%"
|
||||
wrapMode="char"
|
||||
filetype={process.env.DIFF_BENCH_SYNTAX ? "json" : undefined}
|
||||
syntaxStyle={themes.currentSyntax()}
|
||||
showLineNumbers
|
||||
hunkFg={theme.diff.text.hunkHeader}
|
||||
fg={theme.text.default}
|
||||
addedBg={theme.diff.background.added}
|
||||
removedBg={theme.diff.background.removed}
|
||||
contextBg={theme.diff.background.context}
|
||||
addedSignColor={theme.diff.highlight.added}
|
||||
removedSignColor={theme.diff.highlight.removed}
|
||||
lineNumberFg={theme.diff.lineNumber.text}
|
||||
lineNumberBg={theme.diff.background.context}
|
||||
addedLineNumberBg={theme.diff.lineNumber.background.added}
|
||||
removedLineNumberBg={theme.diff.lineNumber.background.removed}
|
||||
/>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
)
|
||||
}
|
||||
const started = performance.now()
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TestTuiContexts>
|
||||
<ConfigProvider config={createTuiResolvedConfig()}>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<Content />
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
),
|
||||
{ width, height: 30 },
|
||||
)
|
||||
try {
|
||||
await app.flush()
|
||||
const ready = performance.now() - started
|
||||
if (!scroll) throw new Error("Missing patch viewport")
|
||||
scroll.scrollTo(scroll.scrollHeight)
|
||||
await app.flush()
|
||||
const code = kind === "long" ? codeNodes(app.renderer.root).at(-1) : undefined
|
||||
if (kind === "long" && !code) throw new Error("Missing diff code owner")
|
||||
const frame = app.captureCharFrame()
|
||||
// A long marker may cross wrap rows; reconstruct its pane without interleaving the other side.
|
||||
const visible = code
|
||||
? frame
|
||||
.split("\n")
|
||||
.map((line) => line.slice(code.x, code.x + code.width))
|
||||
.join("")
|
||||
.replace(/\s/g, "")
|
||||
: frame
|
||||
if (!visible.includes(fixture.tail)) throw new Error("The end of the full diff is not accessible")
|
||||
scroll.scrollTo(Math.floor(scroll.scrollHeight / 2))
|
||||
await app.flush()
|
||||
const frames: number[] = []
|
||||
for (let frame = 0; frame < 20; frame++) {
|
||||
const started = performance.now()
|
||||
app.mockInput.pressKey("j")
|
||||
await app.renderOnce()
|
||||
frames.push(performance.now() - started)
|
||||
}
|
||||
const result = { ready, scroll: frames, rss: process.memoryUsage().rss }
|
||||
if (iteration > 0) results.push(result)
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
iteration,
|
||||
warmup: iteration === 0,
|
||||
ready_ms: ready,
|
||||
scroll_p95_ms: percentile(frames, 0.95),
|
||||
rss_bytes: result.rss,
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
Bun.gc(true)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
kind,
|
||||
count,
|
||||
width,
|
||||
files,
|
||||
iterations,
|
||||
ready_median_ms: percentile(
|
||||
results.map((result) => result.ready),
|
||||
0.5,
|
||||
),
|
||||
ready_min_ms: percentile(
|
||||
results.map((result) => result.ready),
|
||||
0,
|
||||
),
|
||||
ready_max_ms: percentile(
|
||||
results.map((result) => result.ready),
|
||||
1,
|
||||
),
|
||||
scroll_p95_ms: percentile(
|
||||
results.flatMap((result) => result.scroll),
|
||||
0.95,
|
||||
),
|
||||
scroll_max_ms: percentile(
|
||||
results.flatMap((result) => result.scroll),
|
||||
1,
|
||||
),
|
||||
rss_max_bytes: percentile(
|
||||
results.map((result) => result.rss),
|
||||
1,
|
||||
),
|
||||
}),
|
||||
)
|
||||
console.log(
|
||||
`METRIC diff_ready_ms=${percentile(
|
||||
results.map((result) => result.ready),
|
||||
0.5,
|
||||
).toFixed(2)}`,
|
||||
)
|
||||
console.log(
|
||||
`METRIC diff_scroll_p95_ms=${percentile(
|
||||
results.flatMap((result) => result.scroll),
|
||||
0.95,
|
||||
).toFixed(2)}`,
|
||||
)
|
||||
|
||||
function percentile(values: number[], fraction: number) {
|
||||
return [...values].sort((left, right) => left - right)[Math.ceil((values.length - 1) * fraction)]
|
||||
}
|
||||
|
||||
function codeNodes(root: Renderable): CodeRenderable[] {
|
||||
return root instanceof CodeRenderable ? [root] : root.getChildren().flatMap(codeNodes)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { DiffRenderable, LineNumberRenderable, type ColorInput } from "@opentui/core"
|
||||
import type { JSX } from "@opentui/solid"
|
||||
import { createMemo, For, Show, splitProps } from "solid-js"
|
||||
import { createMemo, For, onCleanup, Show, splitProps } from "solid-js"
|
||||
import { splitPatchHunks } from "../util/diff"
|
||||
import { stringWidth } from "../util/string-width"
|
||||
|
||||
@@ -20,6 +20,10 @@ export function PatchDiff(props: Props) {
|
||||
const [local, diffProps] = splitProps(props, ["diff", "hunkFg", "lineNumberBg", "ref"])
|
||||
const hunks = createMemo(() => splitPatchHunks(local.diff))
|
||||
const nodes = new Map<number, DiffRenderable>()
|
||||
let gutterFrame: number | undefined
|
||||
onCleanup(() => {
|
||||
if (gutterFrame !== undefined) cancelAnimationFrame(gutterFrame)
|
||||
})
|
||||
local.ref?.({
|
||||
hunks: () =>
|
||||
[...nodes.entries()]
|
||||
@@ -28,28 +32,31 @@ export function PatchDiff(props: Props) {
|
||||
.filter((node) => !node.isDestroyed),
|
||||
})
|
||||
const syncGutters = (attempt = 0) => {
|
||||
requestAnimationFrame(() => {
|
||||
// Registrations share one whole-file pass instead of repeating it for every hunk.
|
||||
if (gutterFrame !== undefined) return
|
||||
gutterFrame = requestAnimationFrame(() => {
|
||||
gutterFrame = undefined
|
||||
const sides = [...nodes.values()]
|
||||
.filter((item) => !item.isDestroyed)
|
||||
.flatMap((item) => item.getChildren().filter((side) => side instanceof LineNumberRenderable))
|
||||
const lineNumbers = sides.map((side) => new Map([...side.getLineNumbers()].filter(([line]) => line >= 0)))
|
||||
const digits = lineNumbers.map((numbers) => Math.max(0, ...numbers.values()).toString().length)
|
||||
const after = sides.map((side) =>
|
||||
Math.max(
|
||||
0,
|
||||
...[...side.getLineSigns()].filter(([line]) => line >= 0).map(([, sign]) => stringWidth(sign.after ?? "")),
|
||||
),
|
||||
const widths = sides.map((side) => {
|
||||
const width = { line: 0, sign: 0 }
|
||||
side.getLineNumbers().forEach((number, line) => {
|
||||
if (line >= 0) width.line = Math.max(width.line, number)
|
||||
})
|
||||
side.getLineSigns().forEach((sign, line) => {
|
||||
if (line >= 0) width.sign = Math.max(width.sign, stringWidth(sign.after ?? ""))
|
||||
})
|
||||
return { digits: width.line.toString().length, after: width.sign }
|
||||
})
|
||||
const max = widths.reduce(
|
||||
(max, width) => ({ digits: Math.max(max.digits, width.digits), after: Math.max(max.after, width.after) }),
|
||||
{ digits: 0, after: 0 },
|
||||
)
|
||||
const maxDigits = Math.max(...digits)
|
||||
const maxAfter = Math.max(...after)
|
||||
if (!maxDigits && attempt < 2) return syncGutters(attempt + 1)
|
||||
if (!maxDigits) return
|
||||
sides.forEach((side) => {
|
||||
const index = sides.indexOf(side)
|
||||
const signs = new Map([...side.getLineSigns()].filter(([line]) => line >= 0))
|
||||
signs.set(-1, { after: " ".repeat(maxAfter + maxDigits - digits[index]) })
|
||||
side.setLineNumbers(lineNumbers[index])
|
||||
side.setLineSigns(signs)
|
||||
if (!max.digits && attempt < 2) return syncGutters(attempt + 1)
|
||||
if (!max.digits) return
|
||||
sides.forEach((side, index) => {
|
||||
side.setLineSign(-1, { after: " ".repeat(max.after + max.digits - widths[index].digits) })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import {
|
||||
BoxRenderable,
|
||||
CodeRenderable,
|
||||
DiffRenderable,
|
||||
ImageRenderable,
|
||||
MouseButton,
|
||||
@@ -32,6 +33,7 @@ import { createDialogApi } from "../../../src/plugin/api"
|
||||
import { ToastProvider } from "../../../src/ui/toast"
|
||||
import { createSignal, Show } from "solid-js"
|
||||
import { diffImageFixture } from "../../fixture/diff-image"
|
||||
import { largeDiffFixture } from "../../fixture/large-diff"
|
||||
|
||||
test("closing the diff viewer returns to the route it opened from", async () => {
|
||||
const viewer = await renderDiffViewer([])
|
||||
@@ -80,6 +82,154 @@ test("shows an error instead of an empty diff when loading fails", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test.each([80, 160])(
|
||||
"large diffs render automatically and expose their complete content at %i columns",
|
||||
async (width) => {
|
||||
const file = largeDiffFixture("lines", 20000)
|
||||
const viewer = await renderDiffViewer([file], { width, height: 30 })
|
||||
try {
|
||||
await viewer.app.waitForFrame((frame) => frame.includes('"column_0"'))
|
||||
expect(findDiffs(viewer.app.renderer.root)).toHaveLength(1)
|
||||
expect(viewer.commands.has("diff.load_patch")).toBe(false)
|
||||
expect(viewer.app.captureCharFrame()).not.toContain("Large diff hidden")
|
||||
viewer.app.mockInput.pressKey("G")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes(file.tail))
|
||||
expect(
|
||||
viewer.app
|
||||
.captureCharFrame()
|
||||
.split("\n")
|
||||
.find((line) => line.includes(file.tail)),
|
||||
).toMatch(/20000\s+/)
|
||||
viewer.app.mockInput.pressKey("g")
|
||||
viewer.app.mockInput.pressKey("g")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes('"column_0"'))
|
||||
expect(findScrollBox(viewer.app.renderer.root)!.scrollTop).toBe(0)
|
||||
viewer.app.mockInput.pressKey("c", { ctrl: true })
|
||||
await viewer.app.waitFor(() => viewer.current().type !== "plugin")
|
||||
expect(viewer.current()).toEqual(startRoute)
|
||||
} finally {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test("all original hunks stay available for forward, reverse, and end navigation", async () => {
|
||||
const file = largeDiffFixture("hunks", 1000)
|
||||
const viewer = await renderDiffViewer([file], { width: 160, height: 30 })
|
||||
try {
|
||||
await viewer.app.waitForFrame((frame) => frame.includes('"column_1"'))
|
||||
await viewer.app.flush()
|
||||
const hunks = findDiffs(viewer.app.renderer.root)
|
||||
expect(hunks).toHaveLength(1000)
|
||||
const scroll = findScrollBox(viewer.app.renderer.root)!
|
||||
viewer.app.mockInput.pressKey("]")
|
||||
await viewer.app.flush()
|
||||
expect(hunks[1].y).toBeGreaterThanOrEqual(scroll.viewport.y + 2)
|
||||
expect(hunks[1].y).toBeLessThanOrEqual(scroll.viewport.y + 3)
|
||||
viewer.app.mockInput.pressKey("[")
|
||||
await viewer.app.flush()
|
||||
expect(scroll.scrollTop).toBe(0)
|
||||
viewer.app.mockInput.pressKey("G")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes(file.tail))
|
||||
expect(
|
||||
viewer.app
|
||||
.captureCharFrame()
|
||||
.split("\n")
|
||||
.find((line) => line.includes(file.tail)),
|
||||
).toMatch(/39985\s+/)
|
||||
viewer.app.mockInput.pressKey("g")
|
||||
viewer.app.mockInput.pressKey("g")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes('"column_1"'))
|
||||
expect(findDiffs(viewer.app.renderer.root)[0]).toBe(hunks[0])
|
||||
expect(findDiffs(viewer.app.renderer.root).at(-1)).toBe(hunks.at(-1))
|
||||
} finally {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("long wrapped lines retain their tails through narrow and wide resizing", async () => {
|
||||
const file = largeDiffFixture("long", 20000)
|
||||
const viewer = await renderDiffViewer([file], { width: 160, height: 30 })
|
||||
try {
|
||||
await viewer.app.waitForFrame((frame) => frame.includes("xxxx"))
|
||||
await viewer.app.flush()
|
||||
viewer.app.mockInput.pressKey("G")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes(file.tail))
|
||||
viewer.app.resize(80, 24)
|
||||
await viewer.app.flush()
|
||||
expect(findDiffs(viewer.app.renderer.root)[0].view).toBe("unified")
|
||||
viewer.app.mockInput.pressKey("G")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes(file.tail))
|
||||
viewer.app.resize(160, 30)
|
||||
await viewer.app.flush()
|
||||
expect(findDiffs(viewer.app.renderer.root)[0].view).toBe("split")
|
||||
viewer.app.mockInput.pressKey("G")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes(file.tail))
|
||||
viewer.app.mockInput.pressKey("g")
|
||||
viewer.app.mockInput.pressKey("g")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes("xxxx"))
|
||||
} finally {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("large file review and source replacement keep the full patch usable", async () => {
|
||||
const file = largeDiffFixture("lines", 20000)
|
||||
const viewer = await renderDiffViewer([file], { width: 160, height: 30 })
|
||||
try {
|
||||
await viewer.app.waitForFrame((frame) => frame.includes('"column_0"'))
|
||||
viewer.app.mockInput.pressKey("m")
|
||||
await viewer.app.flush()
|
||||
expect(findDiffs(viewer.app.renderer.root)).toHaveLength(0)
|
||||
viewer.app.mockInput.pressKey("m")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes('"column_0"'))
|
||||
const previous = findDiffs(viewer.app.renderer.root)[0]
|
||||
viewer.app.mockInput.pressKey("d")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes("Switch source"))
|
||||
viewer.app.mockInput.pressArrow("down")
|
||||
viewer.app.mockInput.pressEnter()
|
||||
await viewer.app.waitForFrame((frame) => frame.includes("Main branch") && frame.includes('"column_0"'))
|
||||
expect(previous.isDestroyed).toBe(true)
|
||||
viewer.app.mockInput.pressKey("G")
|
||||
await viewer.app.waitForFrame((frame) => frame.includes(file.tail))
|
||||
} finally {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("large diff selection survives offscreen scrolling and same-view resizing", async () => {
|
||||
const viewer = await renderDiffViewer([largeDiffFixture("lines", 20000)], { width: 160, height: 30 })
|
||||
try {
|
||||
await viewer.app.waitForFrame((frame) => frame.includes('"column_0"'))
|
||||
await viewer.app.flush()
|
||||
const code = findDiffs(viewer.app.renderer.root)[0]
|
||||
.getChildren()
|
||||
.flatMap((side) => side.getChildren())
|
||||
.find((node) => node instanceof CodeRenderable)
|
||||
if (!code) throw new Error("Missing diff text owner")
|
||||
const first = code.content.split("\n")[0]
|
||||
await viewer.app.mockMouse.drag(code.x + 2, code.y, code.x + first.length - 1, code.y)
|
||||
const selection = viewer.app.renderer.getSelection()
|
||||
expect(selection?.getSelectedText()).toBe(first.slice(2))
|
||||
const scroll = findScrollBox(viewer.app.renderer.root)!
|
||||
scroll.scrollTo(15000)
|
||||
await viewer.app.flush()
|
||||
expect(viewer.app.renderer.getSelection()).toBe(selection)
|
||||
expect(selection?.getSelectedText()).toBe(first.slice(2))
|
||||
viewer.app.resize(180, 30)
|
||||
await viewer.app.flush()
|
||||
expect(selection?.getSelectedText()).toBe(first.slice(2))
|
||||
scroll.scrollTo(0)
|
||||
await viewer.app.flush()
|
||||
expect(selection?.getSelectedText()).toBe(first.slice(2))
|
||||
expect(code.isDestroyed).toBe(false)
|
||||
viewer.app.renderer.clearSelection()
|
||||
expect(viewer.app.renderer.getSelection()).toBeNull()
|
||||
} finally {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("uses the active location when opened outside a session", async () => {
|
||||
const viewer = await renderDiffViewer([], { initialRoute: { type: "home" } })
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export function largeDiffFixture(kind: "hunks" | "lines" | "long", count: number) {
|
||||
const file = {
|
||||
file: "snapshot.json",
|
||||
additions: kind === "long" ? 1 : count,
|
||||
deletions: kind === "long" ? 1 : count,
|
||||
status: "modified" as const,
|
||||
}
|
||||
const prefix = "--- a/snapshot.json\n+++ b/snapshot.json\n"
|
||||
if (kind === "long") {
|
||||
return {
|
||||
...file,
|
||||
patch: `${prefix}@@ -1 +1 @@\n-${"x".repeat(count)}_OLD_END\n+${"y".repeat(count)}_NEW_END\n`,
|
||||
tail: "_NEW_END",
|
||||
}
|
||||
}
|
||||
if (kind === "lines") {
|
||||
return {
|
||||
...file,
|
||||
patch:
|
||||
prefix +
|
||||
`@@ -1,${count} +1,${count} @@\n` +
|
||||
Array.from({ length: count }, (_, index) => `- "column_${index}": ${index},\n`).join("") +
|
||||
Array.from({ length: count }, (_, index) => `+ "column_${index}": ${index + 1},\n`).join(""),
|
||||
tail: `"column_${count - 1}"`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...file,
|
||||
patch:
|
||||
prefix +
|
||||
Array.from({ length: count }, (_, index) => {
|
||||
const start = index * 40 + 1
|
||||
return (
|
||||
`@@ -${start},25 +${start},25 @@\n` +
|
||||
Array.from({ length: 12 }, (_, offset) => ` "column_${start + offset}": ${start + offset},\n`).join("") +
|
||||
`- "column_${start + 12}": ${start + 12},\n+ "column_${start + 12}": ${start + 13},\n` +
|
||||
Array.from(
|
||||
{ length: 12 },
|
||||
(_, offset) => ` "column_${start + 13 + offset}": ${start + 13 + offset},\n`,
|
||||
).join("")
|
||||
)
|
||||
}).join(""),
|
||||
tail: `"column_${(count - 1) * 40 + 25}"`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user