From 5d173547ff50a97eb9bdb50609fb76cd641c5aa0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 24 Jul 2026 11:21:19 +0200 Subject: [PATCH 01/14] Upgrade Clerk packages and Expo integration (#4440) --- pnpm-lock.yaml | 139 ++++++++++++++++++-------------------------- pnpm-workspace.yaml | 24 ++++---- 2 files changed, 68 insertions(+), 95 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94248a911c3..bea636d3807 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,19 +35,19 @@ catalogs: version: 0.2.2 overrides: - '@clerk/backend': 3.11.2 - '@clerk/clerk-js': 6.25.1 + '@clerk/backend': 3.13.0 + '@clerk/clerk-js': 6.25.7 '@clerk/clerk-js>@base-org/account': '-' '@clerk/clerk-js>@coinbase/wallet-sdk': '-' '@clerk/clerk-js>@solana/wallet-adapter-base': '-' '@clerk/clerk-js>@solana/wallet-adapter-react': '-' '@clerk/clerk-js>@solana/wallet-standard': '-' '@clerk/clerk-js>@wallet-standard/core': '-' - '@clerk/electron': 0.0.11 + '@clerk/electron': 0.0.18 '@clerk/electron-passkeys': 0.0.3 - '@clerk/expo': 3.7.2 - '@clerk/react': 6.12.1 - '@clerk/shared': 4.25.1 + '@clerk/expo': 4.0.2 + '@clerk/react': 6.12.7 + '@clerk/shared': 4.25.7 '@effect/atom-react': 4.0.0-beta.78 '@effect/platform-bun': 4.0.0-beta.78 '@effect/platform-node': 4.0.0-beta.78 @@ -109,8 +109,8 @@ importers: apps/desktop: dependencies: '@clerk/electron': - specifier: 0.0.11 - version: 0.0.11(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.18 + version: 0.0.18(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron-passkeys': specifier: 0.0.3 version: 0.0.3 @@ -195,8 +195,8 @@ importers: specifier: ^0.7.1 version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@clerk/expo': - specifier: 3.7.2 - version: 3.7.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + specifier: 4.0.2 + version: 4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) '@effect/atom-react': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5))(react@19.2.3)(scheduler@0.27.0) @@ -512,11 +512,11 @@ importers: specifier: ^1.4.1 version: 1.5.0(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron': - specifier: 0.0.11 - version: 0.0.11(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.18 + version: 0.0.18(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/react': - specifier: 6.12.1 - version: 6.12.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 6.12.7 + version: 6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -666,8 +666,8 @@ importers: infra/relay: dependencies: '@clerk/backend': - specifier: 3.11.2 - version: 3.11.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 3.13.0 + version: 3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@effect/sql-pg': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5)) @@ -1690,12 +1690,12 @@ packages: resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} engines: {node: '>= 20.12.0'} - '@clerk/backend@3.11.2': - resolution: {integrity: sha512-HaHAnKzThRpdRi+QJkMo3+Cx/3hJUt+UdbQ62Ikzwwt0zIhUE0+qgG+zgUHjAdYhSbfiUzEWYsLdnBsfQ7yv7g==} + '@clerk/backend@3.13.0': + resolution: {integrity: sha512-kYOgeR48iXYypudpYITE0L2yxi4TeFXU+ISAOULlVgOcurw8qnm1/JEuK6Mr2IFUbRS9hcHk1HVO0Ceo0XmL8Q==} engines: {node: '>=20.9.0'} - '@clerk/clerk-js@6.25.1': - resolution: {integrity: sha512-7SXj7ACQSiqg72768cMHDGfNsdmpadKPvViV0b6ozqNbRjYrGkDM74fEf8i0Mg/0nGk5zpYI5fNKgQavpg/iMg==} + '@clerk/clerk-js@6.25.7': + resolution: {integrity: sha512-GOnbEUzPKAdsdCodZsHhku2YloBBuwLGx5UP0VeezUj73wKLwlSYMlVPuntsJoY7c8+lVrMCY9LQWYVZ7VMLBw==} engines: {node: '>=20.9.0'} '@clerk/electron-passkeys-darwin-arm64@0.0.3': @@ -1722,8 +1722,8 @@ packages: resolution: {integrity: sha512-OHhIe88qDL+FxyBalXdXNHAS5eEramr6Rerp+6iNkfkjqT8rx4hHNmfpmjg5/T1/am8QfknbOBZkqoXZlCrjPg==} engines: {node: '>=20.9.0'} - '@clerk/electron@0.0.11': - resolution: {integrity: sha512-QQzSYKwjrd447/kLiObkLQj+J6AyFr5MmPsg7k4TYXR2C4AAlpwkTnCV1ziRHa8V+Miq2MzQhcL6/2DdS7y9eA==} + '@clerk/electron@0.0.18': + resolution: {integrity: sha512-0lWiITlTxCviglPfPSu+uhERnQyXroiWrSMRpZcEO3FgBt3bnrpfgZUrXW8iaLoUYJrFP2byF7bHJPRwEPVXEw==} engines: {node: '>=20.9.0'} peerDependencies: '@clerk/electron-passkeys': 0.0.3 @@ -1739,12 +1739,13 @@ packages: react-dom: optional: true - '@clerk/expo@3.7.2': - resolution: {integrity: sha512-wMTnieTEsTgJ3mJLZaiH+0uLaOp93lUsZLtmS0BQyK32xK49Y28ArFt7QkmnUB39EKT4MZve1XDctxZtCFmvVA==} + '@clerk/expo@4.0.2': + resolution: {integrity: sha512-afBdV2bLRM55p5L7FoAGV/wCamLxXusjIBPmNo+TwAYabU0oUokMsm+0++xWDA0nxzMt/O08Oj+xAGEtE+Qvxg==} engines: {node: '>=20.9.0'} peerDependencies: + '@clerk/expo-google-signin': '>=0.1.0' '@clerk/expo-passkeys': '>=0.0.6' - expo: '>=53 <58' + expo: '>=54 <58' expo-apple-authentication: '>=7.0.0' expo-auth-session: '>=5' expo-constants: '>=12' @@ -1756,6 +1757,8 @@ packages: react-dom: ^18.0.0 || ^19.0.0 react-native: '>=0.75' peerDependenciesMeta: + '@clerk/expo-google-signin': + optional: true '@clerk/expo-passkeys': optional: true expo-apple-authentication: @@ -1775,15 +1778,15 @@ packages: react-dom: optional: true - '@clerk/react@6.12.1': - resolution: {integrity: sha512-FFIv0SUQh9R8lBOCpHfAC5ki+FjU0WlvBrujJUPQgBTJX7uaRUlbzKswB/ZfRlauGKnE9c80H5dnir08cEtLgw==} + '@clerk/react@6.12.7': + resolution: {integrity: sha512-K8CK0tS7My/3RT5sAIJTjYgnH0xU+K4Jdnhi76xQdAY4agD/M07BbrBMWXEE0xUEDst2PqPfX660V1LRLOsxOQ==} engines: {node: '>=20.9.0'} peerDependencies: react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 - '@clerk/shared@4.25.1': - resolution: {integrity: sha512-+FHoFEJ8zGenUymf03gARKEAYOLxOKm6B437tgukxP7oM5ORfI+ZND62W25S8ddVsDLlNTS0VEHlhDm3F+NF0A==} + '@clerk/shared@4.25.7': + resolution: {integrity: sha512-FfBbDeFkxwDuz/2YcVs4DhyZ0VxnqNtdbO0Tn/lkLAiDvFFspWw+PDYqo7LQiBymEQaOQkb094dX21WWnZbmlA==} engines: {node: '>=20.9.0'} peerDependencies: react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 @@ -5677,9 +5680,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bufferutil@4.1.0: resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} engines: {node: '>=6.14.2'} @@ -7223,9 +7223,6 @@ packages: idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -8956,8 +8953,8 @@ packages: react: '*' react-native: '*' - react-native-url-polyfill@2.0.0: - resolution: {integrity: sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==} + react-native-url-polyfill@4.0.0: + resolution: {integrity: sha512-eqYM3wBAA0eL1sPYbBAoNfbES3+NkgcxUdelQ7QzmoVtqKB5qGG0U13MPTRUroAWK+y2EoJFS3MZUK0fwTf0pA==} peerDependencies: react-native: '*' @@ -10187,10 +10184,6 @@ packages: webcrypto-core@1.9.2: resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==} - webidl-conversions@5.0.0: - resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} - engines: {node: '>=8'} - webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -10200,10 +10193,6 @@ packages: whatwg-url-minimum@0.1.2: resolution: {integrity: sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==} - whatwg-url-without-unicode@8.0.0-3: - resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==} - engines: {node: '>=10'} - which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -11412,18 +11401,18 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clerk/backend@3.11.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/backend@3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.25.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) standardwebhooks: 1.0.0 tslib: 2.8.1 transitivePeerDependencies: - react - react-dom - '@clerk/clerk-js@6.25.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/clerk-js@6.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@clerk/shared': 4.25.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@stripe/stripe-js': 5.6.0 '@swc/helpers': 0.5.21 '@tanstack/query-core': 5.100.14 @@ -11438,9 +11427,9 @@ snapshots: - react - react-dom - '@clerk/clerk-js@6.25.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/clerk-js@6.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.25.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@stripe/stripe-js': 5.6.0 '@swc/helpers': 0.5.21 '@tanstack/query-core': 5.100.14 @@ -11474,11 +11463,11 @@ snapshots: '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.3 '@clerk/electron-passkeys-win32-x64-msvc': 0.0.3 - '@clerk/electron@0.0.11(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/electron@0.0.18(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/clerk-js': 6.25.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/react': 6.12.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/shared': 4.25.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/clerk-js': 6.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/react': 6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) electron: 41.5.0 react: 19.2.6 tslib: 2.8.1 @@ -11487,17 +11476,17 @@ snapshots: electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) - '@clerk/expo@3.7.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)': + '@clerk/expo@4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)': dependencies: - '@clerk/clerk-js': 6.25.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@clerk/react': 6.12.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@clerk/shared': 4.25.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/clerk-js': 6.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/react': 6.12.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@expo/config-plugins': 56.0.9(typescript@6.0.3) base-64: 1.0.0 expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + react-native-url-polyfill: 4.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) tslib: 2.8.1 optionalDependencies: expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -11510,21 +11499,21 @@ snapshots: - supports-color - typescript - '@clerk/react@6.12.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/react@6.12.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@clerk/shared': 4.25.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 - '@clerk/react@6.12.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/react@6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.25.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) tslib: 2.8.1 - '@clerk/shared@4.25.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/shared@4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/query-core': 5.100.14 dequal: 2.0.3 @@ -11534,7 +11523,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@clerk/shared@4.25.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/shared@4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/query-core': 5.100.14 dequal: 2.0.3 @@ -15859,11 +15848,6 @@ snapshots: buffer-from@1.1.2: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bufferutil@4.1.0: dependencies: node-gyp-build: 4.8.4 @@ -17806,8 +17790,6 @@ snapshots: idb-keyval@6.2.1: optional: true - ieee754@1.2.1: {} - ignore@5.3.2: {} ignore@7.0.5: {} @@ -19792,10 +19774,9 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) warn-once: 0.1.1 - react-native-url-polyfill@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + react-native-url-polyfill@4.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - whatwg-url-without-unicode: 8.0.0-3 react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: @@ -21362,20 +21343,12 @@ snapshots: asn1js: 3.0.10 tslib: 2.8.1 - webidl-conversions@5.0.0: {} - webpack-virtual-modules@0.6.2: {} whatwg-fetch@3.6.20: {} whatwg-url-minimum@0.1.2: {} - whatwg-url-without-unicode@8.0.0-3: - dependencies: - buffer: 5.7.1 - punycode: 2.3.1 - webidl-conversions: 5.0.0 - which-pm-runs@1.1.0: {} which@2.0.2: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 22757702fc8..ff840659efc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,13 +23,13 @@ allowBuilds: workerd: false catalog: - "@clerk/backend": 3.11.2 - "@clerk/clerk-js": 6.25.1 - "@clerk/electron": 0.0.11 + "@clerk/backend": 3.13.0 + "@clerk/clerk-js": 6.25.7 + "@clerk/electron": 0.0.18 "@clerk/electron-passkeys": 0.0.3 - "@clerk/expo": 3.7.2 - "@clerk/react": 6.12.1 - "@clerk/shared": 4.25.1 + "@clerk/expo": 4.0.2 + "@clerk/react": 6.12.7 + "@clerk/shared": 4.25.7 "@effect/atom-react": 4.0.0-beta.78 "@effect/openapi-generator": 4.0.0-beta.78 "@effect/platform-bun": 4.0.0-beta.78 @@ -52,12 +52,12 @@ catalog: yaml: ^2.9.0 minimumReleaseAgeExclude: - - "@clerk/backend@3.11.2" - - "@clerk/clerk-js@6.25.1" - - "@clerk/electron@0.0.11" - - "@clerk/expo@3.7.2" - - "@clerk/react@6.12.1" - - "@clerk/shared@4.25.1" + - "@clerk/backend@3.13.0" + - "@clerk/clerk-js@6.25.7" + - "@clerk/electron@0.0.18" + - "@clerk/expo@4.0.2" + - "@clerk/react@6.12.7" + - "@clerk/shared@4.25.7" overrides: "@clerk/backend": "catalog:" From 15e875a2d9bdda2fc08909c4b1582a363d288403 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 24 Jul 2026 12:02:36 +0200 Subject: [PATCH 02/14] Increase light-mode contrast for user message bubbles (#4441) --- apps/web/src/components/chat/MessagesTimeline.test.tsx | 1 + apps/web/src/components/chat/MessagesTimeline.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 296a54457a2..83ca7d3e952 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -434,6 +434,7 @@ describe("MessagesTimeline", () => { expect(markup).not.toContain("Show full message"); expect(markup).toContain('data-user-message-collapsible="false"'); + expect(markup).toContain("rounded-2xl bg-accent p-3"); }); it("renders inline terminal labels with the composer chip UI", () => { diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index ae190e00068..a429b54deaf 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -890,7 +890,7 @@ function UserTimelineRow({ row }: { row: Extract -
+
{regularImages.length > 0 && (
{regularImages.map((image: NonNullable[number]) => ( From f7cc7764887a3c0616973043fa77e12671e3f9ef Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 24 Jul 2026 14:01:47 +0200 Subject: [PATCH 03/14] Restore model picker layout and retain iterative test state (#4450) Co-authored-by: codex --- .agents/skills/test-t3-app/SKILL.md | 24 ++++++++++++++++--- .agents/skills/test-t3-app/agents/openai.yaml | 4 ++-- .../components/chat/ModelPickerContent.tsx | 15 ++++++++---- .../components/chat/ModelPickerSidebar.tsx | 7 ++++-- .../components/chat/ProviderModelPicker.tsx | 2 +- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/.agents/skills/test-t3-app/SKILL.md b/.agents/skills/test-t3-app/SKILL.md index c3dc1c103d4..b59ed9ddfe8 100644 --- a/.agents/skills/test-t3-app/SKILL.md +++ b/.agents/skills/test-t3-app/SKILL.md @@ -1,6 +1,6 @@ --- name: test-t3-app -description: Launch and test the T3 Code web app in isolated development environments, including first-try browser authentication with one-time pairing URLs, pairing-token recovery, worktree-safe state directories, dev server lifecycle, and direct SQLite inspection or fixture seeding. Use when an agent needs to run T3 locally, test UI behavior in a browser, recover from an expired or consumed pairing token, isolate dev state, or prepare test data in state.sqlite. +description: Launch, retain, and test the T3 Code web app in isolated development environments, including first-try browser authentication with one-time pairing URLs, pairing-token recovery, worktree-safe state directories, cross-turn dev server lifecycle, and direct SQLite inspection or fixture seeding. Use when an agent needs to run T3 locally, iteratively test UI behavior with a human, recover from an expired or consumed pairing token, isolate dev state, or prepare test data in state.sqlite. --- # Test T3 App @@ -20,6 +20,16 @@ Treat a base directory as disposable only when it was created or deliberately se The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it. +## Preserve the environment while iterating + +Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary. + +- Keep the dev process, base directory, selected ports, authenticated browser tab, registered projects, and seeded fixtures alive while the user may inspect the result or request follow-up changes. +- Do not stop the server merely because one verification pass completed or because you are yielding a response to the user. +- Before starting another environment, check whether the existing process and browser tab still serve the task. Reuse them when healthy instead of discarding useful state. +- On a later turn, verify that the existing process is alive and reuse its printed ports and base directory. If it exited, restart with the same base directory; create a new pairing token only when the browser session is no longer valid. +- Tell the user when a test environment remains available, including its non-secret web URL when useful. Never include a pairing token. + ## Authenticate the browser on the first navigation 1. Wait for the server log that says authentication is required and includes a URL ending in `/pair#token=...`. @@ -58,9 +68,17 @@ Read [references/sqlite-fixtures.md](references/sqlite-fixtures.md) before chang The helper refuses to write to the shared `~/.t3` directory by default and creates a database backup before each mutation. -## Finish the test +## Tear down only when the testing loop is finished + +Tear down when the user explicitly asks, confirms the iteration is finished, or the overall task is genuinely complete with no pending human review. Do not infer completion from the end of an assistant turn. + +When teardown is appropriate: + +1. Stop the dev process with its terminal interrupt. +2. Preserve the isolated base directory when it contains useful reproduction evidence or state for a likely follow-up. +3. Otherwise remove only a path created for this test after resolving and verifying the exact target. -Stop the dev process with its terminal interrupt. Preserve the isolated base directory when it contains useful reproduction evidence; otherwise remove only a path that was created for this test after resolving and verifying the exact target. A fresh isolated base directory is the safest reset when authentication, migrations, or fixture state becomes ambiguous. +If completion is uncertain, keep the environment alive and mention that it is retained for further iteration. A fresh isolated base directory remains the safest reset when authentication, migrations, or fixture state becomes ambiguous. ## Troubleshoot predictably diff --git a/.agents/skills/test-t3-app/agents/openai.yaml b/.agents/skills/test-t3-app/agents/openai.yaml index a3b89c95e60..0445ee4cb9e 100644 --- a/.agents/skills/test-t3-app/agents/openai.yaml +++ b/.agents/skills/test-t3-app/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "T3 App Testing" - short_description: "Launch and seed isolated T3 test environments" - default_prompt: "Use $test-t3-app to launch an isolated T3 development environment and test it in the browser." + short_description: "Launch and retain isolated T3 test environments" + default_prompt: "Use $test-t3-app to launch an isolated T3 environment and iteratively test it in the browser while preserving state." diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 8af23b530e5..1f3e2b1af67 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -521,7 +521,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return (
{/* Sidebar */} @@ -568,18 +568,23 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { handleModelSelect(slug, instanceId); }} > -
+
{/* Search bar */}
-
+
+ } value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} diff --git a/apps/web/src/components/chat/ModelPickerSidebar.tsx b/apps/web/src/components/chat/ModelPickerSidebar.tsx index fe2b840ca8f..e5555cb0115 100644 --- a/apps/web/src/components/chat/ModelPickerSidebar.tsx +++ b/apps/web/src/components/chat/ModelPickerSidebar.tsx @@ -97,7 +97,10 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { }, [props.instanceEntries, props.selectedInstanceId, showFavorites]); return ( -
+
+
Date: Fri, 24 Jul 2026 14:08:39 +0200 Subject: [PATCH 04/14] Color settled PR labels on hover (#4451) Co-authored-by: codex --- apps/web/src/components/SidebarV2.tsx | 9 +++++-- .../components/ThreadStatusIndicators.test.ts | 25 ++++++++++++++++++- .../src/components/ThreadStatusIndicators.tsx | 13 +++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index e018491348e..fd59cb63adc 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -117,7 +117,11 @@ import { sortThreadsForSidebarV2, } from "./Sidebar.logic"; import { resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic"; -import { prStatusIndicator, resolveThreadPr } from "./ThreadStatusIndicators"; +import { + prStatusIndicator, + resolveThreadPr, + settledPrHoverColorClass, +} from "./ThreadStatusIndicators"; import { resolveSnoozePresets, snoozeWakeDescription, @@ -498,6 +502,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { hasDedicatedWorktree: thread.worktreePath !== null, }); const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider); + const settledPrHoverClass = pr ? settledPrHoverColorClass(pr.state) : undefined; // Report the PR state up: the parent partitions rows with effectiveSettled, // and a merged/closed PR auto-settles a thread — data only rows have. const prState = pr?.state ?? null; @@ -714,7 +719,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { variant === "slim" && variantAction === "unsettle" ? props.isActive ? "text-muted-foreground/70" - : "text-muted-foreground/35 transition-colors group-hover/v2-row:text-muted-foreground/65" + : cn("text-muted-foreground/35 transition-colors", settledPrHoverClass) : prStatus.colorClass, )} aria-label={prStatus.tooltip} diff --git a/apps/web/src/components/ThreadStatusIndicators.test.ts b/apps/web/src/components/ThreadStatusIndicators.test.ts index 24ccc6ef33f..9fb4535f266 100644 --- a/apps/web/src/components/ThreadStatusIndicators.test.ts +++ b/apps/web/src/components/ThreadStatusIndicators.test.ts @@ -1,7 +1,11 @@ import type { VcsStatusResult } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; -import { prStatusIndicator, resolveThreadPr } from "./ThreadStatusIndicators"; +import { + prStatusIndicator, + resolveThreadPr, + settledPrHoverColorClass, +} from "./ThreadStatusIndicators"; function status(overrides: Partial = {}): VcsStatusResult { return { @@ -70,4 +74,23 @@ describe("prStatusIndicator", () => { tooltipTitle: "PR branch", }); }); + + it("uses red for closed pull requests", () => { + const closedPr = status().pr; + if (!closedPr) throw new Error("Expected pull request fixture"); + + expect(prStatusIndicator({ ...closedPr, state: "closed" }, undefined)?.colorClass).toContain( + "text-red-600", + ); + }); +}); + +describe("settledPrHoverColorClass", () => { + it.each([ + ["open", "text-emerald-600"], + ["merged", "text-violet-600"], + ["closed", "text-red-600"], + ] as const)("restores the %s pull request color on row hover", (state, colorClass) => { + expect(settledPrHoverColorClass(state)).toContain(`group-hover/v2-row:${colorClass}`); + }); }); diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx index 3c58d6ea989..0d3291a9e28 100644 --- a/apps/web/src/components/ThreadStatusIndicators.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.tsx @@ -35,6 +35,17 @@ export interface TerminalStatusIndicator { export type ThreadPr = VcsStatusResult["pr"]; +export function settledPrHoverColorClass(state: NonNullable["state"]): string { + switch (state) { + case "open": + return "group-hover/v2-row:text-emerald-600 dark:group-hover/v2-row:text-emerald-300/90"; + case "merged": + return "group-hover/v2-row:text-violet-600 dark:group-hover/v2-row:text-violet-300/90"; + case "closed": + return "group-hover/v2-row:text-red-600 dark:group-hover/v2-row:text-red-300/90"; + } +} + export function prStatusIndicator( pr: ThreadPr, provider: VcsStatusResult["sourceControlProvider"] | null | undefined, @@ -65,7 +76,7 @@ export function prStatusIndicator( if (pr.state === "closed") { return { label: `${presentation.shortName} closed`, - colorClass: "text-zinc-500 dark:text-zinc-400/80", + colorClass: "text-red-600 dark:text-red-300/90", tooltip, tooltipLead, tooltipTitle: pr.title, From ece05087a70e94efcd57441337fa1249559362ba Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 24 Jul 2026 14:25:01 +0200 Subject: [PATCH 05/14] [codex] Fix glass hover compositing artifacts (#4446) Co-authored-by: codex --- apps/web/src/components/SidebarV2.tsx | 8 +++++--- apps/web/src/components/chat/ModelListRow.tsx | 2 +- .../components/chat/ModelPickerContent.tsx | 2 +- .../components/chat/ModelPickerSidebar.tsx | 4 ++-- .../components/chat/ProviderModelPicker.tsx | 4 ++-- apps/web/src/index.css | 20 +++++++++++++++++-- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index fd59cb63adc..8c5891ebe7e 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -18,7 +18,6 @@ import { AlarmClockOffIcon, CheckIcon, ChevronDownIcon, - ChevronRightIcon, CircleAlertIcon, CircleCheckIcon, CircleDashedIcon, @@ -240,7 +239,11 @@ function SidebarV2ThreadTooltip({ side="right" align="start" sideOffset={8} - className="dropdown-glass max-w-80 border-0! bg-[color-mix(in_srgb,var(--background)_var(--glass-opacity),transparent)] text-left whitespace-normal shadow-lg/10 before:hidden dark:shadow-none" + className="dropdown-glass max-w-80 border-0! text-left whitespace-normal shadow-lg/10 before:hidden dark:shadow-none" + style={{ + background: + "color-mix(in srgb, var(--popover) 18%, color-mix(in srgb, var(--popover) var(--glass-opacity), transparent))", + }} >
{thread.title}
@@ -652,7 +655,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { // content; surface is reserved for interaction (hover, multi-select, route). const rowSurfaceClassName = cn( "group/v2-row relative w-full cursor-pointer overflow-hidden rounded-md text-left outline-none select-none", - variant === "card" && "backdrop-blur-[16px]", props.isActive ? "bg-sidebar-row-active text-sidebar-foreground" : isSelected diff --git a/apps/web/src/components/chat/ModelListRow.tsx b/apps/web/src/components/chat/ModelListRow.tsx index c62e0639db8..2ec4be70994 100644 --- a/apps/web/src/components/chat/ModelListRow.tsx +++ b/apps/web/src/components/chat/ModelListRow.tsx @@ -51,7 +51,7 @@ export const ModelListRow = memo(function ModelListRow(props: { contentClassName="flex w-full items-center gap-3" className={cn( "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2.5 transition-[background-color,box-shadow,color]", - "data-highlighted:bg-muted/56 data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0", + "hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-highlighted:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0 [&[data-highlighted][data-selected]]:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))]", props.disabledReason && "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent", )} diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 1f3e2b1af67..bbdbd8bd9d9 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -521,7 +521,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return (
{/* Sidebar */} diff --git a/apps/web/src/components/chat/ModelPickerSidebar.tsx b/apps/web/src/components/chat/ModelPickerSidebar.tsx index e5555cb0115..36a608888b2 100644 --- a/apps/web/src/components/chat/ModelPickerSidebar.tsx +++ b/apps/web/src/components/chat/ModelPickerSidebar.tsx @@ -125,7 +125,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { render={
+ {codeViewFiles.length > 0 && ( + + + } + > + {allDiffFilesCollapsed ? ( + + ) : ( + + )} + + + {allDiffFilesCollapsed ? "Expand all files" : "Collapse all files"} + + + )} { + it("reports whether every rendered file is collapsed", () => { + expect(areAllDiffFilesCollapsed(FILE_KEYS, new Set(FILE_KEYS))).toBe(true); + expect(areAllDiffFilesCollapsed(FILE_KEYS, new Set([FIRST_FILE_KEY]))).toBe(false); + expect(areAllDiffFilesCollapsed([], new Set())).toBe(false); + }); + + it("collapses all files when any rendered file is expanded", () => { + expect(toggleAllDiffFiles(FILE_KEYS, new Set([FIRST_FILE_KEY]))).toEqual(new Set(FILE_KEYS)); + }); + + it("expands all files when every rendered file is collapsed", () => { + expect(toggleAllDiffFiles(FILE_KEYS, new Set(FILE_KEYS))).toEqual(new Set()); + }); +}); diff --git a/apps/web/src/lib/diffCollapse.ts b/apps/web/src/lib/diffCollapse.ts new file mode 100644 index 00000000000..51bdcaf94ac --- /dev/null +++ b/apps/web/src/lib/diffCollapse.ts @@ -0,0 +1,13 @@ +export function areAllDiffFilesCollapsed( + fileKeys: ReadonlyArray, + collapsedFileKeys: ReadonlySet, +): boolean { + return fileKeys.length > 0 && fileKeys.every((fileKey) => collapsedFileKeys.has(fileKey)); +} + +export function toggleAllDiffFiles( + fileKeys: ReadonlyArray, + collapsedFileKeys: ReadonlySet, +): ReadonlySet { + return areAllDiffFilesCollapsed(fileKeys, collapsedFileKeys) ? new Set() : new Set(fileKeys); +} From 5719e8ac4020dda0e375ef61d044b61f55a0df8a Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 24 Jul 2026 19:28:37 -0700 Subject: [PATCH 08/14] feat(web): show fast mode as a bolt instead of a "Normal" label (#4488) Co-authored-by: Claude Opus 5 (1M context) --- .../src/components/chat/TraitsPicker.test.ts | 110 ++++++++++++++++++ apps/web/src/components/chat/TraitsPicker.tsx | 79 +++++++++---- 2 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/components/chat/TraitsPicker.test.ts diff --git a/apps/web/src/components/chat/TraitsPicker.test.ts b/apps/web/src/components/chat/TraitsPicker.test.ts new file mode 100644 index 00000000000..93157490090 --- /dev/null +++ b/apps/web/src/components/chat/TraitsPicker.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from "vite-plus/test"; +import type { ProviderOptionDescriptor } from "@t3tools/contracts"; +import { buildTraitsTriggerDisplay } from "./TraitsPicker"; + +function selectDescriptor( + id: string, + options: ReadonlyArray<{ id: string; label: string }>, + currentValue: string, +): Extract { + return { id, label: id, type: "select", options: [...options], currentValue }; +} + +function fastModeDescriptor( + currentValue: boolean, +): Extract { + return { id: "fastMode", label: "Fast Mode", type: "boolean", currentValue }; +} + +const EFFORT = selectDescriptor( + "effort", + [ + { id: "high", label: "High" }, + { id: "max", label: "Max" }, + ], + "high", +); +const CONTEXT_WINDOW = selectDescriptor( + "contextWindow", + [ + { id: "200k", label: "200k" }, + { id: "1m", label: "1M" }, + ], + "1m", +); + +function display(descriptors: ReadonlyArray, fastModeEnabled: boolean) { + return buildTraitsTriggerDisplay({ + descriptors, + primarySelectDescriptorId: "effort", + ultrathinkPromptControlled: false, + fastModeEnabled, + }); +} + +describe("buildTraitsTriggerDisplay", () => { + it("omits fast mode from the label entirely when it is off", () => { + expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW], false)).toEqual({ + label: "High · 1M", + showFastModeIcon: false, + }); + }); + + it("shows the bolt instead of a text label when fast mode is on", () => { + expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW], true)).toEqual({ + label: "High · 1M", + showFastModeIcon: true, + }); + }); + + it("keeps non-fastMode booleans as text labels", () => { + const thinking: Extract = { + id: "thinking", + label: "Thinking", + type: "boolean", + currentValue: true, + }; + expect(display([EFFORT, thinking], false)).toEqual({ + label: "High · Thinking On", + showFastModeIcon: false, + }); + }); + + it("falls back to a text label when fast mode is the only trait", () => { + expect(display([fastModeDescriptor(true)], true)).toEqual({ + label: "Fast", + showFastModeIcon: false, + }); + expect(display([fastModeDescriptor(false)], false)).toEqual({ + label: "Normal", + showFastModeIcon: false, + }); + }); + + it("stays blank when descriptors resolve to no label and there is no fast mode", () => { + // A select with neither a currentValue nor an isDefault option yields no + // label. Without a fastMode descriptor present that must stay blank rather + // than falling through to a bogus "Normal". + const unresolved: Extract = { + id: "effort", + label: "effort", + type: "select", + options: [ + { id: "low", label: "Low" }, + { id: "high", label: "High" }, + ], + }; + expect(display([unresolved], false)).toEqual({ label: "", showFastModeIcon: false }); + }); + + it("still renders the prompt-controlled ultrathink label alongside the bolt", () => { + expect( + buildTraitsTriggerDisplay({ + descriptors: [EFFORT, fastModeDescriptor(true)], + primarySelectDescriptorId: "effort", + ultrathinkPromptControlled: true, + fastModeEnabled: true, + }), + ).toEqual({ label: "Ultrathink", showFastModeIcon: true }); + }); +}); diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index 60f7f828ad4..d48fe484eb6 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -16,7 +16,7 @@ import { } from "@t3tools/shared/model"; import { memo, useCallback, useState } from "react"; import type { VariantProps } from "class-variance-authority"; -import { ChevronDownIcon } from "lucide-react"; +import { ChevronDownIcon, ZapIcon } from "lucide-react"; import { Button, buttonVariants } from "../ui/button"; import { Menu, @@ -379,6 +379,46 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ ); }); +/** + * Build the traits trigger's text label plus whether the fast-mode bolt should + * render. Fast mode is a lightning bolt when on and nothing at all when off — + * "Normal" is the near-universal case and isn't worth the horizontal space. The + * one exception is when fast mode is the only trait, where a bare bolt (or bare + * chevron) would leave the trigger unreadable. + */ +export function buildTraitsTriggerDisplay(input: { + descriptors: ReadonlyArray; + primarySelectDescriptorId: string | null; + ultrathinkPromptControlled: boolean; + fastModeEnabled: boolean; +}): { label: string; showFastModeIcon: boolean } { + let hasFastMode = false; + const labels: Array = []; + for (const descriptor of input.descriptors) { + if (descriptor.id === "fastMode" && descriptor.type === "boolean") { + hasFastMode = true; + continue; + } + const label = + input.ultrathinkPromptControlled && descriptor.id === input.primarySelectDescriptorId + ? "Ultrathink" + : descriptor.type === "boolean" + ? `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}` + : getProviderOptionCurrentLabel(descriptor); + if (typeof label === "string" && label.length > 0) { + labels.push(label); + } + } + + // Only fall back to text when fast mode is genuinely the sole trait. Keying + // off an empty label list alone would also catch descriptors that resolved to + // no label at all, printing a bogus "Normal" for a model without fast mode. + if (labels.length === 0 && hasFastMode) { + return { label: input.fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false }; + } + return { label: labels.join(" · "), showFastModeIcon: input.fastModeEnabled }; +} + export const TraitsPicker = memo(function TraitsPicker({ provider, instanceId, @@ -393,7 +433,7 @@ export const TraitsPicker = memo(function TraitsPicker({ ...persistence }: TraitsMenuContentProps & TraitsPersistence) { const [isMenuOpen, setIsMenuOpen] = useState(false); - const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled } = + const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled, fastModeEnabled } = getTraitsSectionVisibility({ provider, models, @@ -415,23 +455,18 @@ export const TraitsPicker = memo(function TraitsPicker({ return null; } - const triggerLabels: Array = []; - for (const descriptor of descriptors) { - const label = - ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id - ? "Ultrathink" - : descriptor.type === "boolean" - ? descriptor.id === "fastMode" - ? descriptor.currentValue === true - ? "Fast" - : "Normal" - : `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}` - : getProviderOptionCurrentLabel(descriptor); - if (typeof label === "string" && label.length > 0) { - triggerLabels.push(label); - } - } - const triggerLabel = triggerLabels.join(" · "); + const { label: triggerLabel, showFastModeIcon } = buildTraitsTriggerDisplay({ + descriptors, + primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, + ultrathinkPromptControlled, + fastModeEnabled, + }); + const fastModeIcon = showFastModeIcon ? ( + <> +