diff --git a/crates/agent-gui/biome.json b/crates/agent-gui/biome.json new file mode 100644 index 000000000..874b2ef16 --- /dev/null +++ b/crates/agent-gui/biome.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "includes": ["src/**", "!!**/dist", "!!**/node_modules"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "a11y": { + "useKeyWithClickEvents": "warn", + "noStaticElementInteractions": "warn", + "noLabelWithoutControl": "warn" + }, + "correctness": { + "useExhaustiveDependencies": "warn", + "noUnusedImports": "error" + }, + "suspicious": { + "noArrayIndexKey": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "trailingCommas": "all", + "semicolons": "always" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/crates/agent-gui/components.json b/crates/agent-gui/components.json new file mode 100644 index 000000000..10cef78c5 --- /dev/null +++ b/crates/agent-gui/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/shared/utils", + "ui": "@/components/ui" + } +} diff --git a/crates/agent-gui/package.json b/crates/agent-gui/package.json index 64e7554be..d455040bc 100644 --- a/crates/agent-gui/package.json +++ b/crates/agent-gui/package.json @@ -12,19 +12,19 @@ "test:frontend": "node --test test/settings/*.test.mjs test/chat/*.test.mjs test/providers/*.test.mjs test/tools/*.test.mjs test/i18n/*.test.mjs test/skills/*.test.mjs", "test:backend": "node --test test/backend/*.test.mjs", "test:release": "node --test test/backend/release-*.test.mjs", - "export:models": "node ./test/get-providers-get-models-example.js --save" + "export:models": "node ./test/get-providers-get-models-example.js --save", + "lint": "biome check src/", + "format": "biome format --write src/", + "lint:fix": "biome check --write src/" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.96", + "@base-ui/react": "^1.5.0", "@git-diff-view/file": "^0.1.3", "@git-diff-view/react": "^0.1.3", "@mariozechner/pi-agent-core": "^0.65.2", "@mariozechner/pi-ai": "^0.65.2", "@openai/codex-sdk": "^0.118.0", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-slot": "^1.2.4", "@sinclair/typebox": "^0.34.49", "@streamdown/cjk": "^1.0.3", "@streamdown/code": "^1.1.1", @@ -44,6 +44,7 @@ "yet-another-react-lightbox": "^3.31.0" }, "devDependencies": { + "@biomejs/biome": "^2.4.15", "@iconify-json/logos": "^1.2.11", "@iconify-json/lucide": "^1.2.108", "@iconify-json/vscode-icons": "^1.2.50", diff --git a/crates/agent-gui/pnpm-lock.yaml b/crates/agent-gui/pnpm-lock.yaml index 1dd6f1f49..948525951 100644 --- a/crates/agent-gui/pnpm-lock.yaml +++ b/crates/agent-gui/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.96 version: 0.2.96(zod@4.3.6) + '@base-ui/react': + specifier: ^1.5.0 + version: 1.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@git-diff-view/file': specifier: ^0.1.3 version: 0.1.3 @@ -26,18 +29,6 @@ importers: '@openai/codex-sdk': specifier: ^0.118.0 version: 0.118.0 - '@radix-ui/react-dropdown-menu': - specifier: ^2.1.16 - version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-scroll-area': - specifier: ^1.2.10 - version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-select': - specifier: ^2.2.6 - version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': - specifier: ^1.2.4 - version: 1.2.4(@types/react@19.2.14)(react@19.2.4) '@sinclair/typebox': specifier: ^0.34.49 version: 0.34.49 @@ -90,6 +81,9 @@ importers: specifier: ^3.31.0 version: 3.31.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) devDependencies: + '@biomejs/biome': + specifier: ^2.4.15 + version: 2.4.15 '@iconify-json/logos': specifier: ^1.2.11 version: 1.2.11 @@ -364,6 +358,10 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -376,6 +374,90 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.5.0': + resolution: {integrity: sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.2.9': + resolution: {integrity: sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@biomejs/biome@2.4.15': + resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.15': + resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.15': + resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.15': + resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.15': + resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.15': + resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.15': + resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.15': + resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.15': + resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -684,332 +766,6 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@radix-ui/number@1.1.1': - resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} - - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - - '@radix-ui/react-arrow@1.1.7': - resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context@1.1.2': - resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-direction@1.1.1': - resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.11': - resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-dropdown-menu@2.1.16': - resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.3': - resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.7': - resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.1': - resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-menu@2.1.16': - resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popper@1.2.8': - resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-portal@1.1.9': - resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.5': - resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-roving-focus@1.1.11': - resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-scroll-area@1.2.10': - resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-select@2.2.6': - resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-slot@1.2.4': - resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-previous@1.1.1': - resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-rect@1.1.1': - resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-size@1.1.1': - resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-visually-hidden@1.2.3': - resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/rect@1.1.1': - resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/binding-android-arm64@1.0.0-rc.12': resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1801,10 +1557,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -2164,9 +1916,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2357,10 +2106,6 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -3089,36 +2834,6 @@ packages: peerDependencies: react: ^19.2.4 - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.2: - resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react-style-singleton@2.2.3: - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -3194,6 +2909,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3437,26 +3155,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - use-callback-ref@1.3.3: - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sidecar@1.1.3: - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -4098,6 +3796,8 @@ snapshots: '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.7': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -4121,6 +3821,64 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@base-ui/react@1.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.2.9(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + '@base-ui/utils@0.2.9(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + reselect: 5.2.0 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + '@biomejs/biome@2.4.15': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.15 + '@biomejs/cli-darwin-x64': 2.4.15 + '@biomejs/cli-linux-arm64': 2.4.15 + '@biomejs/cli-linux-arm64-musl': 2.4.15 + '@biomejs/cli-linux-x64': 2.4.15 + '@biomejs/cli-linux-x64-musl': 2.4.15 + '@biomejs/cli-win32-arm64': 2.4.15 + '@biomejs/cli-win32-x64': 2.4.15 + + '@biomejs/cli-darwin-arm64@2.4.15': + optional: true + + '@biomejs/cli-darwin-x64@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64@2.4.15': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-x64@2.4.15': + optional: true + + '@biomejs/cli-win32-arm64@2.4.15': + optional: true + + '@biomejs/cli-win32-x64@2.4.15': + optional: true + '@braintree/sanitize-url@7.1.2': {} '@chevrotain/cst-dts-gen@11.1.2': @@ -4458,316 +4216,6 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@radix-ui/number@1.1.1': {} - - '@radix-ui/primitive@1.1.3': {} - - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/rect': 1.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/rect': 1.1.1 - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/rect@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true @@ -5604,10 +5052,6 @@ snapshots: argparse@2.0.1: {} - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -5964,8 +5408,6 @@ snapshots: detect-libc@2.1.2: {} - detect-node-es@1.1.0: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -6172,8 +5614,6 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 - get-nonce@1.0.1: {} - get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -7214,33 +6654,6 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): - dependencies: - get-nonce: 1.0.1 - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - react@19.2.4: {} reactivity-store@0.4.0(react@19.2.4): @@ -7358,6 +6771,8 @@ snapshots: require-from-string@2.0.2: {} + reselect@5.2.0: {} + resolve-from@4.0.0: {} retry@0.13.1: {} @@ -7662,21 +7077,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): - dependencies: - detect-node-es: 1.1.0 - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - use-sync-external-store@1.6.0(react@19.2.4): dependencies: react: 19.2.4 diff --git a/crates/agent-gui/src/App.tsx b/crates/agent-gui/src/App.tsx index d88fcd238..2302b5aab 100644 --- a/crates/agent-gui/src/App.tsx +++ b/crates/agent-gui/src/App.tsx @@ -1,8 +1,11 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import type { Context } from "@mariozechner/pi-ai"; import { listen } from "@tauri-apps/api/event"; - -import { getDefaultSettings, normalizeSettings, type AppSettings } from "./lib/settings"; +import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { CronPromptRunner } from "./components/cron/CronPromptRunner"; +import { MemoryOrganizerRunner } from "./components/memory/MemoryOrganizerRunner"; +import { WindowsTitleBar } from "./components/WindowsTitleBar"; +import { LocaleContext, t as translate } from "./i18n"; +import { type AppSettings, getDefaultSettings, normalizeSettings } from "./lib/settings"; import { loadPersistedSettingsWithDefaults, persistSettings, @@ -14,10 +17,6 @@ import { buildGatewaySettingsSyncPayload, type GatewaySettingsSyncPayload, } from "./lib/settings/sync"; -import { LocaleContext, t as translate } from "./i18n"; -import { CronPromptRunner } from "./components/cron/CronPromptRunner"; -import { MemoryOrganizerRunner } from "./components/memory/MemoryOrganizerRunner"; -import { WindowsTitleBar } from "./components/WindowsTitleBar"; import { ChatPage } from "./pages/ChatPage"; import { SettingsPage } from "./pages/SettingsPage"; import type { SectionId } from "./pages/settings/types"; @@ -140,48 +139,49 @@ export default function App() { }; }, []); - const queueSettingsSave = useCallback(( - prev: AppSettings, - next: AppSettings, - fallback: string, - publishSync: boolean, - ) => { - const saveSequence = ++saveSequenceRef.current; - setSettingsSaveState({ status: "saving" }); + const queueSettingsSave = useCallback( + (prev: AppSettings, next: AppSettings, fallback: string, publishSync: boolean) => { + const saveSequence = ++saveSequenceRef.current; + setSettingsSaveState({ status: "saving" }); - saveChainRef.current = saveChainRef.current - .catch(() => undefined) - .then(() => persistSettings(prev, next)) - .then(async () => { - if (publishSync) { - await publishGatewaySettingsSync(next); - } - }) - .then(() => { - if (saveSequenceRef.current === saveSequence) { - setSettingsSaveState({ status: "saved" }); - } - }) - .catch((error) => { - if (saveSequenceRef.current === saveSequence) { - setSettingsSaveState({ - status: "error", - message: asErrorMessage(error, fallback), - }); - } - }); - }, []); + saveChainRef.current = saveChainRef.current + .catch(() => undefined) + .then(() => persistSettings(prev, next)) + .then(async () => { + if (publishSync) { + await publishGatewaySettingsSync(next); + } + }) + .then(() => { + if (saveSequenceRef.current === saveSequence) { + setSettingsSaveState({ status: "saved" }); + } + }) + .catch((error) => { + if (saveSequenceRef.current === saveSequence) { + setSettingsSaveState({ + status: "error", + message: asErrorMessage(error, fallback), + }); + } + }); + }, + [], + ); - const setSettings = useCallback((updater: (prev: AppSettings) => AppSettings) => { - setSettingsState((prev) => { - const next = applyRuntimeSystemDefaults( - normalizeSettings(updater(prev)), - defaultWorkdirRef.current, - ); - queueSettingsSave(prev, next, "保存设置失败。", hasSettingsSyncChanged(prev, next)); - return next; - }); - }, [queueSettingsSave]); + const setSettings = useCallback( + (updater: (prev: AppSettings) => AppSettings) => { + setSettingsState((prev) => { + const next = applyRuntimeSystemDefaults( + normalizeSettings(updater(prev)), + defaultWorkdirRef.current, + ); + queueSettingsSave(prev, next, "保存设置失败。", hasSettingsSyncChanged(prev, next)); + return next; + }); + }, + [queueSettingsSave], + ); const reloadPersistedSettings = useCallback(async () => { await saveChainRef.current.catch(() => undefined); @@ -198,18 +198,21 @@ export default function App() { })); }, [setSettings]); - const openSettings = useCallback((section: SectionId = "system") => { - setSettingsSection(section); - setSettingsOpen(true); - setOverlay("entering"); - requestAnimationFrame(() => requestAnimationFrame(() => setOverlay("open"))); - void reloadPersistedSettings().catch((error) => { - setSettingsSaveState({ - status: "error", - message: asErrorMessage(error, "重新加载设置失败,当前显示的是旧配置。"), + const openSettings = useCallback( + (section: SectionId = "system") => { + setSettingsSection(section); + setSettingsOpen(true); + setOverlay("entering"); + requestAnimationFrame(() => requestAnimationFrame(() => setOverlay("open"))); + void reloadPersistedSettings().catch((error) => { + setSettingsSaveState({ + status: "error", + message: asErrorMessage(error, "重新加载设置失败,当前显示的是旧配置。"), + }); }); - }); - }, [reloadPersistedSettings]); + }, + [reloadPersistedSettings], + ); const closeSettings = useCallback(() => { setOverlay("leaving"); @@ -223,10 +226,13 @@ export default function App() { }, [overlay]); // 构建 locale context value,避免每次渲染重新创建 - const localeContextValue = useMemo(() => ({ - locale: settings.locale, - t: (key: string) => translate(key, settings.locale), - }), [settings.locale]); + const localeContextValue = useMemo( + () => ({ + locale: settings.locale, + t: (key: string) => translate(key, settings.locale), + }), + [settings.locale], + ); useEffect(() => { if (!settingsReady) { diff --git a/crates/agent-gui/src/components/Markdown.tsx b/crates/agent-gui/src/components/Markdown.tsx index 835810455..2b264d113 100644 --- a/crates/agent-gui/src/components/Markdown.tsx +++ b/crates/agent-gui/src/components/Markdown.tsx @@ -1,22 +1,21 @@ -import { memo, useLayoutEffect, useRef, type ComponentProps } from "react"; import { cjk } from "@streamdown/cjk"; import { code } from "@streamdown/code"; import { math } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { openUrl } from "@tauri-apps/plugin-opener"; -import { Copy, ExternalLink, X } from "./icons"; +import { type ComponentProps, memo, useLayoutEffect, useRef } from "react"; +import remarkBreaks from "remark-breaks"; import { - Streamdown, - defaultRemarkPlugins, type Components, + defaultRemarkPlugins, type ExtraProps, type LinkSafetyModalProps, + Streamdown, type StreamdownTranslations, } from "streamdown"; -import remarkBreaks from "remark-breaks"; - -import { Button } from "./ui/button"; import { cn } from "../lib/shared/utils"; +import { Copy, ExternalLink, X } from "./icons"; +import { Button } from "./ui/button"; type MarkdownProps = { content: string; @@ -165,12 +164,7 @@ const streamdownTranslations = { viewFullscreen: "全屏查看", } satisfies Partial; -function ExternalLinkModal({ - isOpen, - onClose, - onConfirm, - url, -}: LinkSafetyModalProps) { +function ExternalLinkModal({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) { if (!isOpen) { return null; } diff --git a/crates/agent-gui/src/components/WindowsTitleBar.tsx b/crates/agent-gui/src/components/WindowsTitleBar.tsx index 63d2118a9..a0c0042be 100644 --- a/crates/agent-gui/src/components/WindowsTitleBar.tsx +++ b/crates/agent-gui/src/components/WindowsTitleBar.tsx @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useRef, useState, type MouseEvent } from "react"; import { getCurrentWindow } from "@tauri-apps/api/window"; +import { type MouseEvent, useCallback, useEffect, useRef, useState } from "react"; import iconSimpleUrl from "../../src-tauri/icons/icon-simple.png"; -import { Maximize2, Minimize2, Minus, X } from "./icons"; import { useLocale } from "../i18n"; import { cn } from "../lib/shared/utils"; +import { Maximize2, Minimize2, Minus, X } from "./icons"; type TauriRuntimeWindow = Window & { __TAURI__?: unknown; @@ -122,14 +122,17 @@ export function WindowsTitleBar() { }; }, [getAppWindow, isVisible, syncMaximized]); - const startDragging = useCallback((event: MouseEvent) => { - if (event.button !== 0 || event.detail !== 1) { - return; - } - void getAppWindow() - .startDragging() - .catch((error) => reportWindowChromeError("drag", error)); - }, [getAppWindow]); + const startDragging = useCallback( + (event: MouseEvent) => { + if (event.button !== 0 || event.detail !== 1) { + return; + } + void getAppWindow() + .startDragging() + .catch((error) => reportWindowChromeError("drag", error)); + }, + [getAppWindow], + ); const toggleMaximize = useCallback(() => { const appWindow = getAppWindow(); @@ -140,12 +143,15 @@ export function WindowsTitleBar() { .catch((error) => reportWindowChromeError("toggle maximized state for", error)); }, [getAppWindow]); - const handleTitleDoubleClick = useCallback((event: MouseEvent) => { - if (event.button !== 0) { - return; - } - toggleMaximize(); - }, [toggleMaximize]); + const handleTitleDoubleClick = useCallback( + (event: MouseEvent) => { + if (event.button !== 0) { + return; + } + toggleMaximize(); + }, + [toggleMaximize], + ); const minimizeWindow = useCallback(() => { void getAppWindow() diff --git a/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx b/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx index 524a6ec82..5f6b563c8 100644 --- a/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx +++ b/crates/agent-gui/src/components/chat/ChatHistorySidebar.tsx @@ -1,14 +1,9 @@ -import { - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import iconSimpleUrl from "../../../src-tauri/icons/icon-simple.png"; import { useLocale } from "../../i18n"; +import type { ChatHistorySummary } from "../../lib/chat/history/chatHistory"; +import { cn } from "../../lib/shared/utils"; import { Edit3, Link2, @@ -23,9 +18,6 @@ import { SquarePen, Trash2, } from "../icons"; - -import type { ChatHistorySummary } from "../../lib/chat/history/chatHistory"; -import { cn } from "../../lib/shared/utils"; import { Button } from "../ui/button"; import { DropdownMenu, @@ -167,9 +159,7 @@ const HistoryRow = memo(function HistoryRow(props: {

删除「{item.title}」?

-

- 此操作无法撤销 -

+

此操作无法撤销

@@ -277,26 +265,30 @@ const HistoryRow = memo(function HistoryRow(props: { ) : null} - - + ) => + e.stopPropagation() + } + onClick={(e: React.MouseEvent) => e.stopPropagation()} + className={cn( + "h-8 w-8 shrink-0 rounded-xl text-muted-foreground opacity-0 pointer-events-none transition-[opacity,colors]", + "hover:bg-muted/70 hover:text-foreground", + "group-hover/item:opacity-100 group-hover/item:pointer-events-auto", + "group-focus-within/item:opacity-100 group-focus-within/item:pointer-events-auto", + menuOpen && "bg-muted/70 text-foreground", + menuOpen && "opacity-100 pointer-events-auto", + )} + /> + } + > + -
+
{title}
{description ?
{description}
: null} @@ -414,10 +408,7 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi [items, sharedConversationCountProp], ); const historyScrollRef = useRef(null); - const getHistoryItemKey = useCallback( - (index: number) => items[index]?.id ?? index, - [items], - ); + const getHistoryItemKey = useCallback((index: number) => items[index]?.id ?? index, [items]); const historyVirtualizer = useVirtualizer({ count: items.length, getScrollElement: () => historyScrollRef.current, @@ -427,9 +418,7 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi }); const virtualHistoryRows = historyVirtualizer.getVirtualItems(); const lastVirtualHistoryIndex = - virtualHistoryRows.length > 0 - ? virtualHistoryRows[virtualHistoryRows.length - 1].index - : -1; + virtualHistoryRows.length > 0 ? virtualHistoryRows[virtualHistoryRows.length - 1].index : -1; useEffect(() => { if ( @@ -636,10 +625,7 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi

) : ( -
+
{virtualHistoryRows.map((virtualRow) => { const item = items[virtualRow.index]; if (!item) return null; diff --git a/crates/agent-gui/src/components/chat/HistoryShareModal.tsx b/crates/agent-gui/src/components/chat/HistoryShareModal.tsx index 87f035c6e..2312d7c89 100644 --- a/crates/agent-gui/src/components/chat/HistoryShareModal.tsx +++ b/crates/agent-gui/src/components/chat/HistoryShareModal.tsx @@ -1,22 +1,11 @@ import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; -import { - Check, - Copy, - Eye, - ExternalLink, - EyeOff, - Link2, - Loader2, - Share2, - X, -} from "../icons"; - import type { ChatHistoryShareStatus, ChatHistorySummary, } from "../../lib/chat/history/chatHistory"; import { cn } from "../../lib/shared/utils"; +import { Check, Copy, ExternalLink, Eye, EyeOff, Link2, Loader2, Share2, X } from "../icons"; import { Button } from "../ui/button"; type HistoryShareModalProps = { @@ -131,11 +120,7 @@ function RedactionPicker(props: { ); } -function ShareSwitch(props: { - checked: boolean; - disabled: boolean; - onToggle: () => void; -}) { +function ShareSwitch(props: { checked: boolean; disabled: boolean; onToggle: () => void }) { const { checked, disabled, onToggle } = props; return (
分享会话
-
+
{conversation.title}
@@ -279,11 +267,7 @@ export function HistoryShareModal({ : "border-border/60 bg-background text-muted-foreground", )} > - {redactToolContent ? ( - - ) : ( - - )} + {redactToolContent ? : }
工具调用脱敏 diff --git a/crates/agent-gui/src/components/chat/MentionComposer.tsx b/crates/agent-gui/src/components/chat/MentionComposer.tsx index a98909563..0ad0dcd2d 100644 --- a/crates/agent-gui/src/components/chat/MentionComposer.tsx +++ b/crates/agent-gui/src/components/chat/MentionComposer.tsx @@ -1,8 +1,10 @@ -import { fileIconSvg, folderIconSvg } from "../icons"; -import { getFileTypeIcon } from "./fileTypeIcons"; +import { invoke } from "@tauri-apps/api/core"; import { + type ClipboardEvent, forwardRef, + type KeyboardEvent, memo, + type RefObject, useCallback, useEffect, useImperativeHandle, @@ -10,13 +12,11 @@ import { useMemo, useRef, useState, - type ClipboardEvent, - type KeyboardEvent, - type RefObject, } from "react"; import { createPortal } from "react-dom"; -import { invoke } from "@tauri-apps/api/core"; import { cn } from "../../lib/shared/utils"; +import { fileIconSvg, folderIconSvg } from "../icons"; +import { getFileTypeIcon } from "./fileTypeIcons"; /* ------------------------------------------------------------------ */ /* Types */ @@ -178,7 +178,10 @@ function serializeChildrenToSegments( const mentionPath = el.getAttribute(MENTION_TAG_ATTR); if (mentionPath) { const kind = el.getAttribute(MENTION_KIND_ATTR); - pushTextSegment(parts, formatMentionReference(mentionPath, kind === "dir" ? "dir" : "file")); + pushTextSegment( + parts, + formatMentionReference(mentionPath, kind === "dir" ? "dir" : "file"), + ); } else if (el.hasAttribute(SKILL_MENTION_NAME_ATTR)) { const name = el.getAttribute(SKILL_MENTION_NAME_ATTR)?.trim() ?? ""; const skillFile = el.getAttribute(SKILL_MENTION_FILE_ATTR)?.trim() ?? ""; @@ -355,9 +358,7 @@ function isActiveImeKeyboardEvent(event: KeyboardEvent) { function isEnterKeyboardEvent(event: KeyboardEvent) { const nativeEvent = event.nativeEvent as globalThis.KeyboardEvent; return ( - event.key === "Enter" || - nativeEvent.code === "Enter" || - nativeEvent.code === "NumpadEnter" + event.key === "Enter" || nativeEvent.code === "Enter" || nativeEvent.code === "NumpadEnter" ); } @@ -685,7 +686,8 @@ function chipBeforeCursor(root: HTMLElement): HTMLElement | null { el.hasAttribute(MENTION_TAG_ATTR) || el.hasAttribute(SKILL_MENTION_NAME_ATTR) || el.hasAttribute(LARGE_PASTE_TAG_ATTR) - ) return el; + ) + return el; } } @@ -700,7 +702,8 @@ function chipBeforeCursor(root: HTMLElement): HTMLElement | null { ce.hasAttribute(MENTION_TAG_ATTR) || ce.hasAttribute(SKILL_MENTION_NAME_ATTR) || ce.hasAttribute(LARGE_PASTE_TAG_ATTR) - ) return ce; + ) + return ce; } } @@ -778,9 +781,7 @@ function Popup({ {isLoading && (
Indexing files...
)} - {error && !isLoading && ( -
{error}
- )} + {error && !isLoading &&
{error}
} {suggestions.map((suggestion, i) => { const isSkill = suggestion.type === "skill"; const entry = suggestion.type === "file" ? suggestion.entry : null; @@ -794,7 +795,9 @@ function Popup({ const subtitle = skill?.description ?? (dirPath ? `${dirPath}/` : ""); return (
- {Icon ? : $} + {Icon ? ( + + ) : ( + $ + )} {title} @@ -829,10 +836,12 @@ function Popup({ skill - ) : isDir && ( - - dir - + ) : ( + isDir && ( + + dir + + ) )}
); @@ -850,656 +859,661 @@ function Popup({ /* MentionComposer */ /* ------------------------------------------------------------------ */ -export const MentionComposer = memo(forwardRef(function MentionComposer({ - onSend, - onEmptyChange, - onBusyChange, - onPasteFiles, - disabled = false, - placeholder = "", - workdir, - enabledSkills = [], - className, -}: MentionComposerProps, ref) { - const editorRef = useRef(null); - const wrapperRef = useRef(null); - const [isEmpty, setIsEmpty] = useState(true); - const lastIsEmptyRef = useRef(true); - const isComposingRef = useRef(false); - const compositionEnterKeyRef = useRef(false); - const lastCompositionEndAtRef = useRef(0); - const imeEnterSuppressUntilRef = useRef(0); - const busyReleaseTimerRef = useRef(null); - const isBusyRef = useRef(false); - const largePastesRef = useRef(new Map()); - const largePasteCounterRef = useRef(0); - - const setBusy = useCallback( - (nextBusy: boolean) => { - if (isBusyRef.current === nextBusy) return; - isBusyRef.current = nextBusy; - onBusyChange?.(nextBusy); - }, - [onBusyChange], - ); +export const MentionComposer = memo( + forwardRef(function MentionComposer( + { + onSend, + onEmptyChange, + onBusyChange, + onPasteFiles, + disabled = false, + placeholder = "", + workdir, + enabledSkills = [], + className, + }: MentionComposerProps, + ref, + ) { + const editorRef = useRef(null); + const wrapperRef = useRef(null); + const [isEmpty, setIsEmpty] = useState(true); + const lastIsEmptyRef = useRef(true); + const isComposingRef = useRef(false); + const compositionEnterKeyRef = useRef(false); + const lastCompositionEndAtRef = useRef(0); + const imeEnterSuppressUntilRef = useRef(0); + const busyReleaseTimerRef = useRef(null); + const isBusyRef = useRef(false); + const largePastesRef = useRef(new Map()); + const largePasteCounterRef = useRef(0); + + const setBusy = useCallback( + (nextBusy: boolean) => { + if (isBusyRef.current === nextBusy) return; + isBusyRef.current = nextBusy; + onBusyChange?.(nextBusy); + }, + [onBusyChange], + ); - const scheduleBusyRelease = useCallback(() => { - if (busyReleaseTimerRef.current !== null) { - window.clearTimeout(busyReleaseTimerRef.current); - } - busyReleaseTimerRef.current = window.setTimeout(() => { - busyReleaseTimerRef.current = null; - setBusy(false); - }, 140); - }, [setBusy]); - - // ---- File list ---- - const normalizedWorkdir = workdir.trim(); - const [mentionSessionEntries, setMentionSessionEntries] = useState([]); - const [mentionSessionLoading, setMentionSessionLoading] = useState(false); - const [mentionSessionError, setMentionSessionError] = useState(null); - const mentionSessionRequestSeqRef = useRef(0); - const mentionActiveRef = useRef(false); - const mentionSessionQueryRef = useRef(""); - - // ---- Mention state ---- - const [mentionCtx, setMentionCtx] = useState(null); - const [highlightIdx, setHighlightIdx] = useState(0); - - const resetMentionSession = useCallback(() => { - mentionSessionRequestSeqRef.current += 1; - mentionSessionQueryRef.current = ""; - setMentionSessionEntries([]); - setMentionSessionLoading(false); - setMentionSessionError(null); - }, []); - - const closeMentionSession = useCallback(() => { - mentionActiveRef.current = false; - setMentionCtx(null); - setHighlightIdx(0); - resetMentionSession(); - }, [resetMentionSession]); - - const startMentionSession = useCallback( - (ctx: MentionContext) => { - const requestSeq = ++mentionSessionRequestSeqRef.current; - mentionSessionQueryRef.current = ctx.query; + const scheduleBusyRelease = useCallback(() => { + if (busyReleaseTimerRef.current !== null) { + window.clearTimeout(busyReleaseTimerRef.current); + } + busyReleaseTimerRef.current = window.setTimeout(() => { + busyReleaseTimerRef.current = null; + setBusy(false); + }, 140); + }, [setBusy]); + + // ---- File list ---- + const normalizedWorkdir = workdir.trim(); + const [mentionSessionEntries, setMentionSessionEntries] = useState([]); + const [mentionSessionLoading, setMentionSessionLoading] = useState(false); + const [mentionSessionError, setMentionSessionError] = useState(null); + const mentionSessionRequestSeqRef = useRef(0); + const mentionActiveRef = useRef(false); + const mentionSessionQueryRef = useRef(""); + + // ---- Mention state ---- + const [mentionCtx, setMentionCtx] = useState(null); + const [highlightIdx, setHighlightIdx] = useState(0); + + const resetMentionSession = useCallback(() => { + mentionSessionRequestSeqRef.current += 1; + mentionSessionQueryRef.current = ""; setMentionSessionEntries([]); - setMentionSessionLoading(ctx.trigger === "file" && Boolean(normalizedWorkdir)); + setMentionSessionLoading(false); setMentionSessionError(null); + }, []); + + const closeMentionSession = useCallback(() => { + mentionActiveRef.current = false; + setMentionCtx(null); + setHighlightIdx(0); + resetMentionSession(); + }, [resetMentionSession]); + + const startMentionSession = useCallback( + (ctx: MentionContext) => { + const requestSeq = ++mentionSessionRequestSeqRef.current; + mentionSessionQueryRef.current = ctx.query; + setMentionSessionEntries([]); + setMentionSessionLoading(ctx.trigger === "file" && Boolean(normalizedWorkdir)); + setMentionSessionError(null); + + if (ctx.trigger === "skill") { + return; + } + if (!normalizedWorkdir) { + return; + } - if (ctx.trigger === "skill") { - return; - } - if (!normalizedWorkdir) { - return; - } - - invoke("fs_mention_list", { - workdir: normalizedWorkdir, - max_results: MENTION_INDEX_MAX_RESULTS, - query: ctx.query, - }) - .then((resp) => { - if (requestSeq !== mentionSessionRequestSeqRef.current) return; - setMentionSessionEntries(resp.entries); - }) - .catch(() => { - if (requestSeq !== mentionSessionRequestSeqRef.current) return; - setMentionSessionEntries([]); - setMentionSessionError("Could not index files"); + invoke("fs_mention_list", { + workdir: normalizedWorkdir, + max_results: MENTION_INDEX_MAX_RESULTS, + query: ctx.query, }) - .finally(() => { - if (requestSeq !== mentionSessionRequestSeqRef.current) return; - setMentionSessionLoading(false); - }); - }, - [normalizedWorkdir], - ); + .then((resp) => { + if (requestSeq !== mentionSessionRequestSeqRef.current) return; + setMentionSessionEntries(resp.entries); + }) + .catch(() => { + if (requestSeq !== mentionSessionRequestSeqRef.current) return; + setMentionSessionEntries([]); + setMentionSessionError("Could not index files"); + }) + .finally(() => { + if (requestSeq !== mentionSessionRequestSeqRef.current) return; + setMentionSessionLoading(false); + }); + }, + [normalizedWorkdir], + ); + + const mentionSessionSearchIndex = useMemo( + () => + mentionSessionEntries.map((entry) => ({ + entry, + searchPath: entry.path.toLowerCase(), + })), + [mentionSessionEntries], + ); + + useEffect(() => { + closeMentionSession(); + }, [normalizedWorkdir, closeMentionSession]); - const mentionSessionSearchIndex = useMemo( - () => - mentionSessionEntries.map((entry) => ({ - entry, - searchPath: entry.path.toLowerCase(), - })), - [mentionSessionEntries], - ); + useEffect(() => { + return () => { + mentionSessionRequestSeqRef.current += 1; + if (busyReleaseTimerRef.current !== null) { + window.clearTimeout(busyReleaseTimerRef.current); + } + setBusy(false); + }; + }, [setBusy]); - useEffect(() => { - closeMentionSession(); - }, [normalizedWorkdir, closeMentionSession]); + useEffect(() => { + if (!disabled) return; + closeMentionSession(); + setBusy(false); + }, [disabled, closeMentionSession, setBusy]); - useEffect(() => { - return () => { - mentionSessionRequestSeqRef.current += 1; - if (busyReleaseTimerRef.current !== null) { - window.clearTimeout(busyReleaseTimerRef.current); + const normalizedMentionQuery = mentionCtx ? normalizeMentionQuery(mentionCtx.query) : ""; + const suggestions = useMemo(() => { + if (mentionCtx === null) { + return []; } - setBusy(false); - }; - }, [setBusy]); - useEffect(() => { - if (!disabled) return; - closeMentionSession(); - setBusy(false); - }, [disabled, closeMentionSession, setBusy]); - - const normalizedMentionQuery = mentionCtx ? normalizeMentionQuery(mentionCtx.query) : ""; - const suggestions = useMemo(() => { - if (mentionCtx === null) { - return []; - } + if (mentionCtx.trigger === "skill") { + const next: MentionSuggestion[] = []; + for (const skill of enabledSkills) { + const haystack = `${skill.name}\n${skill.description}\n${skill.baseDir}`.toLowerCase(); + if (normalizedMentionQuery && !haystack.includes(normalizedMentionQuery)) { + continue; + } + next.push({ type: "skill", skill }); + if (next.length >= MAX_SUGGESTIONS) { + break; + } + } + return next; + } - if (mentionCtx.trigger === "skill") { const next: MentionSuggestion[] = []; - for (const skill of enabledSkills) { - const haystack = `${skill.name}\n${skill.description}\n${skill.baseDir}`.toLowerCase(); - if (normalizedMentionQuery && !haystack.includes(normalizedMentionQuery)) { + for (const item of mentionSessionSearchIndex) { + if (normalizedMentionQuery && !item.searchPath.includes(normalizedMentionQuery)) { continue; } - next.push({ type: "skill", skill }); + next.push({ type: "file", entry: item.entry }); if (next.length >= MAX_SUGGESTIONS) { break; } } return next; - } + }, [enabledSkills, mentionCtx, mentionSessionSearchIndex, normalizedMentionQuery]); + + useEffect(() => { + setHighlightIdx((current) => { + if (suggestions.length === 0) return 0; + return Math.min(current, suggestions.length - 1); + }); + }, [suggestions.length]); + + const popupLoading = mentionSessionLoading; + const popupError = suggestions.length === 0 ? mentionSessionError : null; + const popupEmptyLabel = + mentionCtx?.trigger === "skill" ? "No matching enabled Skills" : "No matching files"; + const showEmpty = + mentionCtx !== null && !popupLoading && !popupError && suggestions.length === 0; + const popupVisible = + mentionCtx !== null && + (popupLoading || Boolean(popupError) || suggestions.length > 0 || showEmpty); + + const applyEmptyState = useCallback( + (nextEmpty: boolean) => { + if (lastIsEmptyRef.current === nextEmpty) return; + lastIsEmptyRef.current = nextEmpty; + setIsEmpty(nextEmpty); + onEmptyChange?.(nextEmpty); + }, + [onEmptyChange], + ); - const next: MentionSuggestion[] = []; - for (const item of mentionSessionSearchIndex) { - if (normalizedMentionQuery && !item.searchPath.includes(normalizedMentionQuery)) { - continue; - } - next.push({ type: "file", entry: item.entry }); - if (next.length >= MAX_SUGGESTIONS) { - break; - } - } - return next; - }, [enabledSkills, mentionCtx, mentionSessionSearchIndex, normalizedMentionQuery]); + const refreshEmptyState = useCallback(() => { + const el = editorRef.current; + if (!el) return; + applyEmptyState(editorTextIsEmpty(el)); + }, [applyEmptyState]); - useEffect(() => { - setHighlightIdx((current) => { - if (suggestions.length === 0) return 0; - return Math.min(current, suggestions.length - 1); - }); - }, [suggestions.length]); - - const popupLoading = mentionSessionLoading; - const popupError = suggestions.length === 0 ? mentionSessionError : null; - const popupEmptyLabel = - mentionCtx?.trigger === "skill" ? "No matching enabled Skills" : "No matching files"; - const showEmpty = - mentionCtx !== null && - !popupLoading && - !popupError && - suggestions.length === 0; - const popupVisible = - mentionCtx !== null && - (popupLoading || Boolean(popupError) || suggestions.length > 0 || showEmpty); - - const applyEmptyState = useCallback( - (nextEmpty: boolean) => { - if (lastIsEmptyRef.current === nextEmpty) return; - lastIsEmptyRef.current = nextEmpty; - setIsEmpty(nextEmpty); - onEmptyChange?.(nextEmpty); - }, - [onEmptyChange], - ); + const buildDraft = useCallback((): MentionComposerDraft => { + const el = editorRef.current; + if (!el) { + return { + segments: [], + text: "", + textWithoutLargePastes: "", + largePastes: [], + skillMentions: [], + isEmpty: true, + }; + } - const refreshEmptyState = useCallback(() => { - const el = editorRef.current; - if (!el) return; - applyEmptyState(editorTextIsEmpty(el)); - }, [applyEmptyState]); + const segments = serializeChildrenToSegments(el, largePastesRef.current); + const largePastes: MentionComposerLargePaste[] = []; + const skillMentions: MentionComposerSkillMention[] = []; + const textParts: string[] = []; + const textWithoutLargePastesParts: string[] = []; + for (const segment of segments) { + if (segment.type === "text") { + textParts.push(segment.text); + textWithoutLargePastesParts.push(segment.text); + } else if (segment.type === "largePaste") { + largePastes.push(segment.paste); + textParts.push(segment.paste.text); + } else { + skillMentions.push(segment.skill); + const token = formatSkillMentionToken(segment.skill); + textParts.push(token); + textWithoutLargePastesParts.push(token); + } + } - const buildDraft = useCallback((): MentionComposerDraft => { - const el = editorRef.current; - if (!el) { + const text = textParts.join("").replace(/\u00A0/g, " "); + const textWithoutLargePastes = textWithoutLargePastesParts.join("").replace(/\u00A0/g, " "); return { - segments: [], - text: "", - textWithoutLargePastes: "", - largePastes: [], - skillMentions: [], - isEmpty: true, + segments, + text, + textWithoutLargePastes, + largePastes, + skillMentions, + isEmpty: editorTextIsEmpty(el), }; - } + }, []); - const segments = serializeChildrenToSegments(el, largePastesRef.current); - const largePastes: MentionComposerLargePaste[] = []; - const skillMentions: MentionComposerSkillMention[] = []; - const textParts: string[] = []; - const textWithoutLargePastesParts: string[] = []; - for (const segment of segments) { - if (segment.type === "text") { - textParts.push(segment.text); - textWithoutLargePastesParts.push(segment.text); - } else if (segment.type === "largePaste") { - largePastes.push(segment.paste); - textParts.push(segment.paste.text); - } else { - skillMentions.push(segment.skill); - const token = formatSkillMentionToken(segment.skill); - textParts.push(token); - textWithoutLargePastesParts.push(token); - } - } + const createLargePaste = useCallback((text: string): MentionComposerLargePaste => { + const index = largePasteCounterRef.current + 1; + largePasteCounterRef.current = index; + return { + id: `large-paste-${Date.now()}-${crypto.randomUUID()}`, + label: `Pasted text ${index}`, + text, + charCount: text.length, + lineCount: countLargePasteLines(text), + preview: normalizeLargePastePreview(text), + }; + }, []); - const text = textParts.join("").replace(/\u00A0/g, " "); - const textWithoutLargePastes = textWithoutLargePastesParts.join("").replace(/\u00A0/g, " "); - return { - segments, - text, - textWithoutLargePastes, - largePastes, - skillMentions, - isEmpty: editorTextIsEmpty(el), - }; - }, []); - - const createLargePaste = useCallback((text: string): MentionComposerLargePaste => { - const index = largePasteCounterRef.current + 1; - largePasteCounterRef.current = index; - return { - id: `large-paste-${Date.now()}-${crypto.randomUUID()}`, - label: `Pasted text ${index}`, - text, - charCount: text.length, - lineCount: countLargePasteLines(text), - preview: normalizeLargePastePreview(text), - }; - }, []); + const insertLargePaste = useCallback( + (text: string) => { + const el = editorRef.current; + if (!el) return; + const paste = createLargePaste(text); + largePastesRef.current.set(paste.id, paste); + insertNodeAtCursor(el, createLargePasteChip(paste)); + closeMentionSession(); + refreshEmptyState(); + }, + [closeMentionSession, createLargePaste, refreshEmptyState], + ); - const insertLargePaste = useCallback( - (text: string) => { + // ---- Mention detection (called after DOM updates) ---- + const refreshMention = useCallback(() => { const el = editorRef.current; if (!el) return; - const paste = createLargePaste(text); - largePastesRef.current.set(paste.id, paste); - insertNodeAtCursor(el, createLargePasteChip(paste)); - closeMentionSession(); - refreshEmptyState(); - }, - [closeMentionSession, createLargePaste, refreshEmptyState], - ); - - // ---- Mention detection (called after DOM updates) ---- - const refreshMention = useCallback(() => { - const el = editorRef.current; - if (!el) return; - const applyContext = (ctx: MentionContext | null) => { - if (ctx) { - if (!mentionActiveRef.current) { - mentionActiveRef.current = true; - mentionSessionQueryRef.current = ctx.query; + const applyContext = (ctx: MentionContext | null) => { + if (ctx) { + if (!mentionActiveRef.current) { + mentionActiveRef.current = true; + mentionSessionQueryRef.current = ctx.query; + setMentionCtx(ctx); + setHighlightIdx(0); + startMentionSession(ctx); + return; + } setMentionCtx(ctx); - setHighlightIdx(0); - startMentionSession(ctx); - return; - } - setMentionCtx(ctx); - if (ctx.query !== mentionSessionQueryRef.current) { - mentionSessionQueryRef.current = ctx.query; - setHighlightIdx(0); - } - } else if (mentionActiveRef.current) { - closeMentionSession(); - } - }; - - applyContext(detectMention(el, enabledSkills.length > 0)); - window.requestAnimationFrame(() => { - const nextEl = editorRef.current; - if (!nextEl || document.activeElement !== nextEl) return; - applyContext(detectMention(nextEl, enabledSkills.length > 0)); - }); - }, [closeMentionSession, enabledSkills.length, startMentionSession]); - - useImperativeHandle( - ref, - () => ({ - getText: () => { - const el = editorRef.current; - if (!el) return ""; - return serializeChildren(el, largePastesRef.current).replace(/\u00A0/g, " "); - }, - getDraft: buildDraft, - hasContent: () => !buildDraft().isEmpty, - setText: (text: string) => { - const el = editorRef.current; - if (!el) return; - el.innerHTML = ""; - largePastesRef.current.clear(); - if (isLargePasteText(text)) { - insertLargePaste(text); - } else { - el.innerText = text; + if (ctx.query !== mentionSessionQueryRef.current) { + mentionSessionQueryRef.current = ctx.query; + setHighlightIdx(0); + } + } else if (mentionActiveRef.current) { closeMentionSession(); - refreshEmptyState(); } - }, - setDraft: (draft: MentionComposerDraft) => { - const el = editorRef.current; - if (!el) return; - el.innerHTML = ""; - largePastesRef.current.clear(); + }; - if (draft.segments.length === 0 && draft.text) { - if (isLargePasteText(draft.text)) { - insertLargePaste(draft.text); - return; + applyContext(detectMention(el, enabledSkills.length > 0)); + window.requestAnimationFrame(() => { + const nextEl = editorRef.current; + if (!nextEl || document.activeElement !== nextEl) return; + applyContext(detectMention(nextEl, enabledSkills.length > 0)); + }); + }, [closeMentionSession, enabledSkills.length, startMentionSession]); + + useImperativeHandle( + ref, + () => ({ + getText: () => { + const el = editorRef.current; + if (!el) return ""; + return serializeChildren(el, largePastesRef.current).replace(/\u00A0/g, " "); + }, + getDraft: buildDraft, + hasContent: () => !buildDraft().isEmpty, + setText: (text: string) => { + const el = editorRef.current; + if (!el) return; + el.innerHTML = ""; + largePastesRef.current.clear(); + if (isLargePasteText(text)) { + insertLargePaste(text); + } else { + el.innerText = text; + closeMentionSession(); + refreshEmptyState(); } - el.innerText = draft.text; - } else { - for (const segment of draft.segments) { - if (segment.type === "largePaste") { - largePastesRef.current.set(segment.paste.id, segment.paste); - el.appendChild(createLargePasteChip(segment.paste)); - } else if (segment.type === "skillMention") { - el.appendChild(createSkillMentionChip(segment.skill)); - } else if (segment.text) { - el.appendChild(document.createTextNode(segment.text)); + }, + setDraft: (draft: MentionComposerDraft) => { + const el = editorRef.current; + if (!el) return; + el.innerHTML = ""; + largePastesRef.current.clear(); + + if (draft.segments.length === 0 && draft.text) { + if (isLargePasteText(draft.text)) { + insertLargePaste(draft.text); + return; } + el.innerText = draft.text; + } else { + for (const segment of draft.segments) { + if (segment.type === "largePaste") { + largePastesRef.current.set(segment.paste.id, segment.paste); + el.appendChild(createLargePasteChip(segment.paste)); + } else if (segment.type === "skillMention") { + el.appendChild(createSkillMentionChip(segment.skill)); + } else if (segment.text) { + el.appendChild(document.createTextNode(segment.text)); + } + } + largePasteCounterRef.current = Math.max( + largePasteCounterRef.current, + largePastesRef.current.size, + ); } - largePasteCounterRef.current = Math.max( - largePasteCounterRef.current, - largePastesRef.current.size, - ); - } + closeMentionSession(); + refreshEmptyState(); + }, + clear: () => { + const el = editorRef.current; + if (!el) return; + el.innerHTML = ""; + largePastesRef.current.clear(); + closeMentionSession(); + refreshEmptyState(); + }, + focus: () => editorRef.current?.focus(), + }), + [buildDraft, closeMentionSession, insertLargePaste, refreshEmptyState], + ); + + // ---- Select suggestion ---- + const selectSuggestion = useCallback( + (suggestion: MentionSuggestion) => { + if (!mentionCtx) return; + if (suggestion.type === "skill") { + insertSkillMentionChip(mentionCtx, suggestion.skill); + } else { + insertMentionChip(mentionCtx, suggestion.entry.path, suggestion.entry.kind); + } closeMentionSession(); refreshEmptyState(); + editorRef.current?.focus(); }, - clear: () => { - const el = editorRef.current; - if (!el) return; - el.innerHTML = ""; - largePastesRef.current.clear(); - closeMentionSession(); - refreshEmptyState(); - }, - focus: () => editorRef.current?.focus(), - }), - [buildDraft, closeMentionSession, insertLargePaste, refreshEmptyState], - ); + [closeMentionSession, mentionCtx, refreshEmptyState], + ); - // ---- Select suggestion ---- - const selectSuggestion = useCallback( - (suggestion: MentionSuggestion) => { - if (!mentionCtx) return; - if (suggestion.type === "skill") { - insertSkillMentionChip(mentionCtx, suggestion.skill); - } else { - insertMentionChip(mentionCtx, suggestion.entry.path, suggestion.entry.kind); - } - closeMentionSession(); + // ---- Event handlers ---- + const handleInput = useCallback(() => { refreshEmptyState(); - editorRef.current?.focus(); - }, - [closeMentionSession, mentionCtx, refreshEmptyState], - ); - - // ---- Event handlers ---- - const handleInput = useCallback(() => { - refreshEmptyState(); - if (!isComposingRef.current) { - refreshMention(); - } - }, [refreshEmptyState, refreshMention]); - - const handleKeyUp = useCallback((e: KeyboardEvent) => { - if (disabled || isComposingRef.current || isImeKeyboardEvent(e)) return; - if ( - e.key === "ArrowDown" || - e.key === "ArrowUp" || - e.key === "Tab" || - e.key === "Enter" || - e.key === "Escape" - ) { - return; - } - refreshMention(); - }, [disabled, refreshMention]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (disabled) { - e.preventDefault(); - return; + if (!isComposingRef.current) { + refreshMention(); } - const isEnter = isEnterKeyboardEvent(e); - const isActiveCompositionKey = isComposingRef.current || isActiveImeKeyboardEvent(e); - const hasLegacyImeSignal = hasLegacyImeKeyboardSignal(e); + }, [refreshEmptyState, refreshMention]); + + const handleKeyUp = useCallback( + (e: KeyboardEvent) => { + if (disabled || isComposingRef.current || isImeKeyboardEvent(e)) return; + if ( + e.key === "ArrowDown" || + e.key === "ArrowUp" || + e.key === "Tab" || + e.key === "Enter" || + e.key === "Escape" + ) { + return; + } + refreshMention(); + }, + [disabled, refreshMention], + ); - if (isActiveCompositionKey) { - if (isEnter && !e.shiftKey) { - compositionEnterKeyRef.current = true; + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (disabled) { + e.preventDefault(); + return; + } + const isEnter = isEnterKeyboardEvent(e); + const isActiveCompositionKey = isComposingRef.current || isActiveImeKeyboardEvent(e); + const hasLegacyImeSignal = hasLegacyImeKeyboardSignal(e); + + if (isActiveCompositionKey) { + if (isEnter && !e.shiftKey) { + compositionEnterKeyRef.current = true; + refreshEmptyState(); + refreshMention(); + } else { + compositionEnterKeyRef.current = false; + } + return; + } + + if (isEnter && !e.shiftKey && imeEnterSuppressUntilRef.current >= performance.now()) { + e.preventDefault(); + imeEnterSuppressUntilRef.current = 0; + compositionEnterKeyRef.current = false; + lastCompositionEndAtRef.current = 0; refreshEmptyState(); refreshMention(); - } else { + return; + } + + const compositionEndedAgoMs = performance.now() - lastCompositionEndAtRef.current; + if ( + isEnter && + !e.shiftKey && + lastCompositionEndAtRef.current > 0 && + compositionEndedAgoMs >= 0 && + compositionEndedAgoMs <= IME_COMPOSITION_END_ENTER_TAIL_MS + ) { + e.preventDefault(); + imeEnterSuppressUntilRef.current = 0; compositionEnterKeyRef.current = false; + lastCompositionEndAtRef.current = 0; + refreshEmptyState(); + refreshMention(); + return; } - return; - } - if (isEnter && !e.shiftKey && imeEnterSuppressUntilRef.current >= performance.now()) { - e.preventDefault(); - imeEnterSuppressUntilRef.current = 0; - compositionEnterKeyRef.current = false; - lastCompositionEndAtRef.current = 0; - refreshEmptyState(); - refreshMention(); - return; - } + // Legacy keyCode 229 is too broad in WebViews. It is still useful for + // ignoring non-Enter IME key noise, but should not block normal sending. + if (!isEnter && hasLegacyImeSignal) { + return; + } - const compositionEndedAgoMs = performance.now() - lastCompositionEndAtRef.current; - if ( - isEnter && - !e.shiftKey && - lastCompositionEndAtRef.current > 0 && - compositionEndedAgoMs >= 0 && - compositionEndedAgoMs <= IME_COMPOSITION_END_ENTER_TAIL_MS - ) { - e.preventDefault(); - imeEnterSuppressUntilRef.current = 0; - compositionEnterKeyRef.current = false; - lastCompositionEndAtRef.current = 0; - refreshEmptyState(); - refreshMention(); - return; - } + // Popup navigation + if (popupVisible && suggestions.length > 0) { + if (e.key === "ArrowDown") { + e.preventDefault(); + setHighlightIdx((p) => (p + 1) % suggestions.length); + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + setHighlightIdx((p) => (p - 1 + suggestions.length) % suggestions.length); + return; + } + if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) { + e.preventDefault(); + if (suggestions[highlightIdx]) { + selectSuggestion(suggestions[highlightIdx]); + } + return; + } + } + if (popupVisible && e.key === "Escape") { + e.preventDefault(); + closeMentionSession(); + return; + } - // Legacy keyCode 229 is too broad in WebViews. It is still useful for - // ignoring non-Enter IME key noise, but should not block normal sending. - if (!isEnter && hasLegacyImeSignal) { - return; - } + // Backspace: delete mention chip if cursor is right after one + if (e.key === "Backspace") { + const chip = chipBeforeCursor(editorRef.current!); + if (chip) { + e.preventDefault(); + const largePasteId = chip.getAttribute(LARGE_PASTE_TAG_ATTR); + if (largePasteId) { + largePastesRef.current.delete(largePasteId); + } + chip.remove(); + refreshEmptyState(); + refreshMention(); + return; + } + } - // Popup navigation - if (popupVisible && suggestions.length > 0) { - if (e.key === "ArrowDown") { + // Normal Enter → send + if (isEnter && !e.shiftKey) { + imeEnterSuppressUntilRef.current = 0; + compositionEnterKeyRef.current = false; + lastCompositionEndAtRef.current = 0; e.preventDefault(); - setHighlightIdx((p) => (p + 1) % suggestions.length); + onSend(); return; } - if (e.key === "ArrowUp") { + + // Shift+Enter → line break (normalise to
) + if (isEnter && e.shiftKey) { + imeEnterSuppressUntilRef.current = 0; + compositionEnterKeyRef.current = false; + lastCompositionEndAtRef.current = 0; e.preventDefault(); - setHighlightIdx((p) => (p - 1 + suggestions.length) % suggestions.length); + document.execCommand("insertLineBreak"); + scheduleComposerSelectionScroll(editorRef.current); + refreshEmptyState(); + refreshMention(); return; } - if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) { + }, + [ + popupVisible, + suggestions, + highlightIdx, + selectSuggestion, + disabled, + closeMentionSession, + onSend, + refreshEmptyState, + refreshMention, + ], + ); + + const handlePaste = useCallback( + (e: ClipboardEvent) => { + if (disabled) { e.preventDefault(); - if (suggestions[highlightIdx]) { - selectSuggestion(suggestions[highlightIdx]); - } return; } - } - if (popupVisible && e.key === "Escape") { - e.preventDefault(); - closeMentionSession(); - return; - } - - // Backspace: delete mention chip if cursor is right after one - if (e.key === "Backspace") { - const chip = chipBeforeCursor(editorRef.current!); - if (chip) { + const clipboardFiles = extractClipboardFiles(e.clipboardData); + if (clipboardFiles.length > 0) { e.preventDefault(); - const largePasteId = chip.getAttribute(LARGE_PASTE_TAG_ATTR); - if (largePasteId) { - largePastesRef.current.delete(largePasteId); - } - chip.remove(); - refreshEmptyState(); - refreshMention(); + onPasteFiles?.(clipboardFiles); return; } - } - - // Normal Enter → send - if (isEnter && !e.shiftKey) { - imeEnterSuppressUntilRef.current = 0; - compositionEnterKeyRef.current = false; - lastCompositionEndAtRef.current = 0; e.preventDefault(); - onSend(); - return; - } - - // Shift+Enter → line break (normalise to
) - if (isEnter && e.shiftKey) { - imeEnterSuppressUntilRef.current = 0; - compositionEnterKeyRef.current = false; - lastCompositionEndAtRef.current = 0; - e.preventDefault(); - document.execCommand("insertLineBreak"); - scheduleComposerSelectionScroll(editorRef.current); + const text = e.clipboardData.getData("text/plain"); + if (isLargePasteText(text)) { + insertLargePaste(text); + return; + } + document.execCommand("insertText", false, text); refreshEmptyState(); refreshMention(); - return; - } - }, - [ - popupVisible, - suggestions, - highlightIdx, - selectSuggestion, - disabled, - closeMentionSession, - onSend, - refreshEmptyState, - refreshMention, - ], - ); + }, + [disabled, insertLargePaste, onPasteFiles, refreshEmptyState, refreshMention], + ); - const handlePaste = useCallback( - (e: ClipboardEvent) => { - if (disabled) { - e.preventDefault(); - return; - } - const clipboardFiles = extractClipboardFiles(e.clipboardData); - if (clipboardFiles.length > 0) { - e.preventDefault(); - onPasteFiles?.(clipboardFiles); - return; + const handleCompositionStart = useCallback(() => { + isComposingRef.current = true; + compositionEnterKeyRef.current = false; + lastCompositionEndAtRef.current = 0; + imeEnterSuppressUntilRef.current = 0; + if (busyReleaseTimerRef.current !== null) { + window.clearTimeout(busyReleaseTimerRef.current); + busyReleaseTimerRef.current = null; } - e.preventDefault(); - const text = e.clipboardData.getData("text/plain"); - if (isLargePasteText(text)) { - insertLargePaste(text); - return; + setBusy(true); + }, [setBusy]); + + const handleCompositionEnd = useCallback(() => { + isComposingRef.current = false; + lastCompositionEndAtRef.current = performance.now(); + if (compositionEnterKeyRef.current) { + imeEnterSuppressUntilRef.current = performance.now() + IME_ENTER_SUPPRESS_WINDOW_MS; + compositionEnterKeyRef.current = false; } - document.execCommand("insertText", false, text); refreshEmptyState(); refreshMention(); - }, - [disabled, insertLargePaste, onPasteFiles, refreshEmptyState, refreshMention], - ); + scheduleBusyRelease(); + }, [refreshEmptyState, refreshMention, scheduleBusyRelease]); - const handleCompositionStart = useCallback(() => { - isComposingRef.current = true; - compositionEnterKeyRef.current = false; - lastCompositionEndAtRef.current = 0; - imeEnterSuppressUntilRef.current = 0; - if (busyReleaseTimerRef.current !== null) { - window.clearTimeout(busyReleaseTimerRef.current); - busyReleaseTimerRef.current = null; - } - setBusy(true); - }, [setBusy]); - - const handleCompositionEnd = useCallback(() => { - isComposingRef.current = false; - lastCompositionEndAtRef.current = performance.now(); - if (compositionEnterKeyRef.current) { - imeEnterSuppressUntilRef.current = performance.now() + IME_ENTER_SUPPRESS_WINDOW_MS; + const handleBlur = useCallback(() => { + isComposingRef.current = false; compositionEnterKeyRef.current = false; - } - refreshEmptyState(); - refreshMention(); - scheduleBusyRelease(); - }, [refreshEmptyState, refreshMention, scheduleBusyRelease]); - - const handleBlur = useCallback(() => { - isComposingRef.current = false; - compositionEnterKeyRef.current = false; - lastCompositionEndAtRef.current = 0; - imeEnterSuppressUntilRef.current = 0; - if (busyReleaseTimerRef.current !== null) { - window.clearTimeout(busyReleaseTimerRef.current); - busyReleaseTimerRef.current = null; - } - setBusy(false); - closeMentionSession(); - }, [closeMentionSession, setBusy]); - - return ( -
- {popupVisible && ( - - )} -
+ {popupVisible && ( + )} - data-placeholder={placeholder} - /> -
- ); -})); +
+
+ ); + }), +); MentionComposer.displayName = "MentionComposer"; diff --git a/crates/agent-gui/src/components/chat/NotifyToast.tsx b/crates/agent-gui/src/components/chat/NotifyToast.tsx index 15c2d2849..75aeb6981 100644 --- a/crates/agent-gui/src/components/chat/NotifyToast.tsx +++ b/crates/agent-gui/src/components/chat/NotifyToast.tsx @@ -1,5 +1,5 @@ import { memo, useEffect, useRef } from "react"; -import { AlertTriangle, XCircle, X } from "../icons"; +import { AlertTriangle, X, XCircle } from "../icons"; export type NotifyItem = { id: string; @@ -64,9 +64,7 @@ const ToastEntry = memo(function ToastEntry(props: { )}

{item.message} diff --git a/crates/agent-gui/src/components/chat/SharedHistoryManagerModal.tsx b/crates/agent-gui/src/components/chat/SharedHistoryManagerModal.tsx index 3d1bc3ad4..cfafc4ac1 100644 --- a/crates/agent-gui/src/components/chat/SharedHistoryManagerModal.tsx +++ b/crates/agent-gui/src/components/chat/SharedHistoryManagerModal.tsx @@ -1,10 +1,12 @@ import { useMemo, useState } from "react"; import { createPortal } from "react-dom"; +import type { ChatHistorySummary } from "../../lib/chat/history/chatHistory"; +import { cn } from "../../lib/shared/utils"; import { Check, Copy, - Eye, ExternalLink, + Eye, EyeOff, Link2, Loader2, @@ -12,9 +14,6 @@ import { Share2, X, } from "../icons"; - -import type { ChatHistorySummary } from "../../lib/chat/history/chatHistory"; -import { cn } from "../../lib/shared/utils"; import { Button } from "../ui/button"; export type ManagedHistoryShareStatus = { @@ -103,10 +102,7 @@ function formatConversationTime(timestamp?: number) { }).format(new Date(timestamp)); } -function ShareSwitch(props: { - disabled: boolean; - onDisable: () => void; -}) { +function ShareSwitch(props: { disabled: boolean; onDisable: () => void }) { const { disabled, onDisable } = props; return (

-
{conversations.length}
+
+ {conversations.length} +
@@ -350,7 +348,7 @@ export function SharedHistoryManagerModal({
{filteredConversations.map((conversation) => { const status = statuses[conversation.id]; - const token = status?.enabled === true ? status.token?.trim() ?? "" : ""; + const token = status?.enabled === true ? (status.token?.trim() ?? "") : ""; const redactToolContent = isShareStatusRedacted(status); const shareUrl = buildShareUrl(token, publicOrigin); const isLoading = loadingIds.has(conversation.id); @@ -388,7 +386,9 @@ export function SharedHistoryManagerModal({
- {isLoading ? : null} + {isLoading ? ( + + ) : null} onDisableShare(conversation)} diff --git a/crates/agent-gui/src/components/chat/fileTypeIcons.tsx b/crates/agent-gui/src/components/chat/fileTypeIcons.tsx index 08847be71..2b133c878 100644 --- a/crates/agent-gui/src/components/chat/fileTypeIcons.tsx +++ b/crates/agent-gui/src/components/chat/fileTypeIcons.tsx @@ -1,53 +1,52 @@ import type { ComponentType, SVGProps } from "react"; - -import FileTypeTypescript from "~icons/vscode-icons/file-type-typescript-official"; -import FileTypeReactTs from "~icons/vscode-icons/file-type-reactts"; -import FileTypeJs from "~icons/vscode-icons/file-type-js-official"; -import FileTypeReactJs from "~icons/vscode-icons/file-type-reactjs"; -import FileTypePython from "~icons/vscode-icons/file-type-python"; -import FileTypeRust from "~icons/vscode-icons/file-type-rust"; +import DefaultFile from "~icons/vscode-icons/default-file"; +import DefaultFolder from "~icons/vscode-icons/default-folder"; +import FileTypeAudio from "~icons/vscode-icons/file-type-audio"; +import FileTypeBat from "~icons/vscode-icons/file-type-bat"; +import FileTypeBinary from "~icons/vscode-icons/file-type-binary"; +import FileTypeC from "~icons/vscode-icons/file-type-c"; +import FileTypeConfig from "~icons/vscode-icons/file-type-config"; +import FileTypeCpp from "~icons/vscode-icons/file-type-cpp"; +import FileTypeCsharp from "~icons/vscode-icons/file-type-csharp"; +import FileTypeCss from "~icons/vscode-icons/file-type-css"; +import FileTypeDart from "~icons/vscode-icons/file-type-dartlang"; +import FileTypeDocker from "~icons/vscode-icons/file-type-docker"; +import FileTypeExcel from "~icons/vscode-icons/file-type-excel"; +import FileTypeFont from "~icons/vscode-icons/file-type-font"; +import FileTypeGit from "~icons/vscode-icons/file-type-git"; import FileTypeGo from "~icons/vscode-icons/file-type-go"; -import FileTypeJava from "~icons/vscode-icons/file-type-java"; import FileTypeHtml from "~icons/vscode-icons/file-type-html"; -import FileTypeCss from "~icons/vscode-icons/file-type-css"; +import FileTypeImage from "~icons/vscode-icons/file-type-image"; +import FileTypeJava from "~icons/vscode-icons/file-type-java"; +import FileTypeJs from "~icons/vscode-icons/file-type-js-official"; import FileTypeJson from "~icons/vscode-icons/file-type-json"; +import FileTypeKotlin from "~icons/vscode-icons/file-type-kotlin"; +import FileTypeLicense from "~icons/vscode-icons/file-type-license"; +import FileTypeLog from "~icons/vscode-icons/file-type-log"; import FileTypeMarkdown from "~icons/vscode-icons/file-type-markdown"; -import FileTypeYaml from "~icons/vscode-icons/file-type-yaml"; -import FileTypeToml from "~icons/vscode-icons/file-type-toml"; -import FileTypeVue from "~icons/vscode-icons/file-type-vue"; -import FileTypeSvelte from "~icons/vscode-icons/file-type-svelte"; -import FileTypeShell from "~icons/vscode-icons/file-type-shell"; -import FileTypeImage from "~icons/vscode-icons/file-type-image"; -import FileTypeSvg from "~icons/vscode-icons/file-type-svg"; import FileTypePdf from "~icons/vscode-icons/file-type-pdf2"; -import FileTypeZip from "~icons/vscode-icons/file-type-zip"; -import FileTypeXml from "~icons/vscode-icons/file-type-xml"; -import FileTypeSql from "~icons/vscode-icons/file-type-sql"; -import FileTypeC from "~icons/vscode-icons/file-type-c"; -import FileTypeCpp from "~icons/vscode-icons/file-type-cpp"; -import FileTypeCsharp from "~icons/vscode-icons/file-type-csharp"; import FileTypePhp from "~icons/vscode-icons/file-type-php"; +import FileTypePowerpoint from "~icons/vscode-icons/file-type-powerpoint"; +import FileTypePowershell from "~icons/vscode-icons/file-type-powershell"; +import FileTypePython from "~icons/vscode-icons/file-type-python"; +import FileTypeReactJs from "~icons/vscode-icons/file-type-reactjs"; +import FileTypeReactTs from "~icons/vscode-icons/file-type-reactts"; import FileTypeRuby from "~icons/vscode-icons/file-type-ruby"; +import FileTypeRust from "~icons/vscode-icons/file-type-rust"; +import FileTypeShell from "~icons/vscode-icons/file-type-shell"; +import FileTypeSql from "~icons/vscode-icons/file-type-sql"; +import FileTypeSvelte from "~icons/vscode-icons/file-type-svelte"; +import FileTypeSvg from "~icons/vscode-icons/file-type-svg"; import FileTypeSwift from "~icons/vscode-icons/file-type-swift"; -import FileTypeKotlin from "~icons/vscode-icons/file-type-kotlin"; -import FileTypeDart from "~icons/vscode-icons/file-type-dartlang"; -import FileTypeLog from "~icons/vscode-icons/file-type-log"; -import FileTypeExcel from "~icons/vscode-icons/file-type-excel"; -import FileTypeWord from "~icons/vscode-icons/file-type-word"; -import FileTypePowerpoint from "~icons/vscode-icons/file-type-powerpoint"; import FileTypeText from "~icons/vscode-icons/file-type-text"; -import FileTypeFont from "~icons/vscode-icons/file-type-font"; +import FileTypeToml from "~icons/vscode-icons/file-type-toml"; +import FileTypeTypescript from "~icons/vscode-icons/file-type-typescript-official"; import FileTypeVideo from "~icons/vscode-icons/file-type-video"; -import FileTypeAudio from "~icons/vscode-icons/file-type-audio"; -import FileTypeBat from "~icons/vscode-icons/file-type-bat"; -import FileTypePowershell from "~icons/vscode-icons/file-type-powershell"; -import FileTypeDocker from "~icons/vscode-icons/file-type-docker"; -import FileTypeGit from "~icons/vscode-icons/file-type-git"; -import FileTypeLicense from "~icons/vscode-icons/file-type-license"; -import FileTypeConfig from "~icons/vscode-icons/file-type-config"; -import FileTypeBinary from "~icons/vscode-icons/file-type-binary"; -import DefaultFile from "~icons/vscode-icons/default-file"; -import DefaultFolder from "~icons/vscode-icons/default-folder"; +import FileTypeVue from "~icons/vscode-icons/file-type-vue"; +import FileTypeWord from "~icons/vscode-icons/file-type-word"; +import FileTypeXml from "~icons/vscode-icons/file-type-xml"; +import FileTypeYaml from "~icons/vscode-icons/file-type-yaml"; +import FileTypeZip from "~icons/vscode-icons/file-type-zip"; type IconSource = ComponentType>; diff --git a/crates/agent-gui/src/components/cron/CronPromptRunner.tsx b/crates/agent-gui/src/components/cron/CronPromptRunner.tsx index 3a61ea45a..82a2b3f03 100644 --- a/crates/agent-gui/src/components/cron/CronPromptRunner.tsx +++ b/crates/agent-gui/src/components/cron/CronPromptRunner.tsx @@ -1,24 +1,23 @@ -import { useEffect, useRef } from "react"; +import type { Context } from "@mariozechner/pi-ai"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import type { Context } from "@mariozechner/pi-ai"; - -import { createStreamDebugLogger } from "../../lib/debug/agentDebug"; +import { useEffect, useRef } from "react"; import { runAssistantWithTools } from "../../lib/chat/runner/agentRunner"; +import { createStreamDebugLogger } from "../../lib/debug/agentDebug"; import { assistantMessageToText } from "../../lib/providers/llm"; +import { + type AppSettings, + DEFAULT_CHAT_RUNTIME_CONTROLS, + findProviderModelConfig, + isAgentDevMode, + isAgentExecutionMode, +} from "../../lib/settings"; import { buildSkillsSystemPrompt, discoverSkills, isAlwaysEnabledSkillName, type SkillSummary, } from "../../lib/skills"; -import { - DEFAULT_CHAT_RUNTIME_CONTROLS, - findProviderModelConfig, - isAgentExecutionMode, - isAgentDevMode, - type AppSettings, -} from "../../lib/settings"; import { buildBuiltinToolRegistry } from "../../lib/tools/builtinRegistry"; import { createFileToolState } from "../../lib/tools/fileToolState"; import type { SkillAccessPolicy } from "../../lib/tools/skillAccessPolicy"; @@ -274,10 +273,7 @@ function warnIfAlreadyFinishedCompletion( executionId: string, ) { if (result.status === "already_finished") { - console.warn( - "Cron Auto Prompt completion reached an already-finished run", - executionId, - ); + console.warn("Cron Auto Prompt completion reached an already-finished run", executionId); } } @@ -321,10 +317,7 @@ function remainingPromptRunTimeoutMs(startedAt: number) { return CRON_PROMPT_TIMEOUT_MS - elapsed; } -function abortPromptExecution( - executionId: string, - reason: "local_timeout" | "server_expired", -) { +function abortPromptExecution(executionId: string, reason: "local_timeout" | "server_expired") { const normalizedExecutionId = normalizeExecutionId(executionId); if (!normalizedExecutionId) { return; @@ -359,9 +352,7 @@ function registerLocalPromptTimeout( executionTimeoutHandles.set(normalizedExecutionId, timer); } -function normalizeQueuedCompletion( - value: unknown, -): CronCompletePromptRunInput | null { +function normalizeQueuedCompletion(value: unknown): CronCompletePromptRunInput | null { if (!value || typeof value !== "object") { return null; } diff --git a/crates/agent-gui/src/components/hub/HubChrome.tsx b/crates/agent-gui/src/components/hub/HubChrome.tsx index ada50b122..2bab06207 100644 --- a/crates/agent-gui/src/components/hub/HubChrome.tsx +++ b/crates/agent-gui/src/components/hub/HubChrome.tsx @@ -1,9 +1,8 @@ -import { type ReactNode } from "react"; - -import { PanelLeft } from "../icons"; -import { Button } from "../ui/button"; +import type { ReactNode } from "react"; import { useLocale } from "../../i18n"; import { cn } from "../../lib/shared/utils"; +import { PanelLeft } from "../icons"; +import { Button } from "../ui/button"; export function HubBackdrop(props: { tone?: "amber" | "violet" | "neutral" }) { const { tone = "neutral" } = props; diff --git a/crates/agent-gui/src/components/icons.tsx b/crates/agent-gui/src/components/icons.tsx index 54ed07fec..8dcade24e 100644 --- a/crates/agent-gui/src/components/icons.tsx +++ b/crates/agent-gui/src/components/icons.tsx @@ -1,8 +1,6 @@ import type { ComponentType, SVGProps } from "react"; - -import AlertTriangleSource from "~icons/lucide/triangle-alert"; import ClaudeSource from "~icons/logos/claude-icon"; -import FileTypeGeminiSource from "~icons/vscode-icons/file-type-gemini"; +import OpenAISource from "~icons/logos/openai-icon"; import ArrowLeftSource from "~icons/lucide/arrow-left"; import BanSource from "~icons/lucide/ban"; import BookOpenSource from "~icons/lucide/book-open"; @@ -10,35 +8,38 @@ import BotSource from "~icons/lucide/bot"; import BrainSource from "~icons/lucide/brain"; import BrushCleaningSource from "~icons/lucide/brush-cleaning"; import CheckSource from "~icons/lucide/check"; -import CheckCircle2Source from "~icons/lucide/circle-check"; import ChevronDownSource from "~icons/lucide/chevron-down"; import ChevronRightSource from "~icons/lucide/chevron-right"; import ChevronUpSource from "~icons/lucide/chevron-up"; import CircleSource from "~icons/lucide/circle"; +import CheckCircle2Source from "~icons/lucide/circle-check"; +import XCircleSource from "~icons/lucide/circle-x"; import Clock3Source from "~icons/lucide/clock-3"; import CloudSource from "~icons/lucide/cloud"; import CopySource from "~icons/lucide/copy"; import CpuSource from "~icons/lucide/cpu"; import DownloadSource from "~icons/lucide/download"; -import Edit3Source from "~icons/lucide/pen-line"; +import Globe2Source from "~icons/lucide/earth"; +import MoreHorizontalSource from "~icons/lucide/ellipsis"; import ExternalLinkSource from "~icons/lucide/external-link"; import EyeSource from "~icons/lucide/eye"; import EyeOffSource from "~icons/lucide/eye-off"; import FileSource from "~icons/lucide/file"; +import fileIconSvgSource from "~icons/lucide/file?raw"; import FilePenLineSource from "~icons/lucide/file-pen-line"; import FileTextSource from "~icons/lucide/file-text"; import FolderSource from "~icons/lucide/folder"; +import folderIconSvgSource from "~icons/lucide/folder?raw"; import FolderOpenSource from "~icons/lucide/folder-open"; import FolderTreeSource from "~icons/lucide/folder-tree"; import GlobeSource from "~icons/lucide/globe"; -import Globe2Source from "~icons/lucide/earth"; import HistorySource from "~icons/lucide/history"; import ImageIconSource from "~icons/lucide/image"; import ImageOffSource from "~icons/lucide/image-off"; import InfoSource from "~icons/lucide/info"; import KeySource from "~icons/lucide/key"; -import Link2Source from "~icons/lucide/link-2"; import LightbulbSource from "~icons/lucide/lightbulb"; +import Link2Source from "~icons/lucide/link-2"; import Loader2Source from "~icons/lucide/loader-circle"; import LoaderCircleSource from "~icons/lucide/loader-circle"; import LockSource from "~icons/lucide/lock"; @@ -49,11 +50,10 @@ import Minimize2Source from "~icons/lucide/minimize-2"; import MinusSource from "~icons/lucide/minus"; import MonitorSmartphoneSource from "~icons/lucide/monitor-smartphone"; import MoonSource from "~icons/lucide/moon"; -import MoreHorizontalSource from "~icons/lucide/ellipsis"; -import OpenAISource from "~icons/logos/openai-icon"; import PanelLeftSource from "~icons/lucide/panel-left"; import PanelLeftCloseSource from "~icons/lucide/panel-left-close"; import PaperclipSource from "~icons/lucide/paperclip"; +import Edit3Source from "~icons/lucide/pen-line"; import PencilSource from "~icons/lucide/pencil"; import PinSource from "~icons/lucide/pin"; import PinOffSource from "~icons/lucide/pin-off"; @@ -76,15 +76,14 @@ import SquarePenSource from "~icons/lucide/square-pen"; import SunSource from "~icons/lucide/sun"; import TerminalSource from "~icons/lucide/terminal"; import Trash2Source from "~icons/lucide/trash-2"; +import AlertTriangleSource from "~icons/lucide/triangle-alert"; import UploadSource from "~icons/lucide/upload"; import WifiSource from "~icons/lucide/wifi"; import WifiOffSource from "~icons/lucide/wifi-off"; import WrenchSource from "~icons/lucide/wrench"; import XSource from "~icons/lucide/x"; -import XCircleSource from "~icons/lucide/circle-x"; import ZapSource from "~icons/lucide/zap"; -import fileIconSvgSource from "~icons/lucide/file?raw"; -import folderIconSvgSource from "~icons/lucide/folder?raw"; +import FileTypeGeminiSource from "~icons/vscode-icons/file-type-gemini"; type IconSource = ComponentType & { title?: string }>; @@ -97,7 +96,13 @@ type IconProps = SVGProps & { export type IconComponent = ComponentType; function createIcon(Source: IconSource): IconComponent { - return function Icon({ absoluteStrokeWidth: _absoluteStrokeWidth, height, size, width, ...props }) { + return function Icon({ + absoluteStrokeWidth: _absoluteStrokeWidth, + height, + size, + width, + ...props + }) { const nextProps: IconProps = { ...props }; if (size !== undefined) { nextProps.width = width ?? size; @@ -195,4 +200,5 @@ export const XCircle = createIcon(XCircleSource); export const Zap = createIcon(ZapSource); const fileIconSvg = fileIconSvgSource as unknown as string; const folderIconSvg = folderIconSvgSource as unknown as string; + export { fileIconSvg, folderIconSvg }; diff --git a/crates/agent-gui/src/components/memory/MemoryOrganizerRunner.tsx b/crates/agent-gui/src/components/memory/MemoryOrganizerRunner.tsx index e27147146..5a67a6294 100644 --- a/crates/agent-gui/src/components/memory/MemoryOrganizerRunner.tsx +++ b/crates/agent-gui/src/components/memory/MemoryOrganizerRunner.tsx @@ -1,30 +1,29 @@ -import { useEffect, useRef } from "react"; -import { Type } from "@sinclair/typebox"; import type { Context, Tool, ToolCall, ToolResultMessage } from "@mariozechner/pi-ai"; - -import { createStreamDebugLogger } from "../../lib/debug/agentDebug"; +import { Type } from "@sinclair/typebox"; +import { useEffect, useRef } from "react"; import { runAssistantWithTools } from "../../lib/chat/runner/agentRunner"; -import { assistantMessageToText } from "../../lib/providers/llm"; -import { - DEFAULT_CHAT_RUNTIME_CONTROLS, - computeNextMemoryOrganizerRunAt, - findProviderModelConfig, - isAgentDevMode, - type AppSettings, - type MemoryOrganizerMode, -} from "../../lib/settings"; +import { createStreamDebugLogger } from "../../lib/debug/agentDebug"; import { + type MemoryBatchResponse, + type MemoryMeta, + type MemoryOrganizeRun, + type MemoryType, memoryApplyBatch, memoryList, memoryOrganizeDueClaim, memoryOrganizeDueComplete, memoryOrganizeRunUpdate, memoryRead, - type MemoryBatchResponse, - type MemoryMeta, - type MemoryOrganizeRun, - type MemoryType, } from "../../lib/memory/api"; +import { assistantMessageToText } from "../../lib/providers/llm"; +import { + type AppSettings, + computeNextMemoryOrganizerRunAt, + DEFAULT_CHAT_RUNTIME_CONTROLS, + findProviderModelConfig, + isAgentDevMode, + type MemoryOrganizerMode, +} from "../../lib/settings"; import { createMemoryTools } from "../../lib/tools/memoryTools"; type MemoryOrganizerRunnerProps = { @@ -205,9 +204,7 @@ function stringValue(value: unknown) { } function stringArrayValue(value: unknown) { - return Array.isArray(value) - ? value.map((item) => stringValue(item)).filter(Boolean) - : []; + return Array.isArray(value) ? value.map((item) => stringValue(item)).filter(Boolean) : []; } function recordValue(value: unknown): Record { @@ -265,10 +262,7 @@ function riskRank(value: OrganizerRiskLevel) { return 1; } -function maxRisk( - current: OrganizerRiskLevel, - next: OrganizerRiskLevel, -): OrganizerRiskLevel { +function maxRisk(current: OrganizerRiskLevel, next: OrganizerRiskLevel): OrganizerRiskLevel { return riskRank(next) > riskRank(current) ? next : current; } @@ -277,16 +271,19 @@ function isReviewed(entry: OrganizerEntry | undefined) { } const ORGANIZER_PLAN_DECISION_SCHEMA = Type.Object({ - action: Type.Union([ - Type.Literal("keep"), - Type.Literal("merge_into"), - Type.Literal("delete"), - Type.Literal("mark_review"), - Type.Literal("rewrite_hint"), - ], { - description: - "Decision action. Use merge_into to merge source_slugs into target_slug. Use rewrite_hint instead of emitting a replacement body.", - }), + action: Type.Union( + [ + Type.Literal("keep"), + Type.Literal("merge_into"), + Type.Literal("delete"), + Type.Literal("mark_review"), + Type.Literal("rewrite_hint"), + ], + { + description: + "Decision action. Use merge_into to merge source_slugs into target_slug. Use rewrite_hint instead of emitting a replacement body.", + }, + ), slug: Type.Optional( Type.String({ minLength: 3, @@ -368,8 +365,7 @@ const ORGANIZER_PLAN_TOOL: Tool = { const ORGANIZER_TOPIC_TOOL: Tool = { name: ORGANIZER_TOPIC_TOOL_NAME, - description: - "Submit semantic topic clusters for memory organization. Do not propose edits.", + description: "Submit semantic topic clusters for memory organization. Do not propose edits.", parameters: Type.Object({ topic_clusters: Type.Array( Type.Object({ @@ -489,9 +485,7 @@ function buildStructuralClusters(entries: OrganizerEntry[]): OrganizerCluster[] } const clusters: OrganizerCluster[] = []; for (const [key, group] of groups) { - const sorted = group.sort((a, b) => - a.slug.localeCompare(b.slug) || b.updatedAt - a.updatedAt, - ); + const sorted = group.sort((a, b) => a.slug.localeCompare(b.slug) || b.updatedAt - a.updatedAt); for (let index = 0; index < sorted.length; index += ORGANIZER_CLUSTER_SIZE) { clusters.push({ id: `${key}:${Math.floor(index / ORGANIZER_CLUSTER_SIZE) + 1}`, @@ -540,15 +534,23 @@ function buildTopicClustersFromArgs(args: Record, entries: Orga const clusters: OrganizerCluster[] = []; for (const item of topicClusters) { if (!item || typeof item !== "object" || Array.isArray(item)) continue; - const topic = stringValue((item as { topic?: unknown }).topic) || `topic-${clusters.length + 1}`; + const topic = + stringValue((item as { topic?: unknown }).topic) || `topic-${clusters.length + 1}`; const slugs = uniqueStrings(stringArrayValue((item as { slugs?: unknown }).slugs)) .filter((slug) => bySlug.has(slug) && !used.has(slug)) .slice(0, ORGANIZER_TOPIC_CLUSTER_SIZE); if (slugs.length < 2) continue; for (const slug of slugs) used.add(slug); clusters.push({ - id: `topic:${topic.replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 48) || clusters.length + 1}`, - entries: slugs.map((slug) => bySlug.get(slug)).filter((entry): entry is OrganizerEntry => Boolean(entry)), + id: `topic:${ + topic + .replace(/[^a-z0-9_-]+/gi, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 48) || clusters.length + 1 + }`, + entries: slugs + .map((slug) => bySlug.get(slug)) + .filter((entry): entry is OrganizerEntry => Boolean(entry)), }); } const leftovers = entries.filter((entry) => !used.has(entry.slug)); @@ -592,7 +594,12 @@ function buildGlobalInventory(entries: OrganizerEntry[], clusters: OrganizerClus } const inventory = entries .slice() - .sort((a, b) => a.scope.localeCompare(b.scope) || a.memoryType.localeCompare(b.memoryType) || a.slug.localeCompare(b.slug)) + .sort( + (a, b) => + a.scope.localeCompare(b.scope) || + a.memoryType.localeCompare(b.memoryType) || + a.slug.localeCompare(b.slug), + ) .map((entry) => ({ slug: entry.slug, cluster_id: clusterBySlug.get(entry.slug) || "", @@ -682,7 +689,9 @@ function toolResultMessage( }; } -function normalizeOrganizerPlanArgs(args: Record): Omit { +function normalizeOrganizerPlanArgs( + args: Record, +): Omit { const decisions = Array.isArray(args.decisions) ? args.decisions : []; const normalized: OrganizerPlanDecision[] = []; for (const item of decisions) { @@ -729,10 +738,7 @@ async function runOrganizerModelPrompt(params: { systemPrompt: string; workdir: string; tools: Context["tools"]; - executeToolCall: ( - toolCall: ToolCall, - signal?: AbortSignal, - ) => Promise; + executeToolCall: (toolCall: ToolCall, signal?: AbortSignal) => Promise; signal?: AbortSignal; }) { const { provider, model } = resolveOrganizerProvider(params.run, params.settings); @@ -805,12 +811,7 @@ async function runTopicClusterPrompt(params: { return { args: submittedArgs, raw: clip( - [ - rawText, - "", - `[${ORGANIZER_TOPIC_TOOL_NAME}]`, - JSON.stringify(submittedArgs, null, 2), - ] + [rawText, "", `[${ORGANIZER_TOPIC_TOOL_NAME}]`, JSON.stringify(submittedArgs, null, 2)] .filter((part) => part.trim().length > 0) .join("\n"), ORGANIZER_RAW_PROTOCOL_CHARS, @@ -862,12 +863,7 @@ async function runOrganizerPlanPrompt(params: { summary: plan.summary, compression: plan.compression, raw: clip( - [ - rawText, - "", - `[${ORGANIZER_TOOL_NAME}]`, - JSON.stringify(captured.args ?? plan, null, 2), - ] + [rawText, "", `[${ORGANIZER_TOOL_NAME}]`, JSON.stringify(captured.args ?? plan, null, 2)] .filter((part) => part.trim().length > 0) .join("\n"), ORGANIZER_RAW_PROTOCOL_CHARS, @@ -887,10 +883,7 @@ function emptyRejectionBuckets(): OrganizerRejectionBuckets { }; } -function incrementBucket( - buckets: OrganizerRejectionBuckets, - key: keyof OrganizerRejectionBuckets, -) { +function incrementBucket(buckets: OrganizerRejectionBuckets, key: keyof OrganizerRejectionBuckets) { buckets[key] += 1; } @@ -1004,10 +997,7 @@ function shouldQueueOrganizerDecision(params: { ); } -function addRejectionBucketForReasons( - buckets: OrganizerRejectionBuckets, - reasons: string[], -) { +function addRejectionBucketForReasons(buckets: OrganizerRejectionBuckets, reasons: string[]) { if (reasons.includes("review_required_by_llm")) { incrementBucket(buckets, "reviewRequiredByLlm"); } @@ -1139,48 +1129,48 @@ function buildSafeDecisions(results: ParsedClusterResult[], run: MemoryOrganizeR addRejectionBucketForReasons(rejectionBuckets, risk.reasons); continue; } - const description = item.descriptionHint || targetEntry.description; - const body = synthesizeBodyFromSources( - targetEntry, - [targetEntry, ...sourceEntries], - reason, - evidencePreserved, + const description = item.descriptionHint || targetEntry.description; + const body = synthesizeBodyFromSources( + targetEntry, + [targetEntry, ...sourceEntries], + reason, + evidencePreserved, + ); + const groupId = organizerMergeGroupId(cluster.id, targetEntry.slug, [ + targetEntry.slug, + ...sourceSlugs, + ]); + if (utf8ByteLength(body) > ORGANIZER_MEMORY_BODY_LIMIT_BYTES) { + reviewSkipped += 1; + incrementBucket(rejectionBuckets, "missingPayload"); + reviewNotes.push( + `${cluster.id}: merge_into ${targetEntry.slug} - merged body exceeds ${ORGANIZER_MEMORY_BODY_LIMIT_BYTES} bytes; skipped automatic apply and requires a shorter manual rewrite.`, ); - const groupId = organizerMergeGroupId(cluster.id, targetEntry.slug, [ - targetEntry.slug, - ...sourceSlugs, - ]); - if (utf8ByteLength(body) > ORGANIZER_MEMORY_BODY_LIMIT_BYTES) { - reviewSkipped += 1; - incrementBucket(rejectionBuckets, "missingPayload"); - reviewNotes.push( - `${cluster.id}: merge_into ${targetEntry.slug} - merged body exceeds ${ORGANIZER_MEMORY_BODY_LIMIT_BYTES} bytes; skipped automatic apply and requires a shorter manual rewrite.`, - ); - continue; - } - decisions.push({ - op: "upsert", - slug: targetEntry.slug, - scope: targetEntry.scope, - workdirHash: targetEntry.scope === "project" ? targetEntry.workdirHash : undefined, + continue; + } + decisions.push({ + op: "upsert", + slug: targetEntry.slug, + scope: targetEntry.scope, + workdirHash: targetEntry.scope === "project" ? targetEntry.workdirHash : undefined, memoryType: targetEntry.memoryType as MemoryType, description, body, reason: reason || "memory organizer update", confidence, riskLevel: risk.risk, - requiresUserAck: - risk.risk !== "low" || - sourceEntries.some(isReviewed) || - isReviewed(targetEntry) || - risk.reasons.includes("cross_type") || - risk.reasons.includes("cross_scope"), - sourceSlugs: [targetEntry.slug, ...sourceSlugs], - evidencePreserved, - blockedReasons: risk.reasons, - groupId, - }); - for (const sourceEntry of sourceEntries) { + requiresUserAck: + risk.risk !== "low" || + sourceEntries.some(isReviewed) || + isReviewed(targetEntry) || + risk.reasons.includes("cross_type") || + risk.reasons.includes("cross_scope"), + sourceSlugs: [targetEntry.slug, ...sourceSlugs], + evidencePreserved, + blockedReasons: risk.reasons, + groupId, + }); + for (const sourceEntry of sourceEntries) { if (sourceEntry.memoryType === "daily") { reviewSkipped += 1; incrementBucket(rejectionBuckets, "unsupported"); @@ -1207,9 +1197,9 @@ function buildSafeDecisions(results: ParsedClusterResult[], run: MemoryOrganizeR addRejectionBucketForReasons(rejectionBuckets, deleteRisk.reasons); continue; } - decisions.push({ - op: "delete", - slug: sourceEntry.slug, + decisions.push({ + op: "delete", + slug: sourceEntry.slug, scope: sourceEntry.scope, workdirHash: sourceEntry.scope === "project" ? sourceEntry.workdirHash : undefined, reason: `merged into ${targetEntry.slug}`, @@ -1220,11 +1210,11 @@ function buildSafeDecisions(results: ParsedClusterResult[], run: MemoryOrganizeR isReviewed(sourceEntry) || deleteRisk.reasons.includes("cross_type") || deleteRisk.reasons.includes("cross_scope"), - sourceSlugs: [sourceEntry.slug, targetEntry.slug], - evidencePreserved, - blockedReasons: deleteRisk.reasons, - groupId, - }); + sourceSlugs: [sourceEntry.slug, targetEntry.slug], + evidencePreserved, + blockedReasons: deleteRisk.reasons, + groupId, + }); mergedCount += 1; } } @@ -1557,8 +1547,7 @@ export function MemoryOrganizerRunner({ settings, setSettings }: MemoryOrganizer } const onPoke = (event: Event) => { - const force = - !(event instanceof CustomEvent) || event.detail?.force !== false; + const force = !(event instanceof CustomEvent) || event.detail?.force !== false; void tick(force); }; window.addEventListener(MEMORY_ORGANIZER_EVENT, onPoke); diff --git a/crates/agent-gui/src/components/ui/button.tsx b/crates/agent-gui/src/components/ui/button.tsx index 6450dace2..4a8514794 100644 --- a/crates/agent-gui/src/components/ui/button.tsx +++ b/crates/agent-gui/src/components/ui/button.tsx @@ -1,6 +1,5 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; import { cn } from "../../lib/shared/utils"; @@ -10,12 +9,9 @@ const buttonVariants = cva( variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, @@ -35,20 +31,31 @@ const buttonVariants = cva( type ButtonProps = React.ButtonHTMLAttributes & VariantProps & { - asChild?: boolean; + render?: React.ReactElement; }; export const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; + ({ className, variant, size, render: renderProp, children, ...props }, ref) => { + const mergedClass = cn(buttonVariants({ variant, size }), className); + + if (renderProp) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rp = renderProp as React.ReactElement; + return React.cloneElement(rp, { + className: cn(mergedClass, rp.props.className), + ...props, + children: children ?? rp.props.children, + }); + } + return ( - + ); }, ); Button.displayName = "Button"; + +export { buttonVariants }; diff --git a/crates/agent-gui/src/components/ui/confirm-action-popover.tsx b/crates/agent-gui/src/components/ui/confirm-action-popover.tsx index 1ceb280a9..22319161b 100644 --- a/crates/agent-gui/src/components/ui/confirm-action-popover.tsx +++ b/crates/agent-gui/src/components/ui/confirm-action-popover.tsx @@ -1,7 +1,7 @@ -import { useEffect, useRef, useState, type ReactNode } from "react"; -import { AlertTriangle } from "../icons"; - +import { Popover } from "@base-ui/react"; +import type { ReactNode } from "react"; import { useLocale } from "../../i18n"; +import { AlertTriangle } from "../icons"; import { Button } from "./button"; export function ConfirmActionPopover(props: { @@ -13,78 +13,50 @@ export function ConfirmActionPopover(props: { }) { const { title, description, confirmLabel, onConfirm, children } = props; const { t } = useLocale(); - const [show, setShow] = useState(false); - const [flipUp, setFlipUp] = useState(false); - const ref = useRef(null); - - useEffect(() => { - if (!show) return; - - function handleClick(e: MouseEvent) { - if (ref.current && !ref.current.contains(e.target as Node)) setShow(false); - } - - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [show]); - - function handleOpen() { - if (ref.current) { - const rect = ref.current.getBoundingClientRect(); - // Popover is ~160px tall; flip upward if not enough space below - setFlipUp(window.innerHeight - rect.bottom < 170); - } - setShow(true); - } return ( -
- {children(handleOpen)} - {show ? ( -
-
-
-
- -
-
-

{title}

-
- {description} + + {/* Pass no-op — Popover.Trigger merges its own click handler via render prop */} + {}) as React.ReactElement} /> + + + +
+
+
+ +
+
+

{title}

+
+ {description} +
+
+ } + > + {t("settings.cancel")} + + + } + > + {confirmLabel} + +
-
- - -
-
-
- ) : null} -
+ + + + ); } diff --git a/crates/agent-gui/src/components/ui/dropdown-menu.tsx b/crates/agent-gui/src/components/ui/dropdown-menu.tsx index 779e5b64d..34f558ae3 100644 --- a/crates/agent-gui/src/components/ui/dropdown-menu.tsx +++ b/crates/agent-gui/src/components/ui/dropdown-menu.tsx @@ -1,87 +1,97 @@ +import { Menu } from "@base-ui/react"; import * as React from "react"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { cn } from "../../lib/shared/utils"; import { Check } from "../icons"; -import { cn } from "../../lib/shared/utils"; +export const DropdownMenu = Menu.Root; +export const DropdownMenuTrigger = Menu.Trigger; -export const DropdownMenu = DropdownMenuPrimitive.Root; -export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; +type DropdownMenuContentProps = React.ComponentPropsWithoutRef & + Pick< + React.ComponentPropsWithoutRef, + "side" | "align" | "sideOffset" | "collisionPadding" + >; -export const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; +export const DropdownMenuContent = React.forwardRef( + ({ className, side, align, sideOffset = 4, collisionPadding, ...props }, ref) => ( + + + + + + ), +); +DropdownMenuContent.displayName = "DropdownMenuContent"; export const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + HTMLDivElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( - +
)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; +DropdownMenuLabel.displayName = "DropdownMenuLabel"; export const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + HTMLDivElement, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; +DropdownMenuSeparator.displayName = "DropdownMenuSeparator"; export const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + HTMLDivElement, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - + - + {children} - + )); -DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; +DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"; -export const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; +type DropdownMenuItemProps = React.ComponentPropsWithoutRef & { + onSelect?: () => void; +}; + +export const DropdownMenuItem = React.forwardRef( + ({ className, onSelect, onClick, ...props }, ref) => ( + { + onSelect?.(); + onClick?.(e); + }} + {...props} + /> + ), +); +DropdownMenuItem.displayName = "DropdownMenuItem"; diff --git a/crates/agent-gui/src/components/ui/label.tsx b/crates/agent-gui/src/components/ui/label.tsx index 7485a0c15..b35f6a6d0 100644 --- a/crates/agent-gui/src/components/ui/label.tsx +++ b/crates/agent-gui/src/components/ui/label.tsx @@ -6,11 +6,7 @@ export const Label = React.forwardRef< HTMLLabelElement, React.LabelHTMLAttributes >(({ className, ...props }, ref) => ( -