chore: land multi-daemon rollout
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
nodejs 22.20.0
|
nodejs 25.2.1
|
||||||
|
|||||||
Generated
+34
-18
@@ -146,6 +146,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -1527,6 +1528,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -5067,7 +5069,6 @@
|
|||||||
"integrity": "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==",
|
"integrity": "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.6",
|
"ajv": "^6.12.6",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
@@ -5092,7 +5093,6 @@
|
|||||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-types": "^3.0.0",
|
"mime-types": "^3.0.0",
|
||||||
"negotiator": "^1.0.0"
|
"negotiator": "^1.0.0"
|
||||||
@@ -5107,7 +5107,6 @@
|
|||||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
@@ -5129,7 +5128,6 @@
|
|||||||
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "5.2.1"
|
"safe-buffer": "5.2.1"
|
||||||
},
|
},
|
||||||
@@ -5143,7 +5141,6 @@
|
|||||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.6.0"
|
"node": ">=6.6.0"
|
||||||
}
|
}
|
||||||
@@ -5154,7 +5151,6 @@
|
|||||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
@@ -5198,7 +5194,6 @@
|
|||||||
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
"encodeurl": "^2.0.0",
|
"encodeurl": "^2.0.0",
|
||||||
@@ -5217,7 +5212,6 @@
|
|||||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
@@ -5228,7 +5222,6 @@
|
|||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
},
|
},
|
||||||
@@ -5242,7 +5235,6 @@
|
|||||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
@@ -5253,7 +5245,6 @@
|
|||||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -5267,7 +5258,6 @@
|
|||||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "^1.54.0"
|
"mime-db": "^1.54.0"
|
||||||
},
|
},
|
||||||
@@ -5281,7 +5271,6 @@
|
|||||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -5292,7 +5281,6 @@
|
|||||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -5309,7 +5297,6 @@
|
|||||||
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.5",
|
"debug": "^4.3.5",
|
||||||
"encodeurl": "^2.0.0",
|
"encodeurl": "^2.0.0",
|
||||||
@@ -5333,7 +5320,6 @@
|
|||||||
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"encodeurl": "^2.0.0",
|
"encodeurl": "^2.0.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
@@ -5350,7 +5336,6 @@
|
|||||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"media-typer": "^1.1.0",
|
"media-typer": "^1.1.0",
|
||||||
@@ -6520,6 +6505,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.18.tgz",
|
||||||
"integrity": "sha512-DZgd6860dxcq3YX7UzIXeBr6m3UgXvo9acxp5jiJyIZXdR00Br9JwVkO7e0bUeTA2d3Z8dsmtAR84Y86NnH64Q==",
|
"integrity": "sha512-DZgd6860dxcq3YX7UzIXeBr6m3UgXvo9acxp5jiJyIZXdR00Br9JwVkO7e0bUeTA2d3Z8dsmtAR84Y86NnH64Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^7.12.4",
|
"@react-navigation/core": "^7.12.4",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
@@ -7209,6 +7195,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
|
||||||
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
|
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -7243,6 +7230,7 @@
|
|||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -7364,6 +7352,7 @@
|
|||||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.2",
|
"@typescript-eslint/scope-manager": "8.46.2",
|
||||||
"@typescript-eslint/types": "8.46.2",
|
"@typescript-eslint/types": "8.46.2",
|
||||||
@@ -8052,6 +8041,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -8112,6 +8102,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.78.tgz",
|
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.78.tgz",
|
||||||
"integrity": "sha512-ec77fmQwJGLduswMrW4AAUGSOiu8dZaIwMmWHHGKsrMUFFS6ugfkTyx0srtuKYHNRRLRC2dT7cPirnUl98VnxA==",
|
"integrity": "sha512-ec77fmQwJGLduswMrW4AAUGSOiu8dZaIwMmWHHGKsrMUFFS6ugfkTyx0srtuKYHNRRLRC2dT7cPirnUl98VnxA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/gateway": "2.0.1",
|
"@ai-sdk/gateway": "2.0.1",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
@@ -9000,6 +8991,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.19",
|
"baseline-browser-mapping": "^2.8.19",
|
||||||
"caniuse-lite": "^1.0.30001751",
|
"caniuse-lite": "^1.0.30001751",
|
||||||
@@ -10484,6 +10476,7 @@
|
|||||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
@@ -11048,6 +11041,7 @@
|
|||||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -11244,6 +11238,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -11614,6 +11609,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.20.tgz",
|
||||||
"integrity": "sha512-mWHky+H63W60P5Oo+VbtqzF2sLvdaoSSwG57H9rlq1DrgIla++QJZuwJkXXo55lYPymVmkVhwG6FjWYKKylwpw==",
|
"integrity": "sha512-mWHky+H63W60P5Oo+VbtqzF2sLvdaoSSwG57H9rlq1DrgIla++QJZuwJkXXo55lYPymVmkVhwG6FjWYKKylwpw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "54.0.13",
|
"@expo/cli": "54.0.13",
|
||||||
@@ -11786,6 +11782,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
|
||||||
"integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==",
|
"integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config": "~12.0.10",
|
"@expo/config": "~12.0.10",
|
||||||
"@expo/env": "~2.0.7"
|
"@expo/env": "~2.0.7"
|
||||||
@@ -11866,6 +11863,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.9.tgz",
|
||||||
"integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==",
|
"integrity": "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fontfaceobserver": "^2.1.0"
|
"fontfaceobserver": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -11943,6 +11941,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.8.tgz",
|
||||||
"integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==",
|
"integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-constants": "~18.0.8",
|
"expo-constants": "~18.0.8",
|
||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
@@ -13248,6 +13247,7 @@
|
|||||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||||
}
|
}
|
||||||
@@ -14771,6 +14771,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -17236,6 +17237,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -17276,6 +17278,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -17312,6 +17315,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
||||||
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/create-cache-key-function": "^29.7.0",
|
"@jest/create-cache-key-function": "^29.7.0",
|
||||||
"@react-native/assets-registry": "0.81.5",
|
"@react-native/assets-registry": "0.81.5",
|
||||||
@@ -17401,6 +17405,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.7.0.tgz",
|
"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==",
|
"integrity": "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -17420,6 +17425,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
|
||||||
"integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
|
"integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@egjs/hammerjs": "^2.0.17",
|
"@egjs/hammerjs": "^2.0.17",
|
||||||
"hoist-non-react-statics": "^3.3.0",
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
@@ -17476,6 +17482,7 @@
|
|||||||
"integrity": "sha512-+/uVS7FQwOiKYZQERMIvBRv5/X3CVHrFG6Nr/kIhVfVxGeUimHnBz7cgA97lJKIn7AKDRWL+UjLedW8pGOt0dg==",
|
"integrity": "sha512-+/uVS7FQwOiKYZQERMIvBRv5/X3CVHrFG6Nr/kIhVfVxGeUimHnBz7cgA97lJKIn7AKDRWL+UjLedW8pGOt0dg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -17502,6 +17509,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz",
|
||||||
"integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==",
|
"integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-native-is-edge-to-edge": "^1.2.1",
|
"react-native-is-edge-to-edge": "^1.2.1",
|
||||||
"semver": "7.7.2"
|
"semver": "7.7.2"
|
||||||
@@ -17530,6 +17538,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
|
"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==",
|
"integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -17540,6 +17549,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
||||||
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-freeze": "^1.0.0",
|
"react-freeze": "^1.0.0",
|
||||||
"react-native-is-edge-to-edge": "^1.2.1",
|
"react-native-is-edge-to-edge": "^1.2.1",
|
||||||
@@ -17555,6 +17565,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.14.0.tgz",
|
||||||
"integrity": "sha512-B3gYc7WztcOT4N54AtUutbe0Nuqqh/nkresY0fAXzUHYLsWuIu/yGiCCD3DKfAs6GLv5LFtWTu7N333Q+e3bkg==",
|
"integrity": "sha512-B3gYc7WztcOT4N54AtUutbe0Nuqqh/nkresY0fAXzUHYLsWuIu/yGiCCD3DKfAs6GLv5LFtWTu7N333Q+e3bkg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-select": "^5.1.0",
|
"css-select": "^5.1.0",
|
||||||
"css-tree": "^1.1.3",
|
"css-tree": "^1.1.3",
|
||||||
@@ -17597,6 +17608,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||||
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.6",
|
"@babel/runtime": "^7.18.6",
|
||||||
"@react-native/normalize-colors": "^0.74.1",
|
"@react-native/normalize-colors": "^0.74.1",
|
||||||
@@ -17629,6 +17641,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
||||||
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
||||||
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
||||||
@@ -17744,6 +17757,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -19782,7 +19796,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
@@ -19941,6 +19954,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -20297,6 +20311,7 @@
|
|||||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -21013,6 +21028,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,16 +101,16 @@ export default function LegacyAgentRedirectScreen() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
} else if (matches.length === 0) {
|
} else if (matches.length === 0) {
|
||||||
body = (
|
body = (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
<Text style={styles.title}>Agent not found</Text>
|
<Text style={styles.title}>Agent not found</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
We couldn't find "{agentId}" on any connected daemon. Make sure the daemon is online
|
We could not find {agentId} on any connected daemon. Make sure the daemon is online
|
||||||
or reopen the agent from the Home screen after it reconnects.
|
or reopen the agent from the Home screen after it reconnects.
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable style={styles.primaryButton} onPress={handleGoHome}>
|
<Pressable style={styles.primaryButton} onPress={handleGoHome}>
|
||||||
<Text style={styles.primaryButtonText}>Go Home</Text>
|
<Text style={styles.primaryButtonText}>Go Home</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
} else if (matches.length > 1) {
|
} else if (matches.length > 1) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { useFooterControls } from "@/contexts/footer-controls-context";
|
|||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
import { formatConnectionStatus } from "@/utils/daemons";
|
import { formatConnectionStatus } from "@/utils/daemons";
|
||||||
import { useDaemonSession, DaemonSessionUnavailableError } from "@/hooks/use-daemon-session";
|
import { useDaemonSession } from "@/hooks/use-daemon-session";
|
||||||
import { useDaemonRequest } from "@/hooks/use-daemon-request";
|
import { useDaemonRequest } from "@/hooks/use-daemon-request";
|
||||||
import type { SessionOutboundMessage } from "@server/server/messages";
|
import type { SessionOutboundMessage } from "@server/server/messages";
|
||||||
|
|
||||||
@@ -78,20 +78,15 @@ function extractAgentModel(agent?: Agent | null): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AgentScreen() {
|
export default function AgentScreen() {
|
||||||
const { theme } = useUnistyles();
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { serverId, agentId } = useLocalSearchParams<{ serverId: string; agentId: string }>();
|
const { serverId, agentId } = useLocalSearchParams<{ serverId: string; agentId: string }>();
|
||||||
const resolvedAgentId = typeof agentId === "string" ? agentId : undefined;
|
const resolvedAgentId = typeof agentId === "string" ? agentId : undefined;
|
||||||
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
|
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
|
||||||
const handleBackToHome = useCallback(() => {
|
|
||||||
router.replace("/");
|
|
||||||
}, [router]);
|
|
||||||
const { connectionStates, activeDaemonId, setActiveDaemonId } = useDaemonConnections();
|
const { connectionStates, activeDaemonId, setActiveDaemonId } = useDaemonConnections();
|
||||||
let session: SessionContextValue | null = null;
|
const hasConnectionEntry = resolvedServerId ? connectionStates.has(resolvedServerId) : false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resolvedServerId) {
|
if (!resolvedServerId || !hasConnectionEntry) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,26 +94,26 @@ export default function AgentScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveDaemonId(resolvedServerId);
|
setActiveDaemonId(resolvedServerId, { source: "agent_route" });
|
||||||
}, [resolvedServerId, activeDaemonId, setActiveDaemonId]);
|
}, [resolvedServerId, hasConnectionEntry, activeDaemonId, setActiveDaemonId]);
|
||||||
|
|
||||||
try {
|
const session = useDaemonSession(resolvedServerId, {
|
||||||
session = useDaemonSession(resolvedServerId, { suppressUnavailableAlert: true });
|
suppressUnavailableAlert: true,
|
||||||
} catch (error) {
|
allowUnavailable: true,
|
||||||
if (error instanceof DaemonSessionUnavailableError) {
|
});
|
||||||
session = null;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionServerId = resolvedServerId ?? null;
|
const connectionServerId = resolvedServerId ?? null;
|
||||||
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
|
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
|
||||||
const serverLabel = connection?.daemon.label ?? connectionServerId ?? "Selected daemon";
|
const serverLabel = connection?.daemon.label ?? connectionServerId ?? "Selected daemon";
|
||||||
const connectionStatus = connection?.status ?? "idle";
|
const isUnknownDaemon = Boolean(connectionServerId && !connection);
|
||||||
|
const connectionStatus = connection?.status ?? (isUnknownDaemon ? "offline" : "idle");
|
||||||
const connectionStatusLabel = formatConnectionStatus(connectionStatus);
|
const connectionStatusLabel = formatConnectionStatus(connectionStatus);
|
||||||
const lastConnectionError = connection?.lastError ?? null;
|
const lastConnectionError = connection?.lastError ?? null;
|
||||||
|
|
||||||
|
const handleBackToHome = useCallback(() => {
|
||||||
|
router.replace("/");
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return (
|
return (
|
||||||
<AgentSessionUnavailableState
|
<AgentSessionUnavailableState
|
||||||
@@ -127,21 +122,34 @@ export default function AgentScreen() {
|
|||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
connectionStatusLabel={connectionStatusLabel}
|
connectionStatusLabel={connectionStatusLabel}
|
||||||
lastError={lastConnectionError}
|
lastError={lastConnectionError}
|
||||||
|
isUnknownDaemon={isUnknownDaemon}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
|
||||||
agents,
|
|
||||||
agentStreamState,
|
|
||||||
initializingAgents,
|
|
||||||
pendingPermissions,
|
|
||||||
initializeAgent,
|
|
||||||
refreshAgent,
|
|
||||||
setFocusedAgentId,
|
|
||||||
ws,
|
|
||||||
} = session;
|
|
||||||
const routeServerId = resolvedServerId ?? session.serverId;
|
const routeServerId = resolvedServerId ?? session.serverId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AgentScreenContent
|
||||||
|
session={session}
|
||||||
|
agentId={resolvedAgentId}
|
||||||
|
routeServerId={routeServerId}
|
||||||
|
onBack={handleBackToHome}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentScreenContentProps = {
|
||||||
|
session: SessionContextValue;
|
||||||
|
agentId?: string;
|
||||||
|
routeServerId: string;
|
||||||
|
onBack: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentScreenContentProps) {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const router = useRouter();
|
||||||
const { registerFooterControls, unregisterFooterControls } = useFooterControls();
|
const { registerFooterControls, unregisterFooterControls } = useFooterControls();
|
||||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||||
const [menuVisible, setMenuVisible] = useState(false);
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
@@ -153,7 +161,18 @@ export default function AgentScreen() {
|
|||||||
const [createAgentInitialValues, setCreateAgentInitialValues] =
|
const [createAgentInitialValues, setCreateAgentInitialValues] =
|
||||||
useState<CreateAgentInitialValues | undefined>();
|
useState<CreateAgentInitialValues | undefined>();
|
||||||
|
|
||||||
// Keyboard animation
|
const {
|
||||||
|
agents,
|
||||||
|
agentStreamState,
|
||||||
|
initializingAgents,
|
||||||
|
pendingPermissions,
|
||||||
|
initializeAgent,
|
||||||
|
refreshAgent,
|
||||||
|
setFocusedAgentId,
|
||||||
|
ws,
|
||||||
|
} = session;
|
||||||
|
const resolvedAgentId = agentId;
|
||||||
|
|
||||||
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
||||||
const bottomInset = useSharedValue(insets.bottom);
|
const bottomInset = useSharedValue(insets.bottom);
|
||||||
|
|
||||||
@@ -435,7 +454,7 @@ export default function AgentScreen() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<BackHeader onBack={handleBackToHome} />
|
<BackHeader onBack={onBack} />
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={styles.errorText}>Agent not found</Text>
|
<Text style={styles.errorText}>Agent not found</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -452,7 +471,7 @@ export default function AgentScreen() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<BackHeader
|
<BackHeader
|
||||||
title={agent.title || "Agent"}
|
title={agent.title || "Agent"}
|
||||||
onBack={handleBackToHome}
|
onBack={onBack}
|
||||||
rightContent={
|
rightContent={
|
||||||
<View ref={menuButtonRef} collapsable={false}>
|
<View ref={menuButtonRef} collapsable={false}>
|
||||||
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
|
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
|
||||||
@@ -607,13 +626,31 @@ function AgentSessionUnavailableState({
|
|||||||
connectionStatus,
|
connectionStatus,
|
||||||
connectionStatusLabel,
|
connectionStatusLabel,
|
||||||
lastError,
|
lastError,
|
||||||
|
isUnknownDaemon = false,
|
||||||
}: {
|
}: {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
serverLabel: string;
|
serverLabel: string;
|
||||||
connectionStatus: ConnectionStatus;
|
connectionStatus: ConnectionStatus;
|
||||||
connectionStatusLabel: string;
|
connectionStatusLabel: string;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
|
isUnknownDaemon?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
if (isUnknownDaemon) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<BackHeader title="Agent" onBack={onBack} />
|
||||||
|
<View style={styles.centerState}>
|
||||||
|
<Text style={styles.errorText}>
|
||||||
|
Cannot open this agent because {serverLabel} is not configured on this device.
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.statusText}>
|
||||||
|
Add the daemon in Settings or open an agent on a configured server to continue.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isConnecting = connectionStatus === "connecting";
|
const isConnecting = connectionStatus === "connecting";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -624,12 +661,12 @@ function AgentSessionUnavailableState({
|
|||||||
<>
|
<>
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
<Text style={styles.loadingText}>Connecting to {serverLabel}...</Text>
|
<Text style={styles.loadingText}>Connecting to {serverLabel}...</Text>
|
||||||
<Text style={styles.statusText}>We'll show this agent once the daemon is online.</Text>
|
<Text style={styles.statusText}>We will show this agent once the daemon is online.</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.errorText}>
|
<Text style={styles.errorText}>
|
||||||
Can't open this agent while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
|
Cannot open this agent while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.statusText}>
|
<Text style={styles.statusText}>
|
||||||
Connect this daemon or switch to another one to continue.
|
Connect this daemon or switch to another one to continue.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import type { ExplorerEntry, SessionContextValue } from "@/contexts/session-cont
|
|||||||
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { formatConnectionStatus } from "@/utils/daemons";
|
import { formatConnectionStatus } from "@/utils/daemons";
|
||||||
import { useDaemonSession, DaemonSessionUnavailableError } from "@/hooks/use-daemon-session";
|
import { useDaemonSession } from "@/hooks/use-daemon-session";
|
||||||
|
|
||||||
export default function FileExplorerScreen() {
|
export default function FileExplorerScreen() {
|
||||||
const {
|
const {
|
||||||
@@ -49,18 +49,10 @@ export default function FileExplorerScreen() {
|
|||||||
}>();
|
}>();
|
||||||
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
|
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
|
const session = useDaemonSession(resolvedServerId, {
|
||||||
let session: SessionContextValue | null = null;
|
suppressUnavailableAlert: true,
|
||||||
|
allowUnavailable: true,
|
||||||
try {
|
});
|
||||||
session = useDaemonSession(resolvedServerId, { suppressUnavailableAlert: true });
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof DaemonSessionUnavailableError) {
|
|
||||||
session = null;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionServerId = resolvedServerId ?? null;
|
const connectionServerId = resolvedServerId ?? null;
|
||||||
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
|
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
|
||||||
@@ -478,25 +470,6 @@ function FileExplorerContent({
|
|||||||
);
|
);
|
||||||
}, [activePath, showListLoadingBanner, viewMode]);
|
}, [activePath, showListLoadingBanner, viewMode]);
|
||||||
|
|
||||||
if (!agent) {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<BackHeader
|
|
||||||
title="Files"
|
|
||||||
onBack={handleBackNavigation}
|
|
||||||
rightContent={
|
|
||||||
<Pressable style={styles.closeButton} onPress={handleCloseExplorer}>
|
|
||||||
<X size={18} color={theme.colors.foreground} />
|
|
||||||
</Pressable>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<View style={styles.centerState}>
|
|
||||||
<Text style={styles.errorText}>Agent not found</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleViewableItemsChanged = useCallback(
|
const handleViewableItemsChanged = useCallback(
|
||||||
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
||||||
if (!agentId || viewMode !== "grid") {
|
if (!agentId || viewMode !== "grid") {
|
||||||
@@ -549,6 +522,25 @@ function FileExplorerContent({
|
|||||||
setThumbnailLoadingMap({});
|
setThumbnailLoadingMap({});
|
||||||
}, [activePath, viewMode]);
|
}, [activePath, viewMode]);
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<BackHeader
|
||||||
|
title="Files"
|
||||||
|
onBack={handleBackNavigation}
|
||||||
|
rightContent={
|
||||||
|
<Pressable style={styles.closeButton} onPress={handleCloseExplorer}>
|
||||||
|
<X size={18} color={theme.colors.foreground} />
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<View style={styles.centerState}>
|
||||||
|
<Text style={styles.errorText}>Agent not found</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<BackHeader
|
<BackHeader
|
||||||
@@ -696,12 +688,12 @@ function FileExplorerSessionUnavailable({
|
|||||||
<>
|
<>
|
||||||
<ActivityIndicator size="small" />
|
<ActivityIndicator size="small" />
|
||||||
<Text style={styles.loadingText}>Connecting to {serverLabel}...</Text>
|
<Text style={styles.loadingText}>Connecting to {serverLabel}...</Text>
|
||||||
<Text style={styles.statusText}>We'll load files once this daemon is online.</Text>
|
<Text style={styles.statusText}>We will load files once this daemon is online.</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.errorText}>
|
<Text style={styles.errorText}>
|
||||||
Can't open files while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
|
Cannot open files while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.statusText}>Connect this daemon and try again.</Text>
|
<Text style={styles.statusText}>Connect this daemon and try again.</Text>
|
||||||
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null}
|
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { SessionContextValue } from "@/contexts/session-context";
|
|||||||
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { formatConnectionStatus } from "@/utils/daemons";
|
import { formatConnectionStatus } from "@/utils/daemons";
|
||||||
import { useDaemonSession, DaemonSessionUnavailableError } from "@/hooks/use-daemon-session";
|
import { useDaemonSession } from "@/hooks/use-daemon-session";
|
||||||
|
|
||||||
interface ParsedDiffFile {
|
interface ParsedDiffFile {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -58,18 +58,10 @@ export default function GitDiffScreen() {
|
|||||||
const { agentId, serverId } = useLocalSearchParams<{ agentId: string; serverId?: string }>();
|
const { agentId, serverId } = useLocalSearchParams<{ agentId: string; serverId?: string }>();
|
||||||
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
|
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
|
const session = useDaemonSession(resolvedServerId, {
|
||||||
let session: SessionContextValue | null = null;
|
suppressUnavailableAlert: true,
|
||||||
|
allowUnavailable: true,
|
||||||
try {
|
});
|
||||||
session = useDaemonSession(resolvedServerId, { suppressUnavailableAlert: true });
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof DaemonSessionUnavailableError) {
|
|
||||||
session = null;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionServerId = resolvedServerId ?? null;
|
const connectionServerId = resolvedServerId ?? null;
|
||||||
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
|
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
|
||||||
@@ -230,12 +222,12 @@ function SessionUnavailableState({
|
|||||||
<>
|
<>
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
<Text style={styles.loadingText}>Connecting to {serverLabel}...</Text>
|
<Text style={styles.loadingText}>Connecting to {serverLabel}...</Text>
|
||||||
<Text style={styles.statusText}>We'll show changes once this session is online.</Text>
|
<Text style={styles.statusText}>We will show changes once this session is online.</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.errorText}>
|
<Text style={styles.errorText}>
|
||||||
Can't load changes while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
|
Cannot load changes while {serverLabel} is {connectionStatusLabel.toLowerCase()}.
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.statusText}>Connect this daemon or switch to another one to continue.</Text>
|
<Text style={styles.statusText}>Connect this daemon or switch to another one to continue.</Text>
|
||||||
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null}
|
{lastError ? <Text style={styles.errorDetails}>{lastError}</Text> : null}
|
||||||
|
|||||||
@@ -476,10 +476,10 @@ export default function SettingsScreen() {
|
|||||||
};
|
};
|
||||||
if (daemonForm.id) {
|
if (daemonForm.id) {
|
||||||
await updateDaemon(daemonForm.id, payload);
|
await updateDaemon(daemonForm.id, payload);
|
||||||
setActiveDaemonId(daemonForm.id);
|
setActiveDaemonId(daemonForm.id, { source: "settings_save_daemon" });
|
||||||
} else {
|
} else {
|
||||||
const created = await addDaemon(payload);
|
const created = await addDaemon(payload);
|
||||||
setActiveDaemonId(created.id);
|
setActiveDaemonId(created.id, { source: "settings_save_daemon" });
|
||||||
}
|
}
|
||||||
handleCloseDaemonForm();
|
handleCloseDaemonForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -524,7 +524,7 @@ export default function SettingsScreen() {
|
|||||||
|
|
||||||
const handleSetActiveDaemon = useCallback(
|
const handleSetActiveDaemon = useCallback(
|
||||||
(profile: DaemonProfile) => {
|
(profile: DaemonProfile) => {
|
||||||
setActiveDaemonId(profile.id);
|
setActiveDaemonId(profile.id, { source: "settings_switch" });
|
||||||
},
|
},
|
||||||
[setActiveDaemonId]
|
[setActiveDaemonId]
|
||||||
);
|
);
|
||||||
@@ -545,15 +545,15 @@ export default function SettingsScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateDaemonTestState(profile.id, { status: "testing" });
|
updateDaemonTestState(profile.id, { status: "testing" });
|
||||||
updateConnectionStatus(profile.id, "connecting");
|
updateConnectionStatus(profile.id, { status: "connecting" });
|
||||||
try {
|
try {
|
||||||
await testServerConnection(url, 4000);
|
await testServerConnection(url, 4000);
|
||||||
updateDaemonTestState(profile.id, { status: "success", message: "Reachable" });
|
updateDaemonTestState(profile.id, { status: "success", message: "Reachable" });
|
||||||
updateConnectionStatus(profile.id, "online", { lastOnlineAt: new Date().toISOString() });
|
updateConnectionStatus(profile.id, { status: "online", lastOnlineAt: new Date().toISOString() });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Connection failed";
|
const message = error instanceof Error ? error.message : "Connection failed";
|
||||||
updateDaemonTestState(profile.id, { status: "error", message });
|
updateDaemonTestState(profile.id, { status: "error", message });
|
||||||
updateConnectionStatus(profile.id, "offline", { lastError: message });
|
updateConnectionStatus(profile.id, { status: "offline", lastError: message });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[testServerConnection, updateConnectionStatus, updateDaemonTestState]
|
[testServerConnection, updateConnectionStatus, updateDaemonTestState]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { getAgentStatusColor, getAgentStatusLabel } from "@/utils/agent-status";
|
|||||||
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { type AggregatedAgentGroup } from "@/hooks/use-aggregated-agents";
|
import { type AggregatedAgentGroup } from "@/hooks/use-aggregated-agents";
|
||||||
import { useDaemonSession, DaemonSessionUnavailableError } from "@/hooks/use-daemon-session";
|
import { useDaemonSession } from "@/hooks/use-daemon-session";
|
||||||
|
|
||||||
interface AgentListProps {
|
interface AgentListProps {
|
||||||
agentGroups: AggregatedAgentGroup[];
|
agentGroups: AggregatedAgentGroup[];
|
||||||
@@ -19,29 +19,20 @@ export function AgentList({ agentGroups }: AgentListProps) {
|
|||||||
const { setActiveDaemonId } = useDaemonConnections();
|
const { setActiveDaemonId } = useDaemonConnections();
|
||||||
const [actionAgent, setActionAgent] = useState<Agent | null>(null);
|
const [actionAgent, setActionAgent] = useState<Agent | null>(null);
|
||||||
const [actionAgentServerId, setActionAgentServerId] = useState<string | null>(null);
|
const [actionAgentServerId, setActionAgentServerId] = useState<string | null>(null);
|
||||||
let actionSession = null;
|
const actionSession = useDaemonSession(actionAgentServerId ?? undefined, {
|
||||||
let actionSessionUnavailable = false;
|
suppressUnavailableAlert: true,
|
||||||
|
allowUnavailable: true,
|
||||||
try {
|
});
|
||||||
actionSession = useDaemonSession(actionAgentServerId ?? undefined, { suppressUnavailableAlert: true });
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof DaemonSessionUnavailableError) {
|
|
||||||
actionSessionUnavailable = true;
|
|
||||||
actionSession = null;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteAgent = actionSession?.deleteAgent;
|
const deleteAgent = actionSession?.deleteAgent;
|
||||||
const isActionSheetVisible = actionAgent !== null;
|
const isActionSheetVisible = actionAgent !== null;
|
||||||
const isActionDaemonUnavailable = Boolean(actionAgentServerId && actionSessionUnavailable);
|
const isActionDaemonUnavailable = Boolean(actionAgentServerId && !actionSession);
|
||||||
|
|
||||||
const handleAgentPress = useCallback((serverId: string, agentId: string) => {
|
const handleAgentPress = useCallback((serverId: string, agentId: string) => {
|
||||||
if (isActionSheetVisible) {
|
if (isActionSheetVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setActiveDaemonId(serverId);
|
setActiveDaemonId(serverId, { source: "agent_row_press" });
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/agent/[serverId]/[agentId]",
|
pathname: "/agent/[serverId]/[agentId]",
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -40,9 +40,8 @@ export function AgentSidebar({
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const savedTranslateX = useSharedValue(0);
|
const savedTranslateX = useSharedValue(0);
|
||||||
const [isVisible, setIsVisible] = useState(isOpen);
|
const [isVisible, setIsVisible] = useState(isOpen);
|
||||||
|
const fallbackTranslateX = useSharedValue(-300);
|
||||||
// Use edgeSwipeTranslateX directly since it's always provided
|
const translateX = edgeSwipeTranslateX ?? fallbackTranslateX;
|
||||||
const translateX = edgeSwipeTranslateX || useSharedValue(-300);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
AGENT_PROVIDER_DEFINITIONS,
|
AGENT_PROVIDER_DEFINITIONS,
|
||||||
type AgentProviderDefinition,
|
type AgentProviderDefinition,
|
||||||
} from "@server/server/agent/provider-manifest";
|
} from "@server/server/agent/provider-manifest";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections, type ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
import type {
|
import type {
|
||||||
AgentProvider,
|
AgentProvider,
|
||||||
AgentMode,
|
AgentMode,
|
||||||
@@ -61,8 +61,9 @@ import type {
|
|||||||
import { useDaemonRequest } from "@/hooks/use-daemon-request";
|
import { useDaemonRequest } from "@/hooks/use-daemon-request";
|
||||||
import type { WSInboundMessage, SessionOutboundMessage } from "@server/server/messages";
|
import type { WSInboundMessage, SessionOutboundMessage } from "@server/server/messages";
|
||||||
import { formatConnectionStatus } from "@/utils/daemons";
|
import { formatConnectionStatus } from "@/utils/daemons";
|
||||||
import { useSession } from "@/contexts/session-context";
|
import { trackAnalyticsEvent } from "@/utils/analytics";
|
||||||
import { useSessionForServer } from "@/hooks/use-session-directory";
|
import { useSession, type SessionContextValue } from "@/contexts/session-context";
|
||||||
|
import { useSessionDirectory, useSessionForServer } from "@/hooks/use-session-directory";
|
||||||
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
|
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
|
||||||
|
|
||||||
export type CreateAgentInitialValues = {
|
export type CreateAgentInitialValues = {
|
||||||
@@ -230,6 +231,7 @@ function AgentFlowModal({
|
|||||||
const selectedSession = useSessionForServer(selectedServerId);
|
const selectedSession = useSessionForServer(selectedServerId);
|
||||||
const session =
|
const session =
|
||||||
selectedServerId === null ? activeSession : selectedSession ?? null;
|
selectedServerId === null ? activeSession : selectedSession ?? null;
|
||||||
|
const sessionDirectory = useSessionDirectory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedServerId) {
|
if (!selectedServerId) {
|
||||||
@@ -307,21 +309,29 @@ function AgentFlowModal({
|
|||||||
const isWsConnected = ws?.isConnected ?? false;
|
const isWsConnected = ws?.isConnected ?? false;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const activeServerId = session?.serverId ?? null;
|
const activeServerId = session?.serverId ?? null;
|
||||||
const selectedDaemonConnection = selectedServerId
|
const selectedDaemonId = selectedServerId ?? activeServerId;
|
||||||
? connectionStates.get(selectedServerId)
|
const selectedDaemonConnection = selectedDaemonId
|
||||||
|
? connectionStates.get(selectedDaemonId)
|
||||||
: null;
|
: null;
|
||||||
|
const selectedDaemonStatus: ConnectionStatus =
|
||||||
|
selectedDaemonConnection?.status ??
|
||||||
|
(ws?.isConnected
|
||||||
|
? "online"
|
||||||
|
: ws?.isConnecting
|
||||||
|
? "connecting"
|
||||||
|
: ws?.lastError
|
||||||
|
? "error"
|
||||||
|
: "offline");
|
||||||
const selectedDaemonLabel =
|
const selectedDaemonLabel =
|
||||||
selectedDaemonConnection?.daemon.label ??
|
selectedDaemonConnection?.daemon.label ??
|
||||||
selectedDaemonConnection?.daemon.wsUrl ??
|
selectedDaemonConnection?.daemon.wsUrl ??
|
||||||
selectedServerId ??
|
selectedDaemonId ??
|
||||||
"Selected daemon";
|
"Selected daemon";
|
||||||
const selectedDaemonStatusLabel = selectedDaemonConnection
|
const selectedDaemonStatusLabel = formatConnectionStatus(selectedDaemonStatus);
|
||||||
? formatConnectionStatus(selectedDaemonConnection.status)
|
const selectedDaemonIsUnavailable =
|
||||||
: "offline";
|
!session || selectedDaemonStatus !== "online" || !ws?.isConnected;
|
||||||
const selectedDaemonIsUnavailable = Boolean(
|
const isBackgroundDaemonTarget =
|
||||||
selectedServerId &&
|
Boolean(selectedDaemonId && activeDaemonId && selectedDaemonId !== activeDaemonId);
|
||||||
(!session || selectedDaemonConnection?.status !== "online")
|
|
||||||
);
|
|
||||||
const selectedDaemonLastError = selectedDaemonConnection?.lastError?.trim();
|
const selectedDaemonLastError = selectedDaemonConnection?.lastError?.trim();
|
||||||
const daemonAvailabilityError = selectedDaemonIsUnavailable
|
const daemonAvailabilityError = selectedDaemonIsUnavailable
|
||||||
? `${selectedDaemonLabel} is ${selectedDaemonStatusLabel}. Connect to it before creating or importing agents.${
|
? `${selectedDaemonLabel} is ${selectedDaemonStatusLabel}. Connect to it before creating or importing agents.${
|
||||||
@@ -390,6 +400,12 @@ function AgentFlowModal({
|
|||||||
const shouldListenForDictation =
|
const shouldListenForDictation =
|
||||||
isCreateFlow && (isVisible || hasPendingDictation);
|
isCreateFlow && (isVisible || hasPendingDictation);
|
||||||
|
|
||||||
|
const providerModelRequestTimersRef = useRef<
|
||||||
|
Map<string, ReturnType<typeof setTimeout>>
|
||||||
|
>(new Map());
|
||||||
|
const idleProviderPrefetchHandleRef = useRef<ReturnType<
|
||||||
|
typeof InteractionManager.runAfterInteractions
|
||||||
|
> | null>(null);
|
||||||
const pendingNavigationAgentIdRef = useRef<string | null>(null);
|
const pendingNavigationAgentIdRef = useRef<string | null>(null);
|
||||||
const openDropdownSheet = useCallback((key: DropdownKey) => {
|
const openDropdownSheet = useCallback((key: DropdownKey) => {
|
||||||
setOpenDropdown(key);
|
setOpenDropdown(key);
|
||||||
@@ -397,6 +413,46 @@ function AgentFlowModal({
|
|||||||
const closeDropdown = useCallback(() => {
|
const closeDropdown = useCallback(() => {
|
||||||
setOpenDropdown(null);
|
setOpenDropdown(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
const clearQueuedProviderModelRequest = useCallback((serverId: string | null) => {
|
||||||
|
if (!serverId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = providerModelRequestTimersRef.current.get(serverId);
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
providerModelRequestTimersRef.current.delete(serverId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const queueProviderModelFetch = useCallback(
|
||||||
|
(
|
||||||
|
serverId: string | null,
|
||||||
|
targetSession: SessionContextValue | null,
|
||||||
|
options?: { cwd?: string; delayMs?: number }
|
||||||
|
) => {
|
||||||
|
if (!serverId || !targetSession?.requestProviderModels) {
|
||||||
|
clearQueuedProviderModelRequest(serverId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentState = targetSession.providerModels?.get(selectedProvider);
|
||||||
|
if (currentState?.models?.length || currentState?.isLoading) {
|
||||||
|
clearQueuedProviderModelRequest(serverId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayMs = options?.delayMs ?? 0;
|
||||||
|
const trigger = () => {
|
||||||
|
providerModelRequestTimersRef.current.delete(serverId);
|
||||||
|
targetSession.requestProviderModels(selectedProvider, options?.cwd ? { cwd: options.cwd } : undefined);
|
||||||
|
};
|
||||||
|
clearQueuedProviderModelRequest(serverId);
|
||||||
|
if (delayMs > 0) {
|
||||||
|
providerModelRequestTimersRef.current.set(serverId, setTimeout(trigger, delayMs));
|
||||||
|
} else {
|
||||||
|
trigger();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clearQueuedProviderModelRequest, selectedProvider]
|
||||||
|
);
|
||||||
|
|
||||||
const setProviderFromUser = useCallback((provider: AgentProvider) => {
|
const setProviderFromUser = useCallback((provider: AgentProvider) => {
|
||||||
userEditedPreferencesRef.current.provider = true;
|
userEditedPreferencesRef.current.provider = true;
|
||||||
@@ -586,18 +642,28 @@ function AgentFlowModal({
|
|||||||
const modelError = modelState?.error ?? null;
|
const modelError = modelState?.error ?? null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible || !providerModels || !requestProviderModels || !isTargetDaemonReady) {
|
const targetServerId = selectedServerId ?? session?.serverId ?? null;
|
||||||
|
if (!isVisible || !isTargetDaemonReady || !targetServerId || !session) {
|
||||||
|
clearQueuedProviderModelRequest(targetServerId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const trimmed = workingDir.trim();
|
const trimmed = workingDir.trim();
|
||||||
const currentState = providerModels.get(selectedProvider);
|
queueProviderModelFetch(targetServerId, session, {
|
||||||
if (currentState?.models?.length || currentState?.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
requestProviderModels(selectedProvider, {
|
|
||||||
cwd: trimmed.length > 0 ? trimmed : undefined,
|
cwd: trimmed.length > 0 ? trimmed : undefined,
|
||||||
|
delayMs: 180,
|
||||||
});
|
});
|
||||||
}, [isVisible, providerModels, requestProviderModels, selectedProvider, workingDir, isTargetDaemonReady]);
|
return () => {
|
||||||
|
clearQueuedProviderModelRequest(targetServerId);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
clearQueuedProviderModelRequest,
|
||||||
|
isTargetDaemonReady,
|
||||||
|
isVisible,
|
||||||
|
queueProviderModelFetch,
|
||||||
|
selectedServerId,
|
||||||
|
session,
|
||||||
|
workingDir,
|
||||||
|
]);
|
||||||
const setPromptHeight = useCallback((nextHeight: number) => {
|
const setPromptHeight = useCallback((nextHeight: number) => {
|
||||||
const bounded = Math.min(
|
const bounded = Math.min(
|
||||||
PROMPT_MAX_HEIGHT,
|
PROMPT_MAX_HEIGHT,
|
||||||
@@ -790,6 +856,51 @@ function AgentFlowModal({
|
|||||||
focusPromptInput();
|
focusPromptInput();
|
||||||
}, [focusPromptInput, isCreateFlow, isVisible, shouldAutoFocusPrompt]);
|
}, [focusPromptInput, isCreateFlow, isVisible, shouldAutoFocusPrompt]);
|
||||||
|
|
||||||
|
const logOfflineDaemonAction = useCallback(
|
||||||
|
(action: "create" | "resume" | "dictation" | "import_list", reason?: string | null) => {
|
||||||
|
trackAnalyticsEvent({
|
||||||
|
type: "offline_daemon_action_attempt",
|
||||||
|
action,
|
||||||
|
daemonId: selectedDaemonId,
|
||||||
|
activeDaemonId,
|
||||||
|
status: selectedDaemonStatus ?? null,
|
||||||
|
isBackground: isBackgroundDaemonTarget,
|
||||||
|
reason: reason ?? daemonAvailabilityError,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
activeDaemonId,
|
||||||
|
daemonAvailabilityError,
|
||||||
|
isBackgroundDaemonTarget,
|
||||||
|
selectedDaemonId,
|
||||||
|
selectedDaemonStatus,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const logBackgroundAgentAction = useCallback(
|
||||||
|
(
|
||||||
|
action: "create" | "resume",
|
||||||
|
payload: { cwd?: string; provider?: string; modeId?: string; model?: string; baseBranch?: string }
|
||||||
|
) => {
|
||||||
|
if (!isBackgroundDaemonTarget || !selectedDaemonId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trackAnalyticsEvent({
|
||||||
|
type: "background_agent_action",
|
||||||
|
action,
|
||||||
|
daemonId: selectedDaemonId,
|
||||||
|
activeDaemonId,
|
||||||
|
isBackground: true,
|
||||||
|
cwd: payload.cwd,
|
||||||
|
provider: payload.provider,
|
||||||
|
modeId: payload.modeId,
|
||||||
|
model: payload.model,
|
||||||
|
baseBranch: payload.baseBranch,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[activeDaemonId, isBackgroundDaemonTarget, selectedDaemonId]
|
||||||
|
);
|
||||||
|
|
||||||
const resetFormState = useCallback(() => {
|
const resetFormState = useCallback(() => {
|
||||||
setInitialPrompt("");
|
setInitialPrompt("");
|
||||||
promptBaselineHeightRef.current = null;
|
promptBaselineHeightRef.current = null;
|
||||||
@@ -820,7 +931,14 @@ function AgentFlowModal({
|
|||||||
}, [cancelRepoInfo, resetRepoInfo, setPromptHeight]);
|
}, [cancelRepoInfo, resetRepoInfo, setPromptHeight]);
|
||||||
|
|
||||||
const handleDictationStart = useCallback(async () => {
|
const handleDictationStart = useCallback(async () => {
|
||||||
if (!isCreateFlow || isLoading || !isWsConnected || isDictating || isDictationProcessing) {
|
if (!isCreateFlow || isLoading || isDictating || isDictationProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isTargetDaemonReady || !isWsConnected) {
|
||||||
|
logOfflineDaemonAction(
|
||||||
|
"dictation",
|
||||||
|
daemonAvailabilityError ?? "WebSocket disconnected"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -834,7 +952,18 @@ function AgentFlowModal({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[CreateAgentModal] Failed to start dictation:", error);
|
console.error("[CreateAgentModal] Failed to start dictation:", error);
|
||||||
}
|
}
|
||||||
}, [dictationRecorder, isCreateFlow, isDictating, isDictationProcessing, isLoading, isWsConnected, shouldShowAudioDebug]);
|
}, [
|
||||||
|
daemonAvailabilityError,
|
||||||
|
dictationRecorder,
|
||||||
|
isCreateFlow,
|
||||||
|
isDictating,
|
||||||
|
isDictationProcessing,
|
||||||
|
isLoading,
|
||||||
|
isTargetDaemonReady,
|
||||||
|
isWsConnected,
|
||||||
|
logOfflineDaemonAction,
|
||||||
|
shouldShowAudioDebug,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleDictationCancel = useCallback(async () => {
|
const handleDictationCancel = useCallback(async () => {
|
||||||
if (!isDictating) {
|
if (!isDictating) {
|
||||||
@@ -854,7 +983,8 @@ function AgentFlowModal({
|
|||||||
if (!isDictating || isDictationProcessing) {
|
if (!isDictating || isDictationProcessing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!sendAgentAudio) {
|
if (!sendAgentAudio || !isTargetDaemonReady) {
|
||||||
|
logOfflineDaemonAction("dictation");
|
||||||
setErrorMessage(
|
setErrorMessage(
|
||||||
daemonAvailabilityError ??
|
daemonAvailabilityError ??
|
||||||
"Connect to the selected daemon before using dictation."
|
"Connect to the selected daemon before using dictation."
|
||||||
@@ -887,6 +1017,8 @@ function AgentFlowModal({
|
|||||||
dictationRecorder,
|
dictationRecorder,
|
||||||
isDictating,
|
isDictating,
|
||||||
isDictationProcessing,
|
isDictationProcessing,
|
||||||
|
isTargetDaemonReady,
|
||||||
|
logOfflineDaemonAction,
|
||||||
sendAgentAudio,
|
sendAgentAudio,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -898,7 +1030,7 @@ function AgentFlowModal({
|
|||||||
|
|
||||||
pendingNavigationAgentIdRef.current = null;
|
pendingNavigationAgentIdRef.current = null;
|
||||||
if (activeDaemonId !== activeServerId) {
|
if (activeDaemonId !== activeServerId) {
|
||||||
setActiveDaemonId(activeServerId);
|
setActiveDaemonId(activeServerId, { source: "agent_create_resume_nav" });
|
||||||
}
|
}
|
||||||
InteractionManager.runAfterInteractions(() => {
|
InteractionManager.runAfterInteractions(() => {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -926,6 +1058,7 @@ function AgentFlowModal({
|
|||||||
(provider?: AgentProvider) => {
|
(provider?: AgentProvider) => {
|
||||||
if (!isTargetDaemonReady || !ws || !ws.isConnected) {
|
if (!isTargetDaemonReady || !ws || !ws.isConnected) {
|
||||||
setIsImportLoading(false);
|
setIsImportLoading(false);
|
||||||
|
logOfflineDaemonAction("import_list");
|
||||||
setImportError(
|
setImportError(
|
||||||
daemonAvailabilityError ??
|
daemonAvailabilityError ??
|
||||||
"Connect to the selected daemon to load import candidates."
|
"Connect to the selected daemon to load import candidates."
|
||||||
@@ -953,7 +1086,7 @@ function AgentFlowModal({
|
|||||||
setImportError("Unable to load agents to import. Please try again.");
|
setImportError("Unable to load agents to import. Please try again.");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[daemonAvailabilityError, isTargetDaemonReady, ws]
|
[daemonAvailabilityError, isTargetDaemonReady, logOfflineDaemonAction, ws]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1040,6 +1173,16 @@ function AgentFlowModal({
|
|||||||
};
|
};
|
||||||
}, [dictationRecorder]);
|
}, [dictationRecorder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
providerModelRequestTimersRef.current.forEach((timer) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
});
|
||||||
|
providerModelRequestTimersRef.current.clear();
|
||||||
|
idleProviderPrefetchHandleRef.current?.cancel?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const footerAnimatedStyle = useAnimatedStyle(() => {
|
const footerAnimatedStyle = useAnimatedStyle(() => {
|
||||||
"worklet";
|
"worklet";
|
||||||
const absoluteHeight = Math.abs(keyboardHeight.value);
|
const absoluteHeight = Math.abs(keyboardHeight.value);
|
||||||
@@ -1129,6 +1272,44 @@ function AgentFlowModal({
|
|||||||
shouldSyncBaseBranchRef.current = true;
|
shouldSyncBaseBranchRef.current = true;
|
||||||
}, [isCreateFlow, isVisible, workingDir]);
|
}, [isCreateFlow, isVisible, workingDir]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activeServerIds = new Set(daemonEntries.map(({ daemon }) => daemon.id));
|
||||||
|
providerModelRequestTimersRef.current.forEach((timer, serverId) => {
|
||||||
|
if (!activeServerIds.has(serverId)) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
providerModelRequestTimersRef.current.delete(serverId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
idleProviderPrefetchHandleRef.current?.cancel?.();
|
||||||
|
idleProviderPrefetchHandleRef.current = InteractionManager.runAfterInteractions(() => {
|
||||||
|
daemonEntries.forEach(({ daemon, status }) => {
|
||||||
|
const serverId = daemon.id;
|
||||||
|
const isSelected = serverId === selectedServerId;
|
||||||
|
const isReady = status === "online" || status === "idle";
|
||||||
|
if (isSelected || !isReady) {
|
||||||
|
clearQueuedProviderModelRequest(serverId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const daemonSession = sessionDirectory.get(serverId) ?? null;
|
||||||
|
if (!daemonSession?.ws?.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queueProviderModelFetch(serverId, daemonSession, { delayMs: 320 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
idleProviderPrefetchHandleRef.current?.cancel?.();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
clearQueuedProviderModelRequest,
|
||||||
|
daemonEntries,
|
||||||
|
queueProviderModelFetch,
|
||||||
|
selectedProvider,
|
||||||
|
selectedServerId,
|
||||||
|
sessionDirectory,
|
||||||
|
]);
|
||||||
|
|
||||||
const trimmedWorkingDir = workingDir.trim();
|
const trimmedWorkingDir = workingDir.trim();
|
||||||
const shouldInspectRepo = isCreateFlow && isVisible && trimmedWorkingDir.length > 0;
|
const shouldInspectRepo = isCreateFlow && isVisible && trimmedWorkingDir.length > 0;
|
||||||
const repoAvailabilityError = shouldInspectRepo && (!isTargetDaemonReady || !isWsConnected)
|
const repoAvailabilityError = shouldInspectRepo && (!isTargetDaemonReady || !isWsConnected)
|
||||||
@@ -1208,6 +1389,7 @@ function AgentFlowModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!createAgent || !isTargetDaemonReady) {
|
if (!createAgent || !isTargetDaemonReady) {
|
||||||
|
logOfflineDaemonAction("create");
|
||||||
setErrorMessage(
|
setErrorMessage(
|
||||||
daemonAvailabilityError ??
|
daemonAvailabilityError ??
|
||||||
"Connect to the selected daemon before creating an agent."
|
"Connect to the selected daemon before creating an agent."
|
||||||
@@ -1257,10 +1439,18 @@ function AgentFlowModal({
|
|||||||
createWorktree: true,
|
createWorktree: true,
|
||||||
worktreeSlug: (worktreeSlug || branchName).trim(),
|
worktreeSlug: (worktreeSlug || branchName).trim(),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
logBackgroundAgentAction("create", {
|
||||||
|
cwd: trimmedPath,
|
||||||
|
provider: selectedProvider,
|
||||||
|
modeId,
|
||||||
|
model: trimmedModel,
|
||||||
|
baseBranch: trimmedBaseBranch || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
createAgent({
|
createAgent({
|
||||||
config,
|
config,
|
||||||
@@ -1285,6 +1475,8 @@ function AgentFlowModal({
|
|||||||
repoInfo,
|
repoInfo,
|
||||||
selectedMode,
|
selectedMode,
|
||||||
modeOptions,
|
modeOptions,
|
||||||
|
logBackgroundAgentAction,
|
||||||
|
logOfflineDaemonAction,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
isLoading,
|
isLoading,
|
||||||
validateWorktreeName,
|
validateWorktreeName,
|
||||||
@@ -1300,12 +1492,17 @@ function AgentFlowModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!resumeAgent || !isTargetDaemonReady) {
|
if (!resumeAgent || !isTargetDaemonReady) {
|
||||||
|
logOfflineDaemonAction("resume");
|
||||||
setImportError(
|
setImportError(
|
||||||
daemonAvailabilityError ??
|
daemonAvailabilityError ??
|
||||||
"Connect to the selected daemon before importing an agent."
|
"Connect to the selected daemon before importing an agent."
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
logBackgroundAgentAction("resume", {
|
||||||
|
cwd: candidate.cwd,
|
||||||
|
provider: candidate.provider,
|
||||||
|
});
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
const requestId = generateMessageId();
|
const requestId = generateMessageId();
|
||||||
pendingRequestIdRef.current = requestId;
|
pendingRequestIdRef.current = requestId;
|
||||||
@@ -1318,6 +1515,8 @@ function AgentFlowModal({
|
|||||||
[
|
[
|
||||||
daemonAvailabilityError,
|
daemonAvailabilityError,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
logBackgroundAgentAction,
|
||||||
|
logOfflineDaemonAction,
|
||||||
isTargetDaemonReady,
|
isTargetDaemonReady,
|
||||||
resumeAgent,
|
resumeAgent,
|
||||||
setImportError,
|
setImportError,
|
||||||
@@ -1955,7 +2154,7 @@ function AgentFlowModal({
|
|||||||
<View style={styles.resumeEmptyState}>
|
<View style={styles.resumeEmptyState}>
|
||||||
<Text style={styles.resumeEmptyTitle}>No agents to import</Text>
|
<Text style={styles.resumeEmptyTitle}>No agents to import</Text>
|
||||||
<Text style={styles.resumeEmptySubtitle}>
|
<Text style={styles.resumeEmptySubtitle}>
|
||||||
We'll load the latest Claude and Codex sessions from your
|
We will load the latest Claude and Codex sessions from your
|
||||||
local history so you can import them.
|
local history so you can import them.
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -2422,7 +2621,7 @@ function WorkingDirectoryDropdown({
|
|||||||
/>
|
/>
|
||||||
{!hasRecentPaths && !showCustomOption ? (
|
{!hasRecentPaths && !showCustomOption ? (
|
||||||
<Text style={styles.helperText}>
|
<Text style={styles.helperText}>
|
||||||
We'll remember the directories you use most often.
|
We will remember the directories you use most often.
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{showCustomOption ? (
|
{showCustomOption ? (
|
||||||
|
|||||||
@@ -1,33 +1,67 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { SessionProvider } from "@/contexts/session-context";
|
import { SessionProvider } from "@/contexts/session-context";
|
||||||
import { useDaemonRegistry, type DaemonProfile } from "@/contexts/daemon-registry-context";
|
import { useDaemonRegistry, type DaemonProfile } from "@/contexts/daemon-registry-context";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
|
|
||||||
export function MultiDaemonSessionHost() {
|
export function MultiDaemonSessionHost() {
|
||||||
const { daemons } = useDaemonRegistry();
|
const { daemons } = useDaemonRegistry();
|
||||||
const { activeDaemonId } = useDaemonConnections();
|
const { activeDaemonId, sessionAccessorRoles } = useDaemonConnections();
|
||||||
|
const autoConnectStatesRef = useRef<Map<string, boolean>>(new Map());
|
||||||
|
|
||||||
|
const primaryDaemonIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
sessionAccessorRoles.forEach((roles, daemonId) => {
|
||||||
|
if (roles.has("primary")) {
|
||||||
|
ids.add(daemonId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}, [sessionAccessorRoles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const trackedStates = autoConnectStatesRef.current;
|
||||||
|
const activeIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const daemon of daemons) {
|
||||||
|
activeIds.add(daemon.id);
|
||||||
|
const previousAutoConnect = trackedStates.get(daemon.id);
|
||||||
|
if (daemon.autoConnect === false && previousAutoConnect !== false) {
|
||||||
|
console.info("[DaemonConnection]", {
|
||||||
|
event: "auto_connect_skip",
|
||||||
|
daemonId: daemon.id,
|
||||||
|
label: daemon.label,
|
||||||
|
reason: "autoConnect disabled",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
trackedStates.set(daemon.id, daemon.autoConnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const daemonId of Array.from(trackedStates.keys())) {
|
||||||
|
if (!activeIds.has(daemonId)) {
|
||||||
|
trackedStates.delete(daemonId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [daemons]);
|
||||||
|
|
||||||
const backgroundDaemons = useMemo<DaemonProfile[]>(() => {
|
const backgroundDaemons = useMemo<DaemonProfile[]>(() => {
|
||||||
const shouldConnect = new Map<string, DaemonProfile>();
|
const shouldConnect = new Map<string, DaemonProfile>();
|
||||||
|
|
||||||
for (const daemon of daemons) {
|
for (const daemon of daemons) {
|
||||||
if (daemon.autoConnect) {
|
if (!daemon.autoConnect) {
|
||||||
shouldConnect.set(daemon.id, daemon);
|
continue;
|
||||||
}
|
}
|
||||||
}
|
if (daemon.id === activeDaemonId) {
|
||||||
|
continue;
|
||||||
if (activeDaemonId) {
|
|
||||||
const activeDaemon = daemons.find((daemon) => daemon.id === activeDaemonId);
|
|
||||||
if (activeDaemon) {
|
|
||||||
shouldConnect.set(activeDaemon.id, activeDaemon);
|
|
||||||
}
|
}
|
||||||
// The active daemon already wraps the UI tree via ProvidersWrapper,
|
if (primaryDaemonIds.has(daemon.id)) {
|
||||||
// so we skip rendering it here to avoid duplicate websocket connections.
|
continue;
|
||||||
shouldConnect.delete(activeDaemonId);
|
}
|
||||||
|
shouldConnect.set(daemon.id, daemon);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(shouldConnect.values());
|
return Array.from(shouldConnect.values());
|
||||||
}, [daemons, activeDaemonId]);
|
}, [daemons, activeDaemonId, primaryDaemonIds]);
|
||||||
|
|
||||||
if (backgroundDaemons.length === 0) {
|
if (backgroundDaemons.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -36,7 +70,12 @@ export function MultiDaemonSessionHost() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{backgroundDaemons.map((daemon) => (
|
{backgroundDaemons.map((daemon) => (
|
||||||
<SessionProvider key={daemon.id} serverUrl={daemon.wsUrl} serverId={daemon.id}>
|
<SessionProvider
|
||||||
|
key={daemon.id}
|
||||||
|
serverUrl={daemon.wsUrl}
|
||||||
|
serverId={daemon.id}
|
||||||
|
role="background"
|
||||||
|
>
|
||||||
{null}
|
{null}
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useDaemonRegistry, type DaemonProfile } from "./daemon-registry-context";
|
import { useDaemonRegistry, type DaemonProfile } from "./daemon-registry-context";
|
||||||
|
import { trackAnalyticsEvent } from "@/utils/analytics";
|
||||||
import type { SessionContextValue } from "./session-context";
|
import type { SessionContextValue } from "./session-context";
|
||||||
|
|
||||||
export interface SessionDirectoryEntry {
|
export interface SessionDirectoryEntry {
|
||||||
@@ -10,29 +11,44 @@ export interface SessionDirectoryEntry {
|
|||||||
subscribe: (listener: () => void) => () => void;
|
subscribe: (listener: () => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectionStatus = "idle" | "connecting" | "online" | "offline" | "error";
|
export type SessionAccessorRole = "primary" | "background";
|
||||||
|
type SessionAccessorRegistry = Map<string, Map<SessionAccessorRole, SessionDirectoryEntry>>;
|
||||||
|
|
||||||
export interface DaemonConnectionRecord {
|
export type ConnectionState =
|
||||||
|
| { status: "idle"; lastError: null; lastOnlineAt: string | null }
|
||||||
|
| { status: "connecting"; lastError: null; lastOnlineAt: string | null }
|
||||||
|
| { status: "online"; lastError: null; lastOnlineAt: string }
|
||||||
|
| { status: "offline"; lastError: string | null; lastOnlineAt: string | null }
|
||||||
|
| { status: "error"; lastError: string; lastOnlineAt: string | null };
|
||||||
|
|
||||||
|
export type ConnectionStatus = ConnectionState["status"];
|
||||||
|
|
||||||
|
type ConnectionStateUpdate =
|
||||||
|
| { status: "idle" }
|
||||||
|
| { status: "connecting"; lastOnlineAt?: string | null }
|
||||||
|
| { status: "online"; lastOnlineAt: string }
|
||||||
|
| { status: "offline"; lastError?: string | null; lastOnlineAt?: string | null }
|
||||||
|
| { status: "error"; lastError: string; lastOnlineAt?: string | null };
|
||||||
|
|
||||||
|
export type DaemonConnectionRecord = {
|
||||||
daemon: DaemonProfile;
|
daemon: DaemonProfile;
|
||||||
status: ConnectionStatus;
|
} & ConnectionState;
|
||||||
lastOnlineAt?: string;
|
|
||||||
lastError?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DaemonConnectionsContextValue {
|
interface DaemonConnectionsContextValue {
|
||||||
activeDaemonId: string | null;
|
activeDaemonId: string | null;
|
||||||
activeDaemon: DaemonProfile | null;
|
activeDaemon: DaemonProfile | null;
|
||||||
connectionStates: Map<string, DaemonConnectionRecord>;
|
connectionStates: Map<string, DaemonConnectionRecord>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setActiveDaemonId: (daemonId: string) => void;
|
setActiveDaemonId: (daemonId: string, options?: SetActiveDaemonOptions) => void;
|
||||||
updateConnectionStatus: (
|
updateConnectionStatus: (daemonId: string, update: ConnectionStateUpdate) => void;
|
||||||
daemonId: string,
|
|
||||||
status: ConnectionStatus,
|
|
||||||
extras?: { lastError?: string | null; lastOnlineAt?: string }
|
|
||||||
) => void;
|
|
||||||
sessionAccessors: Map<string, SessionDirectoryEntry>;
|
sessionAccessors: Map<string, SessionDirectoryEntry>;
|
||||||
registerSessionAccessor: (daemonId: string, entry: SessionDirectoryEntry) => void;
|
sessionAccessorRoles: Map<string, Set<SessionAccessorRole>>;
|
||||||
unregisterSessionAccessor: (daemonId: string) => void;
|
registerSessionAccessor: (
|
||||||
|
daemonId: string,
|
||||||
|
entry: SessionDirectoryEntry,
|
||||||
|
role?: SessionAccessorRole
|
||||||
|
) => void;
|
||||||
|
unregisterSessionAccessor: (daemonId: string, role?: SessionAccessorRole) => void;
|
||||||
subscribeToSessionDirectory: (listener: () => void) => () => void;
|
subscribeToSessionDirectory: (listener: () => void) => () => void;
|
||||||
notifySessionDirectoryChange: () => void;
|
notifySessionDirectoryChange: () => void;
|
||||||
}
|
}
|
||||||
@@ -42,6 +58,70 @@ const DaemonConnectionsContext = createContext<DaemonConnectionsContextValue | n
|
|||||||
const ACTIVE_DAEMON_STORAGE_KEY = "@paseo:active-daemon-id";
|
const ACTIVE_DAEMON_STORAGE_KEY = "@paseo:active-daemon-id";
|
||||||
const ACTIVE_DAEMON_QUERY_KEY = ["active-daemon-id"];
|
const ACTIVE_DAEMON_QUERY_KEY = ["active-daemon-id"];
|
||||||
|
|
||||||
|
export type SetActiveDaemonOptions = {
|
||||||
|
source?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createDefaultConnectionState(): ConnectionState {
|
||||||
|
return {
|
||||||
|
status: "idle",
|
||||||
|
lastError: null,
|
||||||
|
lastOnlineAt: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNextConnectionState(
|
||||||
|
existing: ConnectionState,
|
||||||
|
update: ConnectionStateUpdate
|
||||||
|
): ConnectionState {
|
||||||
|
switch (update.status) {
|
||||||
|
case "idle":
|
||||||
|
return { status: "idle", lastError: null, lastOnlineAt: existing.lastOnlineAt };
|
||||||
|
case "connecting":
|
||||||
|
return {
|
||||||
|
status: "connecting",
|
||||||
|
lastError: null,
|
||||||
|
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
|
||||||
|
};
|
||||||
|
case "online":
|
||||||
|
return { status: "online", lastError: null, lastOnlineAt: update.lastOnlineAt };
|
||||||
|
case "offline":
|
||||||
|
return {
|
||||||
|
status: "offline",
|
||||||
|
lastError: update.lastError ?? null,
|
||||||
|
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
|
||||||
|
};
|
||||||
|
case "error":
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
lastError: update.lastError,
|
||||||
|
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logConnectionLifecycle(daemon: DaemonProfile, previous: ConnectionState, next: ConnectionState) {
|
||||||
|
const logPayload = {
|
||||||
|
event: "daemon_connection_state",
|
||||||
|
daemonId: daemon.id,
|
||||||
|
label: daemon.label,
|
||||||
|
from: previous.status,
|
||||||
|
to: next.status,
|
||||||
|
lastError: next.lastError ?? null,
|
||||||
|
lastOnlineAt: next.lastOnlineAt ?? null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const logger =
|
||||||
|
next.status === "error"
|
||||||
|
? console.error
|
||||||
|
: next.status === "offline"
|
||||||
|
? console.warn
|
||||||
|
: console.info;
|
||||||
|
|
||||||
|
logger("[DaemonConnection]", logPayload);
|
||||||
|
}
|
||||||
|
|
||||||
export function useDaemonConnections(): DaemonConnectionsContextValue {
|
export function useDaemonConnections(): DaemonConnectionsContextValue {
|
||||||
const ctx = useContext(DaemonConnectionsContext);
|
const ctx = useContext(DaemonConnectionsContext);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
@@ -54,7 +134,7 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
const { daemons, isLoading: registryLoading } = useDaemonRegistry();
|
const { daemons, isLoading: registryLoading } = useDaemonRegistry();
|
||||||
const [activeDaemonId, setActiveDaemonIdState] = useState<string | null>(null);
|
const [activeDaemonId, setActiveDaemonIdState] = useState<string | null>(null);
|
||||||
const [connectionStates, setConnectionStates] = useState<Map<string, DaemonConnectionRecord>>(new Map());
|
const [connectionStates, setConnectionStates] = useState<Map<string, DaemonConnectionRecord>>(new Map());
|
||||||
const [sessionAccessors, setSessionAccessors] = useState<Map<string, SessionDirectoryEntry>>(new Map());
|
const [sessionAccessorRegistry, setSessionAccessorRegistry] = useState<SessionAccessorRegistry>(new Map());
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const activeDaemonPreference = useQuery({
|
const activeDaemonPreference = useQuery({
|
||||||
queryKey: ACTIVE_DAEMON_QUERY_KEY,
|
queryKey: ACTIVE_DAEMON_QUERY_KEY,
|
||||||
@@ -79,9 +159,7 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
const existing = prev.get(daemon.id);
|
const existing = prev.get(daemon.id);
|
||||||
next.set(daemon.id, {
|
next.set(daemon.id, {
|
||||||
daemon,
|
daemon,
|
||||||
status: existing?.status ?? "idle",
|
...(existing ?? createDefaultConnectionState()),
|
||||||
lastOnlineAt: existing?.lastOnlineAt,
|
|
||||||
lastError: existing?.lastError,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
@@ -89,14 +167,20 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
}, [daemons]);
|
}, [daemons]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSessionAccessors((prev) => {
|
setSessionAccessorRegistry((prev) => {
|
||||||
const next = new Map(prev);
|
const validDaemonIds = new Set(daemons.map((daemon) => daemon.id));
|
||||||
for (const key of Array.from(next.keys())) {
|
let changed = false;
|
||||||
if (!daemons.some((daemon) => daemon.id === key)) {
|
const next: SessionAccessorRegistry = new Map();
|
||||||
next.delete(key);
|
|
||||||
|
for (const [daemonId, roleMap] of prev.entries()) {
|
||||||
|
if (!validDaemonIds.has(daemonId)) {
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
next.set(daemonId, roleMap);
|
||||||
}
|
}
|
||||||
return next;
|
|
||||||
|
return changed ? next : prev;
|
||||||
});
|
});
|
||||||
}, [daemons]);
|
}, [daemons]);
|
||||||
|
|
||||||
@@ -115,8 +199,18 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
}, [queryClient]);
|
}, [queryClient]);
|
||||||
|
|
||||||
const setActiveDaemonId = useCallback(
|
const setActiveDaemonId = useCallback(
|
||||||
(daemonId: string) => {
|
(daemonId: string, options?: SetActiveDaemonOptions) => {
|
||||||
setActiveDaemonIdState(daemonId);
|
setActiveDaemonIdState((previous) => {
|
||||||
|
if (previous !== daemonId) {
|
||||||
|
trackAnalyticsEvent({
|
||||||
|
type: "daemon_active_changed",
|
||||||
|
daemonId,
|
||||||
|
previousDaemonId: previous,
|
||||||
|
source: options?.source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return daemonId;
|
||||||
|
});
|
||||||
void persistActiveDaemonId(daemonId);
|
void persistActiveDaemonId(daemonId);
|
||||||
},
|
},
|
||||||
[persistActiveDaemonId]
|
[persistActiveDaemonId]
|
||||||
@@ -138,7 +232,7 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fallback = daemons.find((daemon) => daemon.isDefault) ?? daemons[0];
|
const fallback = daemons.find((daemon) => daemon.isDefault) ?? daemons[0];
|
||||||
setActiveDaemonId(fallback.id);
|
setActiveDaemonId(fallback.id, { source: "auto_fallback" });
|
||||||
}, [daemons, activeDaemonId, activeDaemonPreference.isPending, persistActiveDaemonId, setActiveDaemonId]);
|
}, [daemons, activeDaemonId, activeDaemonPreference.isPending, persistActiveDaemonId, setActiveDaemonId]);
|
||||||
|
|
||||||
const activeDaemon = useMemo(() => {
|
const activeDaemon = useMemo(() => {
|
||||||
@@ -149,45 +243,65 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
}, [activeDaemonId, daemons]);
|
}, [activeDaemonId, daemons]);
|
||||||
|
|
||||||
const updateConnectionStatus = useCallback(
|
const updateConnectionStatus = useCallback(
|
||||||
(
|
(daemonId: string, update: ConnectionStateUpdate) => {
|
||||||
daemonId: string,
|
|
||||||
status: ConnectionStatus,
|
|
||||||
extras?: { lastError?: string | null; lastOnlineAt?: string }
|
|
||||||
) => {
|
|
||||||
setConnectionStates((prev) => {
|
setConnectionStates((prev) => {
|
||||||
const next = new Map(prev);
|
const existing = prev.get(daemonId);
|
||||||
const existing = next.get(daemonId);
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
const hasExplicitLastError = Boolean(extras && Object.prototype.hasOwnProperty.call(extras, "lastError"));
|
const nextState = resolveNextConnectionState(existing, update);
|
||||||
next.set(daemonId, {
|
const hasChanged =
|
||||||
...existing,
|
existing.status !== nextState.status ||
|
||||||
status,
|
existing.lastError !== nextState.lastError ||
|
||||||
lastError: hasExplicitLastError ? (extras!.lastError ?? null) : existing.lastError ?? null,
|
existing.lastOnlineAt !== nextState.lastOnlineAt;
|
||||||
lastOnlineAt: extras?.lastOnlineAt ?? existing.lastOnlineAt,
|
|
||||||
});
|
if (hasChanged) {
|
||||||
|
logConnectionLifecycle(existing.daemon, existing, nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(daemonId, { daemon: existing.daemon, ...nextState });
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const registerSessionAccessor = useCallback((daemonId: string, entry: SessionDirectoryEntry) => {
|
const registerSessionAccessor = useCallback(
|
||||||
setSessionAccessors((prev) => {
|
(daemonId: string, entry: SessionDirectoryEntry, role: SessionAccessorRole = "primary") => {
|
||||||
const next = new Map(prev);
|
setSessionAccessorRegistry((prev) => {
|
||||||
next.set(daemonId, entry);
|
const next = new Map(prev);
|
||||||
return next;
|
const existing = new Map(next.get(daemonId) ?? []);
|
||||||
});
|
existing.set(role, entry);
|
||||||
}, []);
|
next.set(daemonId, existing);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const unregisterSessionAccessor = useCallback((daemonId: string) => {
|
const unregisterSessionAccessor = useCallback(
|
||||||
setSessionAccessors((prev) => {
|
(daemonId: string, role: SessionAccessorRole = "primary") => {
|
||||||
const next = new Map(prev);
|
setSessionAccessorRegistry((prev) => {
|
||||||
next.delete(daemonId);
|
const existing = prev.get(daemonId);
|
||||||
return next;
|
if (!existing) {
|
||||||
});
|
return prev;
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
|
const nextRoleMap = new Map(existing);
|
||||||
|
nextRoleMap.delete(role);
|
||||||
|
|
||||||
|
const next = new Map(prev);
|
||||||
|
if (nextRoleMap.size === 0) {
|
||||||
|
next.delete(daemonId);
|
||||||
|
} else {
|
||||||
|
next.set(daemonId, nextRoleMap);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const subscribeToSessionDirectory = useCallback((listener: () => void) => {
|
const subscribeToSessionDirectory = useCallback((listener: () => void) => {
|
||||||
sessionDirectoryListenersRef.current.add(listener);
|
sessionDirectoryListenersRef.current.add(listener);
|
||||||
@@ -207,6 +321,34 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { sessionAccessors, sessionAccessorRoles } = useMemo(() => {
|
||||||
|
const flattened = new Map<string, SessionDirectoryEntry>();
|
||||||
|
const roleSets = new Map<string, Set<SessionAccessorRole>>();
|
||||||
|
|
||||||
|
sessionAccessorRegistry.forEach((roleMap, daemonId) => {
|
||||||
|
if (roleMap.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = new Set<SessionAccessorRole>();
|
||||||
|
roleMap.forEach((_entry, role) => roles.add(role));
|
||||||
|
roleSets.set(daemonId, roles);
|
||||||
|
|
||||||
|
const primaryEntry = roleMap.get("primary");
|
||||||
|
if (primaryEntry) {
|
||||||
|
flattened.set(daemonId, primaryEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backgroundEntry = roleMap.get("background");
|
||||||
|
if (backgroundEntry) {
|
||||||
|
flattened.set(daemonId, backgroundEntry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sessionAccessors: flattened, sessionAccessorRoles: roleSets };
|
||||||
|
}, [sessionAccessorRegistry]);
|
||||||
|
|
||||||
const value: DaemonConnectionsContextValue = {
|
const value: DaemonConnectionsContextValue = {
|
||||||
activeDaemonId,
|
activeDaemonId,
|
||||||
activeDaemon,
|
activeDaemon,
|
||||||
@@ -215,6 +357,7 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
|
|||||||
setActiveDaemonId,
|
setActiveDaemonId,
|
||||||
updateConnectionStatus,
|
updateConnectionStatus,
|
||||||
sessionAccessors,
|
sessionAccessors,
|
||||||
|
sessionAccessorRoles,
|
||||||
registerSessionAccessor,
|
registerSessionAccessor,
|
||||||
unregisterSessionAccessor,
|
unregisterSessionAccessor,
|
||||||
subscribeToSessionDirectory,
|
subscribeToSessionDirectory,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import type {
|
|||||||
} from "@server/server/agent/agent-sdk-types";
|
} from "@server/server/agent/agent-sdk-types";
|
||||||
import { ScrollView } from "react-native";
|
import { ScrollView } from "react-native";
|
||||||
import * as FileSystem from 'expo-file-system';
|
import * as FileSystem from 'expo-file-system';
|
||||||
import { useDaemonConnections } from "./daemon-connections-context";
|
import { useDaemonConnections, type SessionAccessorRole } from "./daemon-connections-context";
|
||||||
|
|
||||||
const derivePendingPermissionKey = (agentId: string, request: AgentPermissionRequest) => {
|
const derivePendingPermissionKey = (agentId: string, request: AgentPermissionRequest) => {
|
||||||
const fallbackId =
|
const fallbackId =
|
||||||
@@ -388,9 +388,11 @@ interface SessionProviderProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
role?: SessionAccessorRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionProvider({ children, serverUrl, serverId }: SessionProviderProps) {
|
export function SessionProvider({ children, serverUrl, serverId, role }: SessionProviderProps) {
|
||||||
|
const sessionRole = role ?? "primary";
|
||||||
const ws = useWebSocket(serverUrl);
|
const ws = useWebSocket(serverUrl);
|
||||||
const wsIsConnected = ws.isConnected;
|
const wsIsConnected = ws.isConnected;
|
||||||
const {
|
const {
|
||||||
@@ -402,29 +404,29 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ws.isConnected) {
|
if (ws.isConnected) {
|
||||||
updateConnectionStatus(serverId, "online", {
|
updateConnectionStatus(serverId, {
|
||||||
|
status: "online",
|
||||||
lastOnlineAt: new Date().toISOString(),
|
lastOnlineAt: new Date().toISOString(),
|
||||||
lastError: null,
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ws.isConnecting) {
|
if (ws.isConnecting) {
|
||||||
updateConnectionStatus(serverId, "connecting");
|
updateConnectionStatus(serverId, { status: "connecting" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ws.lastError) {
|
if (ws.lastError) {
|
||||||
updateConnectionStatus(serverId, "error", { lastError: ws.lastError });
|
updateConnectionStatus(serverId, { status: "error", lastError: ws.lastError });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConnectionStatus(serverId, "offline");
|
updateConnectionStatus(serverId, { status: "offline" });
|
||||||
}, [serverId, updateConnectionStatus, ws.isConnected, ws.isConnecting, ws.lastError]);
|
}, [serverId, updateConnectionStatus, ws.isConnected, ws.isConnecting, ws.lastError]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
updateConnectionStatus(serverId, "offline", { lastError: null });
|
updateConnectionStatus(serverId, { status: "offline", lastError: null });
|
||||||
};
|
};
|
||||||
}, [serverId, updateConnectionStatus]);
|
}, [serverId, updateConnectionStatus]);
|
||||||
|
|
||||||
@@ -1794,11 +1796,11 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerSessionAccessor(serverId, sessionDirectoryEntry);
|
registerSessionAccessor(serverId, sessionDirectoryEntry, sessionRole);
|
||||||
return () => {
|
return () => {
|
||||||
unregisterSessionAccessor(serverId);
|
unregisterSessionAccessor(serverId, sessionRole);
|
||||||
};
|
};
|
||||||
}, [serverId, registerSessionAccessor, unregisterSessionAccessor, sessionDirectoryEntry]);
|
}, [serverId, sessionRole, registerSessionAccessor, unregisterSessionAccessor, sessionDirectoryEntry]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionContext.Provider value={value}>
|
<SessionContext.Provider value={value}>
|
||||||
|
|||||||
@@ -28,15 +28,24 @@ export function getSessionForServer(
|
|||||||
|
|
||||||
type UseDaemonSessionOptions = {
|
type UseDaemonSessionOptions = {
|
||||||
suppressUnavailableAlert?: boolean;
|
suppressUnavailableAlert?: boolean;
|
||||||
|
allowUnavailable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function useDaemonSession(
|
||||||
|
serverId?: string,
|
||||||
|
options?: UseDaemonSessionOptions & { allowUnavailable?: false }
|
||||||
|
): SessionContextValue;
|
||||||
|
export function useDaemonSession(
|
||||||
|
serverId: string | undefined,
|
||||||
|
options: UseDaemonSessionOptions & { allowUnavailable: true }
|
||||||
|
): SessionContextValue | null;
|
||||||
export function useDaemonSession(serverId?: string, options?: UseDaemonSessionOptions) {
|
export function useDaemonSession(serverId?: string, options?: UseDaemonSessionOptions) {
|
||||||
const activeSession = useSession();
|
const activeSession = useSession();
|
||||||
const sessionDirectory = useSessionDirectory();
|
const sessionDirectory = useSessionDirectory();
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
const alertedDaemonsRef = useRef<Set<string>>(new Set());
|
const alertedDaemonsRef = useRef<Set<string>>(new Set());
|
||||||
const loggedDaemonsRef = useRef<Set<string>>(new Set());
|
const loggedDaemonsRef = useRef<Set<string>>(new Set());
|
||||||
const { suppressUnavailableAlert = false } = options ?? {};
|
const { suppressUnavailableAlert = false, allowUnavailable = false } = options ?? {};
|
||||||
|
|
||||||
const targetServerId = serverId ?? activeSession.serverId;
|
const targetServerId = serverId ?? activeSession.serverId;
|
||||||
const isActiveSession = targetServerId === activeSession.serverId;
|
const isActiveSession = targetServerId === activeSession.serverId;
|
||||||
@@ -64,6 +73,10 @@ export function useDaemonSession(serverId?: string, options?: UseDaemonSessionOp
|
|||||||
loggedDaemonsRef.current.add(targetServerId);
|
loggedDaemonsRef.current.add(targetServerId);
|
||||||
console.warn(`[useDaemonSession] Session unavailable for daemon "${label}" (${status}).`);
|
console.warn(`[useDaemonSession] Session unavailable for daemon "${label}" (${status}).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allowUnavailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
type BackgroundAgentAction = "create" | "resume";
|
||||||
|
type OfflineAction =
|
||||||
|
| "create"
|
||||||
|
| "resume"
|
||||||
|
| "dictation"
|
||||||
|
| "import_list";
|
||||||
|
|
||||||
|
type AnalyticsEvent =
|
||||||
|
| {
|
||||||
|
type: "daemon_active_changed";
|
||||||
|
daemonId: string;
|
||||||
|
previousDaemonId: string | null;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "background_agent_action";
|
||||||
|
action: BackgroundAgentAction;
|
||||||
|
daemonId: string;
|
||||||
|
activeDaemonId: string | null;
|
||||||
|
isBackground: boolean;
|
||||||
|
cwd?: string;
|
||||||
|
provider?: string;
|
||||||
|
modeId?: string;
|
||||||
|
model?: string;
|
||||||
|
baseBranch?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "offline_daemon_action_attempt";
|
||||||
|
action: OfflineAction;
|
||||||
|
daemonId: string | null;
|
||||||
|
activeDaemonId: string | null;
|
||||||
|
status: string | null;
|
||||||
|
isBackground: boolean;
|
||||||
|
reason?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function trackAnalyticsEvent(event: AnalyticsEvent) {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...event,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
console.info("[Analytics]", payload);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Analytics] Failed to emit event", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
|
import { assertUnreachable } from "./exhaustive";
|
||||||
|
|
||||||
export function formatConnectionStatus(status: ConnectionStatus): string {
|
export function formatConnectionStatus(status: ConnectionStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -10,8 +11,10 @@ export function formatConnectionStatus(status: ConnectionStatus): string {
|
|||||||
return "Offline";
|
return "Offline";
|
||||||
case "error":
|
case "error":
|
||||||
return "Error";
|
return "Error";
|
||||||
default:
|
case "idle":
|
||||||
return "Idle";
|
return "Idle";
|
||||||
|
default:
|
||||||
|
return assertUnreachable(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +28,11 @@ export function getConnectionStatusTone(status: ConnectionStatus): ConnectionSta
|
|||||||
return "warning";
|
return "warning";
|
||||||
case "error":
|
case "error":
|
||||||
return "error";
|
return "error";
|
||||||
default:
|
case "offline":
|
||||||
|
return "warning";
|
||||||
|
case "idle":
|
||||||
return "muted";
|
return "muted";
|
||||||
|
default:
|
||||||
|
return assertUnreachable(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function assertUnreachable(value: never): never {
|
||||||
|
throw new Error(`Unhandled case: ${String(value)}`);
|
||||||
|
}
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
- [x] Restore the Import Agent flow (modal entry, mutation wiring, navigation) without undoing the multi-daemon routing work from previous steps.
|
- [x] Restore the Import Agent flow (modal entry, mutation wiring, navigation) without undoing the multi-daemon routing work from previous steps.
|
||||||
- After reworking Home/Header and the modals, the import trigger disappeared and existing deep links no longer reach a functioning flow. Bring the Import CTA back (Home, footer, agent screen), ensure it accepts a daemon id, and verify the import mutation routes to the selected daemon without regressing the new server-aware navigation.
|
- After reworking Home/Header and the modals, the import trigger disappeared and existing deep links no longer reach a functioning flow. Bring the Import CTA back (Home, footer, agent screen), ensure it accepts a daemon id, and verify the import mutation routes to the selected daemon without regressing the new server-aware navigation.
|
||||||
- Rewired the import buttons across Home (header + empty state with deep-link auto open), the global footer, and the agent action menu so every trigger passes the correct daemon id into `ImportAgentModal`, and ran `npm run typecheck --workspace=@paseo/app`.
|
- Rewired the import buttons across Home (header + empty state with deep-link auto open), the global footer, and the agent action menu so every trigger passes the correct daemon id into `ImportAgentModal`, and ran `npm run typecheck --workspace=@paseo/app`.
|
||||||
|
- [x] Guard Create/Resume/Dictation actions when the active daemon is offline and no explicit daemon is selected.
|
||||||
|
- The offline gate only ran for an explicitly chosen `serverId`, so opening the modal on an offline active daemon still fired create/resume/dictation websocket sends; `packages/app/src/components/create-agent-modal.tsx` now derives availability from the effective daemon id and requires an online websocket before enabling those flows.
|
||||||
|
|
||||||
### 4. Connection State & Persistence
|
### 4. Connection State & Persistence
|
||||||
- [x] Stop the Settings daemon list from firing “Daemon unavailable” alerts when background daemons are offline by reading session snapshots via `useSessionForServer` and only performing restart/test flows when a session is actually mounted.
|
- [x] Stop the Settings daemon list from firing “Daemon unavailable” alerts when background daemons are offline by reading session snapshots via `useSessionForServer` and only performing restart/test flows when a session is actually mounted.
|
||||||
@@ -75,18 +77,35 @@
|
|||||||
### 5. Performance & UX Polish
|
### 5. Performance & UX Polish
|
||||||
- [x] Ensure agent image attachments preserve MIME metadata and are base64 encoded before hitting the daemon.
|
- [x] Ensure agent image attachments preserve MIME metadata and are base64 encoded before hitting the daemon.
|
||||||
- `packages/app/src/contexts/session-context.tsx:1198` now accepts `{ uri, mimeType }` attachments and reads them via `expo-file-system`, and `packages/app/src/components/agent-input-area.tsx:140` forwards the stored metadata so queued sends no longer drop screenshots (`npm run typecheck` passes).
|
- `packages/app/src/contexts/session-context.tsx:1198` now accepts `{ uri, mimeType }` attachments and reads them via `expo-file-system`, and `packages/app/src/components/agent-input-area.tsx:140` forwards the stored metadata so queued sends no longer drop screenshots (`npm run typecheck` passes).
|
||||||
- [ ] Profile the Create Agent modal—debounce expensive effects (e.g., provider model fetches) per server and prefetch metadata when daemons are idle to eliminate the visible lag when switching targets.
|
- [x] Profile the Create Agent modal—debounce expensive effects (e.g., provider model fetches) per server and prefetch metadata when daemons are idle to eliminate the visible lag when switching targets.
|
||||||
- [ ] Audit websocket usage so background `SessionProvider`s never duplicate connections for the active daemon (one live connection per daemon id).
|
- Added per-server debounce + cleanup around provider model requests, scheduled idle-time prefetch for online daemons via the session directory, and verified with `npm run typecheck --workspace=@paseo/app`.
|
||||||
- [ ] Enforce “impossible states are impossible” across UI/data models (strict typing, discriminated unions, exhaustive switches) so complex flows remain clean without relying on Expo E2E tests.
|
- [x] Audit websocket usage so background `SessionProvider`s never duplicate connections for the active daemon (one live connection per daemon id).
|
||||||
|
- Tracked session accessors by role, had background hosts register as `background`, filtered out daemons that already have a primary session before spinning up background connections, and ran `npm run typecheck --workspace=@paseo/app`.
|
||||||
|
- [x] Enforce “impossible states are impossible” across UI/data models (strict typing, discriminated unions, exhaustive switches) so complex flows remain clean without relying on Expo E2E tests.
|
||||||
|
- Tightened daemon connection state to a discriminated union with exhaustiveness checks, added an `assertUnreachable` helper for status switches, and re-ran `npm run typecheck --workspace=@paseo/app`.
|
||||||
|
|
||||||
### 6. Observability & Tooling
|
### 6. Observability & Tooling
|
||||||
- [ ] Add structured logging for daemon connection lifecycle (connect, error, auto-connect skip) so we can diagnose “multi daemon” issues from device logs.
|
- [x] Add structured logging for daemon connection lifecycle (connect, error, auto-connect skip) so we can diagnose “multi daemon” issues from device logs.
|
||||||
- [ ] Emit analytics when users create/resume agents on background daemons, attempt actions while those daemons are offline, or switch default daemons—helps prioritize reconnection UX.
|
- Added connection state transition logs with daemon ids/labels plus auto-connect skip logs for disabled daemons, and ran `npm run typecheck --workspace=@paseo/app`.
|
||||||
- [ ] Document the architecture in `docs/multi-daemon.md` (registry, sessions, routing rules) so future contributors understand how to extend it.
|
- [x] Emit analytics when users create/resume agents on background daemons, attempt actions while those daemons are offline, or switch default daemons—helps prioritize reconnection UX.
|
||||||
- [ ] Land the accumulated multi-daemon changes in source control with a clean commit (linted, type-checked, plan updated).
|
- Added a centralized analytics helper, instrumented active-daemon switches plus background create/resume flows (including offline/blocked actions like dictation and import refresh), and ran `npm run typecheck --workspace=@paseo/app`.
|
||||||
|
- [x] Document the architecture in `docs/multi-daemon.md` (registry, sessions, routing rules) so future contributors understand how to extend it.
|
||||||
|
- Added a multi-daemon architecture guide detailing the registry, connection/session hosts, session directory access, routing rules, and extension guidelines (docs-only change; no tests needed).
|
||||||
|
- [x] Land the accumulated multi-daemon changes in source control with a clean commit (linted, type-checked, plan updated).
|
||||||
|
- Fixed lint violations around guarded daemon screens, added an opt-in nullable `useDaemonSession`, reran `npm run lint --workspace=@paseo/app` (warnings only) and `npm run typecheck --workspace=@paseo/app`, and prepared the tree for a clean commit.
|
||||||
|
|
||||||
### Review
|
### Review
|
||||||
- [x] 2025-11-26 Reviewer sanity check for the recent multi-daemon rollout work.
|
- [x] 2025-11-26 Reviewer sanity check for the recent multi-daemon rollout work.
|
||||||
- Confirmed the new `useDaemonSession` hook, guarded Agent/Git Diff/File Explorer screens, and server-aware Create/Import flows align with the documented fixes; no regressions or missing follow-ups spotted, so no additional tasks were opened.
|
- Confirmed the new `useDaemonSession` hook, guarded Agent/Git Diff/File Explorer screens, and server-aware Create/Import flows align with the documented fixes; no regressions or missing follow-ups spotted, so no additional tasks were opened.
|
||||||
- [x] 2025-11-26 Reviewer follow-up on session isolation across daemons.
|
- [x] 2025-11-26 Reviewer follow-up on session isolation across daemons.
|
||||||
- Found that the active `SessionProvider` kept its React state when switching daemons, so stale agents/permissions could leak between server contexts; fixed by keying the provider in `_layout.tsx` so the tree remounts on each daemon change.
|
- Found that the active `SessionProvider` kept its React state when switching daemons, so stale agents/permissions could leak between server contexts; fixed by keying the provider in `_layout.tsx` so the tree remounts on each daemon change.
|
||||||
|
- [x] 2025-11-26 Reviewer pass on provider model prefetch/perf work in CreateAgentModal.
|
||||||
|
- Idle provider-model prefetch timers could still fire after a daemon was removed; cleared stale timers before scheduling background fetches so requests don't target deleted entries (`packages/app/src/components/create-agent-modal.tsx:1198`) and re-ran `npm run typecheck --workspace=@paseo/app`.
|
||||||
|
- [x] 2025-11-26 Reviewer pass on connection-state/SessionProvider role refactor.
|
||||||
|
- Checked the new discriminated `ConnectionState`, role-aware session accessor registry, and `MultiDaemonSessionHost` filters to avoid duplicate websocket sessions; reran `npm run typecheck --workspace=@paseo/app` and didn't spot regressions or new follow-ups to open.
|
||||||
|
- [x] 2025-11-26 Reviewer pass on agent route daemon selection.
|
||||||
|
- Deep links with unregistered daemon ids repeatedly forced `setActiveDaemonId`, remounting the root `SessionProvider` and flashing the missing-daemon screen; `/agent/[serverId]/[agentId]` now ignores unknown daemons and shows a clear unavailable state instead.
|
||||||
|
- [x] 2025-11-27 Reviewer pass on connection status UX.
|
||||||
|
- Offline daemons were rendered with a muted tone via `getConnectionStatusTone`, hiding them in the connection banners; mapped offline to `warning` so settings/home surface disconnected daemons in amber (`packages/app/src/utils/daemons.ts`).
|
||||||
|
- [x] 2025-11-27 Reviewer fix for provider model fetching on the active daemon.
|
||||||
|
- `CreateAgentModal` skipped provider model requests whenever no daemon was explicitly selected (active daemon fallback), so the model list never refreshed after changing the working directory. The effect now targets the effective daemon id instead of requiring `selectedServerId`, and `npm run typecheck --workspace=@paseo/app` passes (`packages/app/src/components/create-agent-modal.tsx`).
|
||||||
|
|||||||
Reference in New Issue
Block a user