feat: add Tauri desktop app support and terminal management

This commit is contained in:
Mohamed Boudra
2026-01-20 10:09:35 +07:00
parent 12f617dd10
commit d9c487dd54
125 changed files with 8906 additions and 1049 deletions

2
.gitignore vendored
View File

@@ -49,3 +49,5 @@ CLAUDE.local.md
# Task CLI
.tasks/
.debug.conversations/

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
rust 1.85.1

282
package-lock.json generated
View File

@@ -13,7 +13,8 @@
"packages/server",
"packages/app",
"packages/relay",
"packages/website"
"packages/website",
"packages/desktop"
],
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.11"
@@ -6313,6 +6314,10 @@
"resolved": "packages/app",
"link": true
},
"node_modules/@paseo/desktop": {
"resolved": "packages/desktop",
"link": true
},
"node_modules/@paseo/relay": {
"resolved": "packages/relay",
"link": true
@@ -8410,6 +8415,233 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tauri-apps/api": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz",
"integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz",
"integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.9.6",
"@tauri-apps/cli-darwin-x64": "2.9.6",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6",
"@tauri-apps/cli-linux-arm64-gnu": "2.9.6",
"@tauri-apps/cli-linux-arm64-musl": "2.9.6",
"@tauri-apps/cli-linux-riscv64-gnu": "2.9.6",
"@tauri-apps/cli-linux-x64-gnu": "2.9.6",
"@tauri-apps/cli-linux-x64-musl": "2.9.6",
"@tauri-apps/cli-win32-arm64-msvc": "2.9.6",
"@tauri-apps/cli-win32-ia32-msvc": "2.9.6",
"@tauri-apps/cli-win32-x64-msvc": "2.9.6"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz",
"integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz",
"integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz",
"integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz",
"integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz",
"integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz",
"integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz",
"integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz",
"integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz",
"integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz",
"integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz",
"integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
@@ -9507,6 +9739,15 @@
"node": ">=10.0.0"
}
},
"node_modules/@xterm/headless": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.0.0.tgz",
"integrity": "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==",
"license": "MIT",
"workspaces": [
"addons/*"
]
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
@@ -17994,9 +18235,7 @@
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
@@ -19886,6 +20125,20 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-native-webview": {
"version": "13.16.0",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz",
"integrity": "sha512-Nh13xKZWW35C0dbOskD7OX01nQQavOzHbCw9XoZmar4eXCo7AvrYJ0jlUfRVVIJzqINxHlpECYLdmAdFsl9xDA==",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^4.0.0",
"invariant": "2.2.4"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-worklets": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
@@ -24597,6 +24850,7 @@
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@tanstack/react-query": "^5.90.11",
"@tauri-apps/api": "^2.9.1",
"buffer": "^6.0.3",
"expo": "^54.0.18",
"expo-audio": "~1.0.13",
@@ -24639,6 +24893,7 @@
"react-native-svg": "^15.14.0",
"react-native-unistyles": "^3.0.15",
"react-native-web": "~0.21.0",
"react-native-webview": "^13.16.0",
"react-native-worklets": "0.5.1",
"zod": "^3.23.8",
"zustand": "^5.0.9"
@@ -24675,6 +24930,13 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
"packages/desktop": {
"name": "@paseo/desktop",
"version": "1.0.0",
"devDependencies": {
"@tauri-apps/cli": "^2.9.6"
}
},
"packages/relay": {
"name": "@paseo/relay",
"version": "0.1.0",
@@ -24706,11 +24968,13 @@
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "^1.1.12",
"@openrouter/ai-sdk-provider": "^1.2.0",
"@xterm/headless": "^6.0.0",
"ai": "^5.0.76",
"dotenv": "^17.2.3",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"mnemonic-id": "^3.2.7",
"node-pty": "^1.0.0",
"openai": "^4.20.0",
"pino": "^10.2.0",
"pino-pretty": "^13.1.3",
@@ -24931,6 +25195,16 @@
"node": ">= 0.6"
}
},
"packages/server/node_modules/node-pty": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"nan": "^2.17.0"
}
},
"packages/server/node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",

View File

