Fix terminal table rendering after workspace restore (#4877)
* Stabilize terminal cursor rendering and typing checks * Fix terminal table rendering after workspace restore * Fix terminal table width underfit * Tighten terminal renderer stabilization
This commit is contained in:
parent
c4dcd88440
commit
3153356d87
|
|
@ -1,20 +1,19 @@
|
|||
diff --git a/package.json b/package.json
|
||||
index c697677b35e736e23702c1abb357b3445c57d949..deb0387e66a8abb301006372ec9775bb54131d5a 100644
|
||||
index c697677b35e736e23702c1abb357b3445c57d949..0a9f8b69e5a6f4ff7f54dbf437cf91bb1e3d3b61 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -6,9 +6,16 @@
|
||||
"name": "The xterm.js authors",
|
||||
@@ -7,8 +7,15 @@
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
- "main": "lib/addon-ligatures.js",
|
||||
+ "main": "lib/addon-ligatures.mjs",
|
||||
"module": "lib/addon-ligatures.mjs",
|
||||
"main": "lib/addon-ligatures.js",
|
||||
- "module": "lib/addon-ligatures.mjs",
|
||||
+ "module": "lib/addon-ligatures.js",
|
||||
"types": "typings/addon-ligatures.d.ts",
|
||||
+ "exports": {
|
||||
+ ".": {
|
||||
+ "types": "./typings/addon-ligatures.d.ts",
|
||||
+ "import": "./lib/addon-ligatures.mjs",
|
||||
+ "default": "./lib/addon-ligatures.mjs"
|
||||
+ "import": "./lib/addon-ligatures.js",
|
||||
+ "default": "./lib/addon-ligatures.js"
|
||||
+ }
|
||||
+ },
|
||||
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-ligatures",
|
||||
File diff suppressed because one or more lines are too long
21
package.json
21
package.json
|
|
@ -74,8 +74,8 @@
|
|||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@linear/sdk": "^82.1.0",
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
"@xterm/addon-serialize": "0.15.0-beta.220",
|
||||
"@xterm/headless": "6.1.0-beta.220",
|
||||
"@xterm/addon-serialize": "0.15.0-beta.285",
|
||||
"@xterm/headless": "6.1.0-beta.285",
|
||||
"agent-browser": "~0.27.0",
|
||||
"electron-updater": "^6.8.3",
|
||||
"node-pty": "^1.1.0",
|
||||
|
|
@ -123,13 +123,13 @@
|
|||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260421.2",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"@xterm/addon-fit": "0.12.0-beta.220",
|
||||
"@xterm/addon-ligatures": "0.11.0-beta.220",
|
||||
"@xterm/addon-search": "0.17.0-beta.220",
|
||||
"@xterm/addon-unicode11": "0.10.0-beta.220",
|
||||
"@xterm/addon-web-links": "0.13.0-beta.220",
|
||||
"@xterm/addon-webgl": "0.20.0-beta.219",
|
||||
"@xterm/xterm": "6.1.0-beta.220",
|
||||
"@xterm/addon-fit": "0.12.0-beta.285",
|
||||
"@xterm/addon-ligatures": "0.11.0-beta.285",
|
||||
"@xterm/addon-search": "0.17.0-beta.285",
|
||||
"@xterm/addon-unicode11": "0.10.0-beta.285",
|
||||
"@xterm/addon-web-links": "0.13.0-beta.285",
|
||||
"@xterm/addon-webgl": "0.20.0-beta.284",
|
||||
"@xterm/xterm": "6.1.0-beta.285",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
@ -223,8 +223,7 @@
|
|||
],
|
||||
"patchedDependencies": {
|
||||
"node-pty@1.1.0": "config/patches/node-pty@1.1.0.patch",
|
||||
"@xterm/addon-ligatures@0.11.0-beta.220": "config/patches/@xterm__addon-ligatures@0.11.0-beta.220.patch",
|
||||
"@xterm/addon-webgl@0.20.0-beta.219": "config/patches/@xterm__addon-webgl@0.20.0-beta.219.patch"
|
||||
"@xterm/addon-ligatures@0.11.0-beta.285": "config/patches/@xterm__addon-ligatures@0.11.0-beta.285.patch"
|
||||
}
|
||||
},
|
||||
"reactDoctor": {
|
||||
|
|
|
|||
150
pnpm-lock.yaml
150
pnpm-lock.yaml
|
|
@ -5,12 +5,9 @@ settings:
|
|||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
'@xterm/addon-ligatures@0.11.0-beta.220':
|
||||
hash: df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634
|
||||
path: config/patches/@xterm__addon-ligatures@0.11.0-beta.220.patch
|
||||
'@xterm/addon-webgl@0.20.0-beta.219':
|
||||
hash: c76ae4cd585a6b2ea39a39e3c021f125f42afdafeffd3b1a767a3200deb4ffb2
|
||||
path: config/patches/@xterm__addon-webgl@0.20.0-beta.219.patch
|
||||
'@xterm/addon-ligatures@0.11.0-beta.285':
|
||||
hash: 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920
|
||||
path: config/patches/@xterm__addon-ligatures@0.11.0-beta.285.patch
|
||||
node-pty@1.1.0:
|
||||
hash: 407ae07e1e0e2ff2e8b58696449c54c31e51d87535bc6aa4a7a7b0b561407282
|
||||
path: config/patches/node-pty@1.1.0.patch
|
||||
|
|
@ -32,11 +29,11 @@ importers:
|
|||
specifier: ^2.5.6
|
||||
version: 2.5.6
|
||||
'@xterm/addon-serialize':
|
||||
specifier: 0.15.0-beta.220
|
||||
version: 0.15.0-beta.220(@xterm/xterm@6.1.0-beta.220)
|
||||
specifier: 0.15.0-beta.285
|
||||
version: 0.15.0-beta.285(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/headless':
|
||||
specifier: 6.1.0-beta.220
|
||||
version: 6.1.0-beta.220
|
||||
specifier: 6.1.0-beta.285
|
||||
version: 6.1.0-beta.285
|
||||
agent-browser:
|
||||
specifier: ~0.27.0
|
||||
version: 0.27.0
|
||||
|
|
@ -174,26 +171,26 @@ importers:
|
|||
specifier: ^5.2.0
|
||||
version: 5.2.0(vite@7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.8.4))
|
||||
'@xterm/addon-fit':
|
||||
specifier: 0.12.0-beta.220
|
||||
version: 0.12.0-beta.220(@xterm/xterm@6.1.0-beta.220)
|
||||
specifier: 0.12.0-beta.285
|
||||
version: 0.12.0-beta.285(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/addon-ligatures':
|
||||
specifier: 0.11.0-beta.220
|
||||
version: 0.11.0-beta.220(patch_hash=df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634)(@xterm/xterm@6.1.0-beta.220)
|
||||
specifier: 0.11.0-beta.285
|
||||
version: 0.11.0-beta.285(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/addon-search':
|
||||
specifier: 0.17.0-beta.220
|
||||
version: 0.17.0-beta.220(@xterm/xterm@6.1.0-beta.220)
|
||||
specifier: 0.17.0-beta.285
|
||||
version: 0.17.0-beta.285(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.220
|
||||
version: 0.10.0-beta.220(@xterm/xterm@6.1.0-beta.220)
|
||||
specifier: 0.10.0-beta.285
|
||||
version: 0.10.0-beta.285(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/addon-web-links':
|
||||
specifier: 0.13.0-beta.220
|
||||
version: 0.13.0-beta.220(@xterm/xterm@6.1.0-beta.220)
|
||||
specifier: 0.13.0-beta.285
|
||||
version: 0.13.0-beta.285(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/addon-webgl':
|
||||
specifier: 0.20.0-beta.219
|
||||
version: 0.20.0-beta.219(patch_hash=c76ae4cd585a6b2ea39a39e3c021f125f42afdafeffd3b1a767a3200deb4ffb2)(@xterm/xterm@6.1.0-beta.220)
|
||||
specifier: 0.20.0-beta.284
|
||||
version: 0.20.0-beta.284(@xterm/xterm@6.1.0-beta.285)
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.220
|
||||
version: 6.1.0-beta.220
|
||||
specifier: 6.1.0-beta.285
|
||||
version: 6.1.0-beta.285
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
|
|
@ -3061,47 +3058,47 @@ packages:
|
|||
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
|
||||
engines: {node: '>=14.6'}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.220':
|
||||
resolution: {integrity: sha512-lTiEjsdJBYTvIdA8USs90j+pu7/9oCQEh8JYBDJoRxzzAcvk4V1GlEA++Ztspbfk4nSh2HVxwJvdeTD6ffAQlg==}
|
||||
'@xterm/addon-fit@0.12.0-beta.285':
|
||||
resolution: {integrity: sha512-rSVi0nMbK+9sgL1QXmJlv3nWvktj0AZeRd52yVzyNhl5mxxEkvxiwFJZlcEBRg27ipeWVAs1PeuCz0A9QnNnAQ==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.220
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-ligatures@0.11.0-beta.220':
|
||||
resolution: {integrity: sha512-fw/mSPqwgBNVX2ejW3rKhXablP3pelKJWYlt1QIr40+zZ//ERcHZWZNLB1I9fojiGW++tCnIHKh0GPpI2h5HgA==}
|
||||
'@xterm/addon-ligatures@0.11.0-beta.285':
|
||||
resolution: {integrity: sha512-ZBqrv60zrIKGspVfv5+m3lRGHeAGDW2U/imu6vER8D2vhxs75FXh/bA+X2/oSdDJQVgpygsN8G3gNQqt16v3eg==}
|
||||
engines: {node: '>8.0.0'}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.220
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-search@0.17.0-beta.220':
|
||||
resolution: {integrity: sha512-2QwmGiZ8sn1ISYLuHIt0heUYN0wR6d+AWFZRGsekXbLCvRw/qjclCSKsDsIO1bTMBjey5K1MeZbse6kTKx+kDg==}
|
||||
'@xterm/addon-search@0.17.0-beta.285':
|
||||
resolution: {integrity: sha512-cGjvwxsCnzlLbDWhMaHF9ZxTbYt6foAvUlURe63XyonXR2DVYH6/sr4YoUhM4S5tUMtdIhPxJhtQ8uF6r+ch3g==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.220
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-serialize@0.15.0-beta.220':
|
||||
resolution: {integrity: sha512-ssZ8rnSU0EOdDsFS5cI5LHKy/T0rQfZBE9iPph7rUxSAwZXbMGz0Vrsob79IKRvfE6oMZlFn+NE0Gg/mKq62Iw==}
|
||||
'@xterm/addon-serialize@0.15.0-beta.285':
|
||||
resolution: {integrity: sha512-ae1Fi0Rceby+Ctf39aCjVlJ5+K3OJMEdeU3LIw0Su4z58k6Yz577laM4OJ7CIAUQTCp7K7WliYaTo29vNVCdBw==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.220
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.220':
|
||||
resolution: {integrity: sha512-83VMUPOp7VKWsVlw+a6TrZP38VAQM7xigu0JcSj2gVO1xsYRyme9wlgKDX+lmgWrfJPSjgB0vfXpcxI4dt/Wvw==}
|
||||
'@xterm/addon-unicode11@0.10.0-beta.285':
|
||||
resolution: {integrity: sha512-rfijFu7UcYpaFx5wzxvTpQbIyyq/amf2PuS9pktywcFQr4ITxRgid5EVzKLRG1vchkApNcQplWeYxGEtjiw0Cw==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.220
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-web-links@0.13.0-beta.220':
|
||||
resolution: {integrity: sha512-XILFvFY0lOpTafIQxUOkS6x/MfZzarW3WbHZ40KEzAE8ImJqwOQiiqG17noLMUDD6rSOv7BRWNJACq4FflBNvg==}
|
||||
'@xterm/addon-web-links@0.13.0-beta.285':
|
||||
resolution: {integrity: sha512-deEjcrV/LQFM031l6Jop2mVK79RJru6SlNCjtFXb0Ex5GwIcPZCGW4VdZYMU5toQ3mMDR6yrzWDsb9SFtDrMeA==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.220
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.219':
|
||||
resolution: {integrity: sha512-IaAcDDP0lhHIdYJuTFKZIMsRqlN8iR5U8lW9MiTK4mUAjettqZN2z/tkSsl4yV3GZ9/siOZofDl+GO1TK7y7ww==}
|
||||
'@xterm/addon-webgl@0.20.0-beta.284':
|
||||
resolution: {integrity: sha512-tzkUiEfdpHCY8mXCbuIaP9V67QDfBJvDr9jdxs5jjxNCIQvw+NCoKD97y5sUrQhrIlr7xrDGniPgPYThQ/1FWg==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.220
|
||||
'@xterm/xterm': ^6.1.0-beta.285
|
||||
|
||||
'@xterm/headless@6.1.0-beta.220':
|
||||
resolution: {integrity: sha512-oc+cI28eP/ktv91ddk1MxsEdfS41dnNvB3eMjNxfP7v+SaM7aXgXAL8Kj0/zzbpX/dG0nbA9wi3YvsybzCN3OQ==}
|
||||
'@xterm/headless@6.1.0-beta.285':
|
||||
resolution: {integrity: sha512-NJud+XEUjKMT2LwPqcIh/gazktV+R2AHjEPMQsn/l6+53rgFusuifmJjVkWLwZ228YYsUaN5+lJELeExS26q4A==}
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.220':
|
||||
resolution: {integrity: sha512-rwEKE75wfwfNWeGnjl0iQRA1W50HigKoGcGMeF3o5CqDSdI9IjmwzpV9xHXqLH6CYj6s9N1g6bbKFVDou9dJ/A==}
|
||||
'@xterm/xterm@6.1.0-beta.285':
|
||||
resolution: {integrity: sha512-S3K58tepMkbpWRBzOGKd0In6AVvt9QPAnNs8DJ8rPUPODYtsCYWAtINHKYtC2OpXcE5EBKM35dl+Dgv03OoE/w==}
|
||||
|
||||
abbrev@4.0.0:
|
||||
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
|
||||
|
|
@ -4771,6 +4768,10 @@ packages:
|
|||
lowlight@3.3.0:
|
||||
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
||||
|
||||
lru-cache@11.5.1:
|
||||
resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
|
|
@ -5168,8 +5169,8 @@ packages:
|
|||
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
opentype.js@0.8.0:
|
||||
resolution: {integrity: sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==}
|
||||
opentype.js@2.0.0:
|
||||
resolution: {integrity: sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==}
|
||||
hasBin: true
|
||||
|
||||
ora@8.2.0:
|
||||
|
|
@ -5985,9 +5986,6 @@ packages:
|
|||
tiny-async-pool@1.3.0:
|
||||
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
|
|
@ -8971,39 +8969,39 @@ snapshots:
|
|||
'@xmldom/xmldom@0.9.10':
|
||||
optional: true
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.220(@xterm/xterm@6.1.0-beta.220)':
|
||||
'@xterm/addon-fit@0.12.0-beta.285(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.220
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-ligatures@0.11.0-beta.220(patch_hash=df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634)(@xterm/xterm@6.1.0-beta.220)':
|
||||
'@xterm/addon-ligatures@0.11.0-beta.285(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.220
|
||||
lru-cache: 6.0.0
|
||||
opentype.js: 0.8.0
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
lru-cache: 11.5.1
|
||||
opentype.js: 2.0.0
|
||||
|
||||
'@xterm/addon-search@0.17.0-beta.220(@xterm/xterm@6.1.0-beta.220)':
|
||||
'@xterm/addon-search@0.17.0-beta.285(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.220
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-serialize@0.15.0-beta.220(@xterm/xterm@6.1.0-beta.220)':
|
||||
'@xterm/addon-serialize@0.15.0-beta.285(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.220
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.220(@xterm/xterm@6.1.0-beta.220)':
|
||||
'@xterm/addon-unicode11@0.10.0-beta.285(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.220
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-web-links@0.13.0-beta.220(@xterm/xterm@6.1.0-beta.220)':
|
||||
'@xterm/addon-web-links@0.13.0-beta.285(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.220
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.219(patch_hash=c76ae4cd585a6b2ea39a39e3c021f125f42afdafeffd3b1a767a3200deb4ffb2)(@xterm/xterm@6.1.0-beta.220)':
|
||||
'@xterm/addon-webgl@0.20.0-beta.284(@xterm/xterm@6.1.0-beta.285)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.220
|
||||
'@xterm/xterm': 6.1.0-beta.285
|
||||
|
||||
'@xterm/headless@6.1.0-beta.220': {}
|
||||
'@xterm/headless@6.1.0-beta.285': {}
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.220': {}
|
||||
'@xterm/xterm@6.1.0-beta.285': {}
|
||||
|
||||
abbrev@4.0.0: {}
|
||||
|
||||
|
|
@ -10827,6 +10825,8 @@ snapshots:
|
|||
devlop: 1.1.0
|
||||
highlight.js: 11.11.1
|
||||
|
||||
lru-cache@11.5.1: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
|
@ -11473,9 +11473,7 @@ snapshots:
|
|||
powershell-utils: 0.1.0
|
||||
wsl-utils: 0.3.1
|
||||
|
||||
opentype.js@0.8.0:
|
||||
dependencies:
|
||||
tiny-inflate: 1.0.3
|
||||
opentype.js@2.0.0: {}
|
||||
|
||||
ora@8.2.0:
|
||||
dependencies:
|
||||
|
|
@ -12515,8 +12513,6 @@ snapshots:
|
|||
dependencies:
|
||||
semver: 5.7.2
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tiny-typed-emitter@2.1.0: {}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ describe('buildDefaultTerminalOptions', () => {
|
|||
expect(buildDefaultTerminalOptions().cursorInactiveStyle).toBe('outline')
|
||||
})
|
||||
|
||||
it('does not reserve a classic scrollbar gutter when fitting terminal columns', () => {
|
||||
expect(buildDefaultTerminalOptions().scrollbar?.width).toBe(0)
|
||||
})
|
||||
|
||||
it('only uses inactive outline for block cursors', () => {
|
||||
expect(resolveTerminalCursorInactiveStyle('block')).toBe('outline')
|
||||
expect(resolveTerminalCursorInactiveStyle('bar')).toBe('bar')
|
||||
|
|
@ -88,10 +92,6 @@ describe('buildDefaultTerminalOptions', () => {
|
|||
// silently breaks enhanced chords, especially inside tmux.
|
||||
expect(buildDefaultTerminalOptions().vtExtensions?.kittyKeyboard).toBe(true)
|
||||
})
|
||||
|
||||
it('uses xterm scrollbar options for the slimmer terminal scrollbar', () => {
|
||||
expect(buildDefaultTerminalOptions().scrollbar?.width).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('attachWebgl', () => {
|
||||
|
|
@ -116,6 +116,7 @@ describe('attachWebgl', () => {
|
|||
|
||||
it('keeps a pane on the DOM renderer after WebGL context loss', () => {
|
||||
const pane = createPane()
|
||||
pane.terminalGpuAcceleration = 'on'
|
||||
|
||||
attachWebgl(pane)
|
||||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
|
|
@ -136,6 +137,7 @@ describe('attachWebgl', () => {
|
|||
|
||||
it('repaints the current buffer after WebGL attaches', () => {
|
||||
const pane = createPane()
|
||||
pane.terminalGpuAcceleration = 'on'
|
||||
|
||||
attachWebgl(pane)
|
||||
|
||||
|
|
@ -144,6 +146,7 @@ describe('attachWebgl', () => {
|
|||
|
||||
it('does not attach WebGL while initial rendering is deferred', () => {
|
||||
const pane = createPane()
|
||||
pane.terminalGpuAcceleration = 'on'
|
||||
pane.webglAttachmentDeferred = true
|
||||
|
||||
attachWebgl(pane)
|
||||
|
|
@ -162,6 +165,15 @@ describe('attachWebgl', () => {
|
|||
expect(pane.terminal.loadAddon).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses DOM rendering for auto GPU acceleration', () => {
|
||||
const pane = createPane()
|
||||
|
||||
attachWebgl(pane)
|
||||
|
||||
expect(pane.webglAddon).toBeNull()
|
||||
expect(pane.terminal.loadAddon).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses DOM rendering for auto GPU acceleration on Linux', () => {
|
||||
vi.stubGlobal('navigator', {
|
||||
platform: 'Linux x86_64',
|
||||
|
|
@ -188,58 +200,21 @@ describe('attachWebgl', () => {
|
|||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('uses DOM for later auto panes after WebGL attach fails until the suggestion resets', () => {
|
||||
const firstPane = createPane()
|
||||
vi.mocked(WebglAddon).mockImplementationOnce(() => {
|
||||
throw new Error('webgl unavailable')
|
||||
})
|
||||
|
||||
attachWebgl(firstPane)
|
||||
|
||||
expect(firstPane.webglAddon).toBeNull()
|
||||
|
||||
const laterAutoPane = createPane()
|
||||
attachWebgl(laterAutoPane)
|
||||
|
||||
expect(laterAutoPane.terminal.loadAddon).not.toHaveBeenCalled()
|
||||
|
||||
resetTerminalWebglSuggestion()
|
||||
const retriedAutoPane = createPane()
|
||||
attachWebgl(retriedAutoPane)
|
||||
|
||||
expect(retriedAutoPane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('still attempts WebGL in on mode after auto mode suggests DOM', () => {
|
||||
const autoPane = createPane()
|
||||
vi.mocked(WebglAddon).mockImplementationOnce(() => {
|
||||
throw new Error('webgl unavailable')
|
||||
})
|
||||
|
||||
attachWebgl(autoPane)
|
||||
|
||||
const forcedPane = createPane()
|
||||
forcedPane.terminalGpuAcceleration = 'on'
|
||||
attachWebgl(forcedPane)
|
||||
|
||||
expect(forcedPane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps auto-mode panes on DOM after complex-script output', () => {
|
||||
const pane = createPane()
|
||||
|
||||
attachWebgl(pane)
|
||||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
expect(pane.terminal.loadAddon).not.toHaveBeenCalled()
|
||||
|
||||
markComplexScriptOutput(pane)
|
||||
|
||||
expect(pane.hasComplexScriptOutput).toBe(true)
|
||||
expect(pane.webglAddon).toBeNull()
|
||||
expect(webglMock.dispose).toHaveBeenCalledTimes(1)
|
||||
expect(webglMock.dispose).not.toHaveBeenCalled()
|
||||
|
||||
attachWebgl(pane)
|
||||
|
||||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
expect(pane.terminal.loadAddon).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows explicit on mode to override complex-script DOM fallback', () => {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from './pane-fit-resize-observer'
|
||||
import { clearPendingSplitScrollRestore } from './pane-split-scroll'
|
||||
import { buildDefaultTerminalOptions } from './pane-terminal-options'
|
||||
import { activateOrcaTerminalUnicodeProvider } from './pane-terminal-unicode-provider'
|
||||
import { attachDomRendererFocusClassSync } from './pane-dom-focus-class-sync'
|
||||
import {
|
||||
ENABLE_WEBGL_RENDERER,
|
||||
|
|
@ -197,7 +198,7 @@ export function openTerminal(pane: ManagedPaneInternal): void {
|
|||
terminal.loadAddon(unicode11Addon)
|
||||
terminal.loadAddon(webLinksAddon)
|
||||
|
||||
// Activate Unicode 11 widths *before* any caller-driven write. CJK / emoji /
|
||||
// Activate Orca's Unicode 11 width shim *before* any caller-driven write. CJK / emoji /
|
||||
// ZWJ codepoints get baked into the buffer at the active unicode version on
|
||||
// write — if a restore (snapshot, scrollback, cold-restore) writes bytes
|
||||
// through xterm while the default v6 width tables are still active, wide
|
||||
|
|
@ -206,7 +207,7 @@ export function openTerminal(pane: ManagedPaneInternal): void {
|
|||
// (replayTerminalLayout → splitPane/createInitialPane → openTerminal,
|
||||
// restoreScrollbackBuffers, handleReattachResult) run after openTerminal,
|
||||
// so the activation must stay at this position.
|
||||
terminal.unicode.activeVersion = '11'
|
||||
activateOrcaTerminalUnicodeProvider(terminal)
|
||||
|
||||
// Why: the OS reads the focused textarea's screen rect at compositionstart to
|
||||
// decide where to display the IME candidate window. xterm.js only repositions
|
||||
|
|
|
|||
|
|
@ -26,18 +26,16 @@ export function buildDefaultTerminalOptions(): ITerminalOptions {
|
|||
fontWeight: '300',
|
||||
fontWeightBold: '500',
|
||||
scrollback: 10000,
|
||||
// Why: xterm 6.x renders its own DOM scrollbar (default 14px), which no CSS
|
||||
// on .xterm-viewport can resize. Slim it via the option. Setting width also
|
||||
// enables the overview ruler, whose border is hidden via overviewRulerBorder
|
||||
// in composeActiveTerminalTheme.
|
||||
scrollbar: {
|
||||
width: 7
|
||||
},
|
||||
allowTransparency: false,
|
||||
// Why: on macOS, non-US layouts rely on Option to compose characters like @ and €.
|
||||
macOptionIsMeta: false,
|
||||
macOptionClickForcesSelection: true,
|
||||
drawBoldTextInBrightColors: true,
|
||||
scrollbar: {
|
||||
// Why: xterm's DOM scrollbar overlays the terminal in Electron; reserving
|
||||
// gutter width in FitAddon underfits wide table/TUI output by a column.
|
||||
width: 0
|
||||
},
|
||||
// Why: advertise kitty keyboard protocol support so CLIs that probe
|
||||
// (CSI ? u) know Orca accepts enhanced key reporting. Orca still writes
|
||||
// CSI-u for Shift+Enter on non-Windows platforms; programs that respect
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import type { IUnicodeHandling, IUnicodeVersionProvider } from '@xterm/xterm'
|
||||
|
||||
type XtermTerminalWithUnicodeCore = {
|
||||
unicode: IUnicodeHandling
|
||||
_core?: {
|
||||
unicodeService?: {
|
||||
_providers?: Record<string, IUnicodeVersionProvider>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ORCA_UNICODE_VERSION = 'orca-11-zwj'
|
||||
const UNICODE11_VERSION = '11'
|
||||
const ZERO_WIDTH_JOINER = 0x200d
|
||||
|
||||
function extractWidth(properties: number): 0 | 1 | 2 {
|
||||
return ((properties >> 1) & 3) as 0 | 1 | 2
|
||||
}
|
||||
|
||||
function extractCharKind(properties: number): number {
|
||||
return properties >> 3
|
||||
}
|
||||
|
||||
function createProperties(charKind: number, width: 0 | 1 | 2, shouldJoin: boolean): number {
|
||||
return ((charKind & 0xffffff) << 3) | ((width & 3) << 1) | (shouldJoin ? 1 : 0)
|
||||
}
|
||||
|
||||
class OrcaUnicodeProvider implements IUnicodeVersionProvider {
|
||||
public readonly version = ORCA_UNICODE_VERSION
|
||||
|
||||
public constructor(private readonly baseProvider: IUnicodeVersionProvider) {}
|
||||
|
||||
public wcwidth(codepoint: number): 0 | 1 | 2 {
|
||||
return this.baseProvider.wcwidth(codepoint)
|
||||
}
|
||||
|
||||
public charProperties(codepoint: number, preceding: number): number {
|
||||
const precedingWidth = extractWidth(preceding)
|
||||
const precedingKind = extractCharKind(preceding)
|
||||
|
||||
if (codepoint === ZERO_WIDTH_JOINER && precedingWidth > 0) {
|
||||
return createProperties(ZERO_WIDTH_JOINER, precedingWidth, true)
|
||||
}
|
||||
|
||||
if (precedingKind === ZERO_WIDTH_JOINER && precedingWidth > 0 && this.wcwidth(codepoint) > 0) {
|
||||
// Why: CLIs render ZWJ emoji as one visible glyph and budget them as one
|
||||
// wide cell pair; xterm Unicode11 otherwise advances for both emoji parts.
|
||||
return createProperties(codepoint, precedingWidth, true)
|
||||
}
|
||||
|
||||
return this.baseProvider.charProperties(codepoint, preceding)
|
||||
}
|
||||
}
|
||||
|
||||
export function activateOrcaTerminalUnicodeProvider(terminal: XtermTerminalWithUnicodeCore): void {
|
||||
const { unicode } = terminal
|
||||
if (unicode.activeVersion === ORCA_UNICODE_VERSION) {
|
||||
return
|
||||
}
|
||||
|
||||
const baseProvider = terminal._core?.unicodeService?._providers?.[UNICODE11_VERSION]
|
||||
if (!baseProvider) {
|
||||
unicode.activeVersion = UNICODE11_VERSION
|
||||
return
|
||||
}
|
||||
|
||||
if (!unicode.versions.includes(ORCA_UNICODE_VERSION)) {
|
||||
unicode.register(new OrcaUnicodeProvider(baseProvider))
|
||||
}
|
||||
unicode.activeVersion = ORCA_UNICODE_VERSION
|
||||
}
|
||||
|
|
@ -2,12 +2,10 @@ import { WebglAddon } from '@xterm/addon-webgl'
|
|||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
|
||||
export const ENABLE_WEBGL_RENDERER = true
|
||||
let suggestedRendererType: 'dom' | undefined
|
||||
|
||||
export function resetTerminalWebglSuggestion(): void {
|
||||
// Why: VS Code clears its suggested renderer when gpuAcceleration changes,
|
||||
// letting "auto" retry WebGL after a user toggles the setting.
|
||||
suggestedRendererType = undefined
|
||||
// Why: retained for settings/tests while "auto" intentionally stays on DOM
|
||||
// for this release to avoid renderer metric changes corrupting TUI tables.
|
||||
}
|
||||
|
||||
function isLinuxRenderer(): boolean {
|
||||
|
|
@ -21,16 +19,17 @@ export function shouldUseTerminalWebgl(pane: ManagedPaneInternal): boolean {
|
|||
if (pane.terminalGpuAcceleration === 'on') {
|
||||
return true
|
||||
}
|
||||
if (pane.terminalGpuAcceleration === 'auto') {
|
||||
// Why: WebGL and DOM report different cell metrics on current xterm beta;
|
||||
// switching renderers after emoji/table output rewraps completed TUI rows.
|
||||
return false
|
||||
}
|
||||
if (isLinuxRenderer()) {
|
||||
// Why: multiple Linux/Wayland GPU stacks corrupt xterm's WebGL glyph atlas
|
||||
// without raising context loss; tab switching only masks it by rebuilding WebGL.
|
||||
return false
|
||||
}
|
||||
return (
|
||||
pane.terminalGpuAcceleration === 'auto' &&
|
||||
suggestedRendererType === undefined &&
|
||||
!pane.hasComplexScriptOutput
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
function refreshTerminalAfterWebglAttach(pane: ManagedPaneInternal): void {
|
||||
|
|
@ -117,12 +116,6 @@ export function attachWebgl(pane: ManagedPaneInternal): void {
|
|||
pane.webglAddon = webglAddon
|
||||
refreshTerminalAfterWebglAttach(pane)
|
||||
} catch (err) {
|
||||
if (pane.terminalGpuAcceleration === 'auto') {
|
||||
// Why: mirrors VS Code's `terminal.integrated.gpuAcceleration=auto`
|
||||
// behavior: once WebGL fails, keep subsequent auto panes on DOM until
|
||||
// the setting changes and resets the suggestion.
|
||||
suggestedRendererType = 'dom'
|
||||
}
|
||||
// WebGL not available — default DOM renderer is fine, but log it for debugging
|
||||
console.warn('[terminal] WebGL unavailable for pane', pane.id, '— using DOM renderer:', err)
|
||||
pane.webglAddon = null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
| Emoji | Name | Category | Unicode | Description | Mood | Popularity | Year Added |
|
||||
|-------|------|----------|---------|-------------|------|------------|------------|
|
||||
| 😀 | Grinning Face | Smileys | U+1F600 | A happy grinning face | Positive | 95 | 2015 |
|
||||
| 🚀 | Rocket | Travel | U+1F680 | A soaring rocket | Exciting | 88 | 2010 |
|
||||
| 🧑💻 | Technologist | People | U+1F9D1 U+200D U+1F4BB | Person at computer | Focused | 92 | 2020 |
|
||||
| 🎉 | Party Popper | Events | U+1F389 | Celebration popper | Festive | 97 | 2010 |
|
||||
| 🐱 | Cat Face | Animals | U+1F431 | A cute cat face | Cute | 85 | 2010 |
|
||||
| 🍕 | Pizza | Food | U+1F355 | Slice of pizza | Hungry | 90 | 2010 |
|
||||
| 💻 | Laptop | Objects | U+1F4BB | A personal computer | Productive | 78 | 2010 |
|
||||
| 🌍 | Globe Europe-Africa | Travel | U+1F30D | Earth showing Europe/Africa | Wanderlust | 72 | 2010 |
|
||||
| ❤️ | Red Heart | Symbols | U+2764 | A red heart symbol | Loving | 99 | 2010 |
|
||||
| 🎵 | Musical Note | Music | U+1F3B5 | Single eighth note | Musical | 76 | 2010 |
|
||||
| 🤖 | Robot Face | Smileys | U+1F916 | A mechanical robot | Neutral | 81 | 2016 |
|
||||
| ☕ | Hot Beverage | Food | U+1F375 | A steaming coffee cup | Awake | 89 | 2010 |
|
||||
| 🌈 | Rainbow | Nature | U+1F308 | A colorful rainbow | Hopeful | 83 | 2010 |
|
||||
| 🦄 | Unicorn Face | Animals | U+1F984 | A fantasy unicorn | Magical | 91 | 2015 |
|
||||
| ⚡ | High Voltage | Symbols | U+26A1 | Lightning bolt | Energetic | 86 | 2010 |
|
||||
| 🎸 | Guitar | Music | U+1F3B8 | A six-string guitar | Rocking | 71 | 2010 |
|
||||
| 🍀 | Four Leaf Clover | Nature | U+1F340 | A lucky clover | Lucky | 82 | 2010 |
|
||||
| 🏆 | Trophy | Awards | U+1F3C6 | A gold championship cup | Victorious | 87 | 2010 |
|
||||
| 🧠 | Brain | Body | U+1F9E0 | A human brain | Smart | 93 | 2018 |
|
||||
| 🌮 | Taco | Food | U+1F32E | A folded taco | Tasty | 84 | 2015 |
|
||||
| 🛸 | Flying Saucer | Travel | U+1F6F8 | An alien spacecraft | Mysterious | 69 | 2017 |
|
||||
| 🎨 | Artist Palette | Arts | U+1F3A8 | A painter's palette | Creative | 77 | 2010 |
|
||||
| 🐧 | Penguin | Animals | U+1F427 | A cute penguin | Chill | 79 | 2010 |
|
||||
| 💎 | Gem Stone | Objects | U+1F48E | A sparkling diamond | Precious | 74 | 2010 |
|
||||
| 🎮 | Video Game | Objects | U+1F3AE | A game controller | Gaming | 88 | 2010 |
|
||||
| 🌊 | Water Wave | Nature | U+1F30A | A crashing ocean wave | Calm | 80 | 2010 |
|
||||
| 🔥 | Fire | Nature | U+1F525 | A burning flame | Lit | 98 | 2010 |
|
||||
| 🌟 | Glowing Star | Symbols | U+1F31F | A twinkling star | Bright | 85 | 2010 |
|
||||
| 🎤 | Microphone | Music | U+1F3A4 | A stage microphone | Singing | 73 | 2010 |
|
||||
| 🐶 | Dog Face | Animals | U+1F436 | A friendly dog face | Loyal | 86 | 2010 |
|
||||
| 🍩 | Doughnut | Food | U+1F369 | A frosted doughnut | Sweet | 82 | 2010 |
|
||||
| 🚲 | Bicycle | Travel | U+1F6B2 | A push bike | Active | 67 | 2010 |
|
||||
| 🎭 | Performing Arts | Arts | U+1F3AD | Theater masks | Dramatic | 64 | 2010 |
|
||||
| 🐉 | Dragon | Animals | U+1F409 | A mythical dragon | Powerful | 78 | 2010 |
|
||||
| 🎪 | Circus Tent | Events | U+1F3AA | A striped circus tent | Fun | 61 | 2010 |
|
||||
| 🧊 | Ice Cube | Objects | U+1F9CA | A block of ice | Cool | 70 | 2019 |
|
||||
| 🎯 | Bullseye | Symbols | U+1F3AF | A dart hitting target | Focused | 83 | 2010 |
|
||||
| 🐬 | Dolphin | Animals | U+1F42C | A leaping dolphin | Playful | 80 | 2010 |
|
||||
| 🚁 | Helicopter | Travel | U+1F681 | A flying helicopter | Aerial | 63 | 2010 |
|
||||
| 🦊 | Fox Face | Animals | U+1F98A | A clever fox | Sneaky | 79 | 2016 |
|
||||
| 🎲 | Game Die | Objects | U+1F3B2 | A six-sided die | Random | 68 | 2010 |
|
||||
| 🍔 | Hamburger | Food | U+1F354 | A classic burger | Juicy | 91 | 2010 |
|
||||
| 🦋 | Butterfly | Animals | U+1F98B | A colorful butterfly | Graceful | 84 | 2016 |
|
||||
| 🏔️ | Snow-Capped Mountain | Travel | U+1F3D4 | A snowy mountain peak | Adventurous | 71 | 2010 |
|
||||
| 🎩 | Top Hat | Objects | U+1F3A9 | A formal top hat | Classy | 62 | 2010 |
|
||||
| 🐳 | Spouting Whale | Animals | U+1F433 | A whale spraying water | Majestic | 76 | 2010 |
|
||||
| 🚀 | Rocket | Travel | U+1F680 | A soaring rocket | Exciting | 88 | 2010 |
|
||||
| 🎹 | Musical Keyboard | Music | U+1F3B9 | A piano keyboard | Melodic | 66 | 2010 |
|
||||
| 🍉 | Watermelon | Food | U+1F349 | A slice of watermelon | Refreshing | 81 | 2010 |
|
||||
| 🧩 | Puzzle Piece | Objects | U+1F9E9 | A jigsaw puzzle piece | Challenging | 75 | 2018 |
|
||||
| 🦀 | Crab | Animals | U+1F980 | A red crab | Feisty | 68 | 2015 |
|
||||
| 🏄 | Surfing | Sports | U+1F3C4 | Someone riding a wave | Gnarly | 65 | 2010 |
|
||||
| 🧑🎤 | Singer | People | U+1F9D1 U+200D U+1F3A4 | A stage performer | Talented | 78 | 2020 |
|
||||
| 🌵 | Cactus | Nature | U+1F335 | A desert cactus | Tough | 73 | 2010 |
|
||||
| 🧲 | Magnet | Objects | U+1F9F2 | A horseshoe magnet | Attractive | 58 | 2018 |
|
||||
| 🦆 | Duck | Animals | U+1F986 | A yellow duck | Quirky | 74 | 2016 |
|
||||
| 🚇 | Metro | Travel | U+1F687 | A subway train | Urban | 59 | 2010 |
|
||||
| 🧾 | Receipt | Objects | U+1F9FE | A paper receipt | Transactional | 56 | 2018 |
|
||||
| 🫘 | Beans | Food | U+1FAD8 | A bowl of beans | Nutritious | 42 | 2021 |
|
||||
| 🦕 | Sauropod | Animals | U+1F995 | A long-neck dinosaur | Prehistoric | 72 | 2016 |
|
||||
| 🏵️ | Rosette | Awards | U+1F3F5 | A decorative rosette | Honorable | 51 | 2010 |
|
||||
| 🦮 | Guide Dog | Animals | U+1F9AE | A seeing-eye dog | Helpful | 67 | 2019 |
|
||||
| 🦴 | Bone | Body | U+1F9B4 | A dog bone | Gnawing | 60 | 2019 |
|
||||
| 🥏 | Flying Disc | Sports | U+1F94F | A frisbee | Airborne | 48 | 2017 |
|
||||
| 🦧 | Orangutan | Animals | U+1F9A7 | A red-haired ape | Wise | 55 | 2019 |
|
||||
| 🥭 | Mango | Food | U+1F96D | A ripe tropical fruit | Tropical | 71 | 2018 |
|
||||
| 🧋 | Bubble Tea | Food | U+1F9CB | Tapioca pearl drink | Trendy | 78 | 2020 |
|
||||
| 🪀 | Yo-Yo | Objects | U+1FA80 | A string toy | Nostalgic | 44 | 2020 |
|
||||
| 🦤 | Dodo | Animals | U+1F9A4 | An extinct flightless bird | Extinct | 53 | 2019 |
|
||||
| 🪐 | Ringed Planet | Space | U+1FA90 | Saturn with rings | Astronomical | 77 | 2020 |
|
||||
| 🛹 | Skateboard | Sports | U+1F6F9 | A kick push skateboard | Rad | 67 | 2019 |
|
||||
| 🥲 | Smiling Face with Tear | Smileys | U+1F972 | Happy but crying inside | Bittersweet | 90 | 2020 |
|
||||
| 🫠 | Melting Face | Smileys | U+1FAE0 | Face slowly melting | Uncomfortable | 69 | 2021 |
|
||||
| 🪩 | Mirror Ball | Objects | U+1FAA9 | A disco ball | Groovy | 73 | 2022 |
|
||||
| 🪭 | Folding Hand Fan | Objects | U+1FAAF | A collapsible hand fan | Elegant | 38 | 2023 |
|
||||
| 🫎 | Moose | Animals | U+1FACE | A large antlered mammal | Majestic | 45 | 2023 |
|
||||
| 🪸 | Coral | Nature | U+1FAB8 | A coral reef formation | Vibrant | 52 | 2023 |
|
||||
| 🪼 | Jellyfish | Animals | U+1FABC | A floating gelatinous sea creature | Ethereal | 49 | 2023 |
|
||||
| 🫏 | Donkey | Animals | U+1FACF | A gray donkey | Stubborn | 41 | 2023 |
|
||||
| 🫅 | Person with Crown | People | U+1FAC5 | A crowned royal figure | Regal | 58 | 2022 |
|
||||
| 🪇 | Maracas | Music | U+1FA87 | A handheld percussion shaker | Rhythmic | 43 | 2023 |
|
||||
| 🫘 | Beans | Food | U+1FAD8 | A bowl of legumes | Hearty | 42 | 2021 |
|
||||
| 🦪 | Oyster | Food | U+1F9AA | A closed shellfish | Briny | 54 | 2019 |
|
||||
| 🪨 | Rock | Nature | U+1FAA8 | A grey stone | Solid | 47 | 2020 |
|
||||
| 🥸 | Disguised Face | Smileys | U+1F978 | Face with fake nose/glasses | Sneaky | 66 | 2021 |
|
||||
| 🦬 | Bison | Animals | U+1F9AC | A large North American bovine | Sturdy | 50 | 2019 |
|
||||
| 🥟 | Dumpling | Food | U+1F95F | A folded filled dough | Steamy | 76 | 2018 |
|
||||
| 🪴 | Potted Plant | Nature | U+1FAB4 | A houseplant in a pot | Green | 68 | 2020 |
|
||||
| 🛼 | Roller Skate | Sports | U+1F6FC | A quad-wheeled skate | Retro | 56 | 2021 |
|
||||
| 🥏 | Flying Disc | Sports | U+1F94F | A plastic throwing disc | Soaring | 48 | 2017 |
|
||||
| 🧃 | Beverage Box | Food | U+1F9C3 | A juice box | Convenient | 63 | 2018 |
|
||||
| 🦚 | Peacock | Animals | U+1F99A | A bird with iridescent tail | Proud | 75 | 2018 |
|
||||
| 🫧 | Bubbles | Nature | U+1FAE7 | Floating iridescent spheres | Light | 57 | 2023 |
|
||||
| 🥅 | Goal Net | Sports | U+1F945 | A soccer/hockey goal | Scoring | 44 | 2017 |
|
||||
| 🦺 | Safety Vest | Objects | U+1F9BA | A high-visibility vest | Protected | 51 | 2019 |
|
||||
| ✈️ | Airplane | Travel | U+2708 | A flying commercial jet | Traveling | 94 | 2010 |
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { randomUUID } from 'node:crypto'
|
||||
import { rmSync, writeFileSync } from 'node:fs'
|
||||
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import type { Page, TestInfo } from '@stablyai/playwright-test'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
|
|
@ -47,6 +47,11 @@ type LongTableDebugWindow = Window & {
|
|||
}
|
||||
}
|
||||
|
||||
const EMOJI_TABLE_FIXTURE = readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'terminal-emoji-table.md'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
function longMarkdownTableScript(runId: string): string {
|
||||
const names = [
|
||||
['Sam Syntax', 'Compiler', 'Online', '😀', '9200', 'Semicolons are optional (rage ensues)'],
|
||||
|
|
@ -129,6 +134,177 @@ const timer = setInterval(() => {
|
|||
`
|
||||
}
|
||||
|
||||
function emojiFixtureMarkdownTableScript(table: string, runId: string): string {
|
||||
const marker = `EMOJI_FIXTURE_TABLE_RESTORE_${runId}`
|
||||
return `
|
||||
const table = ${JSON.stringify(table)}
|
||||
const minimumWidths = [2, 5, 4, 7, 7, 4, 3, 4]
|
||||
const preferredWidths = [5, 17, 10, 18, 30, 12, 10, 10]
|
||||
const tableOverhead = preferredWidths.length * 3 + 1
|
||||
const widthBudget = Math.max(
|
||||
minimumWidths.reduce((sum, width) => sum + width, 0),
|
||||
Math.min(
|
||||
preferredWidths.reduce((sum, width) => sum + width, 0),
|
||||
(process.stdout.columns || 100) - tableOverhead - 1
|
||||
)
|
||||
)
|
||||
let remaining = widthBudget
|
||||
let remainingPreferred = preferredWidths.reduce((sum, width) => sum + width, 0)
|
||||
const widths = preferredWidths.map((preferred, index) => {
|
||||
const minimum = minimumWidths[index]
|
||||
const width = Math.max(minimum, Math.floor((remaining * preferred) / remainingPreferred))
|
||||
remaining -= width
|
||||
remainingPreferred -= preferred
|
||||
return width
|
||||
})
|
||||
const border = {
|
||||
top: ['┌', '┬', '┐'],
|
||||
middle: ['├', '┼', '┤'],
|
||||
bottom: ['└', '┴', '┘'],
|
||||
vertical: '│',
|
||||
horizontal: '─'
|
||||
}
|
||||
function splitMarkdownRow(row) {
|
||||
return row.trim().slice(1, -1).split('|').map((cell) => cell.trim())
|
||||
}
|
||||
function isSeparatorRow(row) {
|
||||
return /^\\|(?:\\s*:?-+:?\\s*\\|)+\\s*$/.test(row)
|
||||
}
|
||||
function cellWidth(text) {
|
||||
let width = 0
|
||||
for (const char of String(text)) {
|
||||
const codePoint = char.codePointAt(0)
|
||||
if (codePoint === undefined || (codePoint >= 0x0300 && codePoint <= 0x036f)) continue
|
||||
if (codePoint === 0xfe0f || codePoint === 0x200d) continue
|
||||
width += codePoint > 0xffff || (codePoint >= 0x1100 && codePoint <= 0x115f) ? 2 : 1
|
||||
}
|
||||
return width
|
||||
}
|
||||
function padCell(value, width) {
|
||||
const text = String(value)
|
||||
return text + ' '.repeat(Math.max(0, width - cellWidth(text)))
|
||||
}
|
||||
function splitToWidth(text, width) {
|
||||
const parts = []
|
||||
let line = ''
|
||||
for (const char of String(text)) {
|
||||
const next = line + char
|
||||
if (line && cellWidth(next) > width) {
|
||||
parts.push(line)
|
||||
line = char
|
||||
} else {
|
||||
line = next
|
||||
}
|
||||
}
|
||||
if (line) parts.push(line)
|
||||
return parts
|
||||
}
|
||||
function wrapCell(value, width) {
|
||||
const words = String(value).split(/\\s+/)
|
||||
const lines = []
|
||||
let line = ''
|
||||
for (const word of words) {
|
||||
if (cellWidth(word) > width) {
|
||||
if (line) {
|
||||
lines.push(line)
|
||||
line = ''
|
||||
}
|
||||
lines.push(...splitToWidth(word, width))
|
||||
continue
|
||||
}
|
||||
const next = line ? line + ' ' + word : word
|
||||
if (line && cellWidth(next) > width) {
|
||||
lines.push(line)
|
||||
line = word
|
||||
} else {
|
||||
line = next
|
||||
}
|
||||
}
|
||||
if (line) lines.push(line)
|
||||
return lines.length ? lines : ['']
|
||||
}
|
||||
function rule(parts) {
|
||||
return parts[0] + widths.map((width) => border.horizontal.repeat(width + 2)).join(parts[1]) + parts[2]
|
||||
}
|
||||
function renderRow(cells) {
|
||||
const wrappedCells = widths.map((width, index) => wrapCell(cells[index] ?? '', width))
|
||||
const height = Math.max(...wrappedCells.map((cell) => cell.length))
|
||||
const rows = []
|
||||
for (let line = 0; line < height; line += 1) {
|
||||
rows.push(
|
||||
border.vertical +
|
||||
widths
|
||||
.map((width, index) => ' ' + padCell(wrappedCells[index][line] ?? '', width) + ' ')
|
||||
.join(border.vertical) +
|
||||
border.vertical
|
||||
)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
const parsedRows = table
|
||||
.split(/\\r?\\n/)
|
||||
.filter((row) => row.trim().startsWith('|') && !isSeparatorRow(row))
|
||||
.map(splitMarkdownRow)
|
||||
const rendered = [rule(border.top)]
|
||||
for (const [index, row] of parsedRows.entries()) {
|
||||
rendered.push(...renderRow(row))
|
||||
rendered.push(rule(index === parsedRows.length - 1 ? border.bottom : border.middle))
|
||||
}
|
||||
process.stdout.write('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
process.stdout.write(rendered.join('\\r\\n'))
|
||||
process.stdout.write('\\r\\n')
|
||||
process.stdout.write('\\r\\n${marker}\\r\\n')
|
||||
process.stdout.write('\\x1b[?2026l')
|
||||
`
|
||||
}
|
||||
|
||||
function narrowSignerMarkdownTableScript(runId: string): string {
|
||||
const marker = `NARROW_SIGNER_TABLE_RESTORE_${runId}`
|
||||
const rows = [
|
||||
'| # | Status | Signer | Action |',
|
||||
'| ---: | --- | --- | --- |',
|
||||
'| 1 | signed | did:key:z6Mkuw5kQqz1QvZ9f3d2aB7f19f0cAC7B4F3c9E725aD19cD12e6A8B3F4c5D6e7F8a9B0c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c9D0e1F2 | approve deployment |',
|
||||
'| 2 | waiting | did:web:example.signing.service:teams:release:prod:primary-key-2026-06-08-with-extra-qualifiers-and-long-human-readable-suffix | counter-sign |',
|
||||
'| 3 | signed | 0x742d35Cc6634C0532925a3b844Bc454e4438f44e9E8F12A7C4D9B6530F9D2C8E7A6B5C4D3E2F1A0B998877665544332211 | archive receipt |'
|
||||
]
|
||||
const repeatedRows = Array.from({ length: 8 }, (_, index) =>
|
||||
rows.concat(`| ${index + 4} | signed | signer-row-${index}-${'a'.repeat(96)} | verify |`)
|
||||
).flat()
|
||||
return `
|
||||
const rows = ${JSON.stringify(repeatedRows)}
|
||||
process.stdout.write('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
for (const row of rows) {
|
||||
process.stdout.write(row + '\\r\\n')
|
||||
}
|
||||
process.stdout.write('${marker}\\r\\n')
|
||||
process.stdout.write('\\x1b[?2026l')
|
||||
`
|
||||
}
|
||||
|
||||
async function setNarrowTerminalViewport(page: Page): Promise<void> {
|
||||
await page.setViewportSize({ width: 920, height: 820 })
|
||||
await page.waitForTimeout(250)
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
if (store?.getState().rightSidebarOpen) {
|
||||
store.setState({ rightSidebarOpen: false })
|
||||
}
|
||||
})
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
|
||||
async function setRenderedTableViewport(page: Page): Promise<void> {
|
||||
await page.setViewportSize({ width: 1180, height: 820 })
|
||||
await page.waitForTimeout(250)
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
if (store?.getState().rightSidebarOpen) {
|
||||
store.setState({ rightSidebarOpen: false })
|
||||
}
|
||||
})
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
|
||||
async function scrollActiveTerminalLikeUser(page: Page): Promise<void> {
|
||||
const target = await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
|
|
@ -164,6 +340,185 @@ async function scrollActiveTerminalLikeUser(page: Page): Promise<void> {
|
|||
await page.waitForTimeout(250)
|
||||
}
|
||||
|
||||
async function scrollActiveTerminalToText(page: Page, text: string): Promise<void> {
|
||||
const target = await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
const state = store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
pane.terminal.focus()
|
||||
pane.terminal.scrollToBottom()
|
||||
const viewport =
|
||||
pane.container.querySelector<HTMLElement>('.xterm-viewport') ??
|
||||
pane.container.querySelector<HTMLElement>('.xterm')
|
||||
if (!viewport) {
|
||||
throw new Error('Active terminal viewport unavailable')
|
||||
}
|
||||
const rect = viewport.getBoundingClientRect()
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
}
|
||||
})
|
||||
await page.mouse.move(target.x, target.y)
|
||||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||||
if ((await readActiveTerminalVisibleText(page)).includes(text)) {
|
||||
return
|
||||
}
|
||||
await page.mouse.wheel(0, -600)
|
||||
await page.waitForTimeout(50)
|
||||
}
|
||||
throw new Error(`Text not visible after scrolling terminal: ${text}`)
|
||||
}
|
||||
|
||||
async function readActiveTerminalVisibleText(page: Page): Promise<string> {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__store
|
||||
const state = store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
const buffer = pane.terminal.buffer.active
|
||||
return Array.from({ length: pane.terminal.rows }, (_, row) => {
|
||||
const line = buffer.getLine(buffer.viewportY + row)
|
||||
return line?.translateToString(true) ?? ''
|
||||
}).join('\n')
|
||||
})
|
||||
}
|
||||
|
||||
async function forceDarkTerminalRendererPath(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('window.__store unavailable')
|
||||
}
|
||||
const state = store.getState()
|
||||
store.setState({
|
||||
settings: {
|
||||
...state.settings!,
|
||||
terminalGpuAcceleration: 'auto',
|
||||
theme: 'dark'
|
||||
}
|
||||
})
|
||||
})
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
|
||||
async function readTerminalRightEdgeOverpaint(page: Page): Promise<{
|
||||
screenRight: number
|
||||
offenderCount: number
|
||||
offenders: { text: string; right: number; width: number }[]
|
||||
}> {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__store
|
||||
const state = store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
const screen = pane?.container.querySelector<HTMLElement>('.xterm-screen')
|
||||
const rows = pane?.container.querySelector<HTMLElement>('.xterm-rows')
|
||||
if (!pane || !screen || !rows) {
|
||||
throw new Error('Active terminal DOM unavailable')
|
||||
}
|
||||
|
||||
const screenRect = screen.getBoundingClientRect()
|
||||
const cellWidth = pane.terminal._core?._renderService?.dimensions?.css?.cell?.width ?? 0
|
||||
const maxRight = screenRect.right + Math.max(1, cellWidth * 0.5)
|
||||
const offenders = Array.from(rows.querySelectorAll<HTMLElement>('span'))
|
||||
.map((span) => {
|
||||
const rect = span.getBoundingClientRect()
|
||||
return {
|
||||
text: span.textContent ?? '',
|
||||
right: rect.right,
|
||||
width: rect.width
|
||||
}
|
||||
})
|
||||
.filter((span) => span.width > 0 && span.right > maxRight)
|
||||
.slice(0, 12)
|
||||
|
||||
return {
|
||||
screenRight: screenRect.right,
|
||||
offenderCount: offenders.length,
|
||||
offenders
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function readTerminalBoxTableWrapDiagnostics(page: Page): Promise<{
|
||||
cols: number
|
||||
rows: number
|
||||
baseY: number
|
||||
viewportY: number
|
||||
wrappedBoxLines: { index: number; text: string }[]
|
||||
nearSinger: { index: number; isWrapped: boolean; text: string }[]
|
||||
}> {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__store
|
||||
const state = store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
const buffer = pane.terminal.buffer.active
|
||||
const lineCount = buffer.baseY + buffer.length
|
||||
const lines = Array.from({ length: lineCount }, (_, index) => {
|
||||
const line = buffer.getLine(index)
|
||||
return {
|
||||
index,
|
||||
isWrapped: line?.isWrapped === true,
|
||||
text: line?.translateToString(true) ?? ''
|
||||
}
|
||||
})
|
||||
const wrappedBoxLines = lines
|
||||
.filter((line) => line.isWrapped && /[┌┬┐├┼┤└┴┘│─]/.test(line.text))
|
||||
.slice(0, 20)
|
||||
const singerIndex = lines.findIndex((line) => line.text.includes('Singer'))
|
||||
const nearSinger =
|
||||
singerIndex === -1 ? [] : lines.slice(Math.max(0, singerIndex - 4), singerIndex + 7)
|
||||
return {
|
||||
cols: pane.terminal.cols,
|
||||
rows: pane.terminal.rows,
|
||||
baseY: buffer.baseY,
|
||||
viewportY: buffer.viewportY,
|
||||
wrappedBoxLines,
|
||||
nearSinger
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function closeFeatureTips(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
|
|
@ -302,4 +657,162 @@ test.describe('Terminal long table scroll restore repro', () => {
|
|||
rmSync(scriptPath, { force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('keeps narrow wrapped signer markdown table coherent after restore and scroll', async ({
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}, testInfo: TestInfo) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await orcaPage.evaluate(() => {
|
||||
window.__store
|
||||
?.getState()
|
||||
.markFeatureTipsSeen(['orca-cli', 'cmd-j-palette', 'voice-dictation'])
|
||||
;(window as LongTableDebugWindow).__terminalPtyOutputDebug?.reset()
|
||||
})
|
||||
const firstWorktreeId = await waitForActiveWorktree(orcaPage)
|
||||
const secondWorktreeId = (await getAllWorktreeIds(orcaPage)).find(
|
||||
(id) => id !== firstWorktreeId
|
||||
)
|
||||
test.skip(!secondWorktreeId, 'narrow signer table repro needs the seeded secondary worktree')
|
||||
if (!secondWorktreeId) {
|
||||
return
|
||||
}
|
||||
|
||||
await setRenderedTableViewport(orcaPage)
|
||||
await forceDarkTerminalRendererPath(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||||
const runId = randomUUID()
|
||||
const marker = `NARROW_SIGNER_TABLE_RESTORE_${runId}`
|
||||
const scriptPath = path.join(testRepoPath, `.orca-narrow-signer-table-${runId}.mjs`)
|
||||
writeFileSync(scriptPath, narrowSignerMarkdownTableScript(runId))
|
||||
|
||||
try {
|
||||
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
|
||||
await orcaPage.waitForTimeout(80)
|
||||
await switchToWorktree(orcaPage, secondWorktreeId)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await orcaPage.waitForTimeout(1_000)
|
||||
await switchToWorktree(orcaPage, firstWorktreeId)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await expect
|
||||
.poll(() => getTerminalContent(orcaPage, 30_000), {
|
||||
timeout: 10_000,
|
||||
message: 'narrow signer table marker did not survive workspace switch'
|
||||
})
|
||||
.toContain(marker)
|
||||
|
||||
await scrollActiveTerminalLikeUser(orcaPage)
|
||||
await closeFeatureTips(orcaPage)
|
||||
const diagnostics = await readTerminalRenderDiagnostics(orcaPage)
|
||||
const hiddenDebug = await orcaPage.evaluate(() =>
|
||||
(window as LongTableDebugWindow).__terminalPtyOutputDebug?.snapshot()
|
||||
)
|
||||
expect(hiddenDebug?.hiddenRendererSkipCount).toBe(0)
|
||||
expect(diagnostics.cols).toBeLessThanOrEqual(110)
|
||||
expect(diagnostics.hasWebgl).toBe(false)
|
||||
expect(diagnostics.cursorHidden).toBe(false)
|
||||
|
||||
const content = await getTerminalContent(orcaPage, 30_000)
|
||||
expect(content).toContain('Signer')
|
||||
expect(content).toContain('did:key:z6Mkuw5kQqz1QvZ9f3d2aB7f19f0cAC7B4F3c9E725')
|
||||
expect(content).toContain(marker)
|
||||
|
||||
const screenshotPath = testInfo.outputPath('narrow-signer-table-after-switch-scroll.png')
|
||||
await orcaPage.screenshot({ path: screenshotPath, fullPage: true })
|
||||
await testInfo.attach('narrow-signer-table-after-switch-scroll.png', {
|
||||
path: screenshotPath,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
} finally {
|
||||
rmSync(scriptPath, { force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('keeps real emoji markdown table right edge clean after restore and scroll', async ({
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}, testInfo: TestInfo) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await orcaPage.evaluate(() => {
|
||||
window.__store
|
||||
?.getState()
|
||||
.markFeatureTipsSeen(['orca-cli', 'cmd-j-palette', 'voice-dictation'])
|
||||
;(window as LongTableDebugWindow).__terminalPtyOutputDebug?.reset()
|
||||
})
|
||||
const firstWorktreeId = await waitForActiveWorktree(orcaPage)
|
||||
const secondWorktreeId = (await getAllWorktreeIds(orcaPage)).find(
|
||||
(id) => id !== firstWorktreeId
|
||||
)
|
||||
test.skip(!secondWorktreeId, 'real emoji table repro needs the seeded secondary worktree')
|
||||
if (!secondWorktreeId) {
|
||||
return
|
||||
}
|
||||
|
||||
await setNarrowTerminalViewport(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||||
const runId = randomUUID()
|
||||
const marker = `EMOJI_FIXTURE_TABLE_RESTORE_${runId}`
|
||||
const scriptPath = path.join(testRepoPath, `.orca-emoji-fixture-table-${runId}.mjs`)
|
||||
writeFileSync(scriptPath, emojiFixtureMarkdownTableScript(EMOJI_TABLE_FIXTURE, runId))
|
||||
|
||||
try {
|
||||
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
|
||||
await orcaPage.waitForTimeout(80)
|
||||
await switchToWorktree(orcaPage, secondWorktreeId)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await orcaPage.waitForTimeout(1_000)
|
||||
await switchToWorktree(orcaPage, firstWorktreeId)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await expect
|
||||
.poll(() => getTerminalContent(orcaPage, 30_000), {
|
||||
timeout: 10_000,
|
||||
message: 'real emoji table marker did not survive workspace switch'
|
||||
})
|
||||
.toContain(marker)
|
||||
|
||||
await scrollActiveTerminalToText(orcaPage, 'Singer')
|
||||
await closeFeatureTips(orcaPage)
|
||||
await expect
|
||||
.poll(() => readActiveTerminalVisibleText(orcaPage), {
|
||||
timeout: 5_000,
|
||||
message: 'Singer row should be visible before screenshot'
|
||||
})
|
||||
.toContain('Singer')
|
||||
const diagnostics = await readTerminalRenderDiagnostics(orcaPage)
|
||||
const overpaint = await readTerminalRightEdgeOverpaint(orcaPage)
|
||||
const wrapDiagnostics = await readTerminalBoxTableWrapDiagnostics(orcaPage)
|
||||
const hiddenDebug = await orcaPage.evaluate(() =>
|
||||
(window as LongTableDebugWindow).__terminalPtyOutputDebug?.snapshot()
|
||||
)
|
||||
expect(hiddenDebug?.hiddenRendererSkipCount).toBe(0)
|
||||
expect(diagnostics.cols).toBeLessThan(100)
|
||||
expect(diagnostics.hasWebgl).toBe(false)
|
||||
expect(diagnostics.cursorHidden).toBe(false)
|
||||
testInfo.annotations.push({
|
||||
type: 'real-emoji-table-overpaint',
|
||||
description: JSON.stringify(overpaint)
|
||||
})
|
||||
testInfo.annotations.push({
|
||||
type: 'real-emoji-table-wrap-diagnostics',
|
||||
description: JSON.stringify(wrapDiagnostics)
|
||||
})
|
||||
|
||||
const screenshotPath = testInfo.outputPath('real-emoji-table-after-switch-scroll.png')
|
||||
await orcaPage.screenshot({ path: screenshotPath, fullPage: true })
|
||||
await testInfo.attach('real-emoji-table-after-switch-scroll.png', {
|
||||
path: screenshotPath,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
expect(overpaint.offenders).toEqual([])
|
||||
expect(wrapDiagnostics.wrappedBoxLines).toEqual([])
|
||||
} finally {
|
||||
rmSync(scriptPath, { force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -255,12 +255,7 @@ test.describe('OpenCode emoji table terminal rendering', () => {
|
|||
|
||||
const renderState = await readActiveTerminalRenderState(orcaPage)
|
||||
const blinkSamples = await sampleCursorBlink(orcaPage)
|
||||
const screenshotPath = testInfo.outputPath('opencode-emoji-table-final.png')
|
||||
await orcaPage.screenshot({ path: screenshotPath, fullPage: true })
|
||||
await testInfo.attach('opencode-emoji-table-final.png', {
|
||||
path: screenshotPath,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
testInfo.annotations.push({
|
||||
type: 'opencode-emoji-table-rendering',
|
||||
description: JSON.stringify({ renderState, blinkSamples })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,454 @@
|
|||
import { randomUUID } from 'node:crypto'
|
||||
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import type { Page, TestInfo } from '@stablyai/playwright-test'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import {
|
||||
ensureTerminalVisible,
|
||||
getAllWorktreeIds,
|
||||
switchToWorktree,
|
||||
waitForActiveWorktree,
|
||||
waitForSessionReady
|
||||
} from './helpers/store'
|
||||
import {
|
||||
getTerminalContent,
|
||||
sendToTerminal,
|
||||
waitForActivePanePtyId,
|
||||
waitForActiveTerminalManager
|
||||
} from './helpers/terminal'
|
||||
|
||||
type BrowserTerminalPane = {
|
||||
terminal: {
|
||||
cols: number
|
||||
rows: number
|
||||
buffer: {
|
||||
active: {
|
||||
baseY: number
|
||||
length: number
|
||||
viewportY: number
|
||||
getLine: (index: number) =>
|
||||
| {
|
||||
isWrapped?: boolean
|
||||
translateToString: (trimRight?: boolean) => string
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
}
|
||||
focus: () => void
|
||||
scrollToBottom: () => void
|
||||
_core?: {
|
||||
coreService?: { isCursorHidden?: boolean }
|
||||
_renderService?: { dimensions?: { css?: { cell?: { width?: number } } } }
|
||||
}
|
||||
}
|
||||
container: HTMLElement
|
||||
webglAddon?: unknown
|
||||
}
|
||||
|
||||
type RawTableDebugWindow = Window & {
|
||||
getActiveTestPane?: () => BrowserTerminalPane
|
||||
}
|
||||
|
||||
const EMOJI_TABLE_FIXTURE = readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'terminal-emoji-table.md'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
function rawEmojiFixtureBoxTableScript(table: string, runId: string): string {
|
||||
const marker = `RAW_EMOJI_FIXTURE_TABLE_RESTORE_${runId}`
|
||||
return `
|
||||
const table = ${JSON.stringify(table)}
|
||||
const widths = [5, 17, 10, 25, 23, 12, 10, 10]
|
||||
const border = {
|
||||
top: ['┌', '┬', '┐'],
|
||||
middle: ['├', '┼', '┤'],
|
||||
bottom: ['└', '┴', '┘'],
|
||||
vertical: '│',
|
||||
horizontal: '─'
|
||||
}
|
||||
const segmenter =
|
||||
typeof Intl.Segmenter === 'function'
|
||||
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
||||
: null
|
||||
function splitMarkdownRow(row) {
|
||||
return row.trim().slice(1, -1).split('|').map((cell) => cell.trim())
|
||||
}
|
||||
function isSeparatorRow(row) {
|
||||
return /^\\|(?:\\s*:?-+:?\\s*\\|)+\\s*$/.test(row)
|
||||
}
|
||||
function graphemes(text) {
|
||||
if (!segmenter) return Array.from(String(text))
|
||||
return Array.from(segmenter.segment(String(text)), (part) => part.segment)
|
||||
}
|
||||
function isWideCodePoint(codePoint) {
|
||||
return codePoint > 0xffff ||
|
||||
(codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
||||
(codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
|
||||
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
||||
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
||||
(codePoint >= 0xfe10 && codePoint <= 0xfe6f) ||
|
||||
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
||||
(codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
||||
}
|
||||
function cellWidth(text) {
|
||||
let width = 0
|
||||
for (const cluster of graphemes(text)) {
|
||||
if (cluster.includes('\\u200d')) {
|
||||
width += 2
|
||||
continue
|
||||
}
|
||||
const codePoint = cluster.codePointAt(0)
|
||||
if (codePoint === undefined || (codePoint >= 0x0300 && codePoint <= 0x036f)) continue
|
||||
if (codePoint === 0xfe0f || codePoint === 0x200d) continue
|
||||
width += isWideCodePoint(codePoint) ? 2 : 1
|
||||
}
|
||||
return width
|
||||
}
|
||||
function clipCell(value, width) {
|
||||
let text = ''
|
||||
let used = 0
|
||||
for (const cluster of graphemes(value)) {
|
||||
const nextWidth = cellWidth(cluster)
|
||||
if (used + nextWidth > width) break
|
||||
text += cluster
|
||||
used += nextWidth
|
||||
}
|
||||
return text + ' '.repeat(Math.max(0, width - used))
|
||||
}
|
||||
function rule(parts) {
|
||||
return parts[0] + widths.map((width) => border.horizontal.repeat(width + 2)).join(parts[1]) + parts[2]
|
||||
}
|
||||
function renderRow(cells) {
|
||||
return (
|
||||
border.vertical +
|
||||
widths.map((width, index) => ' ' + clipCell(cells[index] ?? '', width) + ' ').join(border.vertical) +
|
||||
border.vertical
|
||||
)
|
||||
}
|
||||
const parsedRows = table
|
||||
.split(/\\r?\\n/)
|
||||
.filter((row) => row.trim().startsWith('|') && !isSeparatorRow(row))
|
||||
.map(splitMarkdownRow)
|
||||
const rendered = [rule(border.top)]
|
||||
for (const [index, row] of parsedRows.entries()) {
|
||||
rendered.push(renderRow(row))
|
||||
rendered.push(rule(index === parsedRows.length - 1 ? border.bottom : border.middle))
|
||||
}
|
||||
process.stdout.write('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
process.stdout.write(rendered.join('\\r\\n'))
|
||||
process.stdout.write('\\r\\n${marker}\\r\\n')
|
||||
process.stdout.write('\\x1b[?2026l')
|
||||
`
|
||||
}
|
||||
|
||||
async function setWideRenderedTableViewport(page: Page): Promise<void> {
|
||||
await page.setViewportSize({ width: 1480, height: 820 })
|
||||
await page.waitForTimeout(250)
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
if (store?.getState().rightSidebarOpen) {
|
||||
store.setState({ rightSidebarOpen: false })
|
||||
}
|
||||
})
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
|
||||
async function scrollActiveTerminalToText(page: Page, text: string): Promise<void> {
|
||||
const target = await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
const state = store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
pane.terminal.focus()
|
||||
pane.terminal.scrollToBottom()
|
||||
const viewport =
|
||||
pane.container.querySelector<HTMLElement>('.xterm-viewport') ??
|
||||
pane.container.querySelector<HTMLElement>('.xterm')
|
||||
if (!viewport) {
|
||||
throw new Error('Active terminal viewport unavailable')
|
||||
}
|
||||
const rect = viewport.getBoundingClientRect()
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
}
|
||||
})
|
||||
await page.mouse.move(target.x, target.y)
|
||||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||||
if ((await readActiveTerminalVisibleText(page)).includes(text)) {
|
||||
return
|
||||
}
|
||||
await page.mouse.wheel(0, -600)
|
||||
await page.waitForTimeout(50)
|
||||
}
|
||||
throw new Error(`Text not visible after scrolling terminal: ${text}`)
|
||||
}
|
||||
|
||||
async function readActiveTerminalVisibleText(page: Page): Promise<string> {
|
||||
return page.evaluate(() => {
|
||||
const pane = (window as RawTableDebugWindow).getActiveTestPane?.()
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
const buffer = pane.terminal.buffer.active
|
||||
return Array.from({ length: pane.terminal.rows }, (_, row) => {
|
||||
const line = buffer.getLine(buffer.viewportY + row)
|
||||
return line?.translateToString(true) ?? ''
|
||||
}).join('\n')
|
||||
})
|
||||
}
|
||||
|
||||
async function readTerminalBoxTableWrapDiagnostics(page: Page): Promise<{
|
||||
cols: number
|
||||
rows: number
|
||||
baseY: number
|
||||
viewportY: number
|
||||
wrappedBoxLines: { index: number; text: string }[]
|
||||
wrappedSingerContinuationLines: { index: number; text: string }[]
|
||||
nearSinger: { index: number; isWrapped: boolean; text: string }[]
|
||||
}> {
|
||||
return page.evaluate(() => {
|
||||
const pane = (window as RawTableDebugWindow).getActiveTestPane?.()
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
const buffer = pane.terminal.buffer.active
|
||||
const lineCount = buffer.baseY + buffer.length
|
||||
const lines = Array.from({ length: lineCount }, (_, index) => {
|
||||
const line = buffer.getLine(index)
|
||||
return {
|
||||
index,
|
||||
isWrapped: line?.isWrapped === true,
|
||||
text: line?.translateToString(true) ?? ''
|
||||
}
|
||||
})
|
||||
const wrappedBoxLines = lines
|
||||
.filter((line) => line.isWrapped && /[┌┬┐├┼┤└┴┘│─]/.test(line.text))
|
||||
.slice(0, 20)
|
||||
const wrappedSingerContinuationLines = lines
|
||||
.filter((line) => line.isWrapped && /U\\+1F3A4|A stage performer|Talented/.test(line.text))
|
||||
.slice(0, 20)
|
||||
const singerIndex = lines.findIndex((line) => line.text.includes('Singer'))
|
||||
const nearSinger =
|
||||
singerIndex === -1 ? [] : lines.slice(Math.max(0, singerIndex - 4), singerIndex + 7)
|
||||
return {
|
||||
cols: pane.terminal.cols,
|
||||
rows: pane.terminal.rows,
|
||||
baseY: buffer.baseY,
|
||||
viewportY: buffer.viewportY,
|
||||
wrappedBoxLines,
|
||||
wrappedSingerContinuationLines,
|
||||
nearSinger
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function readTerminalRightEdgeOverpaint(page: Page): Promise<{
|
||||
screenRight: number
|
||||
offenderCount: number
|
||||
offenders: { text: string; right: number; width: number }[]
|
||||
}> {
|
||||
return page.evaluate(() => {
|
||||
const pane = (window as RawTableDebugWindow).getActiveTestPane?.()
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
const screen = pane.container.querySelector<HTMLElement>('.xterm-screen')
|
||||
const rows = pane.container.querySelector<HTMLElement>('.xterm-rows')
|
||||
if (!screen || !rows) {
|
||||
throw new Error('Active terminal DOM unavailable')
|
||||
}
|
||||
|
||||
const screenRect = screen.getBoundingClientRect()
|
||||
const cellWidth = pane.terminal._core?._renderService?.dimensions?.css?.cell?.width ?? 0
|
||||
const maxRight = screenRect.right + Math.max(1, cellWidth * 0.5)
|
||||
const offenders = Array.from(rows.querySelectorAll<HTMLElement>('span'))
|
||||
.map((span) => {
|
||||
const rect = span.getBoundingClientRect()
|
||||
return {
|
||||
text: span.textContent ?? '',
|
||||
right: rect.right,
|
||||
width: rect.width
|
||||
}
|
||||
})
|
||||
.filter((span) => span.width > 0 && span.right > maxRight)
|
||||
.slice(0, 12)
|
||||
|
||||
return {
|
||||
screenRight: screenRect.right,
|
||||
offenderCount: offenders.length,
|
||||
offenders
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function readVisibleSingerRowGeometry(page: Page): Promise<{
|
||||
cols: number
|
||||
screenRight: number
|
||||
rowRight: number
|
||||
rowText: string
|
||||
}> {
|
||||
return page.evaluate(() => {
|
||||
const pane = (window as RawTableDebugWindow).getActiveTestPane?.()
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
const screen = pane.container.querySelector<HTMLElement>('.xterm-screen')
|
||||
const rows = pane.container.querySelector<HTMLElement>('.xterm-rows')
|
||||
if (!screen || !rows) {
|
||||
throw new Error('Active terminal DOM unavailable')
|
||||
}
|
||||
const row = Array.from(rows.children).find((element) =>
|
||||
(element.textContent ?? '').includes('Singer')
|
||||
) as HTMLElement | undefined
|
||||
if (!row) {
|
||||
throw new Error('Singer row DOM unavailable')
|
||||
}
|
||||
const screenRect = screen.getBoundingClientRect()
|
||||
const rowRect = row.getBoundingClientRect()
|
||||
return {
|
||||
cols: pane.terminal.cols,
|
||||
screenRight: screenRect.right,
|
||||
rowRight: rowRect.right,
|
||||
rowText: row.textContent ?? ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function readTerminalRenderDiagnostics(page: Page): Promise<{
|
||||
hasWebgl: boolean
|
||||
cursorHidden: boolean | null
|
||||
}> {
|
||||
return page.evaluate(() => {
|
||||
const pane = (window as RawTableDebugWindow).getActiveTestPane?.()
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
const terminalCore = pane.terminal._core
|
||||
return {
|
||||
hasWebgl: Boolean(pane.webglAddon),
|
||||
cursorHidden: terminalCore?.coreService?.isCursorHidden ?? null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function closeFeatureTips(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
store?.getState().markFeatureTipsSeen(['orca-cli', 'cmd-j-palette', 'voice-dictation'])
|
||||
if (store?.getState().activeModal === 'feature-tips') {
|
||||
store.getState().closeModal()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Terminal raw emoji table scroll restore repro', () => {
|
||||
test.beforeEach(async ({ orcaPage }) => {
|
||||
await orcaPage.evaluate(() => {
|
||||
;(window as RawTableDebugWindow).getActiveTestPane = () => {
|
||||
const store = window.__store
|
||||
const state = store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
return pane as BrowserTerminalPane
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('keeps raw emoji box table aligned after restore and scroll', async ({
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}, testInfo: TestInfo) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await closeFeatureTips(orcaPage)
|
||||
const firstWorktreeId = await waitForActiveWorktree(orcaPage)
|
||||
const secondWorktreeId = (await getAllWorktreeIds(orcaPage)).find(
|
||||
(id) => id !== firstWorktreeId
|
||||
)
|
||||
test.skip(!secondWorktreeId, 'raw emoji table repro needs the seeded secondary worktree')
|
||||
if (!secondWorktreeId) {
|
||||
return
|
||||
}
|
||||
|
||||
await setWideRenderedTableViewport(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||||
const runId = randomUUID()
|
||||
const marker = `RAW_EMOJI_FIXTURE_TABLE_RESTORE_${runId}`
|
||||
const scriptPath = path.join(testRepoPath, `.orca-raw-emoji-fixture-table-${runId}.mjs`)
|
||||
writeFileSync(scriptPath, rawEmojiFixtureBoxTableScript(EMOJI_TABLE_FIXTURE, runId))
|
||||
|
||||
try {
|
||||
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
|
||||
await orcaPage.waitForTimeout(80)
|
||||
await switchToWorktree(orcaPage, secondWorktreeId)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await orcaPage.waitForTimeout(1_000)
|
||||
await switchToWorktree(orcaPage, firstWorktreeId)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await expect
|
||||
.poll(() => getTerminalContent(orcaPage, 30_000), {
|
||||
timeout: 10_000,
|
||||
message: 'raw emoji table marker did not survive workspace switch'
|
||||
})
|
||||
.toContain(marker)
|
||||
|
||||
await scrollActiveTerminalToText(orcaPage, 'Singer')
|
||||
await closeFeatureTips(orcaPage)
|
||||
const diagnostics = await readTerminalRenderDiagnostics(orcaPage)
|
||||
const overpaint = await readTerminalRightEdgeOverpaint(orcaPage)
|
||||
const wrapDiagnostics = await readTerminalBoxTableWrapDiagnostics(orcaPage)
|
||||
const singerGeometry = await readVisibleSingerRowGeometry(orcaPage)
|
||||
testInfo.annotations.push({
|
||||
type: 'raw-emoji-table-singer-geometry',
|
||||
description: JSON.stringify(singerGeometry)
|
||||
})
|
||||
testInfo.annotations.push({
|
||||
type: 'raw-emoji-table-overpaint',
|
||||
description: JSON.stringify(overpaint)
|
||||
})
|
||||
testInfo.annotations.push({
|
||||
type: 'raw-emoji-table-wrap-diagnostics',
|
||||
description: JSON.stringify(wrapDiagnostics)
|
||||
})
|
||||
|
||||
const screenshotPath = testInfo.outputPath('raw-emoji-table-after-switch-scroll.png')
|
||||
await orcaPage.screenshot({ path: screenshotPath, fullPage: true })
|
||||
await testInfo.attach('raw-emoji-table-after-switch-scroll.png', {
|
||||
path: screenshotPath,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
expect(diagnostics.hasWebgl).toBe(false)
|
||||
expect(diagnostics.cursorHidden).toBe(false)
|
||||
expect(overpaint.offenders).toEqual([])
|
||||
expect(wrapDiagnostics.wrappedBoxLines).toEqual([])
|
||||
expect(wrapDiagnostics.wrappedSingerContinuationLines).toEqual([])
|
||||
expect(singerGeometry.rowRight).toBeLessThanOrEqual(singerGeometry.screenRight + 1)
|
||||
} finally {
|
||||
rmSync(scriptPath, { force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue