diff --git a/package-lock.json b/package-lock.json index 0f01cbfee..b214c2492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -146,7 +146,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1528,7 +1527,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -5069,6 +5067,7 @@ "integrity": "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", @@ -5093,6 +5092,7 @@ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -5107,6 +5107,7 @@ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -5128,6 +5129,7 @@ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -5141,6 +5143,7 @@ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=6.6.0" } @@ -5151,6 +5154,7 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -5194,6 +5198,7 @@ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -5212,6 +5217,7 @@ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -5222,6 +5228,7 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -5235,6 +5242,7 @@ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -5245,6 +5253,7 @@ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=18" }, @@ -5258,6 +5267,7 @@ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -5271,6 +5281,7 @@ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -5281,6 +5292,7 @@ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "optional": true, + "peer": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -5297,6 +5309,7 @@ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -5320,6 +5333,7 @@ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -5336,6 +5350,7 @@ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -6505,7 +6520,6 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.18.tgz", "integrity": "sha512-DZgd6860dxcq3YX7UzIXeBr6m3UgXvo9acxp5jiJyIZXdR00Br9JwVkO7e0bUeTA2d3Z8dsmtAR84Y86NnH64Q==", "license": "MIT", - "peer": true, "dependencies": { "@react-navigation/core": "^7.12.4", "escape-string-regexp": "^4.0.0", @@ -7195,7 +7209,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -7230,7 +7243,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7352,7 +7364,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -8041,7 +8052,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8102,7 +8112,6 @@ "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.78.tgz", "integrity": "sha512-ec77fmQwJGLduswMrW4AAUGSOiu8dZaIwMmWHHGKsrMUFFS6ugfkTyx0srtuKYHNRRLRC2dT7cPirnUl98VnxA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@ai-sdk/gateway": "2.0.1", "@ai-sdk/provider": "2.0.0", @@ -8991,7 +9000,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -10476,7 +10484,6 @@ "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -11041,7 +11048,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11238,7 +11244,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -11609,7 +11614,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.20.tgz", "integrity": "sha512-mWHky+H63W60P5Oo+VbtqzF2sLvdaoSSwG57H9rlq1DrgIla++QJZuwJkXXo55lYPymVmkVhwG6FjWYKKylwpw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.13", @@ -11782,7 +11786,6 @@ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz", "integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==", "license": "MIT", - "peer": true, "dependencies": { "@expo/config": "~12.0.10", "@expo/env": "~2.0.7" @@ -11863,7 +11866,6 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz", "integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -11941,7 +11943,6 @@ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz", "integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==", "license": "MIT", - "peer": true, "dependencies": { "expo-constants": "~18.0.8", "invariant": "^2.2.4" @@ -13247,7 +13248,6 @@ "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -14771,7 +14771,6 @@ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -17237,7 +17236,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -17278,7 +17276,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -17315,7 +17312,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -17405,7 +17401,6 @@ "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.7.0.tgz", "integrity": "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17425,7 +17420,6 @@ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", "license": "MIT", - "peer": true, "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -17482,7 +17476,6 @@ "integrity": "sha512-+/uVS7FQwOiKYZQERMIvBRv5/X3CVHrFG6Nr/kIhVfVxGeUimHnBz7cgA97lJKIn7AKDRWL+UjLedW8pGOt0dg==", "hasInstallScript": true, "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17509,7 +17502,6 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz", "integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==", "license": "MIT", - "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" @@ -17538,7 +17530,6 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz", "integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -17549,7 +17540,6 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "license": "MIT", - "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -17565,7 +17555,6 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.14.0.tgz", "integrity": "sha512-B3gYc7WztcOT4N54AtUutbe0Nuqqh/nkresY0fAXzUHYLsWuIu/yGiCCD3DKfAs6GLv5LFtWTu7N333Q+e3bkg==", "license": "MIT", - "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -17608,7 +17597,6 @@ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -17641,7 +17629,6 @@ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", "license": "MIT", - "peer": true, "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", @@ -17757,7 +17744,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -19796,6 +19782,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -19954,7 +19941,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20311,7 +20297,6 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -21028,7 +21013,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -21042,6 +21026,35 @@ "zod": "^3.24.1" } }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "packages/app": { "name": "@paseo/app", "version": "1.0.0", @@ -21093,7 +21106,8 @@ "react-native-unistyles": "^3.0.15", "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^5.0.8" }, "devDependencies": { "@types/react": "~19.2.0", diff --git a/packages/app/package.json b/packages/app/package.json index 1ede8830d..9e0799129 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -60,7 +60,8 @@ "react-native-unistyles": "^3.0.15", "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^5.0.8" }, "devDependencies": { "@types/react": "~19.2.0", diff --git a/packages/app/src/contexts/daemon-connections-context.tsx b/packages/app/src/contexts/daemon-connections-context.tsx index d8e99e003..5121d3c31 100644 --- a/packages/app/src/contexts/daemon-connections-context.tsx +++ b/packages/app/src/contexts/daemon-connections-context.tsx @@ -1,9 +1,6 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { createContext, useCallback, useContext, useEffect, useState } from "react"; import type { ReactNode } from "react"; import { useDaemonRegistry, type DaemonProfile } from "./daemon-registry-context"; -import type { SessionContextValue } from "./session-context"; - -export type SessionSnapshot = SessionContextValue; export type ConnectionState = | { status: "idle"; lastError: null; lastOnlineAt: string | null; sessionReady: false } @@ -29,9 +26,6 @@ interface DaemonConnectionsContextValue { connectionStates: Map; isLoading: boolean; updateConnectionStatus: (daemonId: string, update: ConnectionStateUpdate) => void; - sessionSnapshots: Map; - updateSessionSnapshot: (daemonId: string, snapshot: SessionSnapshot | null) => void; - clearSessionSnapshot: (daemonId: string) => void; } const DaemonConnectionsContext = createContext(null); @@ -116,7 +110,6 @@ export function useDaemonConnections(): DaemonConnectionsContextValue { export function DaemonConnectionsProvider({ children }: { children: ReactNode }) { const { daemons, isLoading: registryLoading } = useDaemonRegistry(); const [connectionStates, setConnectionStates] = useState>(new Map()); - const [sessionSnapshotRegistry, setSessionSnapshotRegistry] = useState>(new Map()); // Ensure connection states stay in sync with registry entries useEffect(() => { @@ -133,24 +126,6 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode }) }); }, [daemons]); - useEffect(() => { - setSessionSnapshotRegistry((prev) => { - const validDaemonIds = new Set(daemons.map((daemon) => daemon.id)); - let changed = false; - const next = new Map(); - - for (const [daemonId, snapshot] of prev.entries()) { - if (!validDaemonIds.has(daemonId)) { - changed = true; - continue; - } - next.set(daemonId, snapshot); - } - - return changed ? next : prev; - }); - }, [daemons]); - const updateConnectionStatus = useCallback( (daemonId: string, update: ConnectionStateUpdate) => { @@ -177,38 +152,10 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode }) [] ); - const updateSessionSnapshot = useCallback((serverId: string, snapshot: SessionSnapshot | null) => { - setSessionSnapshotRegistry((prev) => { - const next = new Map(prev); - if (snapshot === null) { - next.delete(serverId); - } else { - next.set(serverId, snapshot); - } - return next; - }); - }, []); - - const clearSessionSnapshot = useCallback((serverId: string) => { - setSessionSnapshotRegistry((prev) => { - if (!prev.has(serverId)) { - return prev; - } - const next = new Map(prev); - next.delete(serverId); - return next; - }); - }, []); - - const sessionSnapshots = useMemo(() => new Map(sessionSnapshotRegistry), [sessionSnapshotRegistry]); - const value: DaemonConnectionsContextValue = { connectionStates, isLoading: registryLoading, updateConnectionStatus, - sessionSnapshots, - updateSessionSnapshot, - clearSessionSnapshot, }; return ( diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 5dc2dad7f..f0834ce5c 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -30,6 +30,7 @@ import type { import { ScrollView } from "react-native"; import * as FileSystem from 'expo-file-system'; import { useDaemonConnections } from "./daemon-connections-context"; +import { useSessionStore } from "@/stores/session-store"; const derivePendingPermissionKey = (agentId: string, request: AgentPermissionRequest) => { const fallbackId = @@ -396,9 +397,10 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid const wsIsConnected = ws.isConnected; const { updateConnectionStatus, - updateSessionSnapshot, - clearSessionSnapshot, } = useDaemonConnections(); + const setSession = useSessionStore((state) => state.setSession); + const updateSession = useSessionStore((state) => state.updateSession); + const clearSession = useSessionStore((state) => state.clearSession); useEffect(() => { if (ws.isConnected) { @@ -1843,21 +1845,19 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid ); useEffect(() => { - updateSessionSnapshot(serverId, value); - }, [serverId, updateSessionSnapshot, value]); + setSession(serverId, { ...value }); + }, [serverId, setSession]); useEffect(() => { - if (wsIsConnected) { - return; - } - clearSessionSnapshot(serverId); - }, [clearSessionSnapshot, serverId, wsIsConnected]); + const payload = { ...value }; + updateSession(serverId, payload); + }, [serverId, updateSession, value]); useEffect(() => { return () => { - clearSessionSnapshot(serverId); + clearSession(serverId); }; - }, [serverId, clearSessionSnapshot]); + }, [clearSession, serverId]); return ( diff --git a/packages/app/src/hooks/use-aggregated-agents.ts b/packages/app/src/hooks/use-aggregated-agents.ts index 6c09678cc..36752e537 100644 --- a/packages/app/src/hooks/use-aggregated-agents.ts +++ b/packages/app/src/hooks/use-aggregated-agents.ts @@ -1,6 +1,7 @@ -import { useMemo } from "react"; import type { Agent } from "@/contexts/session-context"; +import { useMemo } from "react"; import { useDaemonConnections } from "@/contexts/daemon-connections-context"; +import { useSessionStore } from "@/stores/session-store"; export interface AggregatedAgent extends Agent { serverId: string; @@ -13,17 +14,27 @@ export interface AggregatedAgentsResult { } export function useAggregatedAgents(): AggregatedAgentsResult { - const { connectionStates, sessionSnapshots } = useDaemonConnections(); + const { connectionStates } = useDaemonConnections(); + const sessions = useSessionStore((state) => state.sessions); + const sessionAgents = useMemo(() => { + const agentsByServer: Record> = {}; + for (const [serverId, session] of Object.entries(sessions)) { + if (session?.agents) { + agentsByServer[serverId] = session.agents; + } + } + return agentsByServer; + }, [sessions]); return useMemo(() => { const allAgents: AggregatedAgent[] = []; - for (const [serverId, snapshot] of sessionSnapshots) { - if (snapshot.agents.size === 0) { + for (const [serverId, agents] of Object.entries(sessionAgents)) { + if (!agents || agents.size === 0) { continue; } const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; - for (const agent of snapshot.agents.values()) { + for (const agent of agents.values()) { allAgents.push({ ...agent, serverId, @@ -58,7 +69,6 @@ export function useAggregatedAgents(): AggregatedAgentsResult { return false; }); - console.log("[useAggregatedAgents] aggregated agent count:", allAgents.length, "isLoading:", isLoading); return { agents: allAgents, isLoading }; - }, [sessionSnapshots, connectionStates]); + }, [sessionAgents, connectionStates]); } diff --git a/packages/app/src/hooks/use-session-directory.ts b/packages/app/src/hooks/use-session-directory.ts index 97d611eba..1ce81c02d 100644 --- a/packages/app/src/hooks/use-session-directory.ts +++ b/packages/app/src/hooks/use-session-directory.ts @@ -1,20 +1,14 @@ import { useMemo } from "react"; -import { useDaemonConnections } from "@/contexts/daemon-connections-context"; -import type { SessionContextValue } from "@/contexts/session-context"; +import { useSessionStore, type SessionData } from "@/stores/session-store"; -export function useSessionDirectory(): Map { - const { sessionSnapshots } = useDaemonConnections(); +export function useSessionDirectory(): Map { + const sessions = useSessionStore((state) => state.sessions); return useMemo(() => { - const entries = new Map(); - sessionSnapshots.forEach((snapshot, serverId) => { - entries.set(serverId, snapshot); - }); - return entries; - }, [sessionSnapshots]); + return new Map(Object.entries(sessions)); + }, [sessions]); } -export function useSessionForServer(serverId: string | null): SessionContextValue | null { - const { sessionSnapshots } = useDaemonConnections(); - return serverId ? sessionSnapshots.get(serverId) ?? null : null; +export function useSessionForServer(serverId: string | null): SessionData | null { + return useSessionStore((state) => (serverId ? state.sessions[serverId] ?? null : null)); } diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts new file mode 100644 index 000000000..207f8adfc --- /dev/null +++ b/packages/app/src/stores/session-store.ts @@ -0,0 +1,55 @@ +import { create } from "zustand"; +import type { SessionContextValue } from "@/contexts/session-context"; + +// SessionData mirrors SessionContextValue so consumers can subscribe to a single source of truth. +export type SessionData = SessionContextValue; + +interface SessionStore { + sessions: Record; + setSession: (serverId: string, data: SessionData) => void; + updateSession: (serverId: string, partial: Partial) => void; + clearSession: (serverId: string) => void; + getSession: (serverId: string) => SessionData | undefined; +} + +export const useSessionStore = create((set, get) => ({ + sessions: {}, + setSession: (serverId, data) => { + set((state) => ({ + sessions: { + ...state.sessions, + [serverId]: data, + }, + })); + }, + updateSession: (serverId, partial) => { + set((state) => { + const existing = state.sessions[serverId]; + const next: SessionData | undefined = existing + ? { ...existing, ...partial, serverId } + : (partial.serverId ? { ...(partial as SessionData), serverId } : undefined); + + if (!next) { + return state; + } + + return { + sessions: { + ...state.sessions, + [serverId]: next, + }, + }; + }); + }, + clearSession: (serverId) => { + set((state) => { + if (!(serverId in state.sessions)) { + return state; + } + const nextSessions = { ...state.sessions }; + delete nextSessions[serverId]; + return { sessions: nextSessions }; + }); + }, + getSession: (serverId) => get().sessions[serverId], +}));