@@ -6,7 +6,8 @@
"packages/server",
"packages/app",
"packages/relay",
"packages/website"
"packages/website",
"packages/desktop"
],
"scripts": {
"dev": "NODE_ENV=development tsx packages/server/scripts/dev-runner.ts",
@@ -21,6 +22,8 @@
"android:dev-release": "ANDROID_VARIANT=devRelease npm run android --workspace=@paseo/app",
"ios": "npm run ios --workspace=@paseo/app",
"web": "npm run web --workspace=@paseo/app",
"desktop": "npm run dev --workspace=@paseo/desktop",
"desktop:build": "npm run build --workspace=@paseo/desktop",
"postinstall": "patch-package"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
<svg width="48" height="48" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="700" height="700" rx="100" fill="black"/>
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<rect width="700" height="700" rx="156" fill="black"/>
<path transform="translate(350,350) scale(1.05) translate(-350,-350)" d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<circle cx="570" cy="570" r="130" fill="#22c55e"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,5 +1,5 @@
<svg width="48" height="48" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="700" height="700" rx="100" fill="black"/>
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<rect width="700" height="700" rx="156" fill="black"/>
<path transform="translate(350,350) scale(1.05) translate(-350,-350)" d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<circle cx="570" cy="570" r="130" fill="#3b82f6"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,4 +1,4 @@
<svg width="48" height="48" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="700" height="700" rx="100" fill="black"/>
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<rect width="700" height="700" rx="156" fill="black"/>
<path transform="translate(350,350) scale(1.05) translate(-350,-350)" d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,5 +1,5 @@
<svg width="48" height="48" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="700" height="700" rx="100" fill="black"/>
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<rect width="700" height="700" rx="156" fill="black"/>
<path transform="translate(350,350) scale(1.05) translate(-350,-350)" d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<circle cx="570" cy="570" r="130" fill="#22c55e"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,5 +1,5 @@
<svg width="48" height="48" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="700" height="700" rx="100" fill="black"/>
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<rect width="700" height="700" rx="156" fill="black"/>
<path transform="translate(350,350) scale(1.05) translate(-350,-350)" d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<circle cx="570" cy="570" r="130" fill="#3b82f6"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,4 +1,4 @@
<svg width="48" height="48" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="700" height="700" rx="100" fill="black"/>
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
<rect width="700" height="700" rx="156" fill="black"/>
<path transform="translate(350,350) scale(1.05) translate(-350,-350)" d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -36,6 +36,7 @@
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@tanstack/react-query": "^5.90.11",
"@tauri-apps/api": "^2.9.1",
"buffer": "^6.0.3",
"expo": "^54.0.18",
"expo-audio": "~1.0.13",
@@ -78,6 +79,7 @@
"react-native-svg": "^15.14.0",
"react-native-unistyles": "^3.0.15",
"react-native-web": "~0.21.0",
"react-native-webview": "^13.16.0",
"react-native-worklets": "0.5.1",
"zod": "^3.23.8",
"zustand": "^5.0.9"

View File

@@ -28,6 +28,8 @@ import {
HorizontalScrollProvider,
useHorizontalScrollOptional,
} from "@/contexts/horizontal-scroll-context";
import { getIsTauriMac } from "@/constants/layout";
import { useTrafficLightPadding } from "@/utils/tauri-window";
function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(
@@ -152,11 +154,17 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
[isMobile, isOpen, windowWidth, translateX, backdropOpacity, animateToOpen, animateToClose, openAgentList, isGesturing, horizontalScroll?.isAnyScrolledRight, touchStartX]
);
// When sidebar is collapsed on desktop Tauri macOS, add left padding for traffic lights
const trafficLightPadding = useTrafficLightPadding();
const needsTrafficLightPadding = !isMobile && !isOpen && getIsTauriMac();
const content = (
<View style={{ flex: 1, backgroundColor: theme.colors.surface0 }}>
<View style={{ flex: 1, flexDirection: "row" }}>
{!isMobile && <SlidingSidebar selectedAgentId={selectedAgentId} />}
<View style={{ flex: 1 }}>{children}</View>
<View style={{ flex: 1, paddingLeft: needsTrafficLightPadding ? trafficLightPadding.left : 0 }}>
{children}
</View>
</View>
{isMobile && <SlidingSidebar selectedAgentId={selectedAgentId} />}
<DownloadToast />

View File

@@ -30,8 +30,6 @@ import {
GitBranch,
Folder,
RotateCcw,
Users,
ChevronRight,
PlusIcon,
PanelRight,
} from "lucide-react-native";
@@ -51,7 +49,6 @@ import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
import { formatConnectionStatus } from "@/utils/daemons";
import { useSessionStore } from "@/stores/session-store";
import type { Agent } from "@/stores/session-store";
import type { StreamItem } from "@/types/stream";
import {
buildAgentNavigationKey,
@@ -61,6 +58,7 @@ import {
} from "@/utils/navigation-timing";
import { extractAgentModel } from "@/utils/extract-agent-model";
import { startPerfMonitor } from "@/utils/perf-monitor";
import { shortenPath } from "@/utils/shorten-path";
const DROPDOWN_WIDTH = 220;
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
@@ -294,35 +292,6 @@ function AgentScreenContent({
: undefined
);
// Select the agents Map directly - this is a stable reference that only changes when agents are added/removed
const allAgents = useSessionStore(
(state) => state.sessions[serverId]?.agents
);
// Derive child agents in useMemo to avoid creating new references on every store update
// This prevents infinite loops caused by useShallow not doing deep equality on object arrays
const childAgents = useMemo(() => {
if (!resolvedAgentId || !allAgents) return [];
const children: Array<{
id: string;
title: string | null;
createdAt: Date;
status: Agent["status"];
}> = [];
for (const [id, a] of allAgents) {
if (a.parentAgentId === resolvedAgentId) {
children.push({
id,
title: a.title,
createdAt: a.createdAt,
status: a.status,
});
}
}
children.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return children;
}, [allAgents, resolvedAgentId]);
// Select only the specific stream tail - use stable empty array to avoid infinite loop
const streamItemsRaw = useSessionStore((state) =>
resolvedAgentId
@@ -613,20 +582,6 @@ function AgentScreenContent({
router.push({ pathname: "/", params });
}, [agent, agentModel, handleCloseMenu, router, serverId]);
const handleNavigateToChildAgent = useCallback(
(childAgentId: string) => {
handleCloseMenu();
router.push({
pathname: "/agent/[serverId]/[agentId]",
params: {
serverId: serverId,
agentId: childAgentId,
},
});
},
[handleCloseMenu, router, serverId]
);
if (!agent) {
return (
<View style={styles.container}>
@@ -736,7 +691,7 @@ function AgentScreenContent({
numberOfLines={2}
ellipsizeMode="middle"
>
{agent.cwd}
{shortenPath(agent.cwd)}
</Text>
</View>
@@ -784,50 +739,6 @@ function AgentScreenContent({
<View style={styles.menuDivider} />
{/* Sub-Agents Section */}
<View style={styles.menuSubAgentsSection}>
<View style={styles.menuSubAgentsHeader}>
<Users size={16} color={theme.colors.foregroundMuted} />
<Text style={styles.menuSubAgentsLabel}>Sub-Agents</Text>
</View>
{childAgents.length === 0 ? (
<Text style={styles.menuSubAgentsEmpty}>No sub-agents</Text>
) : (
<ScrollView
style={styles.menuSubAgentsList}
contentContainerStyle={styles.menuSubAgentsListContent}
showsVerticalScrollIndicator={childAgents.length > 3}
>
{childAgents.map((child) => (
<Pressable
key={child.id}
onPress={() => handleNavigateToChildAgent(child.id)}
style={styles.menuSubAgentItem}
>
<Text
style={styles.menuSubAgentText}
numberOfLines={1}
ellipsizeMode="tail"
>
{child.title || "Untitled Agent"}
</Text>
<View style={styles.menuSubAgentMeta}>
{child.status === "running" ? (
<View style={styles.menuSubAgentDot} />
) : null}
<ChevronRight
size={16}
color={theme.colors.foregroundMuted}
/>
</View>
</Pressable>
))}
</ScrollView>
)}
</View>
<View style={styles.menuDivider} />
<Pressable onPress={handleViewChanges} style={styles.menuItem}>
<GitBranch size={16} color={theme.colors.foreground} />
<Text style={styles.menuItemText}>View Changes</Text>
@@ -1108,57 +1019,4 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
fontWeight: theme.fontWeight.normal,
},
menuSubAgentsSection: {
gap: theme.spacing[2],
},
menuSubAgentsHeader: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
},
menuSubAgentsLabel: {
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
letterSpacing: 0.5,
textTransform: "uppercase",
},
menuSubAgentsEmpty: {
fontSize: theme.fontSize.base,
color: theme.colors.foregroundMuted,
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[3],
},
menuSubAgentsList: {
maxHeight: (theme.spacing[2] * 2 + theme.fontSize.base) * 3,
},
menuSubAgentsListContent: {
gap: theme.spacing[1],
},
menuSubAgentItem: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.md,
},
menuSubAgentText: {
fontSize: theme.fontSize.base,
color: theme.colors.foreground,
flex: 1,
marginRight: theme.spacing[2],
},
menuSubAgentMeta: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
menuSubAgentDot: {
width: 6,
height: 6,
borderRadius: 999,
backgroundColor: "#3b82f6",
},
}));

View File

@@ -106,7 +106,7 @@ export default function HomeScreen() {
const resolvedModel = getParamValue(params.model);
const resolvedWorkingDir = getParamValue(params.workingDir);
const initialValues = useMemo(() => {
const initialValues = useMemo((): CreateAgentInitialValues => {
const values: CreateAgentInitialValues = {};
if (resolvedWorkingDir) {
values.workingDir = resolvedWorkingDir;
@@ -117,8 +117,12 @@ export default function HomeScreen() {
if (resolvedMode) {
values.modeId = resolvedMode;
}
if (resolvedModel) {
values.model = resolvedModel;
}
return values;
}, [resolvedMode, resolvedProvider, resolvedWorkingDir]);
}, [resolvedMode, resolvedModel, resolvedProvider, resolvedWorkingDir]);
const {
selectedServerId,
setSelectedServerIdFromUser,
@@ -143,21 +147,6 @@ export default function HomeScreen() {
isVisible: true,
isCreateFlow: true,
});
const hasAppliedModelParamRef = useRef(false);
useEffect(() => {
if (!resolvedModel || hasAppliedModelParamRef.current) {
return;
}
if (availableModels.length === 0) {
return;
}
const isValidModel = availableModels.some((model) => model.id === resolvedModel);
hasAppliedModelParamRef.current = true;
if (!isValidModel) {
return;
}
setModelFromUser(resolvedModel);
}, [availableModels, resolvedModel, setModelFromUser]);
const hostEntry = selectedServerId
? connectionStates.get(selectedServerId)
: undefined;

View File

@@ -340,6 +340,37 @@ const styles = StyleSheet.create((theme) => ({
themeToggleTextActive: {
color: theme.colors.foreground,
},
// Dev section
devCard: {
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
overflow: "hidden",
},
devButton: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[3],
paddingVertical: theme.spacing[4],
paddingHorizontal: theme.spacing[4],
},
devButtonBorder: {
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
devButtonContent: {
flex: 1,
},
devButtonTitle: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
},
devButtonDescription: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
marginTop: 2,
},
}));
type DaemonTestState = {
@@ -760,6 +791,24 @@ export default function SettingsScreen() {
</View>
</View>
{/* Developer */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Developer</Text>
<View style={styles.devCard}>
<Pressable
style={styles.devButton}
onPress={() => router.push("/audio-test")}
>
<View style={styles.devButtonContent}>
<Text style={styles.devButtonTitle}>Audio Test</Text>
<Text style={styles.devButtonDescription}>
Test audio recording and playback
</Text>
</View>
</Pressable>
</View>
</View>
{/* Footer */}
<View style={styles.footer}>
<View style={styles.footerAppInfo}>

View File

@@ -101,10 +101,6 @@ export function AgentInputArea({
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
// Noop fallback for sendAgentAudio (required by MessageInput)
const noopSendAgentAudio = useCallback(async () => {}, []);
const sendAgentAudio = methods?.sendAgentAudio ?? noopSendAgentAudio;
const { startRealtime, stopRealtime, isRealtimeMode } = useRealtime();
const [internalInput, setInternalInput] = useState("");
@@ -448,52 +444,47 @@ export function AgentInputArea({
]
);
const rightContent = (
<>
<Pressable
onPress={handleRealtimePress}
disabled={!isConnected && !isRealtimeMode}
style={[
styles.realtimeButton as any,
(isRealtimeMode ? styles.realtimeButtonActive : undefined) as any,
(!isConnected && !isRealtimeMode
? styles.buttonDisabled
: undefined) as any,
]}
>
{isRealtimeMode ? (
<Square
size={18}
color={theme.colors.background}
fill={theme.colors.background}
/>
) : (
<AudioLines size={20} color={theme.colors.background} />
)}
</Pressable>
{isAgentRunning && (
<Pressable
onPress={handleCancelAgent}
disabled={!isConnected || isCancellingAgent}
accessibilityLabel={
isCancellingAgent ? "Canceling agent" : "Stop agent"
}
accessibilityRole="button"
style={[
styles.cancelButton as any,
(!isConnected || isCancellingAgent
? styles.buttonDisabled
: undefined) as any,
]}
>
{isCancellingAgent ? (
<ActivityIndicator size="small" color="white" />
) : (
<Square size={18} color="white" fill="white" />
)}
</Pressable>
const rightContent = isAgentRunning ? (
<Pressable
onPress={handleCancelAgent}
disabled={!isConnected || isCancellingAgent}
accessibilityLabel={isCancellingAgent ? "Canceling agent" : "Stop agent"}
accessibilityRole="button"
style={[
styles.cancelButton as any,
(!isConnected || isCancellingAgent
? styles.buttonDisabled
: undefined) as any,
]}
>
{isCancellingAgent ? (
<ActivityIndicator size="small" color="white" />
) : (
<Square size={18} color="white" fill="white" />
)}
</>
</Pressable>
) : (
<Pressable
onPress={handleRealtimePress}
disabled={!isConnected && !isRealtimeMode}
style={[
styles.realtimeButton as any,
(isRealtimeMode ? styles.realtimeButtonActive : undefined) as any,
(!isConnected && !isRealtimeMode
? styles.buttonDisabled
: undefined) as any,
]}
>
{isRealtimeMode ? (
<Square
size={18}
color={theme.colors.background}
fill={theme.colors.background}
/>
) : (
<AudioLines size={20} color={theme.colors.background} />
)}
</Pressable>
);
const leftContent = <AgentStatusBar agentId={agentId} serverId={serverId} />;
@@ -574,7 +565,6 @@ export function AgentInputArea({
onPickImages={handlePickImage}
onRemoveImage={handleRemoveImage}
client={client}
sendAgentAudio={sendAgentAudio}
placeholder="Message agent..."
autoFocus={autoFocus}
disabled={isRealtimeMode}
@@ -630,10 +620,10 @@ const styles = StyleSheet.create(((theme: Theme) => ({
backgroundColor: theme.colors.palette.blue[600],
},
cancelButton: {
width: BUTTON_SIZE,
height: BUTTON_SIZE,
width: 34,
height: 34,
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.palette.red[500],
backgroundColor: theme.colors.palette.red[600],
alignItems: "center",
justifyContent: "center",
},

View File

@@ -11,6 +11,7 @@ import { useCallback, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { formatTimeAgo } from "@/utils/time";
import { shortenPath } from "@/utils/shorten-path";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
import {
@@ -144,7 +145,7 @@ export function AgentList({
</View>
<Text style={styles.secondaryRow} numberOfLines={1}>
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
{shortenPath(agent.cwd)} · {timeAgo}
</Text>
</View>
)}

View File

@@ -495,12 +495,6 @@ export function AgentStreamView({
</View>
) : null}
{showWorkingIndicator ? (
<View style={stylesheet.workingIndicatorWrapper}>
<WorkingIndicator />
</View>
) : null}
{hasHeadItems
? [...streamHead].reverse().map((item, index) => {
const rendered = renderStreamItemContent(item, index);
@@ -511,6 +505,12 @@ export function AgentStreamView({
) : null;
})
: null}
{showWorkingIndicator ? (
<View style={stylesheet.workingIndicatorWrapper}>
<WorkingIndicator />
</View>
) : null}
</View>
</View>
);
@@ -555,10 +555,12 @@ export function AgentStreamView({
}
ListHeaderComponent={listHeaderComponent}
extraData={flatListExtraData}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 40,
}}
maintainVisibleContentPosition={
// Disable when streaming and user is at bottom - we handle auto-scroll ourselves
agent.status === "running" && isNearBottom
? undefined
: { minIndexForVisible: 0, autoscrollToTopThreshold: 40 }
}
initialNumToRender={12}
windowSize={10}
inverted
@@ -777,8 +779,8 @@ function PermissionRequestCard({
}, [request]);
const toolCallDisplay = useMemo(
() => parseToolCallDisplay(request.input, null),
[request.input]
() => parseToolCallDisplay(request.name ?? "unknown", request.input, null),
[request.name, request.input]
);
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
@@ -792,7 +794,7 @@ function PermissionRequestCard({
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
<Text key={node.key} style={[inheritedStyles, styles.text]}>
{node.content}
</Text>
),
@@ -806,7 +808,6 @@ function PermissionRequestCard({
<Text
key={node.key}
style={[inheritedStyles, styles.textgroup]}
selectable
>
{children}
</Text>
@@ -821,7 +822,6 @@ function PermissionRequestCard({
<Text
key={node.key}
style={[inheritedStyles, styles.code_block]}
selectable
>
{node.content}
</Text>
@@ -833,7 +833,7 @@ function PermissionRequestCard({
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
{node.content}
</Text>
),
@@ -847,7 +847,6 @@ function PermissionRequestCard({
<Text
key={node.key}
style={[inheritedStyles, styles.code_inline]}
selectable
>
{node.content}
</Text>
@@ -893,7 +892,6 @@ function PermissionRequestCard({
<Text style={iconStyle}>{bullet}</Text>
<Text
style={[contentStyle, { flex: 1, flexShrink: 1, minWidth: 0 }]}
selectable
>
{children}
</Text>

View File

@@ -117,13 +117,13 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: "rgba(46, 160, 67, 0.15)",
},
addText: {
color: theme.colors.palette.green[800],
color: theme.colors.foreground,
},
removeLine: {
backgroundColor: "rgba(248, 81, 73, 0.1)",
},
removeText: {
color: theme.colors.palette.red[800],
color: theme.colors.foreground,
},
addHighlight: {
backgroundColor: "rgba(46, 160, 67, 0.4)",

View File

@@ -14,14 +14,13 @@ import {
MIN_EXPLORER_SIDEBAR_WIDTH,
MAX_EXPLORER_SIDEBAR_WIDTH,
type ViewMode,
type ExplorerTab,
} from "@/stores/panel-store";
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
import { GitDiffPane } from "./git-diff-pane";
import { FileExplorerPane } from "./file-explorer-pane";
type ExplorerTab = "changes" | "files";
interface ExplorerSidebarProps {
serverId: string;
agentId: string;
@@ -309,13 +308,11 @@ function SidebarContent({
{/* Content based on active tab */}
<View style={styles.contentArea} testID="explorer-content-area">
{activeTab === "changes" ? (
{activeTab === "changes" && (
<GitDiffPane serverId={serverId} agentId={agentId} />
) : (
<FileExplorerPane
serverId={serverId}
agentId={agentId}
/>
)}
{activeTab === "files" && (
<FileExplorerPane serverId={serverId} agentId={agentId} />
)}
</View>
</View>

View File

@@ -130,7 +130,6 @@ export function FileExplorerPane({
return sorted;
}, [rawEntries, sortOption]);
const showInitialListLoading = isListingLoading && entries.length === 0;
const showListLoadingBanner = isListingLoading && entries.length > 0;
const isPreviewLoading = Boolean(
isExplorerLoading &&
pendingRequest?.mode === "file" &&
@@ -585,14 +584,9 @@ export function FileExplorerPane({
<View style={styles.entryIcon}>
{renderEntryIcon(displayKind, theme.colors)}
</View>
<View style={styles.entryTextContainer}>
<Text style={styles.entryName} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.entryMeta} numberOfLines={1}>
{formatFileSize({ size: item.size })} · {formatTimeAgo(new Date(item.modifiedAt))}
</Text>
</View>
<Text style={styles.entryName} numberOfLines={1}>
{item.name}
</Text>
</View>
<Pressable
onPress={(event) => handleOpenMenu(item, event)}
@@ -642,17 +636,9 @@ export function FileExplorerPane({
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
</Pressable>
</View>
{showListLoadingBanner && (
<View style={styles.loadingBanner}>
<ActivityIndicator size="small" />
<Text style={styles.loadingBannerText}>
Loading {formatDirectoryLabel(activePath)}...
</Text>
</View>
)}
</View>
);
}, [activePath, currentSortLabel, explorerState?.history?.length, handleNavigateBack, handleSortCycle, showListLoadingBanner, theme.colors.mutedForeground]);
}, [activePath, currentSortLabel, explorerState?.history?.length, handleNavigateBack, handleSortCycle]);
// Watch for completed file previews and process queue
useEffect(() => {
@@ -789,6 +775,15 @@ export function FileExplorerPane({
]}
onLayout={handleMenuLayout}
>
<View style={styles.entryMenuHeader}>
<Text style={styles.entryMenuMeta}>
{formatFileSize({ size: menuEntry.size })}
</Text>
<Text style={styles.entryMenuMeta}>
{formatTimeAgo(new Date(menuEntry.modifiedAt))}
</Text>
</View>
<View style={styles.entryMenuDivider} />
<Pressable
style={styles.entryMenuItem}
onPress={() => {
@@ -866,7 +861,7 @@ export function FileExplorerPane({
) : (
<View style={styles.sheetCenterState}>
<Text style={styles.emptyText}>Binary preview unavailable</Text>
<Text style={styles.entryMeta}>
<Text style={styles.entryMenuMeta}>
{formatFileSize({ size: preview.size })}
</Text>
</View>
@@ -1035,17 +1030,6 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.lg,
color: theme.colors.foreground,
},
loadingBanner: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
},
loadingBannerText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
sortButton: {
flexDirection: "row",
alignItems: "center",
@@ -1114,8 +1098,8 @@ const styles = StyleSheet.create((theme) => ({
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[1],
paddingLeft: theme.spacing[2],
borderRadius: theme.borderRadius.md,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
@@ -1128,24 +1112,16 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
flexDirection: "row",
alignItems: "center",
columnGap: theme.spacing[2],
marginRight: theme.spacing[3],
gap: theme.spacing[2],
minWidth: 0,
},
entryIcon: {
width: 28,
alignItems: "center",
},
entryTextContainer: {
flex: 1,
flexShrink: 0,
},
entryName: {
flex: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
},
entryMeta: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
marginTop: theme.spacing[1],
fontSize: theme.fontSize.sm,
},
menuButton: {
width: 36,
@@ -1168,6 +1144,22 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface2,
padding: theme.spacing[1],
},
entryMenuHeader: {
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
},
entryMenuMeta: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
entryMenuDivider: {
height: 1,
backgroundColor: theme.colors.border,
marginHorizontal: theme.spacing[2],
marginVertical: theme.spacing[1],
},
entryMenuItem: {
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],

View File

@@ -3,6 +3,7 @@ import { View, type StyleProp, type ViewStyle } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
import { useTauriDragHandlers } from "@/utils/tauri-window";
interface ScreenHeaderProps {
left?: ReactNode;
@@ -21,10 +22,14 @@ export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeade
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
const topPadding = isMobile ? 8 : 0;
// On Tauri macOS, enable window dragging and double-click to maximize
// Left padding for traffic lights is handled by _layout.tsx when sidebar is collapsed
const dragHandlers = useTauriDragHandlers();
return (
<View style={styles.header}>
<View style={[styles.inner, { paddingTop: insets.top + topPadding }]}>
<View style={styles.row}>
<View style={styles.row} {...dragHandlers}>
<View style={[styles.left, leftStyle]}>{left}</View>
<View style={[styles.right, rightStyle]}>{right}</View>
</View>
@@ -46,6 +51,7 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[2],
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.border,
userSelect: "none",
},
left: {
flex: 1,

View File

@@ -27,7 +27,6 @@ import Animated, {
import { useDictation } from "@/hooks/use-dictation";
import { DictationOverlay } from "./dictation-controls";
import type { DaemonClientV2 } from "@server/client/daemon-client-v2";
import type { SessionContextValue } from "@/contexts/session-context";
import { usePanelStore } from "@/stores/panel-store";
export interface ImageAttachment {
@@ -52,7 +51,6 @@ export interface MessageInputProps {
onPickImages?: () => void;
onRemoveImage?: (index: number) => void;
client: DaemonClientV2 | null;
sendAgentAudio: SessionContextValue["sendAgentAudio"];
placeholder?: string;
autoFocus?: boolean;
disabled?: boolean;
@@ -104,7 +102,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
onPickImages,
onRemoveImage,
client,
sendAgentAudio,
placeholder = "Message...",
autoFocus = false,
disabled = false,
@@ -200,9 +197,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
retryFailedDictation,
discardFailedDictation,
} = useDictation({
sendAgentAudio,
client,
mode: "transcribe_only",
onTranscript: handleDictationTranscript,
onError: handleDictationError,
canStart: canStartDictation,
@@ -538,8 +533,23 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
{leftContent}
</View>
{/* Right: send/queue button, voice button, rightContent slot */}
{/* Right: voice button, contextual button (realtime/send/cancel) */}
<View style={styles.rightButtonGroup}>
<Pressable
onPress={handleVoicePress}
disabled={!isConnected || disabled}
style={[
styles.voiceButton,
(!isConnected || disabled) && styles.buttonDisabled,
isDictating && styles.voiceButtonRecording,
]}
>
{isDictating ? (
<Square size={14} color="white" fill="white" />
) : (
<Mic size={20} color={theme.colors.foreground} />
)}
</Pressable>
{shouldShowSendButton && (
<Pressable
onPress={handleSendMessage}
@@ -561,22 +571,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
<ArrowUp size={20} color="white" />
</Pressable>
)}
<Pressable
onPress={handleVoicePress}
disabled={!isConnected || disabled}
style={[
styles.voiceButton,
(!isConnected || disabled) && styles.buttonDisabled,
isDictating && styles.voiceButtonRecording,
]}
>
{isDictating ? (
<Square size={14} color="white" fill="white" />
) : (
<Mic size={20} color={theme.colors.foreground} />
)}
</Pressable>
{rightContent}
{!shouldShowSendButton && rightContent}
</View>
</View>
</Animated.View>

View File

@@ -387,16 +387,21 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
flexShrink: 0,
},
detailWrapper: {
marginTop: theme.spacing[2],
borderRadius: theme.borderRadius.md,
borderBottomLeftRadius: theme.borderRadius.lg,
borderBottomRightRadius: theme.borderRadius.lg,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
borderTopWidth: 0,
borderColor: theme.colors.borderAccent,
backgroundColor: theme.colors.surface0,
padding: theme.spacing[2],
gap: theme.spacing[2],
flexShrink: 1,
minWidth: 0,
},
pressableExpanded: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
}));
function isLikelyPathToken(value: string): boolean {
@@ -511,7 +516,7 @@ export const AssistantMessage = memo(function AssistantMessage({
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
<Text key={node.key} style={[inheritedStyles, styles.text]}>
{node.content}
</Text>
),
@@ -525,7 +530,6 @@ export const AssistantMessage = memo(function AssistantMessage({
<Text
key={node.key}
style={[inheritedStyles, styles.textgroup]}
selectable
>
{children}
</Text>
@@ -540,7 +544,6 @@ export const AssistantMessage = memo(function AssistantMessage({
<Text
key={node.key}
style={[inheritedStyles, styles.code_block]}
selectable
>
{node.content}
</Text>
@@ -552,7 +555,7 @@ export const AssistantMessage = memo(function AssistantMessage({
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
{node.content}
</Text>
),
@@ -576,7 +579,6 @@ export const AssistantMessage = memo(function AssistantMessage({
inheritedStyles,
assistantMessageStylesheet.markdownCodeInline,
]}
selectable
>
{content}
</Text>
@@ -1153,6 +1155,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
pressed && hasDetails
? expandableBadgeStylesheet.pressablePressed
: null,
isExpanded && expandableBadgeStylesheet.pressableExpanded,
]}
>
<View style={expandableBadgeStylesheet.headerRow}>
@@ -1181,12 +1184,12 @@ const ExpandableBadge = memo(function ExpandableBadge({
/>
) : null}
</View>
{detailContent ? (
<View style={expandableBadgeStylesheet.detailWrapper}>
{detailContent}
</View>
) : null}
</Pressable>
{detailContent ? (
<View style={expandableBadgeStylesheet.detailWrapper}>
{detailContent}
</View>
) : null}
</View>
);
});
@@ -1226,7 +1229,7 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
<Text key={node.key} style={[inheritedStyles, styles.text]}>
{node.content}
</Text>
),
@@ -1240,7 +1243,6 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
<Text
key={node.key}
style={[inheritedStyles, styles.textgroup]}
selectable
>
{children}
</Text>
@@ -1255,7 +1257,6 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
<Text
key={node.key}
style={[inheritedStyles, styles.code_block]}
selectable
>
{node.content}
</Text>
@@ -1267,7 +1268,7 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
{node.content}
</Text>
),
@@ -1281,7 +1282,6 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
<Text
key={node.key}
style={[inheritedStyles, styles.code_inline]}
selectable
>
{node.content}
</Text>
@@ -1432,7 +1432,7 @@ export const ToolCall = memo(function ToolCall({
args !== undefined || result !== undefined || error !== undefined;
// Parse tool call details for inline rendering
const { display, errorText } = useToolCallDetails({ args, result, error });
const { display, errorText } = useToolCallDetails({ toolName, args, result, error });
const handleToggle = useCallback(() => {
if (!isMobile && isPerfLoggingEnabled()) {
@@ -1498,7 +1498,7 @@ export const ToolCall = memo(function ToolCall({
return (
<ExpandableBadge
testID="tool-call-badge"
label={toolName}
label={display.toolName}
secondaryLabel={principalParam}
icon={IconComponent}
isExpanded={!isMobile && isExpanded}

View File

@@ -15,6 +15,7 @@ import { usePanelStore } from "@/stores/panel-store";
import { AgentList } from "./agent-list";
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window";
const DESKTOP_SIDEBAR_WIDTH = 320;
const SIDEBAR_AGENT_LIMIT = 15;
@@ -44,6 +45,8 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
animateToClose,
isGesturing,
} = useSidebarAnimation();
const trafficLightPadding = useTrafficLightPadding();
const dragHandlers = useTauriDragHandlers();
// Track user-initiated refresh to avoid showing spinner on background revalidation
const [isManualRefresh, setIsManualRefresh] = useState(false);
@@ -249,8 +252,11 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
return (
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
{/* Header: New Agent button */}
<View style={styles.sidebarHeader}>
{/* Header: New Agent button - top padding area is draggable on Tauri */}
<View
style={[styles.sidebarHeader, { paddingTop: trafficLightPadding.top || styles.sidebarHeader.paddingTop }]}
{...dragHandlers}
>
<Pressable
style={({ hovered }) => [
styles.newAgentButton,
@@ -316,6 +322,7 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[4],
paddingBottom: theme.spacing[2],
userSelect: "none",
},
newAgentButton: {
flexDirection: "row",

View File

@@ -7,6 +7,7 @@ import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
import {
parseToolCallDisplay,
buildLineDiff,
parseUnifiedDiff,
type ToolCallDisplay,
} from "@/utils/tool-call-parsers";
import { DiffViewer } from "./diff-viewer";
@@ -14,6 +15,7 @@ import { DiffViewer } from "./diff-viewer";
// ---- Types ----
export interface ToolCallDetailsData {
toolName: string;
args?: unknown;
result?: unknown;
error?: unknown;
@@ -139,6 +141,10 @@ export function ToolCallDetailsContent({
// Compute diff lines for edit type
const diffLines = useMemo(() => {
if (display.type !== "edit") return undefined;
// Use pre-computed unified diff if available (e.g., from apply_patch)
if (display.unifiedDiff) {
return parseUnifiedDiff(display.unifiedDiff);
}
return buildLineDiff(display.oldString, display.newString);
}, [display]);
@@ -304,12 +310,12 @@ export function ToolCallDetailsContent({
// ---- Hook for parsing tool call data ----
export function useToolCallDetails(data: ToolCallDetailsData) {
const { args, result, error } = data;
const { toolName, args, result, error } = data;
return useMemo(() => {
const shouldLog = isPerfLoggingEnabled();
const startMs = shouldLog ? getNowMs() : 0;
const display = parseToolCallDisplay(args, result);
const display = parseToolCallDisplay(toolName, args, result);
const errorText = error !== undefined ? formatValue(error) : undefined;
if (shouldLog) {
const durationMs = getNowMs() - startMs;
@@ -329,7 +335,7 @@ export function useToolCallDetails(data: ToolCallDetailsData) {
}
}
return { display, errorText };
}, [args, result, error]);
}, [toolName, args, result, error]);
}
// ---- Styles ----

View File

@@ -144,7 +144,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
? toolKindIcons[kind.toLowerCase()] || Wrench
: Wrench;
const { display, errorText } = useToolCallDetails({ args, result, error });
const { display, errorText } = useToolCallDetails({ toolName, args, result, error });
return (
<View style={styles.container}>
@@ -153,7 +153,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
<View style={styles.headerLeft}>
<IconComponent size={20} color={styles.headerIcon.color} />
<Text style={styles.headerTitle} numberOfLines={1}>
{toolName}
{display.toolName}
</Text>
</View>
<Pressable onPress={onClose} style={styles.closeButton}>

View File

@@ -1,3 +1,5 @@
import { Platform } from "react-native";
export const FOOTER_HEIGHT = 75;
// Shared header inner height (excluding safe area insets and border)
@@ -7,3 +9,43 @@ export const HEADER_INNER_HEIGHT = 48;
// Max width for chat content (stream view, input area, new agent form)
export const MAX_CONTENT_WIDTH = 820;
// Tauri desktop app constants for macOS traffic light buttons
// These buttons (close/minimize/maximize) overlay the top-left corner
export const TAURI_TRAFFIC_LIGHT_WIDTH = 78;
export const TAURI_TRAFFIC_LIGHT_HEIGHT = 56;
// Check if running in Tauri desktop app on macOS
function isTauriMac(): boolean {
if (Platform.OS !== "web") return false;
if (typeof window === "undefined") return false;
if (!("__TAURI__" in window)) return false;
// Check for macOS via user agent
const ua = navigator.userAgent;
return ua.includes("Mac OS") || ua.includes("Macintosh");
}
// Cached result - only cache true, keep checking if false (in case __TAURI__ loads later)
let _isTauriMacCached: boolean | null = null;
export function getIsTauriMac(): boolean {
if (_isTauriMacCached === true) {
return true;
}
const result = isTauriMac();
if (result) {
_isTauriMacCached = true;
}
return result;
}
// Get traffic light padding values (only non-zero on Tauri macOS)
export function getTrafficLightPadding(): { left: number; top: number } {
if (!getIsTauriMac()) {
return { left: 0, top: 0 };
}
return {
left: TAURI_TRAFFIC_LIGHT_WIDTH,
top: TAURI_TRAFFIC_LIGHT_HEIGHT,
};
}

View File

@@ -352,12 +352,6 @@ export interface SessionContextValue {
message: string,
images?: Array<{ uri: string; mimeType?: string }>
) => Promise<void>;
sendAgentAudio: (
agentId: string | undefined,
audioBlob: Blob,
requestId?: string,
options?: { mode?: "transcribe_only" | "auto_run" }
) => Promise<void>;
deleteAgent: (agentId: string) => void;
createAgent: (options: {
config: any;
@@ -1972,69 +1966,6 @@ export function SessionProvider({
[client]
);
const sendAgentAudio = useCallback(
async (
agentId: string | undefined,
audioBlob: Blob,
requestId?: string,
options?: { mode?: "transcribe_only" | "auto_run" }
) => {
try {
if (!client) {
throw new Error("Daemon client unavailable");
}
if (!client.isConnected) {
throw new Error("Daemon client is disconnected");
}
const resolvedRequestId = requestId ?? generateMessageId();
const arrayBuffer = await audioBlob.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64Audio = btoa(binary);
const deriveFormat = (mimeType: string | undefined): string => {
if (!mimeType || mimeType.length === 0) {
return "webm";
}
const slashIndex = mimeType.indexOf("/");
let formatPart =
slashIndex >= 0 ? mimeType.slice(slashIndex + 1) : mimeType;
const semicolonIndex = formatPart.indexOf(";");
if (semicolonIndex >= 0) {
formatPart = formatPart.slice(0, semicolonIndex);
}
return formatPart.trim().length > 0 ? formatPart.trim() : "webm";
};
const format = deriveFormat(audioBlob.type);
await client.sendAgentAudio({
...(agentId ? { agentId } : {}),
audio: base64Audio,
format,
isLast: true,
requestId: resolvedRequestId,
...(options?.mode ? { mode: options.mode } : {}),
});
console.log(
"[Session] Sent audio:",
agentId ?? "(no agent)",
format,
audioBlob.size,
"bytes",
`(requestId: ${resolvedRequestId})`
);
} catch (error) {
console.error("[Session] Failed to send audio:", error);
throw error;
}
},
[client]
);
const createAgent = useCallback(
async ({
config,
@@ -2344,7 +2275,6 @@ export function SessionProvider({
cancelAgentRun,
deleteAgent,
sendAgentMessage,
sendAgentAudio,
createAgent,
setAgentMode,
respondToPermission,
@@ -2368,7 +2298,6 @@ export function SessionProvider({
cancelAgentRun,
deleteAgent,
sendAgentMessage,
sendAgentAudio,
createAgent,
setAgentMode,
respondToPermission,
@@ -2394,7 +2323,6 @@ export function SessionProvider({
refreshSession,
cancelAgentRun,
sendAgentMessage,
sendAgentAudio,
deleteAgent,
createAgent,
setAgentMode,
@@ -2415,7 +2343,6 @@ export function SessionProvider({
refreshSession,
cancelAgentRun,
sendAgentMessage,
sendAgentAudio,
deleteAgent,
createAgent,
setAgentMode,

View File

@@ -8,20 +8,47 @@ import type {
AgentModelDefinition,
AgentProvider,
} from "@server/server/agent/agent-sdk-types";
import type { ProviderModelState } from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store";
import { useFormPreferences } from "./use-form-preferences";
import { useFormPreferences, type FormPreferences } from "./use-form-preferences";
export type CreateAgentInitialValues = {
workingDir?: string;
// Explicit overrides from URL params or "New Agent" button
export interface FormInitialValues {
serverId?: string | null;
provider?: AgentProvider;
modeId?: string | null;
model?: string | null;
workingDir?: string;
}
// Tracks which fields the user has explicitly modified in this session
interface UserModifiedFields {
serverId: boolean;
provider: boolean;
modeId: boolean;
model: boolean;
workingDir: boolean;
}
const INITIAL_USER_MODIFIED: UserModifiedFields = {
serverId: false,
provider: false,
modeId: false,
model: false,
workingDir: false,
};
// Internal form state
interface FormState {
serverId: string | null;
provider: AgentProvider;
modeId: string;
model: string;
workingDir: string;
}
type UseAgentFormStateOptions = {
initialServerId?: string | null;
initialValues?: CreateAgentInitialValues;
initialValues?: FormInitialValues;
isVisible?: boolean;
isCreateFlow?: boolean;
isTargetDaemonReady?: boolean;
@@ -66,6 +93,106 @@ const DEFAULT_PROVIDER: AgentProvider = fallbackDefinition?.id ?? "claude";
const DEFAULT_MODE_FOR_DEFAULT_PROVIDER =
fallbackDefinition?.defaultModeId ?? "";
/**
* Pure function that resolves form state from multiple data sources.
* Priority: explicit (URL params) > preferences > provider defaults > fallback
*
* Only resolves fields that haven't been user-modified.
*/
function resolveFormState(
initialValues: FormInitialValues | undefined,
preferences: FormPreferences | null,
availableModels: AgentModelDefinition[] | null,
userModified: UserModifiedFields,
currentState: FormState
): FormState {
// Start with current state - we only update non-user-modified fields
const result = { ...currentState };
// 1. Resolve provider first (other fields depend on it)
if (!userModified.provider) {
if (initialValues?.provider && providerDefinitionMap.has(initialValues.provider)) {
result.provider = initialValues.provider;
} else if (
preferences?.provider &&
providerDefinitionMap.has(preferences.provider as AgentProvider)
) {
result.provider = preferences.provider as AgentProvider;
}
// else keep current (initialized to DEFAULT_PROVIDER)
}
const providerDef = providerDefinitionMap.get(result.provider);
const providerPrefs = preferences?.providerPreferences?.[result.provider];
// 2. Resolve modeId (depends on provider)
if (!userModified.modeId) {
const validModeIds = providerDef?.modes.map((m) => m.id) ?? [];
if (
typeof initialValues?.modeId === "string" &&
initialValues.modeId.length > 0 &&
validModeIds.includes(initialValues.modeId)
) {
result.modeId = initialValues.modeId;
} else if (
providerPrefs?.mode &&
validModeIds.includes(providerPrefs.mode)
) {
result.modeId = providerPrefs.mode;
} else {
result.modeId = providerDef?.defaultModeId ?? validModeIds[0] ?? "";
}
}
// 3. Resolve model (depends on provider + availableModels)
if (!userModified.model) {
const isValidModel = (m: string) =>
availableModels?.some((am) => am.id === m) ?? false;
if (
typeof initialValues?.model === "string" &&
initialValues.model.length > 0
) {
// If models aren't loaded yet, trust the initial value
// It will be validated once models load
if (!availableModels || isValidModel(initialValues.model)) {
result.model = initialValues.model;
} else if (providerPrefs?.model && isValidModel(providerPrefs.model)) {
result.model = providerPrefs.model;
} else {
result.model = "";
}
} else if (providerPrefs?.model && isValidModel(providerPrefs.model)) {
result.model = providerPrefs.model;
} else {
result.model = "";
}
}
// 4. Resolve serverId (independent)
if (!userModified.serverId) {
if (initialValues?.serverId !== undefined) {
result.serverId = initialValues.serverId;
} else if (preferences?.serverId) {
result.serverId = preferences.serverId;
}
// else keep current
}
// 5. Resolve workingDir (independent)
if (!userModified.workingDir) {
if (initialValues?.workingDir !== undefined) {
result.workingDir = initialValues.workingDir;
} else if (preferences?.workingDir) {
result.workingDir = preferences.workingDir;
}
// else keep current (empty string)
}
return result;
}
export function useAgentFormState(
options: UseAgentFormStateOptions = {}
): UseAgentFormStateResult {
@@ -80,29 +207,36 @@ export function useAgentFormState(
const {
preferences,
isLoading: isPreferencesLoading,
getProviderPreferences,
updatePreferences,
updateProviderPreferences,
} = useFormPreferences();
const [selectedServerId, setSelectedServerId] = useState<string | null>(
initialServerId
);
const [workingDir, setWorkingDir] = useState("");
const [selectedProvider, setSelectedProvider] =
useState<AgentProvider>(DEFAULT_PROVIDER);
const [selectedMode, setSelectedMode] = useState(
DEFAULT_MODE_FOR_DEFAULT_PROVIDER
);
const [selectedModel, setSelectedModel] = useState("");
// Track which fields the user has explicitly modified
const [userModified, setUserModified] = useState<UserModifiedFields>(INITIAL_USER_MODIFIED);
const hasHydratedRef = useRef(false);
const hasAppliedInitialValuesRef = useRef(false);
const providerModelRequestTimersRef = useRef<
Map<string, ReturnType<typeof setTimeout>>
>(new Map());
// Form state
const [formState, setFormState] = useState<FormState>(() => ({
serverId: initialServerId,
provider: DEFAULT_PROVIDER,
modeId: DEFAULT_MODE_FOR_DEFAULT_PROVIDER,
model: "",
workingDir: "",
}));
// Track if we've done initial resolution (to avoid flickering)
const hasResolvedRef = useRef(false);
// Reset user modifications when form becomes invisible
useEffect(() => {
if (!isVisible) {
setUserModified(INITIAL_USER_MODIFIED);
hasResolvedRef.current = false;
}
}, [isVisible]);
// Get session state for provider models
const sessionState = useSessionStore((state) =>
selectedServerId ? state.sessions[selectedServerId] : undefined
formState.serverId ? state.sessions[formState.serverId] : undefined
);
const providerModels = sessionState?.providerModels;
const requestProviderModels = sessionState?.methods?.requestProviderModels;
@@ -111,9 +245,73 @@ export function useAgentFormState(
[]
);
// Get available models for current provider
const modelState = providerModels?.get(formState.provider);
const availableModels = modelState?.models ?? null;
// Combine initialValues with initialServerId for resolution
const combinedInitialValues = useMemo((): FormInitialValues | undefined => {
if (!initialValues && initialServerId === null) {
return undefined;
}
return {
...initialValues,
serverId: initialValues?.serverId ?? initialServerId,
};
}, [initialValues, initialServerId]);
// Resolve form state when data sources change
useEffect(() => {
if (!isVisible || !isCreateFlow) {
return;
}
// Wait for preferences to load before first resolution
if (isPreferencesLoading && !hasResolvedRef.current) {
return;
}
const resolved = resolveFormState(
combinedInitialValues,
preferences,
availableModels,
userModified,
formState
);
// Only update if something changed
if (
resolved.serverId !== formState.serverId ||
resolved.provider !== formState.provider ||
resolved.modeId !== formState.modeId ||
resolved.model !== formState.model ||
resolved.workingDir !== formState.workingDir
) {
setFormState(resolved);
}
hasResolvedRef.current = true;
}, [
isVisible,
isCreateFlow,
isPreferencesLoading,
combinedInitialValues,
preferences,
availableModels,
userModified,
formState,
]);
// Provider model request timers
const providerModelRequestTimersRef = useRef<
Map<string, ReturnType<typeof setTimeout>>
>(new Map());
// User setters - mark fields as modified and persist to preferences
const setSelectedServerIdFromUser = useCallback(
(value: string | null) => {
setSelectedServerId(value);
setFormState((prev) => ({ ...prev, serverId: value }));
setUserModified((prev) => ({ ...prev, serverId: true }));
void updatePreferences({ serverId: value ?? undefined });
},
[updatePreferences]
@@ -121,124 +319,86 @@ export function useAgentFormState(
const setProviderFromUser = useCallback(
(provider: AgentProvider) => {
setSelectedProvider(provider);
setFormState((prev) => ({ ...prev, provider }));
setUserModified((prev) => ({ ...prev, provider: true }));
void updatePreferences({ provider });
// Restore per-provider preferences if available
const providerPrefs = getProviderPreferences(provider);
// When provider changes, reset mode and model to provider defaults
// (unless user has explicitly set them)
const providerDef = providerDefinitionMap.get(provider);
const providerPrefs = preferences?.providerPreferences?.[provider];
setSelectedModel(providerPrefs?.model ?? "");
setSelectedMode(providerPrefs?.mode ?? providerDef?.defaultModeId ?? "");
setFormState((prev) => ({
...prev,
provider,
modeId: providerPrefs?.mode ?? providerDef?.defaultModeId ?? "",
model: providerPrefs?.model ?? "",
}));
},
[getProviderPreferences, updatePreferences]
[preferences?.providerPreferences, updatePreferences]
);
const setModeFromUser = useCallback(
(modeId: string) => {
setSelectedMode(modeId);
void updateProviderPreferences(selectedProvider, { mode: modeId });
setFormState((prev) => ({ ...prev, modeId }));
setUserModified((prev) => ({ ...prev, modeId: true }));
void updateProviderPreferences(formState.provider, { mode: modeId });
},
[selectedProvider, updateProviderPreferences]
[formState.provider, updateProviderPreferences]
);
const setModelFromUser = useCallback(
(modelId: string) => {
setSelectedModel(modelId);
void updateProviderPreferences(selectedProvider, { model: modelId });
setFormState((prev) => ({ ...prev, model: modelId }));
setUserModified((prev) => ({ ...prev, model: true }));
void updateProviderPreferences(formState.provider, { model: modelId });
},
[selectedProvider, updateProviderPreferences]
[formState.provider, updateProviderPreferences]
);
const setWorkingDir = useCallback((value: string) => {
setFormState((prev) => ({ ...prev, workingDir: value }));
}, []);
const setWorkingDirFromUser = useCallback(
(value: string) => {
setWorkingDir(value);
setFormState((prev) => ({ ...prev, workingDir: value }));
setUserModified((prev) => ({ ...prev, workingDir: true }));
void updatePreferences({ workingDir: value });
},
[updatePreferences]
);
const applyInitialValues = useCallback(() => {
if (!isCreateFlow || !initialValues) {
return;
}
if (Object.prototype.hasOwnProperty.call(initialValues, "workingDir")) {
setWorkingDir(initialValues.workingDir ?? "");
}
if (initialValues.provider && providerDefinitionMap.has(initialValues.provider)) {
setSelectedProvider(initialValues.provider);
}
if (typeof initialValues.modeId === "string" && initialValues.modeId.length > 0) {
setSelectedMode(initialValues.modeId);
}
if (typeof initialValues.model === "string" && initialValues.model.length > 0) {
setSelectedModel(initialValues.model);
}
}, [initialValues, isCreateFlow]);
useEffect(() => {
if (!isVisible) {
hasAppliedInitialValuesRef.current = false;
return;
}
if (hasAppliedInitialValuesRef.current) {
return;
}
applyInitialValues();
hasAppliedInitialValuesRef.current = true;
}, [applyInitialValues, isVisible]);
const setSelectedServerId = useCallback((value: string | null) => {
setFormState((prev) => ({ ...prev, serverId: value }));
}, []);
const refreshProviderModels = useCallback(() => {
if (!requestProviderModels) {
return;
}
const trimmed = workingDir.trim();
requestProviderModels(selectedProvider, {
const trimmed = formState.workingDir.trim();
requestProviderModels(formState.provider, {
cwd: trimmed.length > 0 ? trimmed : undefined,
});
}, [requestProviderModels, selectedProvider, workingDir]);
// Hydrate form state from preferences once loaded
useEffect(() => {
if (isPreferencesLoading || hasHydratedRef.current) return;
hasHydratedRef.current = true;
const activeProvider =
preferences.provider &&
providerDefinitionMap.has(preferences.provider as AgentProvider)
? (preferences.provider as AgentProvider)
: DEFAULT_PROVIDER;
const providerPrefs = preferences.providerPreferences?.[activeProvider];
const providerDef = providerDefinitionMap.get(activeProvider);
setSelectedProvider(activeProvider);
if (preferences.workingDir) setWorkingDir(preferences.workingDir);
setSelectedMode(providerPrefs?.mode ?? providerDef?.defaultModeId ?? "");
if (providerPrefs?.model) setSelectedModel(providerPrefs.model);
if (preferences.serverId) setSelectedServerId(preferences.serverId);
}, [isPreferencesLoading, preferences]);
}, [requestProviderModels, formState.provider, formState.workingDir]);
const persistFormPreferences = useCallback(async () => {
await updatePreferences({
workingDir,
provider: selectedProvider,
serverId: selectedServerId ?? undefined,
workingDir: formState.workingDir,
provider: formState.provider,
serverId: formState.serverId ?? undefined,
});
await updateProviderPreferences(selectedProvider, {
mode: selectedMode,
model: selectedModel,
await updateProviderPreferences(formState.provider, {
mode: formState.modeId,
model: formState.model,
});
}, [
selectedMode,
selectedModel,
selectedProvider,
selectedServerId,
workingDir,
formState.modeId,
formState.model,
formState.provider,
formState.serverId,
formState.workingDir,
updatePreferences,
updateProviderPreferences,
]);
@@ -270,7 +430,7 @@ export function useAgentFormState(
return;
}
const currentState = sessionState.providerModels?.get(selectedProvider);
const currentState = sessionState.providerModels?.get(formState.provider);
if (currentState?.models?.length || currentState?.isLoading) {
clearQueuedProviderModelRequest(serverId);
return;
@@ -279,7 +439,7 @@ export function useAgentFormState(
const delayMs = options?.delayMs ?? 0;
const trigger = () => {
providerModelRequestTimersRef.current.delete(serverId);
sessionState.methods?.requestProviderModels(selectedProvider, {
sessionState.methods?.requestProviderModels(formState.provider, {
...(options?.cwd ? { cwd: options.cwd } : {}),
});
};
@@ -290,7 +450,7 @@ export function useAgentFormState(
trigger();
}
},
[clearQueuedProviderModelRequest, getSessionState, selectedProvider]
[clearQueuedProviderModelRequest, getSessionState, formState.provider]
);
useEffect(() => {
@@ -303,74 +463,53 @@ export function useAgentFormState(
}, []);
useEffect(() => {
if (!isVisible || !isTargetDaemonReady || !selectedServerId) {
clearQueuedProviderModelRequest(selectedServerId);
if (!isVisible || !isTargetDaemonReady || !formState.serverId) {
clearQueuedProviderModelRequest(formState.serverId);
return;
}
const trimmed = workingDir.trim();
queueProviderModelFetch(selectedServerId, {
const trimmed = formState.workingDir.trim();
queueProviderModelFetch(formState.serverId, {
cwd: trimmed.length > 0 ? trimmed : undefined,
delayMs: 180,
});
return () => {
clearQueuedProviderModelRequest(selectedServerId);
clearQueuedProviderModelRequest(formState.serverId);
};
}, [
clearQueuedProviderModelRequest,
isTargetDaemonReady,
isVisible,
queueProviderModelFetch,
selectedServerId,
workingDir,
formState.serverId,
formState.workingDir,
]);
const agentDefinition = providerDefinitionMap.get(selectedProvider);
const agentDefinition = providerDefinitionMap.get(formState.provider);
const modeOptions = agentDefinition?.modes ?? [];
const modelState = providerModels?.get(selectedProvider);
const availableModels = modelState?.models ?? [];
const isModelLoading = modelState?.isLoading ?? false;
const modelError = modelState?.error ?? null;
useEffect(() => {
if (!agentDefinition) {
return;
}
if (modeOptions.length === 0) {
if (selectedMode !== "") {
setSelectedMode("");
}
return;
}
const availableModeIds = modeOptions.map((mode) => mode.id);
if (!availableModeIds.includes(selectedMode)) {
const fallbackModeId = agentDefinition.defaultModeId ?? availableModeIds[0];
setSelectedMode(fallbackModeId);
}
}, [agentDefinition, modeOptions, selectedMode]);
const workingDirIsEmpty = !workingDir.trim();
const workingDirIsEmpty = !formState.workingDir.trim();
return useMemo(
() => ({
selectedServerId,
selectedServerId: formState.serverId,
setSelectedServerId,
setSelectedServerIdFromUser,
selectedProvider,
selectedProvider: formState.provider,
setProviderFromUser,
selectedMode,
selectedMode: formState.modeId,
setModeFromUser,
selectedModel,
selectedModel: formState.model,
setModelFromUser,
workingDir,
workingDir: formState.workingDir,
setWorkingDir,
setWorkingDirFromUser,
providerDefinitions,
providerDefinitionMap,
agentDefinition,
modeOptions,
availableModels,
availableModels: availableModels ?? [],
isModelLoading,
modelError,
refreshProviderModels,
@@ -380,26 +519,31 @@ export function useAgentFormState(
persistFormPreferences,
}),
[
agentDefinition,
availableModels,
isModelLoading,
modelError,
modeOptions,
queueProviderModelFetch,
clearQueuedProviderModelRequest,
refreshProviderModels,
selectedMode,
selectedModel,
selectedProvider,
selectedServerId,
formState.serverId,
formState.provider,
formState.modeId,
formState.model,
formState.workingDir,
setSelectedServerId,
setSelectedServerIdFromUser,
setProviderFromUser,
setModeFromUser,
setModelFromUser,
setWorkingDir,
setWorkingDirFromUser,
workingDir,
agentDefinition,
modeOptions,
availableModels,
isModelLoading,
modelError,
refreshProviderModels,
queueProviderModelFetch,
clearQueuedProviderModelRequest,
workingDirIsEmpty,
persistFormPreferences,
]
);
}
// Re-export for backwards compatibility
export type CreateAgentInitialValues = FormInitialValues;

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import type { SessionContextValue } from "@/contexts/session-context";
import { useAudioRecorder } from "@/hooks/use-audio-recorder";
import type { DaemonClientV2 } from "@server/client/daemon-client-v2";
import type { TranscriptionResultMessage } from "@server/shared/messages";
@@ -33,10 +32,7 @@ export type DictationOutcome =
| { type: "failure"; requestId: string; errorMessage: string; timestamp: number };
export type UseDictationOptions = {
agentId?: string;
sendAgentAudio: SessionContextValue["sendAgentAudio"];
client: DaemonClientV2 | null;
mode?: "transcribe_only" | "auto_run";
onTranscript: (text: string, meta: { requestId: string }) => void;
onError?: (error: Error) => void;
onRetryAttempt?: (info: DictationRetryInfo) => void;
@@ -121,6 +117,16 @@ type CapturedAudioPayload = {
type TranscriptionPayload = TranscriptionResultMessage["payload"];
const blobToBase64 = async (blob: Blob): Promise<string> => {
const arrayBuffer = await blob.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
};
const deriveFormatFromMime = (mimeType?: string): string => {
if (!mimeType || mimeType.length === 0) {
return "webm";
@@ -144,10 +150,7 @@ const buildCapturedAudioPayload = (blob: Blob, durationSeconds: number): Capture
export function useDictation(options: UseDictationOptions): UseDictationResult {
const {
agentId,
sendAgentAudio,
client,
mode = "transcribe_only",
onTranscript,
onError,
onRetryAttempt,
@@ -172,7 +175,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
const maxRetryAttempts = MAX_AUTO_RETRY_ATTEMPTS;
const transcriptionMutation = useMutation({
mutationFn: async (requestId: string): Promise<TranscriptionPayload> => {
mutationFn: async (): Promise<TranscriptionPayload> => {
const capturedAudio = pendingAudioRef.current;
if (!capturedAudio) {
throw new Error("No recorded audio available for transcription");
@@ -199,9 +202,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
}
console.info("[useDictation] sending transcription request", {
requestId,
attempt,
agentId,
size: capturedAudio.sizeBytes,
durationSeconds: capturedAudio.durationSeconds,
});
@@ -210,20 +211,12 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
setRetryAttempt(attempt);
try {
const transcriptionPromise = client.waitForTranscriptionResult(
requestId,
TRANSCRIPTION_TIMEOUT_MS
);
try {
await sendAgentAudio(agentId, capturedAudio.blob, requestId, {
mode,
});
} catch (error) {
transcriptionPromise.catch(() => {});
throw new DictationAttemptError("dispatch", toError(error));
}
return await transcriptionPromise;
const base64Audio = await blobToBase64(capturedAudio.blob);
return await client.transcribeAudio({
audio: base64Audio,
format: capturedAudio.format,
timeout: TRANSCRIPTION_TIMEOUT_MS,
});
} catch (error) {
const normalized = toError(error);
const reason: DictationRetryReason = isTimeoutError(normalized)
@@ -256,7 +249,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
setRetryInfo(info);
onRetryAttemptRef.current?.(info);
console.warn("[useDictation] retry scheduled", {
requestId,
attempt: nextAttempt,
maxAttempts: MAX_AUTO_RETRY_ATTEMPTS,
reason: attemptError.reason,
@@ -383,12 +375,13 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
}, []);
const transmitDictation = useCallback(
async (requestId: string) => runTranscription(requestId),
async () => runTranscription(),
[runTranscription]
);
const handleTranscriptionSuccess = useCallback(
(transcription: TranscriptionPayload, requestId: string) => {
(transcription: TranscriptionPayload) => {
const requestId = transcription.requestId;
pendingRequestIdRef.current = null;
setPendingRequestId(null);
setIsProcessing(false);
@@ -409,16 +402,15 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
requestId,
textLength: transcriptText.length,
});
onTranscriptRef.current?.(transcriptText, {
requestId: transcription.requestId ?? requestId,
});
onTranscriptRef.current?.(transcriptText, { requestId });
},
[onTranscriptRef]
);
const handleDictationFailure = useCallback(
(failure: unknown, requestId: string | null) => {
(failure: unknown) => {
const normalized = toError(failure);
const failureId = generateMessageId();
pendingRequestIdRef.current = null;
setPendingRequestId(null);
setIsProcessing(false);
@@ -431,16 +423,14 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
if (capturedAudio) {
setStatus("failed");
setFailedRecording({
requestId: requestId ?? generateMessageId(),
requestId: failureId,
durationSeconds: capturedAudio.durationSeconds,
sizeBytes: capturedAudio.sizeBytes,
format: capturedAudio.format,
recordedAt: capturedAudio.recordedAt,
errorMessage: normalized.message,
});
if (requestId) {
onPermanentFailureRef.current?.(normalized, { requestId });
}
onPermanentFailureRef.current?.(normalized, { requestId: failureId });
} else {
setStatus("idle");
}
@@ -448,7 +438,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
setRetryAttempt(0);
setLastOutcome({
type: "failure",
requestId: requestId ?? generateMessageId(),
requestId: failureId,
errorMessage: normalized.message,
timestamp: Date.now(),
});
@@ -571,7 +561,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
setRetryAttempt(0);
setLastOutcome(null);
let requestId: string | null = null;
try {
const audioData = await stopRecorder();
const recordedDurationSeconds = durationRef.current;
@@ -581,21 +570,15 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
setIsRecording(false);
setVolume(0);
requestId = generateMessageId();
pendingRequestIdRef.current = requestId;
setPendingRequestId(requestId);
const transcription = await transmitDictation(requestId);
handleTranscriptionSuccess(transcription, requestId);
const transcription = await transmitDictation();
handleTranscriptionSuccess(transcription);
} catch (err) {
resetTranscriptionMutation();
handleDictationFailure(err, requestId);
handleDictationFailure(err);
}
}, [
agentId,
canConfirm,
isProcessing,
mode,
handleDictationFailure,
handleTranscriptionSuccess,
resetTranscriptionMutation,
@@ -615,16 +598,12 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
setIsProcessing(true);
setLastOutcome(null);
const requestId = generateMessageId();
pendingRequestIdRef.current = requestId;
setPendingRequestId(requestId);
try {
const transcription = await transmitDictation(requestId);
handleTranscriptionSuccess(transcription, requestId);
const transcription = await transmitDictation();
handleTranscriptionSuccess(transcription);
} catch (err) {
resetTranscriptionMutation();
handleDictationFailure(err, requestId);
handleDictationFailure(err);
}
}, [
handleDictationFailure,
@@ -687,17 +666,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult {
}
}, [autoStopWhenHidden?.isVisible]);
useEffect(() => {
pendingRequestIdRef.current = null;
setPendingRequestId(null);
setIsProcessing(false);
pendingAudioRef.current = null;
setStatus("idle");
setRetryAttempt(0);
setRetryInfo(null);
setFailedRecording(null);
resetTranscriptionMutation();
}, [agentId, resetTranscriptionMutation]);
useEffect(() => {
return () => {

View File

@@ -27,7 +27,7 @@ interface DesktopSidebarState {
fileExplorerOpen: boolean;
}
type ExplorerTab = "changes" | "files";
export type ExplorerTab = "changes" | "files";
export type ViewMode = "list" | "grid";
export type SortOption = "name" | "modified" | "size";

View File

@@ -182,12 +182,6 @@ export interface SessionState {
message: string,
images?: Array<{ uri: string; mimeType?: string }>
) => Promise<void>;
sendAgentAudio: (
agentId: string | undefined,
audioBlob: Blob,
requestId?: string,
options?: { mode?: "transcribe_only" | "auto_run" }
) => Promise<void>;
deleteAgent: (agentId: string) => void;
createAgent: (options: {
config: any;

View File

@@ -0,0 +1,10 @@
/**
* Shortens a file path by replacing the home directory prefix with ~.
* Handles both macOS (/Users/username) and Linux (/home/username) paths.
*/
export function shortenPath(path: string | undefined | null): string {
if (!path) {
return "";
}
return path.replace(/^\/(?:Users|home)\/[^/]+/, "~");
}

View File

@@ -0,0 +1,104 @@
import { Platform } from "react-native";
import { useState, useEffect } from "react";
import { getIsTauriMac, TAURI_TRAFFIC_LIGHT_WIDTH, TAURI_TRAFFIC_LIGHT_HEIGHT } from "@/constants/layout";
let tauriWindow: any = null;
async function getTauriWindow() {
if (tauriWindow) return tauriWindow;
if (!getIsTauriMac()) return null;
try {
const { getCurrentWindow } = await import("@tauri-apps/api/window");
tauriWindow = getCurrentWindow();
return tauriWindow;
} catch {
return null;
}
}
export async function startDragging() {
const win = await getTauriWindow();
if (win) {
await win.startDragging();
}
}
export async function toggleMaximize() {
const win = await getTauriWindow();
if (win) {
await win.toggleMaximize();
}
}
// Returns event handlers for drag region behavior
export function useTauriDragHandlers() {
if (Platform.OS !== "web" || !getIsTauriMac()) {
return {};
}
return {
onMouseDown: (e: React.MouseEvent) => {
// Only handle left click, ignore if clicking on interactive elements
if (e.button !== 0) return;
const target = e.target as HTMLElement;
if (target.closest("button, a, input, [role='button']")) return;
// Prevent text selection when dragging
e.preventDefault();
// Start dragging on mouse down
startDragging();
},
onDoubleClick: (e: React.MouseEvent) => {
// Ignore if clicking on interactive elements
const target = e.target as HTMLElement;
if (target.closest("button, a, input, [role='button']")) return;
e.preventDefault();
toggleMaximize();
},
};
}
// Hook that returns traffic light padding, accounting for fullscreen state
// In fullscreen, traffic lights are hidden so no padding is needed
export function useTrafficLightPadding(): { left: number; top: number } {
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
if (Platform.OS !== "web" || !getIsTauriMac()) return;
let unlisten: (() => void) | undefined;
async function setup() {
const win = await getTauriWindow();
if (!win) return;
// Check initial fullscreen state
const fullscreen = await win.isFullscreen();
setIsFullscreen(fullscreen);
// Listen for resize events which include fullscreen changes
unlisten = await win.onResized(async () => {
const fs = await win.isFullscreen();
setIsFullscreen(fs);
});
}
setup();
return () => {
unlisten?.();
};
}, []);
if (!getIsTauriMac() || isFullscreen) {
return { left: 0, top: 0 };
}
return {
left: TAURI_TRAFFIC_LIGHT_WIDTH,
top: TAURI_TRAFFIC_LIGHT_HEIGHT,
};
}

View File

@@ -42,8 +42,9 @@ describe("parseToolCallDisplay", () => {
const input = { command: "pwd", description: "Print working directory" };
const result = { type: "command", command: "pwd", output: "/some/path" };
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
const display: ToolCallDisplay = parseToolCallDisplay("Bash", input, result);
expect(display.type).toBe("shell");
expect(display.toolName).toBe("Shell");
if (display.type === "shell") {
expect(display.command).toBe("pwd");
expect(display.output).toBe("/some/path");
@@ -55,8 +56,9 @@ describe("parseToolCallDisplay", () => {
const input = { command: "pwd", description: "Print working directory" };
const result = undefined;
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
const display: ToolCallDisplay = parseToolCallDisplay("Bash", input, result);
expect(display.type).toBe("shell");
expect(display.toolName).toBe("Shell");
if (display.type === "shell") {
expect(display.command).toBe("pwd");
expect(display.output).toBe("");
@@ -67,19 +69,44 @@ describe("parseToolCallDisplay", () => {
const input = { command: ["git", "status"] };
const result = { type: "command", output: "On branch main" };
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
const display: ToolCallDisplay = parseToolCallDisplay("shell", input, result);
expect(display.type).toBe("shell");
expect(display.toolName).toBe("Shell");
if (display.type === "shell") {
expect(display.command).toBe("git status");
expect(display.output).toBe("On branch main");
}
});
test("normalizes tool names - shell to Shell", () => {
const input = { command: "pwd" };
const display = parseToolCallDisplay("shell", input, undefined);
expect(display.toolName).toBe("Shell");
});
test("normalizes tool names - Bash to Shell", () => {
const input = { command: "pwd" };
const display = parseToolCallDisplay("Bash", input, undefined);
expect(display.toolName).toBe("Shell");
});
test("normalizes tool names - read_file to Read", () => {
const input = { file_path: "/some/file.txt" };
const display = parseToolCallDisplay("read_file", input, undefined);
expect(display.toolName).toBe("Read");
});
test("preserves unknown tool names", () => {
const input = { some_arg: "value" };
const display = parseToolCallDisplay("MyCustomTool", input, undefined);
expect(display.toolName).toBe("MyCustomTool");
});
test("parses non-command tool call into generic type", () => {
const input = { file_path: "/some/file.txt" };
const result = { content: "file contents here", lineCount: 42 };
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
const display: ToolCallDisplay = parseToolCallDisplay("SomeTool", input, result);
expect(display.type).toBe("generic");
if (display.type === "generic") {
expect(display.input).toContainEqual({ key: "file_path", value: "/some/file.txt" });
@@ -92,12 +119,12 @@ describe("parseToolCallDisplay", () => {
const input = { file_path: "/some/file.txt", content: "new content" };
const result = { type: "file_write", filePath: "/some/file.txt" };
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
const display: ToolCallDisplay = parseToolCallDisplay("Write", input, result);
expect(display.type).toBe("generic");
});
test("handles undefined input and result gracefully", () => {
const display: ToolCallDisplay = parseToolCallDisplay(undefined, undefined);
const display: ToolCallDisplay = parseToolCallDisplay("unknown", undefined, undefined);
expect(display.type).toBe("generic");
if (display.type === "generic") {
expect(display.input).toEqual([]);
@@ -116,7 +143,7 @@ describe("parseToolCallDisplay", () => {
filePath: "/some/file.txt",
};
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
const display: ToolCallDisplay = parseToolCallDisplay("Edit", input, result);
expect(display.type).toBe("edit");
if (display.type === "edit") {
expect(display.filePath).toBe("/some/file.txt");
@@ -133,7 +160,7 @@ describe("parseToolCallDisplay", () => {
};
const result = undefined;
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
const display: ToolCallDisplay = parseToolCallDisplay("Edit", input, result);
expect(display.type).toBe("edit");
if (display.type === "edit") {
expect(display.filePath).toBe("/some/file.txt");
@@ -149,7 +176,7 @@ describe("parseToolCallDisplay", () => {
new_string: "new content",
};
const display: ToolCallDisplay = parseToolCallDisplay(input, undefined);
const display: ToolCallDisplay = parseToolCallDisplay("Edit", input, undefined);
expect(display.type).toBe("edit");
if (display.type === "edit") {
expect(display.filePath).toBe("/some/file.txt");
@@ -158,3 +185,111 @@ describe("parseToolCallDisplay", () => {
}
});
});
describe("parseToolCallDisplay - apply_patch (Codex)", () => {
test("parses apply_patch into edit type with unified diff", () => {
const input = {
files: [
{
path: "/Users/moboudra/dev/blankpage/editor/.tasks/578561c8.md",
kind: "update",
},
],
};
const result = {
files: [
{
path: "/Users/moboudra/dev/blankpage/editor/.tasks/578561c8.md",
patch: "@@ -15,3 +15,2 @@\n-This task defines the **design philosophy**\n+This task defines the **updated philosophy**",
kind: "update",
},
],
message: "Success. Updated the following files:\nM .tasks/578561c8.md",
success: true,
};
const display = parseToolCallDisplay("apply_patch", input, result);
expect(display.type).toBe("edit");
expect(display.toolName).toBe("Edit");
if (display.type === "edit") {
expect(display.filePath).toBe("/Users/moboudra/dev/blankpage/editor/.tasks/578561c8.md");
expect(display.unifiedDiff).toBe("@@ -15,3 +15,2 @@\n-This task defines the **design philosophy**\n+This task defines the **updated philosophy**");
expect(display.oldString).toBe("");
expect(display.newString).toBe("");
}
});
test("parses pending apply_patch (no result yet)", () => {
const input = {
files: [
{
path: "/some/file.txt",
kind: "create",
},
],
};
const display = parseToolCallDisplay("apply_patch", input, undefined);
expect(display.type).toBe("edit");
expect(display.toolName).toBe("Edit");
if (display.type === "edit") {
expect(display.filePath).toBe("/some/file.txt");
expect(display.unifiedDiff).toBeUndefined();
}
});
test("handles apply_patch with multiple files (uses first file)", () => {
const input = {
files: [
{ path: "/first/file.txt", kind: "update" },
{ path: "/second/file.txt", kind: "create" },
],
};
const result = {
files: [
{ path: "/first/file.txt", patch: "@@ -1 +1 @@\n-old\n+new", kind: "update" },
{ path: "/second/file.txt", patch: "@@ -0,0 +1 @@\n+content", kind: "create" },
],
success: true,
};
const display = parseToolCallDisplay("apply_patch", input, result);
expect(display.type).toBe("edit");
if (display.type === "edit") {
expect(display.filePath).toBe("/first/file.txt");
expect(display.unifiedDiff).toBe("@@ -1 +1 @@\n-old\n+new");
}
});
});
describe("parseToolCallDisplay - read_file (Codex)", () => {
test("parses Codex read_file into read type", () => {
const input = {
path: "/Users/moboudra/dev/blankpage/editor/.tasks/578561c8.md",
};
const result = {
type: "read_file",
path: "/Users/moboudra/dev/blankpage/editor/.tasks/578561c8.md",
content: "260 - Source: `**bold**|`\n261 - Action: `Shift+ArrowLeft`",
};
const display = parseToolCallDisplay("read_file", input, result);
expect(display.type).toBe("read");
expect(display.toolName).toBe("Read");
if (display.type === "read") {
expect(display.filePath).toBe("/Users/moboudra/dev/blankpage/editor/.tasks/578561c8.md");
expect(display.content).toBe("260 - Source: `**bold**|`\n261 - Action: `Shift+ArrowLeft`");
}
});
test("Codex read_file falls through to generic when result is missing", () => {
const input = {
path: "/some/file.txt",
};
const display = parseToolCallDisplay("read_file", input, undefined);
// Without result, it can't match the schema so falls through to generic
expect(display.type).toBe("generic");
expect(display.toolName).toBe("Read");
});
});

View File

@@ -270,7 +270,7 @@ export function buildLineDiff(originalText: string, updatedText: string): DiffLi
return diff;
}
function parseUnifiedDiff(diffText?: string): DiffLine[] {
export function parseUnifiedDiff(diffText?: string): DiffLine[] {
if (!diffText) {
return [];
}
@@ -1023,7 +1023,7 @@ const EditToolCallSchema = z
input: EditInputSchema,
result: z.unknown(),
})
.transform((data): { type: "edit"; filePath: string; oldString: string; newString: string } => {
.transform((data): { type: "edit"; filePath: string; oldString: string; newString: string; unifiedDiff?: string } => {
const filePath = data.input.file_path;
const oldString = "old_string" in data.input
? (data.input as { old_string: string }).old_string
@@ -1040,6 +1040,52 @@ const EditToolCallSchema = z
};
});
// Codex apply_patch input: { files: [{ path: string, kind: string }] }
const ApplyPatchInputSchema = z.object({
files: z.array(z.object({
path: z.string(),
kind: z.string().optional(),
})).min(1),
}).passthrough();
// Codex apply_patch result: { files: [{ path: string, patch: string, kind: string }], message: string, success: boolean }
const ApplyPatchResultSchema = z.object({
files: z.array(z.object({
path: z.string(),
patch: z.string().optional(),
kind: z.string().optional(),
})).optional(),
message: z.string().optional(),
success: z.boolean().optional(),
}).passthrough();
// Apply patch tool call display schema - transforms to edit type with unified diff
const ApplyPatchToolCallSchema = z
.object({
input: ApplyPatchInputSchema,
result: z.unknown(),
})
.transform((data): { type: "edit"; filePath: string; oldString: string; newString: string; unifiedDiff?: string } => {
const firstFile = data.input.files[0];
const filePath = firstFile.path;
// Try to get the patch from the result
const resultParsed = ApplyPatchResultSchema.safeParse(data.result);
let unifiedDiff: string | undefined;
if (resultParsed.success && resultParsed.data.files) {
const resultFile = resultParsed.data.files.find(f => f.path === filePath) ?? resultParsed.data.files[0];
unifiedDiff = resultFile?.patch;
}
return {
type: "edit",
filePath,
oldString: "",
newString: "",
unifiedDiff,
};
});
// Read input: { file_path: string, offset?: number, limit?: number }
const ReadInputSchema = z.object({
file_path: z.string(),
@@ -1054,7 +1100,7 @@ const ReadResultSchema = z.object({
content: z.string(),
}).passthrough();
// Read tool call display schema
// Read tool call display schema (Claude)
const ReadToolCallSchema = z
.object({
input: ReadInputSchema,
@@ -1068,6 +1114,30 @@ const ReadToolCallSchema = z
limit: data.input.limit,
}));
// Codex read_file input: { path: string }
const CodexReadInputSchema = z.object({
path: z.string(),
}).passthrough();
// Codex read_file result: { type: "read_file", path: string, content: string }
const CodexReadResultSchema = z.object({
type: z.literal("read_file"),
path: z.string(),
content: z.string(),
}).passthrough();
// Codex read_file tool call display schema
const CodexReadToolCallSchema = z
.object({
input: CodexReadInputSchema,
result: CodexReadResultSchema,
})
.transform((data): { type: "read"; filePath: string; content: string; offset?: number; limit?: number } => ({
type: "read",
filePath: data.input.path,
content: data.result.content,
}));
// Generic tool call display schema (fallback)
const GenericToolCallSchema = z
.object({
@@ -1085,12 +1155,60 @@ const GenericToolCallSchema = z
};
});
const ToolCallDisplaySchema = z.union([ShellToolCallSchema, EditToolCallSchema, ReadToolCallSchema, GenericToolCallSchema]);
// Normalizes tool names for consistent display across agents
const TOOL_NAME_MAP: Record<string, string> = {
shell: "Shell",
Bash: "Shell",
read_file: "Read",
apply_patch: "Edit",
};
const ToolCallDisplaySchema = z
.object({
toolName: z.string(),
input: z.unknown(),
result: z.unknown(),
})
.transform((data) => {
const normalizedToolName = TOOL_NAME_MAP[data.toolName] ?? data.toolName;
// Try each schema in order
const shellParsed = ShellToolCallSchema.safeParse({ input: data.input, result: data.result });
if (shellParsed.success) {
return { ...shellParsed.data, toolName: normalizedToolName };
}
const editParsed = EditToolCallSchema.safeParse({ input: data.input, result: data.result });
if (editParsed.success) {
return { ...editParsed.data, toolName: normalizedToolName };
}
// Codex apply_patch - try before read since it also uses files array
const applyPatchParsed = ApplyPatchToolCallSchema.safeParse({ input: data.input, result: data.result });
if (applyPatchParsed.success) {
return { ...applyPatchParsed.data, toolName: normalizedToolName };
}
const readParsed = ReadToolCallSchema.safeParse({ input: data.input, result: data.result });
if (readParsed.success) {
return { ...readParsed.data, toolName: normalizedToolName };
}
// Codex read_file
const codexReadParsed = CodexReadToolCallSchema.safeParse({ input: data.input, result: data.result });
if (codexReadParsed.success) {
return { ...codexReadParsed.data, toolName: normalizedToolName };
}
// Fallback to generic
const genericParsed = GenericToolCallSchema.parse({ input: data.input, result: data.result });
return { ...genericParsed, toolName: normalizedToolName };
});
export type ToolCallDisplay = z.infer<typeof ToolCallDisplaySchema>;
export function parseToolCallDisplay(input: unknown, result: unknown): ToolCallDisplay {
return ToolCallDisplaySchema.parse({ input, result });
export function parseToolCallDisplay(toolName: string, input: unknown, result: unknown): ToolCallDisplay {
return ToolCallDisplaySchema.parse({ toolName, input, result });
}
// ---- Principal Parameter Extraction ----

233
packages/desktop/package-lock.json generated Normal file
View File

@@ -0,0 +1,233 @@
{
"name": "desktop",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "desktop",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@tauri-apps/cli": "^2.9.6"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz",
"integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.9.6",
"@tauri-apps/cli-darwin-x64": "2.9.6",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6",
"@tauri-apps/cli-linux-arm64-gnu": "2.9.6",
"@tauri-apps/cli-linux-arm64-musl": "2.9.6",
"@tauri-apps/cli-linux-riscv64-gnu": "2.9.6",
"@tauri-apps/cli-linux-x64-gnu": "2.9.6",
"@tauri-apps/cli-linux-x64-musl": "2.9.6",
"@tauri-apps/cli-win32-arm64-msvc": "2.9.6",
"@tauri-apps/cli-win32-ia32-msvc": "2.9.6",
"@tauri-apps/cli-win32-x64-msvc": "2.9.6"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz",
"integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz",
"integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz",
"integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz",
"integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz",
"integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz",
"integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz",
"integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz",
"integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz",
"integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz",
"integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz",
"integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "@paseo/desktop",
"version": "1.0.0",
"private": true,
"description": "Paseo desktop app (Tauri wrapper)",
"scripts": {
"dev": "tauri dev",
"build": "tauri build",
"tauri": "tauri"
},
"devDependencies": {
"@tauri-apps/cli": "^2.9.6"
}
}

4
packages/desktop/src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

4952
packages/desktop/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
[package]
name = "paseo"
version = "0.1.0"
description = "Paseo Desktop"
authors = ["moboudra"]
license = "MIT"
repository = "https://github.com/moboudra/paseo"
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "paseo_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "Paseo"
path = "src/main.rs"
[build-dependencies]
tauri-build = { version = "2.5.3", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.9.5", features = [] }
tauri-plugin-log = "2"

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,72 @@
use std::sync::atomic::{AtomicU64, Ordering};
use tauri::menu::{Menu, MenuItemBuilder, Submenu};
use tauri::Manager;
use tauri::WebviewWindow;
// Store zoom as u64 bits (f64 * 100 as integer for atomic ops)
static ZOOM_LEVEL: AtomicU64 = AtomicU64::new(100);
fn get_zoom_factor() -> f64 {
ZOOM_LEVEL.load(Ordering::Relaxed) as f64 / 100.0
}
fn set_zoom_factor(webview: &WebviewWindow, factor: f64) {
let clamped = factor.clamp(0.5, 3.0);
ZOOM_LEVEL.store((clamped * 100.0) as u64, Ordering::Relaxed);
let _ = webview.set_zoom(clamped);
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
// Build the View menu with zoom controls
let zoom_in = MenuItemBuilder::with_id("zoom_in", "Zoom In")
.accelerator("CmdOrCtrl+=")
.build(app)?;
let zoom_out = MenuItemBuilder::with_id("zoom_out", "Zoom Out")
.accelerator("CmdOrCtrl+-")
.build(app)?;
let zoom_reset = MenuItemBuilder::with_id("zoom_reset", "Actual Size")
.accelerator("CmdOrCtrl+0")
.build(app)?;
let view_menu = Submenu::with_items(
app,
"View",
true,
&[&zoom_in, &zoom_out, &zoom_reset],
)?;
let menu = Menu::with_items(app, &[&view_menu])?;
app.set_menu(menu)?;
let window = app.get_webview_window("main").unwrap();
let window_clone = window.clone();
app.on_menu_event(move |_app, event| {
let id = event.id().as_ref();
if id == "zoom_in" {
let current = get_zoom_factor();
set_zoom_factor(&window_clone, current + 0.1);
} else if id == "zoom_out" {
let current = get_zoom_factor();
set_zoom_factor(&window_clone, current - 0.1);
} else if id == "zoom_reset" {
set_zoom_factor(&window_clone, 1.0);
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
paseo_lib::run();
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Paseo",
"version": "0.1.0",
"identifier": "dev.paseo.desktop",
"build": {
"frontendDist": "../../app/dist",
"devUrl": "http://localhost:8081"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "Paseo",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"trafficLightPosition": { "x": 16, "y": 22 }
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

Some files were not shown because too many files have changed in this diff Show More