Compare commits
35 Commits
v0.13.3
...
fix_local_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2236350f1b | ||
|
|
5f3e9317ca | ||
|
|
750e73f540 | ||
|
|
61e06ef883 | ||
|
|
b0b0a55e14 | ||
|
|
984743f3c7 | ||
|
|
27bccb8d6b | ||
|
|
153065d025 | ||
|
|
2abda0e6b4 | ||
|
|
800133361d | ||
|
|
034cb5dea9 | ||
|
|
d7ab84f245 | ||
|
|
201988b97c | ||
|
|
6a6fcff2c8 | ||
|
|
f29f197b9a | ||
|
|
dbde403b3e | ||
|
|
230c981cc2 | ||
|
|
34978c87fb | ||
|
|
3e6d0a402c | ||
|
|
e81c5f6443 | ||
|
|
b0d27bd127 | ||
|
|
7576470295 | ||
|
|
6d32e09db0 | ||
|
|
503cb3a02e | ||
|
|
0250c6350f | ||
|
|
24cc8fe939 | ||
|
|
282b234a7c | ||
|
|
4ba088a876 | ||
|
|
7b1817d606 | ||
|
|
5bc3c23ec5 | ||
|
|
127a51e3c3 | ||
|
|
daa22b6d8c | ||
|
|
23f2de2d7e | ||
|
|
80c9b76709 | ||
|
|
a29b77d60b |
81
package-lock.json
generated
81
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"version": "7.28.5",
|
"version": "7.28.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"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",
|
||||||
@@ -3380,6 +3381,7 @@
|
|||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.20.7",
|
"@babel/parser": "^7.20.7",
|
||||||
"@babel/types": "^7.20.7",
|
"@babel/types": "^7.20.7",
|
||||||
@@ -3481,6 +3483,7 @@
|
|||||||
"version": "22.19.0",
|
"version": "22.19.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -3555,6 +3558,7 @@
|
|||||||
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
@@ -3637,6 +3641,7 @@
|
|||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -3839,7 +3844,6 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
@@ -3857,7 +3861,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -3878,7 +3881,6 @@
|
|||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -3892,14 +3894,12 @@
|
|||||||
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -4213,7 +4213,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
@@ -4277,6 +4276,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -4767,7 +4767,6 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^4.0.2",
|
||||||
@@ -4897,7 +4896,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"crc32": "bin/crc32.njs"
|
"crc32": "bin/crc32.njs"
|
||||||
},
|
},
|
||||||
@@ -4909,7 +4907,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"crc-32": "^1.2.0",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
@@ -5275,6 +5272,7 @@
|
|||||||
"version": "24.13.3",
|
"version": "24.13.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"builder-util": "24.13.1",
|
"builder-util": "24.13.1",
|
||||||
@@ -5441,7 +5439,6 @@
|
|||||||
"version": "24.13.3",
|
"version": "24.13.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
@@ -5453,7 +5450,6 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
@@ -5467,7 +5463,6 @@
|
|||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -5479,7 +5474,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@@ -6197,8 +6191,7 @@
|
|||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@@ -7415,8 +7408,7 @@
|
|||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/isbinaryfile": {
|
"node_modules/isbinaryfile": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.6",
|
||||||
@@ -7466,6 +7458,7 @@
|
|||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -7597,7 +7590,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "^2.0.5"
|
"readable-stream": "^2.0.5"
|
||||||
},
|
},
|
||||||
@@ -7609,7 +7601,6 @@
|
|||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -7623,14 +7614,12 @@
|
|||||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lazystream/node_modules/string_decoder": {
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -7695,26 +7684,22 @@
|
|||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.difference": {
|
"node_modules/lodash.difference": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.flatten": {
|
"node_modules/lodash.flatten": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.sortby": {
|
"node_modules/lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
@@ -7726,8 +7711,7 @@
|
|||||||
"node_modules/lodash.union": {
|
"node_modules/lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -8531,6 +8515,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -8678,8 +8663,7 @@
|
|||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/process-warning": {
|
"node_modules/process-warning": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -8928,7 +8912,6 @@
|
|||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"string_decoder": "^1.1.1",
|
"string_decoder": "^1.1.1",
|
||||||
@@ -8942,7 +8925,6 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"minimatch": "^5.1.0"
|
||||||
}
|
}
|
||||||
@@ -9245,6 +9227,7 @@
|
|||||||
"version": "4.52.5",
|
"version": "4.52.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -9468,6 +9451,7 @@
|
|||||||
"node_modules/seroval": {
|
"node_modules/seroval": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@@ -9791,6 +9775,7 @@
|
|||||||
"node_modules/solid-js": {
|
"node_modules/solid-js": {
|
||||||
"version": "1.9.10",
|
"version": "1.9.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.1.0",
|
"csstype": "^3.1.0",
|
||||||
"seroval": "~1.3.0",
|
"seroval": "~1.3.0",
|
||||||
@@ -9931,7 +9916,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
@@ -10265,7 +10249,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
@@ -10458,6 +10441,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -10707,6 +10691,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"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"
|
||||||
@@ -11054,6 +11039,7 @@
|
|||||||
"version": "5.4.21",
|
"version": "5.4.21",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -11538,6 +11524,7 @@
|
|||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -11732,6 +11719,7 @@
|
|||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
@@ -12020,7 +12008,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^3.0.4",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^4.1.2",
|
||||||
@@ -12034,7 +12021,6 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.2.3",
|
"glob": "^7.2.3",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -12054,6 +12040,7 @@
|
|||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -12068,7 +12055,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12105,7 +12092,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12147,7 +12134,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12155,7 +12142,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app",
|
"build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app",
|
||||||
"build:binaries": "npm run build:binaries --workspace @neuralnomads/codenomad-electron-app",
|
"build:binaries": "npm run build:binaries --workspace @neuralnomads/codenomad-electron-app",
|
||||||
"typecheck": "npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @neuralnomads/codenomad-electron-app",
|
"typecheck": "npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @neuralnomads/codenomad-electron-app",
|
||||||
"bumpVersion": "node ./scripts/bump-version.js"
|
"bumpVersion": "npm version --workspaces --include-workspace-root --no-git-tag-version"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"minServerVersion": "0.13.3",
|
"minServerVersion": "0.13.1",
|
||||||
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/electron-app/.gitignore
vendored
1
packages/electron-app/.gitignore
vendored
@@ -2,4 +2,3 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
release/
|
release/
|
||||||
.vite/
|
.vite/
|
||||||
electron/resources/server/
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { BrowserWindow, Notification, dialog, ipcMain, powerSaveBlocker, type OpenDialogOptions } from "electron"
|
import { BrowserWindow, Notification, dialog, ipcMain, powerSaveBlocker, type OpenDialogOptions } from "electron"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { requestMicrophoneAccess } from "./permissions"
|
|
||||||
import type { CliProcessManager, CliStatus } from "./process-manager"
|
import type { CliProcessManager, CliStatus } from "./process-manager"
|
||||||
|
|
||||||
let wakeLockId: number | null = null
|
let wakeLockId: number | null = null
|
||||||
@@ -112,11 +111,6 @@ export function setupCliIPC(mainWindow: BrowserWindow, cliManager: CliProcessMan
|
|||||||
return { enabled: false }
|
return { enabled: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"media:requestMicrophoneAccess",
|
|
||||||
async (): Promise<{ granted: boolean }> => ({ granted: await requestMicrophoneAccess() }),
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"notifications:show",
|
"notifications:show",
|
||||||
async (_event, payload: { title?: unknown; body?: unknown }): Promise<{ ok: boolean; reason?: string }> => {
|
async (_event, payload: { title?: unknown; body?: unknown }): Promise<{ ok: boolean; reason?: string }> => {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { dirname, join } from "path"
|
|||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
import { createApplicationMenu } from "./menu"
|
import { createApplicationMenu } from "./menu"
|
||||||
import { setupCliIPC } from "./ipc"
|
import { setupCliIPC } from "./ipc"
|
||||||
import { configureMediaPermissionHandlers } from "./permissions"
|
|
||||||
import { CliProcessManager } from "./process-manager"
|
import { CliProcessManager } from "./process-manager"
|
||||||
|
|
||||||
const mainFilename = fileURLToPath(import.meta.url)
|
const mainFilename = fileURLToPath(import.meta.url)
|
||||||
@@ -328,7 +327,6 @@ function finalizeCliSwap(url: string) {
|
|||||||
mainWindow.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error))
|
mainWindow.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
const SESSION_COOKIE_NAME = "codenomad_session"
|
|
||||||
let bootstrapExchangeInFlight = false
|
let bootstrapExchangeInFlight = false
|
||||||
|
|
||||||
function extractCookieValue(setCookieHeader: string | string[] | undefined, name: string): string | null {
|
function extractCookieValue(setCookieHeader: string | string[] | undefined, name: string): string | null {
|
||||||
@@ -351,6 +349,7 @@ function extractCookieValue(setCookieHeader: string | string[] | undefined, name
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function exchangeBootstrapToken(baseUrl: string, token: string): Promise<boolean> {
|
async function exchangeBootstrapToken(baseUrl: string, token: string): Promise<boolean> {
|
||||||
|
const sessionCookieName = cliManager.getAuthCookieName()
|
||||||
const target = new URL("/api/auth/token", baseUrl)
|
const target = new URL("/api/auth/token", baseUrl)
|
||||||
const body = JSON.stringify({ token })
|
const body = JSON.stringify({ token })
|
||||||
|
|
||||||
@@ -381,14 +380,14 @@ async function exchangeBootstrapToken(baseUrl: string, token: string): Promise<b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = extractCookieValue(result.setCookie, SESSION_COOKIE_NAME)
|
const sessionId = extractCookieValue(result.setCookie, sessionCookieName)
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
await session.defaultSession.cookies.set({
|
await session.defaultSession.cookies.set({
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
name: SESSION_COOKIE_NAME,
|
name: sessionCookieName,
|
||||||
value: sessionId,
|
value: sessionId,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -490,7 +489,6 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
session.defaultSession.setSpellCheckerEnabled(false)
|
session.defaultSession.setSpellCheckerEnabled(false)
|
||||||
configureMediaPermissionHandlers(getAllowedRendererOrigins)
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
window.webContents.session.setSpellCheckerEnabled(false)
|
window.webContents.session.setSpellCheckerEnabled(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { session, systemPreferences } from "electron"
|
|
||||||
|
|
||||||
const isMac = process.platform === "darwin"
|
|
||||||
|
|
||||||
export function isAllowedRendererOrigin(origin: string | undefined | null, allowedOrigins: string[]): boolean {
|
|
||||||
if (!origin) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const normalized = new URL(origin).origin
|
|
||||||
return allowedOrigins.includes(normalized)
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function configureMediaPermissionHandlers(getAllowedOrigins: () => string[]) {
|
|
||||||
const isAudioMediaRequest = (permission: string, details?: unknown) => {
|
|
||||||
if (permission !== "media") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaTypes = (details as { mediaTypes?: string[] } | undefined)?.mediaTypes ?? []
|
|
||||||
return mediaTypes.length === 0 || mediaTypes.includes("audio")
|
|
||||||
}
|
|
||||||
|
|
||||||
session.defaultSession.setPermissionCheckHandler((_webContents, permission, requestingOrigin, details) => {
|
|
||||||
if (!isAudioMediaRequest(permission, details)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAllowedRendererOrigin(requestingOrigin, getAllowedOrigins())
|
|
||||||
})
|
|
||||||
|
|
||||||
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
|
||||||
if (!isAudioMediaRequest(permission, details)) {
|
|
||||||
callback(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestingOrigin = (details as { requestingOrigin?: string } | undefined)?.requestingOrigin || webContents.getURL()
|
|
||||||
callback(isAllowedRendererOrigin(requestingOrigin, getAllowedOrigins()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requestMicrophoneAccess(): Promise<boolean> {
|
|
||||||
if (!isMac) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = systemPreferences.getMediaAccessStatus("microphone")
|
|
||||||
if (status === "granted") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return systemPreferences.askForMediaAccess("microphone")
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
import { spawn, spawnSync, type ChildProcess } from "child_process"
|
import { spawn, spawnSync, type ChildProcess } from "child_process"
|
||||||
import { app, utilityProcess, type UtilityProcess } from "electron"
|
import { app } from "electron"
|
||||||
import { createRequire } from "module"
|
import { createRequire } from "module"
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
import { existsSync, readFileSync } from "fs"
|
import { existsSync, readFileSync } from "fs"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { fileURLToPath } from "url"
|
|
||||||
import { parse as parseYaml } from "yaml"
|
import { parse as parseYaml } from "yaml"
|
||||||
import { buildUserShellCommand, getUserShellEnv, supportsUserShell } from "./user-shell"
|
import { buildUserShellCommand, getUserShellEnv, supportsUserShell } from "./user-shell"
|
||||||
|
|
||||||
const nodeRequire = createRequire(import.meta.url)
|
const nodeRequire = createRequire(import.meta.url)
|
||||||
const mainFilename = fileURLToPath(import.meta.url)
|
|
||||||
const mainDirname = path.dirname(mainFilename)
|
|
||||||
|
|
||||||
const BOOTSTRAP_TOKEN_PREFIX = "CODENOMAD_BOOTSTRAP_TOKEN:"
|
const BOOTSTRAP_TOKEN_PREFIX = "CODENOMAD_BOOTSTRAP_TOKEN:"
|
||||||
|
const SESSION_COOKIE_NAME_PREFIX = "codenomad_session"
|
||||||
|
|
||||||
type CliState = "starting" | "ready" | "error" | "stopped"
|
type CliState = "starting" | "ready" | "error" | "stopped"
|
||||||
type ListeningMode = "local" | "all"
|
type ListeningMode = "local" | "all"
|
||||||
@@ -41,9 +39,6 @@ interface CliEntryResolution {
|
|||||||
runnerPath?: string
|
runnerPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManagedChild = ChildProcess | UtilityProcess
|
|
||||||
type ChildLaunchMode = "spawn" | "utility"
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json"
|
const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json"
|
||||||
|
|
||||||
function isYamlPath(filePath: string): boolean {
|
function isYamlPath(filePath: string): boolean {
|
||||||
@@ -123,12 +118,12 @@ export declare interface CliProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CliProcessManager extends EventEmitter {
|
export class CliProcessManager extends EventEmitter {
|
||||||
private child?: ManagedChild
|
private child?: ChildProcess
|
||||||
private childLaunchMode: ChildLaunchMode = "spawn"
|
|
||||||
private status: CliStatus = { state: "stopped" }
|
private status: CliStatus = { state: "stopped" }
|
||||||
private stdoutBuffer = ""
|
private stdoutBuffer = ""
|
||||||
private stderrBuffer = ""
|
private stderrBuffer = ""
|
||||||
private bootstrapToken: string | null = null
|
private bootstrapToken: string | null = null
|
||||||
|
private authCookieName = `${SESSION_COOKIE_NAME_PREFIX}_${process.pid}_${Date.now()}`
|
||||||
private requestedStop = false
|
private requestedStop = false
|
||||||
|
|
||||||
async start(options: StartOptions): Promise<CliStatus> {
|
async start(options: StartOptions): Promise<CliStatus> {
|
||||||
@@ -139,66 +134,37 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
this.stdoutBuffer = ""
|
this.stdoutBuffer = ""
|
||||||
this.stderrBuffer = ""
|
this.stderrBuffer = ""
|
||||||
this.bootstrapToken = null
|
this.bootstrapToken = null
|
||||||
|
this.authCookieName = `${SESSION_COOKIE_NAME_PREFIX}_${process.pid}_${Date.now()}`
|
||||||
this.requestedStop = false
|
this.requestedStop = false
|
||||||
this.updateStatus({ state: "starting", port: undefined, pid: undefined, url: undefined, error: undefined })
|
this.updateStatus({ state: "starting", port: undefined, pid: undefined, url: undefined, error: undefined })
|
||||||
|
|
||||||
|
const cliEntry = this.resolveCliEntry(options)
|
||||||
const listeningMode = this.resolveListeningMode()
|
const listeningMode = this.resolveListeningMode()
|
||||||
const host = resolveHostForMode(listeningMode)
|
const host = resolveHostForMode(listeningMode)
|
||||||
const args = this.buildCliArgs(options, host)
|
const args = this.buildCliArgs(options, host)
|
||||||
|
|
||||||
let child: ManagedChild
|
console.info(
|
||||||
|
`[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) using ${cliEntry.runner} at ${cliEntry.entry} (host=${host})`,
|
||||||
|
)
|
||||||
|
|
||||||
if (this.shouldUsePackagedShellSupervisor(options)) {
|
const env = supportsUserShell() ? getUserShellEnv() : { ...process.env }
|
||||||
const runtimePath = this.resolveShellNodeCommand()
|
env.ELECTRON_RUN_AS_NODE = "1"
|
||||||
const entryPath = this.resolveBundledProdEntry()
|
|
||||||
const supervisorPath = this.resolveCliSupervisorPath()
|
|
||||||
const shellEnv = supportsUserShell() ? getUserShellEnv() : { ...process.env }
|
|
||||||
const shellCommand = buildUserShellCommand(`exec ${this.buildExecutableCommand(runtimePath, [entryPath, ...args])}`)
|
|
||||||
const supervisorPayload = JSON.stringify({
|
|
||||||
command: shellCommand.command,
|
|
||||||
args: shellCommand.args,
|
|
||||||
cwd: process.cwd(),
|
|
||||||
})
|
|
||||||
|
|
||||||
console.info(
|
const spawnDetails = supportsUserShell()
|
||||||
`[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) via utility supervisor using node at ${runtimePath} (host=${host})`,
|
? buildUserShellCommand(`ELECTRON_RUN_AS_NODE=1 exec ${this.buildCommand(cliEntry, args)}`)
|
||||||
)
|
: this.buildDirectSpawn(cliEntry, args)
|
||||||
console.info(`[cli] utility supervisor: ${supervisorPath}`)
|
|
||||||
console.info(`[cli] shell command: ${shellCommand.command} ${shellCommand.args.join(" ")}`)
|
|
||||||
|
|
||||||
child = utilityProcess.fork(supervisorPath, [supervisorPayload], {
|
const detached = process.platform !== "win32"
|
||||||
env: shellEnv,
|
const child = spawn(spawnDetails.command, spawnDetails.args, {
|
||||||
stdio: "pipe",
|
cwd: process.cwd(),
|
||||||
serviceName: "CodeNomad CLI Supervisor",
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
})
|
env,
|
||||||
this.childLaunchMode = "utility"
|
shell: false,
|
||||||
} else {
|
detached,
|
||||||
const cliEntry = this.resolveCliEntry(options)
|
})
|
||||||
console.info(
|
|
||||||
`[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) using ${cliEntry.runner} at ${cliEntry.entry} (host=${host})`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const env = supportsUserShell() ? getUserShellEnv() : { ...process.env }
|
console.info(`[cli] spawn command: ${spawnDetails.command} ${spawnDetails.args.join(" ")}`)
|
||||||
env.ELECTRON_RUN_AS_NODE = "1"
|
if (!child.pid) {
|
||||||
|
|
||||||
const spawnDetails = supportsUserShell()
|
|
||||||
? buildUserShellCommand(`ELECTRON_RUN_AS_NODE=1 exec ${this.buildCommand(cliEntry, args)}`)
|
|
||||||
: this.buildDirectSpawn(cliEntry, args)
|
|
||||||
|
|
||||||
const detached = process.platform !== "win32"
|
|
||||||
child = spawn(spawnDetails.command, spawnDetails.args, {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
env,
|
|
||||||
shell: false,
|
|
||||||
detached,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.info(`[cli] spawn command: ${spawnDetails.command} ${spawnDetails.args.join(" ")}`)
|
|
||||||
this.childLaunchMode = "spawn"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.childLaunchMode === "spawn" && !child.pid) {
|
|
||||||
console.error("[cli] spawn failed: no pid")
|
console.error("[cli] spawn failed: no pid")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,48 +179,23 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
this.handleStream(data.toString(), "stderr")
|
this.handleStream(data.toString(), "stderr")
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.childLaunchMode === "utility") {
|
child.on("error", (error) => {
|
||||||
const utilityChild = child as UtilityProcess
|
console.error("[cli] failed to start CLI:", error)
|
||||||
|
this.updateStatus({ state: "error", error: error.message })
|
||||||
|
this.emit("error", error)
|
||||||
|
})
|
||||||
|
|
||||||
utilityChild.on("error", (error) => {
|
child.on("exit", (code, signal) => {
|
||||||
const message = this.describeUtilityProcessError(error)
|
const failed = this.status.state !== "ready"
|
||||||
console.error("[cli] utility supervisor failed:", error)
|
const error = failed ? this.status.error ?? `CLI exited with code ${code ?? 0}${signal ? ` (${signal})` : ""}` : undefined
|
||||||
this.updateStatus({ state: "error", error: message })
|
console.info(`[cli] exit (code=${code}, signal=${signal || ""})${error ? ` error=${error}` : ""}`)
|
||||||
this.emit("error", new Error(message))
|
this.updateStatus({ state: failed ? "error" : "stopped", error })
|
||||||
})
|
if (failed && error) {
|
||||||
|
this.emit("error", new Error(error))
|
||||||
utilityChild.on("exit", (code) => {
|
}
|
||||||
const failed = this.status.state !== "ready"
|
this.emit("exit", this.status)
|
||||||
const error = failed ? this.status.error ?? `CLI exited with code ${code ?? 0}` : undefined
|
this.child = undefined
|
||||||
console.info(`[cli] exit (code=${code ?? ""})${error ? ` error=${error}` : ""}`)
|
})
|
||||||
this.updateStatus({ state: failed ? "error" : "stopped", error })
|
|
||||||
if (failed && error) {
|
|
||||||
this.emit("error", new Error(error))
|
|
||||||
}
|
|
||||||
this.emit("exit", this.status)
|
|
||||||
this.child = undefined
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const spawnedChild = child as ChildProcess
|
|
||||||
|
|
||||||
spawnedChild.on("error", (error) => {
|
|
||||||
console.error("[cli] failed to start CLI:", error)
|
|
||||||
this.updateStatus({ state: "error", error: error.message })
|
|
||||||
this.emit("error", error)
|
|
||||||
})
|
|
||||||
|
|
||||||
spawnedChild.on("exit", (code, signal) => {
|
|
||||||
const failed = this.status.state !== "ready"
|
|
||||||
const error = failed ? this.status.error ?? `CLI exited with code ${code ?? 0}${signal ? ` (${signal})` : ""}` : undefined
|
|
||||||
console.info(`[cli] exit (code=${code}, signal=${signal || ""})${error ? ` error=${error}` : ""}`)
|
|
||||||
this.updateStatus({ state: failed ? "error" : "stopped", error })
|
|
||||||
if (failed && error) {
|
|
||||||
this.emit("error", new Error(error))
|
|
||||||
}
|
|
||||||
this.emit("exit", this.status)
|
|
||||||
this.child = undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<CliStatus>((resolve, reject) => {
|
return new Promise<CliStatus>((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
@@ -281,22 +222,16 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.childLaunchMode === "utility") {
|
|
||||||
return this.stopUtilityChild(child as UtilityProcess)
|
|
||||||
}
|
|
||||||
|
|
||||||
const spawnedChild = child as ChildProcess
|
|
||||||
|
|
||||||
this.requestedStop = true
|
this.requestedStop = true
|
||||||
|
|
||||||
const pid = spawnedChild.pid
|
const pid = child.pid
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
this.child = undefined
|
this.child = undefined
|
||||||
this.updateStatus({ state: "stopped" })
|
this.updateStatus({ state: "stopped" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAlreadyExited = () => spawnedChild.exitCode !== null || spawnedChild.signalCode !== null
|
const isAlreadyExited = () => child.exitCode !== null || child.signalCode !== null
|
||||||
|
|
||||||
const tryKillPosixGroup = (signal: NodeJS.Signals) => {
|
const tryKillPosixGroup = (signal: NodeJS.Signals) => {
|
||||||
try {
|
try {
|
||||||
@@ -372,7 +307,7 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
sendStopSignal("SIGKILL")
|
sendStopSignal("SIGKILL")
|
||||||
}, 30000)
|
}, 30000)
|
||||||
|
|
||||||
spawnedChild.on("exit", () => {
|
child.on("exit", () => {
|
||||||
clearTimeout(killTimeout)
|
clearTimeout(killTimeout)
|
||||||
this.child = undefined
|
this.child = undefined
|
||||||
console.info("[cli] CLI process exited")
|
console.info("[cli] CLI process exited")
|
||||||
@@ -392,50 +327,14 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopUtilityChild(child: UtilityProcess): Promise<void> {
|
|
||||||
this.requestedStop = true
|
|
||||||
|
|
||||||
const pid = child.pid
|
|
||||||
if (!pid) {
|
|
||||||
this.child = undefined
|
|
||||||
this.updateStatus({ state: "stopped" })
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const killTimeout = setTimeout(() => {
|
|
||||||
console.warn(`[cli] stop timed out after 30000ms; sending SIGKILL (pid=${pid})`)
|
|
||||||
try {
|
|
||||||
process.kill(pid, "SIGKILL")
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}, 30000)
|
|
||||||
|
|
||||||
child.once("exit", () => {
|
|
||||||
clearTimeout(killTimeout)
|
|
||||||
this.child = undefined
|
|
||||||
console.info("[cli] CLI process exited")
|
|
||||||
this.updateStatus({ state: "stopped" })
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (child.pid === undefined) {
|
|
||||||
clearTimeout(killTimeout)
|
|
||||||
this.child = undefined
|
|
||||||
this.updateStatus({ state: "stopped" })
|
|
||||||
resolve()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
child.kill()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatus(): CliStatus {
|
getStatus(): CliStatus {
|
||||||
return { ...this.status }
|
return { ...this.status }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAuthCookieName(): string {
|
||||||
|
return this.authCookieName
|
||||||
|
}
|
||||||
|
|
||||||
private resolveListeningMode(): ListeningMode {
|
private resolveListeningMode(): ListeningMode {
|
||||||
return readListeningModeFromConfig()
|
return readListeningModeFromConfig()
|
||||||
}
|
}
|
||||||
@@ -443,22 +342,14 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
private handleTimeout() {
|
private handleTimeout() {
|
||||||
if (this.child) {
|
if (this.child) {
|
||||||
const pid = this.child.pid
|
const pid = this.child.pid
|
||||||
if (this.childLaunchMode === "utility") {
|
if (pid && process.platform !== "win32") {
|
||||||
if (pid) {
|
|
||||||
try {
|
|
||||||
process.kill(pid, "SIGKILL")
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pid && process.platform !== "win32") {
|
|
||||||
try {
|
try {
|
||||||
process.kill(-pid, "SIGKILL")
|
process.kill(-pid, "SIGKILL")
|
||||||
} catch {
|
} catch {
|
||||||
;(this.child as ChildProcess).kill("SIGKILL")
|
this.child.kill("SIGKILL")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
;(this.child as ChildProcess).kill("SIGKILL")
|
this.child.kill("SIGKILL")
|
||||||
}
|
}
|
||||||
this.child = undefined
|
this.child = undefined
|
||||||
}
|
}
|
||||||
@@ -532,7 +423,7 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildCliArgs(options: StartOptions, host: string): string[] {
|
private buildCliArgs(options: StartOptions, host: string): string[] {
|
||||||
const args = ["serve", "--host", host, "--generate-token"]
|
const args = ["serve", "--host", host, "--generate-token", "--auth-cookie-name", this.authCookieName]
|
||||||
|
|
||||||
if (options.dev) {
|
if (options.dev) {
|
||||||
// Dev: run plain HTTP + Vite dev server proxy.
|
// Dev: run plain HTTP + Vite dev server proxy.
|
||||||
@@ -565,10 +456,6 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
return parts.join(" ")
|
return parts.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildExecutableCommand(command: string, args: string[]): string {
|
|
||||||
return [JSON.stringify(command), ...args.map((arg) => JSON.stringify(arg))].join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildDirectSpawn(cliEntry: CliEntryResolution, args: string[]) {
|
private buildDirectSpawn(cliEntry: CliEntryResolution, args: string[]) {
|
||||||
if (cliEntry.runner === "tsx") {
|
if (cliEntry.runner === "tsx") {
|
||||||
return { command: process.execPath, args: [cliEntry.runnerPath!, cliEntry.entry, ...args] }
|
return { command: process.execPath, args: [cliEntry.runnerPath!, cliEntry.entry, ...args] }
|
||||||
@@ -639,58 +526,4 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Run npm run build --workspace @neuralnomads/codenomad.")
|
throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Run npm run build --workspace @neuralnomads/codenomad.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldUsePackagedShellSupervisor(options: StartOptions): boolean {
|
|
||||||
return !options.dev && app.isPackaged && process.platform === "darwin"
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveCliSupervisorPath(): string {
|
|
||||||
const candidates = [
|
|
||||||
path.join(process.resourcesPath, "cli-supervisor.cjs"),
|
|
||||||
path.join(mainDirname, "../resources/cli-supervisor.cjs"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (existsSync(candidate)) {
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Unable to locate CodeNomad CLI supervisor script.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveShellNodeCommand(): string {
|
|
||||||
const configured = process.env.NODE_BINARY?.trim()
|
|
||||||
return configured && configured.length > 0 ? configured : "node"
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveBundledProdEntry(): string {
|
|
||||||
const candidates = [
|
|
||||||
path.join(process.resourcesPath, "server", "dist", "bin.js"),
|
|
||||||
path.join(mainDirname, "../resources/server/dist/bin.js"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (existsSync(candidate)) {
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Unable to locate bundled CodeNomad CLI build in app resources.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private describeUtilityProcessError(error: unknown): string {
|
|
||||||
if (error instanceof Error && error.message) {
|
|
||||||
return error.message
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error && typeof error === "object") {
|
|
||||||
const typed = error as { type?: unknown; location?: unknown }
|
|
||||||
if (typeof typed.type === "string") {
|
|
||||||
return typeof typed.location === "string" ? `${typed.type} at ${typed.location}` : typed.type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const electronAPI = {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestMicrophoneAccess: () => ipcRenderer.invoke("media:requestMicrophoneAccess"),
|
|
||||||
setWakeLock: (enabled) => ipcRenderer.invoke("power:setWakeLock", Boolean(enabled)),
|
setWakeLock: (enabled) => ipcRenderer.invoke("power:setWakeLock", Boolean(enabled)),
|
||||||
showNotification: (payload) => ipcRenderer.invoke("notifications:show", payload),
|
showNotification: (payload) => ipcRenderer.invoke("notifications:show", payload),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const { spawn } = require("child_process")
|
|
||||||
|
|
||||||
const SHUTDOWN_GRACE_MS = 30_000
|
|
||||||
|
|
||||||
let child = null
|
|
||||||
let shutdownTimer = null
|
|
||||||
|
|
||||||
function log(message, error) {
|
|
||||||
if (error) {
|
|
||||||
console.error(`[cli-supervisor] ${message}`, error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(`[cli-supervisor] ${message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearShutdownTimer() {
|
|
||||||
if (shutdownTimer) {
|
|
||||||
clearTimeout(shutdownTimer)
|
|
||||||
shutdownTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function forwardStream(stream, target) {
|
|
||||||
if (!stream) return
|
|
||||||
stream.on("data", (chunk) => {
|
|
||||||
target.write(chunk)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function terminateChild(force) {
|
|
||||||
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
child.kill(force ? "SIGKILL" : "SIGTERM")
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestShutdown(force = false) {
|
|
||||||
if (!child) {
|
|
||||||
process.exit(force ? 1 : 0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
terminateChild(force)
|
|
||||||
if (force) {
|
|
||||||
process.exit(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearShutdownTimer()
|
|
||||||
shutdownTimer = setTimeout(() => {
|
|
||||||
log(`shutdown timed out after ${SHUTDOWN_GRACE_MS}ms; forcing child termination`)
|
|
||||||
terminateChild(true)
|
|
||||||
}, SHUTDOWN_GRACE_MS)
|
|
||||||
shutdownTimer.unref()
|
|
||||||
}
|
|
||||||
|
|
||||||
function installShutdownHandlers() {
|
|
||||||
process.on("SIGTERM", () => requestShutdown(false))
|
|
||||||
process.on("SIGINT", () => requestShutdown(false))
|
|
||||||
process.on("disconnect", () => requestShutdown(false))
|
|
||||||
process.on("uncaughtException", (error) => {
|
|
||||||
log("uncaught exception", error)
|
|
||||||
requestShutdown(true)
|
|
||||||
})
|
|
||||||
process.on("unhandledRejection", (error) => {
|
|
||||||
log("unhandled rejection", error)
|
|
||||||
requestShutdown(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePayload() {
|
|
||||||
const raw = process.argv[2]
|
|
||||||
if (!raw) {
|
|
||||||
throw new Error("Supervisor payload is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = JSON.parse(raw)
|
|
||||||
if (!parsed || typeof parsed !== "object") {
|
|
||||||
throw new Error("Supervisor payload must be an object")
|
|
||||||
}
|
|
||||||
if (typeof parsed.command !== "string" || parsed.command.trim().length === 0) {
|
|
||||||
throw new Error("Supervisor payload command is required")
|
|
||||||
}
|
|
||||||
if (!Array.isArray(parsed.args) || !parsed.args.every((value) => typeof value === "string")) {
|
|
||||||
throw new Error("Supervisor payload args must be a string array")
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
command: parsed.command,
|
|
||||||
args: parsed.args,
|
|
||||||
cwd: typeof parsed.cwd === "string" && parsed.cwd.trim().length > 0 ? parsed.cwd : process.cwd(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
installShutdownHandlers()
|
|
||||||
|
|
||||||
const payload = parsePayload()
|
|
||||||
log(`launching shell command: ${payload.command} ${payload.args.join(" ")}`)
|
|
||||||
|
|
||||||
child = spawn(payload.command, payload.args, {
|
|
||||||
cwd: payload.cwd,
|
|
||||||
env: process.env,
|
|
||||||
shell: false,
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
})
|
|
||||||
|
|
||||||
forwardStream(child.stdout, process.stdout)
|
|
||||||
forwardStream(child.stderr, process.stderr)
|
|
||||||
|
|
||||||
child.on("error", (error) => {
|
|
||||||
log("failed to spawn shell command", error)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
child.on("exit", (code, signal) => {
|
|
||||||
clearShutdownTimer()
|
|
||||||
log(`child exited code=${code ?? ""} signal=${signal ?? ""}`)
|
|
||||||
process.exitCode = typeof code === "number" ? code : signal ? 1 : 0
|
|
||||||
process.exit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.disable-library-validation</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.audio-input</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -20,8 +20,6 @@
|
|||||||
"dev:debug": "cross-env CLI_LOG_LEVEL=debug electron-vite dev",
|
"dev:debug": "cross-env CLI_LOG_LEVEL=debug electron-vite dev",
|
||||||
"dev:trace": "cross-env CLI_LOG_LEVEL=trace electron-vite dev",
|
"dev:trace": "cross-env CLI_LOG_LEVEL=trace electron-vite dev",
|
||||||
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
|
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
|
||||||
"prepare:resources": "node scripts/prepare-resources.js",
|
|
||||||
"prebuild": "npm run prepare:resources",
|
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
"preview": "electron-vite preview",
|
"preview": "electron-vite preview",
|
||||||
@@ -35,11 +33,8 @@
|
|||||||
"build:linux-arm64": "node scripts/build.js linux-arm64",
|
"build:linux-arm64": "node scripts/build.js linux-arm64",
|
||||||
"build:linux-rpm": "node scripts/build.js linux-rpm",
|
"build:linux-rpm": "node scripts/build.js linux-rpm",
|
||||||
"build:all": "node scripts/build.js all",
|
"build:all": "node scripts/build.js all",
|
||||||
"prepackage:mac": "npm run prepare:resources",
|
|
||||||
"package:mac": "electron-builder --mac",
|
"package:mac": "electron-builder --mac",
|
||||||
"prepackage:win": "npm run prepare:resources",
|
|
||||||
"package:win": "electron-builder --win",
|
"package:win": "electron-builder --win",
|
||||||
"prepackage:linux": "npm run prepare:resources",
|
|
||||||
"package:linux": "electron-builder --linux"
|
"package:linux": "electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -87,12 +82,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
"entitlements": "electron/resources/entitlements.mac.plist",
|
|
||||||
"entitlementsInherit": "electron/resources/entitlements.mac.plist",
|
|
||||||
"extendInfo": {
|
|
||||||
"NSMicrophoneUsageDescription": "CodeNomad needs microphone access for speech-to-text prompt input.",
|
|
||||||
"NSLocalNetworkUsageDescription": "CodeNomad needs local network access to connect to locally hosted AI and speech services."
|
|
||||||
},
|
|
||||||
"category": "public.app-category.developer-tools",
|
"category": "public.app-category.developer-tools",
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,12 +111,6 @@ async function build(platform) {
|
|||||||
env: { NODE_PATH: workspaceNodeModulesPath },
|
env: { NODE_PATH: workspaceNodeModulesPath },
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("\n📦 Step 1.5/3: Preparing packaged server resources...\n")
|
|
||||||
await run(process.execPath, [join(appDir, "scripts", "prepare-resources.js")], {
|
|
||||||
cwd: workspaceRoot,
|
|
||||||
env: { NODE_PATH: workspaceNodeModulesPath },
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("\n📦 Step 2/3: Building Electron app...\n")
|
console.log("\n📦 Step 2/3: Building Electron app...\n")
|
||||||
await run(npmCmd, ["run", "build"])
|
await run(npmCmd, ["run", "build"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import fs from "fs"
|
|
||||||
import path, { join } from "path"
|
|
||||||
import { spawnSync } from "child_process"
|
|
||||||
import { fileURLToPath } from "url"
|
|
||||||
|
|
||||||
const __dirname = fileURLToPath(new URL(".", import.meta.url))
|
|
||||||
const appDir = join(__dirname, "..")
|
|
||||||
const workspaceRoot = join(appDir, "..", "..")
|
|
||||||
const serverRoot = join(appDir, "..", "server")
|
|
||||||
const resourcesRoot = join(appDir, "electron", "resources")
|
|
||||||
const serverDest = join(resourcesRoot, "server")
|
|
||||||
const npmExecPath = process.env.npm_execpath
|
|
||||||
const npmNodeExecPath = process.env.npm_node_execpath
|
|
||||||
|
|
||||||
const serverSources = ["dist", "public", "node_modules", "package.json"]
|
|
||||||
const serverDepsMarker = join(serverRoot, "node_modules", "fastify", "package.json")
|
|
||||||
|
|
||||||
function log(message) {
|
|
||||||
console.log(`[prepare-resources] ${message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureServerBuild() {
|
|
||||||
const distPath = join(serverRoot, "dist")
|
|
||||||
const publicPath = join(serverRoot, "public")
|
|
||||||
if (!fs.existsSync(distPath) || !fs.existsSync(publicPath)) {
|
|
||||||
throw new Error("Server build artifacts are missing. Run the server build before packaging Electron.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureServerDependencies() {
|
|
||||||
if (fs.existsSync(serverDepsMarker)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log("installing production server dependencies")
|
|
||||||
const npmArgs = [
|
|
||||||
"install",
|
|
||||||
"--omit=dev",
|
|
||||||
"--ignore-scripts",
|
|
||||||
"--workspaces=false",
|
|
||||||
"--package-lock=false",
|
|
||||||
"--install-strategy=shallow",
|
|
||||||
"--fund=false",
|
|
||||||
"--audit=false",
|
|
||||||
]
|
|
||||||
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
PATH: `${join(workspaceRoot, "node_modules", ".bin")}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
||||||
npm_config_workspaces: "false",
|
|
||||||
}
|
|
||||||
|
|
||||||
const npmCli = npmExecPath && npmNodeExecPath ? [npmNodeExecPath, [npmExecPath, ...npmArgs]] : null
|
|
||||||
const result = npmCli
|
|
||||||
? spawnSync(npmCli[0], npmCli[1], { cwd: serverRoot, stdio: "inherit", env })
|
|
||||||
: spawnSync("npm", npmArgs, { cwd: serverRoot, stdio: "inherit", env, shell: process.platform === "win32" })
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
if (result.error) {
|
|
||||||
throw result.error
|
|
||||||
}
|
|
||||||
throw new Error(`npm install exited with code ${result.status ?? 1}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyServerArtifacts() {
|
|
||||||
fs.rmSync(serverDest, { recursive: true, force: true })
|
|
||||||
fs.mkdirSync(serverDest, { recursive: true })
|
|
||||||
|
|
||||||
for (const name of serverSources) {
|
|
||||||
const from = join(serverRoot, name)
|
|
||||||
const to = join(serverDest, name)
|
|
||||||
if (!fs.existsSync(from)) {
|
|
||||||
throw new Error(`Missing required server artifact: ${from}`)
|
|
||||||
}
|
|
||||||
fs.cpSync(from, to, { recursive: true, dereference: true })
|
|
||||||
log(`copied ${name} to Electron resources`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripNodeModuleBins() {
|
|
||||||
const root = join(serverDest, "node_modules")
|
|
||||||
if (!fs.existsSync(root)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const stack = [root]
|
|
||||||
let removed = 0
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const current = stack.pop()
|
|
||||||
if (!current) break
|
|
||||||
|
|
||||||
let entries
|
|
||||||
try {
|
|
||||||
entries = fs.readdirSync(current, { withFileTypes: true })
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const full = join(current, entry.name)
|
|
||||||
if (entry.name === ".bin") {
|
|
||||||
fs.rmSync(full, { recursive: true, force: true })
|
|
||||||
removed += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
stack.push(full)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
log(`removed ${removed} node_modules/.bin directories`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
ensureServerBuild()
|
|
||||||
ensureServerDependencies()
|
|
||||||
copyServerArtifacts()
|
|
||||||
stripNodeModuleBins()
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("[prepare-resources] failed:", error)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@@ -14,5 +14,5 @@
|
|||||||
"noEmit": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["electron/**/*.ts", "electron.vite.config.ts"],
|
"include": ["electron/**/*.ts", "electron.vite.config.ts"],
|
||||||
"exclude": ["node_modules", "dist", "electron/resources/server"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.3.7"
|
"@opencode-ai/plugin": "1.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,6 @@ import type { PluginInput } from "@opencode-ai/plugin"
|
|||||||
import { createCodeNomadClient, getCodeNomadConfig } from "./lib/client"
|
import { createCodeNomadClient, getCodeNomadConfig } from "./lib/client"
|
||||||
import { createBackgroundProcessTools } from "./lib/background-process"
|
import { createBackgroundProcessTools } from "./lib/background-process"
|
||||||
|
|
||||||
let voiceModeEnabled = false
|
|
||||||
|
|
||||||
export async function CodeNomadPlugin(input: PluginInput) {
|
export async function CodeNomadPlugin(input: PluginInput) {
|
||||||
const config = getCodeNomadConfig()
|
const config = getCodeNomadConfig()
|
||||||
const client = createCodeNomadClient(config)
|
const client = createCodeNomadClient(config)
|
||||||
@@ -18,11 +16,6 @@ export async function CodeNomadPlugin(input: PluginInput) {
|
|||||||
pingTs: (event.properties as any)?.ts,
|
pingTs: (event.properties as any)?.ts,
|
||||||
},
|
},
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "codenomad.voiceMode") {
|
|
||||||
voiceModeEnabled = Boolean((event.properties as { enabled?: unknown } | undefined)?.enabled)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -30,13 +23,6 @@ export async function CodeNomadPlugin(input: PluginInput) {
|
|||||||
tool: {
|
tool: {
|
||||||
...backgroundProcessTools,
|
...backgroundProcessTools,
|
||||||
},
|
},
|
||||||
async "chat.message"(_input: { sessionID: string }, output: { message: { system?: string } }) {
|
|
||||||
if (!voiceModeEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
output.message.system = [output.message.system, buildVoiceModePrompt()].filter(Boolean).join("\n\n")
|
|
||||||
},
|
|
||||||
async event(input: { event: any }) {
|
async event(input: { event: any }) {
|
||||||
const opencodeEvent = input?.event
|
const opencodeEvent = input?.event
|
||||||
if (!opencodeEvent || typeof opencodeEvent !== "object") return
|
if (!opencodeEvent || typeof opencodeEvent !== "object") return
|
||||||
@@ -44,19 +30,3 @@ export async function CodeNomadPlugin(input: PluginInput) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVoiceModePrompt(): string {
|
|
||||||
return [
|
|
||||||
"Voice conversation mode is enabled.",
|
|
||||||
"Prepend your reply with a fenced code block using language `spoken`.",
|
|
||||||
"The `spoken` block should be the natural conversational reply you would say out loud to the user. It should be a concise spoken gist of the full response in 2 to 4 natural sentences.",
|
|
||||||
"In the spoken block, summarize the main outcome, recommendation, or next step. Sound conversational and natural, not like a document summary.",
|
|
||||||
"Do not include code, bullet lists, markdown formatting, or long technical detail in the spoken block.",
|
|
||||||
"Do not add generic phrases about whether the user should read more.",
|
|
||||||
"Only mention additional written detail when there is something specific that may matter for the user's next response, such as a tradeoff, caveat, risk, open question, exact diff, or test result.",
|
|
||||||
"When referring to that written detail, say `below` or `in the message` rather than `detailed section`.",
|
|
||||||
"After the `spoken` block, continue with your normal detailed response.",
|
|
||||||
"Example:",
|
|
||||||
"```spoken\nI implemented the relay-based voice-mode flow and it works with the current plugin bridge. The reconnect caveat is explained below.\n```",
|
|
||||||
].join("\n\n")
|
|
||||||
}
|
|
||||||
|
|||||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -240,10 +240,6 @@ export interface SpeechSynthesisResponse {
|
|||||||
mimeType: string
|
mimeType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VoiceModeStateResponse {
|
|
||||||
enabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorkspaceEventType =
|
export type WorkspaceEventType =
|
||||||
| "workspace.created"
|
| "workspace.created"
|
||||||
| "workspace.started"
|
| "workspace.started"
|
||||||
|
|||||||
@@ -16,16 +16,18 @@ export interface AuthManagerInit {
|
|||||||
password?: string
|
password?: string
|
||||||
generateToken: boolean
|
generateToken: boolean
|
||||||
dangerouslySkipAuth?: boolean
|
dangerouslySkipAuth?: boolean
|
||||||
|
cookieName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
private readonly authStore: AuthStore | null
|
private readonly authStore: AuthStore | null
|
||||||
private readonly tokenManager: TokenManager | null
|
private readonly tokenManager: TokenManager | null
|
||||||
private readonly sessionManager = new SessionManager()
|
private readonly sessionManager = new SessionManager()
|
||||||
private readonly cookieName = DEFAULT_AUTH_COOKIE_NAME
|
private readonly cookieName: string
|
||||||
private readonly authEnabled: boolean
|
private readonly authEnabled: boolean
|
||||||
|
|
||||||
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
||||||
|
this.cookieName = sanitizeCookieName(init.cookieName)
|
||||||
this.authEnabled = !Boolean(init.dangerouslySkipAuth)
|
this.authEnabled = !Boolean(init.dangerouslySkipAuth)
|
||||||
|
|
||||||
if (!this.authEnabled) {
|
if (!this.authEnabled) {
|
||||||
@@ -139,6 +141,16 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeCookieName(value: string | undefined): string {
|
||||||
|
const trimmed = value?.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
return DEFAULT_AUTH_COOKIE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = trimmed.replace(/[^A-Za-z0-9_-]/g, "_")
|
||||||
|
return sanitized.length > 0 ? sanitized : DEFAULT_AUTH_COOKIE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAuthFilePath(configPath: string) {
|
function resolveAuthFilePath(configPath: string) {
|
||||||
const resolvedConfigPath = resolvePath(configPath)
|
const resolvedConfigPath = resolvePath(configPath)
|
||||||
return path.join(path.dirname(resolvedConfigPath), "auth.json")
|
return path.join(path.dirname(resolvedConfigPath), "auth.json")
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
import type { Logger } from "../logger"
|
|
||||||
|
|
||||||
const STALE_CONNECTION_TIMEOUT_MS = 45000
|
|
||||||
const STALE_SWEEP_INTERVAL_MS = 5000
|
|
||||||
|
|
||||||
export interface ClientConnectionRef {
|
|
||||||
clientId: string
|
|
||||||
connectionId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientConnectionRecord extends ClientConnectionRef {
|
|
||||||
key: string
|
|
||||||
connectedAt: number
|
|
||||||
lastSeenAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionChangeEvent = {
|
|
||||||
type: "connected" | "disconnected"
|
|
||||||
connection: ClientConnectionRecord
|
|
||||||
reason?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegisteredConnection extends ClientConnectionRecord {
|
|
||||||
close: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClientConnectionManager {
|
|
||||||
private readonly connections = new Map<string, RegisteredConnection>()
|
|
||||||
private readonly subscribers = new Set<(event: ConnectionChangeEvent) => void>()
|
|
||||||
private readonly sweepTimer: NodeJS.Timeout
|
|
||||||
|
|
||||||
constructor(private readonly logger: Logger) {
|
|
||||||
this.sweepTimer = setInterval(() => this.sweepStaleConnections(), STALE_SWEEP_INTERVAL_MS)
|
|
||||||
this.sweepTimer.unref?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown(): void {
|
|
||||||
clearInterval(this.sweepTimer)
|
|
||||||
for (const connection of Array.from(this.connections.values())) {
|
|
||||||
this.disconnect(connection.key, "shutdown", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(listener: (event: ConnectionChangeEvent) => void): () => void {
|
|
||||||
this.subscribers.add(listener)
|
|
||||||
return () => this.subscribers.delete(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
register(input: ClientConnectionRef & { close: () => void }): () => void {
|
|
||||||
const key = getConnectionKey(input)
|
|
||||||
const now = Date.now()
|
|
||||||
const existing = this.connections.get(key)
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
this.logger.debug({ clientId: input.clientId, connectionId: input.connectionId }, "Replacing existing client connection")
|
|
||||||
this.disconnect(key, "replaced")
|
|
||||||
}
|
|
||||||
|
|
||||||
const connection: RegisteredConnection = {
|
|
||||||
key,
|
|
||||||
clientId: input.clientId,
|
|
||||||
connectionId: input.connectionId,
|
|
||||||
connectedAt: now,
|
|
||||||
lastSeenAt: now,
|
|
||||||
close: input.close,
|
|
||||||
}
|
|
||||||
this.connections.set(key, connection)
|
|
||||||
this.logger.debug({ clientId: input.clientId, connectionId: input.connectionId }, "Client connected")
|
|
||||||
this.notify({ type: "connected", connection })
|
|
||||||
return () => this.disconnect(key, "closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
pong(input: ClientConnectionRef): boolean {
|
|
||||||
const key = getConnectionKey(input)
|
|
||||||
const connection = this.connections.get(key)
|
|
||||||
if (!connection) {
|
|
||||||
this.logger.debug({ clientId: input.clientId, connectionId: input.connectionId }, "Ignoring pong for unknown client connection")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.lastSeenAt = Date.now()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected(input: ClientConnectionRef): boolean {
|
|
||||||
return this.connections.has(getConnectionKey(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
private sweepStaleConnections(): void {
|
|
||||||
const cutoff = Date.now() - STALE_CONNECTION_TIMEOUT_MS
|
|
||||||
for (const connection of Array.from(this.connections.values())) {
|
|
||||||
if (connection.lastSeenAt > cutoff) continue
|
|
||||||
this.logger.debug({ clientId: connection.clientId, connectionId: connection.connectionId }, "Client connection timed out")
|
|
||||||
this.disconnect(connection.key, "timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private disconnect(key: string, reason: string, invokeClose = true): void {
|
|
||||||
const connection = this.connections.get(key)
|
|
||||||
if (!connection) return
|
|
||||||
this.connections.delete(key)
|
|
||||||
this.logger.debug({ clientId: connection.clientId, connectionId: connection.connectionId, reason }, "Client disconnected")
|
|
||||||
|
|
||||||
if (invokeClose) {
|
|
||||||
try {
|
|
||||||
connection.close()
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn({ err: error, clientId: connection.clientId, connectionId: connection.connectionId }, "Failed to close stale client connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notify({ type: "disconnected", connection, reason })
|
|
||||||
}
|
|
||||||
|
|
||||||
private notify(event: ConnectionChangeEvent): void {
|
|
||||||
for (const subscriber of this.subscribers) {
|
|
||||||
try {
|
|
||||||
subscriber(event)
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn({ err: error, eventType: event.type }, "Client connection subscriber failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConnectionKey(input: ClientConnectionRef): string {
|
|
||||||
return `${input.clientId}:${input.connectionId}`
|
|
||||||
}
|
|
||||||
@@ -81,14 +81,6 @@ export class FileSystemBrowser {
|
|||||||
return { path: relativePath, absolutePath }
|
return { path: relativePath, absolutePath }
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(relativePath: string, contents: string): void {
|
|
||||||
if (this.unrestricted) {
|
|
||||||
throw new Error("writeFile is not available in unrestricted mode")
|
|
||||||
}
|
|
||||||
const resolved = this.toRestrictedAbsolute(relativePath)
|
|
||||||
fs.writeFileSync(resolved, contents, "utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
readFile(relativePath: string): string {
|
readFile(relativePath: string): string {
|
||||||
if (this.unrestricted) {
|
if (this.unrestricted) {
|
||||||
throw new Error("readFile is not available in unrestricted mode")
|
throw new Error("readFile is not available in unrestricted mode")
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import { InstanceEventBridge } from "./workspaces/instance-events"
|
|||||||
import { createLogger } from "./logger"
|
import { createLogger } from "./logger"
|
||||||
import { launchInBrowser } from "./launcher"
|
import { launchInBrowser } from "./launcher"
|
||||||
import { resolveUi } from "./ui/remote-ui"
|
import { resolveUi } from "./ui/remote-ui"
|
||||||
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_COOKIE_NAME, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
||||||
import { resolveHttpsOptions } from "./server/tls"
|
import { resolveHttpsOptions } from "./server/tls"
|
||||||
import { resolveNetworkAddresses } from "./server/network-addresses"
|
import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/network-addresses"
|
||||||
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
|
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
|
||||||
import { SpeechService } from "./speech/service"
|
import { SpeechService } from "./speech/service"
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ interface CliOptions {
|
|||||||
launch: boolean
|
launch: boolean
|
||||||
authUsername: string
|
authUsername: string
|
||||||
authPassword?: string
|
authPassword?: string
|
||||||
|
authCookieName: string
|
||||||
generateToken: boolean
|
generateToken: boolean
|
||||||
dangerouslySkipAuth: boolean
|
dangerouslySkipAuth: boolean
|
||||||
}
|
}
|
||||||
@@ -100,6 +101,11 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
.default(DEFAULT_AUTH_USERNAME),
|
.default(DEFAULT_AUTH_USERNAME),
|
||||||
)
|
)
|
||||||
.addOption(new Option("--password <password>", "Password for server authentication").env("CODENOMAD_SERVER_PASSWORD"))
|
.addOption(new Option("--password <password>", "Password for server authentication").env("CODENOMAD_SERVER_PASSWORD"))
|
||||||
|
.addOption(
|
||||||
|
new Option("--auth-cookie-name <name>", "Cookie name for server authentication")
|
||||||
|
.env("CODENOMAD_AUTH_COOKIE_NAME")
|
||||||
|
.default(DEFAULT_AUTH_COOKIE_NAME),
|
||||||
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("--generate-token", "Emit a one-time bootstrap token for desktop")
|
new Option("--generate-token", "Emit a one-time bootstrap token for desktop")
|
||||||
.env("CODENOMAD_GENERATE_TOKEN")
|
.env("CODENOMAD_GENERATE_TOKEN")
|
||||||
@@ -139,6 +145,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
launch?: boolean
|
launch?: boolean
|
||||||
username: string
|
username: string
|
||||||
password?: string
|
password?: string
|
||||||
|
authCookieName: string
|
||||||
generateToken?: boolean
|
generateToken?: boolean
|
||||||
dangerouslySkipAuth?: boolean
|
dangerouslySkipAuth?: boolean
|
||||||
}>()
|
}>()
|
||||||
@@ -185,6 +192,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
launch: Boolean(parsed.launch),
|
launch: Boolean(parsed.launch),
|
||||||
authUsername: parsed.username,
|
authUsername: parsed.username,
|
||||||
authPassword: parsed.password,
|
authPassword: parsed.password,
|
||||||
|
authCookieName: parsed.authCookieName,
|
||||||
generateToken: Boolean(parsed.generateToken),
|
generateToken: Boolean(parsed.generateToken),
|
||||||
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
||||||
}
|
}
|
||||||
@@ -266,6 +274,7 @@ async function main() {
|
|||||||
configPath: configLocation.configYamlPath,
|
configPath: configLocation.configYamlPath,
|
||||||
username: options.authUsername,
|
username: options.authUsername,
|
||||||
password: options.authPassword,
|
password: options.authPassword,
|
||||||
|
cookieName: options.authCookieName,
|
||||||
generateToken: options.generateToken,
|
generateToken: options.generateToken,
|
||||||
dangerouslySkipAuth: options.dangerouslySkipAuth,
|
dangerouslySkipAuth: options.dangerouslySkipAuth,
|
||||||
},
|
},
|
||||||
@@ -442,18 +451,22 @@ async function main() {
|
|||||||
// which can lead clients to talk to the wrong process.
|
// which can lead clients to talk to the wrong process.
|
||||||
const localUrl = `${localProtocol}://127.0.0.1:${localStart.port}`
|
const localUrl = `${localProtocol}://127.0.0.1:${localStart.port}`
|
||||||
let remoteUrl: string | undefined
|
let remoteUrl: string | undefined
|
||||||
|
let remoteAddresses = [] as ReturnType<typeof resolveNetworkAddresses>
|
||||||
if (remoteStart) {
|
if (remoteStart) {
|
||||||
const wantsAll = options.host === "0.0.0.0" || !isLoopbackHost(options.host)
|
const wantsAll = options.host === "0.0.0.0" || !isLoopbackHost(options.host)
|
||||||
let remoteHost = options.host
|
let remoteHost = options.host
|
||||||
if (wantsAll) {
|
if (wantsAll) {
|
||||||
if (options.host === "0.0.0.0") {
|
if (options.host === "0.0.0.0") {
|
||||||
const candidates = resolveNetworkAddresses({ host: options.host, protocol: remoteProtocol, port: remoteStart.port })
|
const resolved = resolveRemoteAddresses({ host: options.host, protocol: remoteProtocol, port: remoteStart.port })
|
||||||
remoteHost = candidates.find((addr) => addr.scope === "external")?.ip ?? "localhost"
|
remoteAddresses = resolved.userVisible
|
||||||
|
remoteUrl = resolved.primaryRemoteUrl ?? `${remoteProtocol}://localhost:${remoteStart.port}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
remoteHost = "localhost"
|
remoteHost = "localhost"
|
||||||
}
|
}
|
||||||
remoteUrl = `${remoteProtocol}://${remoteHost}:${remoteStart.port}`
|
if (!remoteUrl) {
|
||||||
|
remoteUrl = `${remoteProtocol}://${remoteHost}:${remoteStart.port}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverMeta.localUrl = localUrl
|
serverMeta.localUrl = localUrl
|
||||||
@@ -464,7 +477,9 @@ async function main() {
|
|||||||
serverMeta.listeningMode = options.host === "0.0.0.0" || !isLoopbackHost(options.host) ? "all" : "local"
|
serverMeta.listeningMode = options.host === "0.0.0.0" || !isLoopbackHost(options.host) ? "all" : "local"
|
||||||
|
|
||||||
if (serverMeta.remotePort && remoteUrl) {
|
if (serverMeta.remotePort && remoteUrl) {
|
||||||
serverMeta.addresses = resolveNetworkAddresses({ host: options.host, protocol: remoteProtocol, port: serverMeta.remotePort })
|
serverMeta.addresses = remoteAddresses.length
|
||||||
|
? remoteAddresses
|
||||||
|
: resolveNetworkAddresses({ host: options.host, protocol: remoteProtocol, port: serverMeta.remotePort })
|
||||||
} else {
|
} else {
|
||||||
serverMeta.addresses = []
|
serverMeta.addresses = []
|
||||||
}
|
}
|
||||||
@@ -472,6 +487,16 @@ async function main() {
|
|||||||
console.log(`Local Connection URL : ${serverMeta.localUrl}`)
|
console.log(`Local Connection URL : ${serverMeta.localUrl}`)
|
||||||
if (serverMeta.remoteUrl) {
|
if (serverMeta.remoteUrl) {
|
||||||
console.log(`Remote Connection URL : ${serverMeta.remoteUrl}`)
|
console.log(`Remote Connection URL : ${serverMeta.remoteUrl}`)
|
||||||
|
const additionalRemoteUrls = serverMeta.addresses
|
||||||
|
.map((addr) => addr.remoteUrl)
|
||||||
|
.filter((url) => url !== serverMeta.remoteUrl)
|
||||||
|
|
||||||
|
if (additionalRemoteUrls.length > 0) {
|
||||||
|
console.log("Other Accessible URLs:")
|
||||||
|
for (const url of additionalRemoteUrls) {
|
||||||
|
console.log(` - ${url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.launch) {
|
if (options.launch) {
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import type { Logger } from "../logger"
|
|
||||||
import type { ClientConnectionManager, ClientConnectionRef } from "../clients/connection-manager"
|
|
||||||
import type { PluginChannelManager } from "./channel"
|
|
||||||
|
|
||||||
interface VoiceModeManagerOptions {
|
|
||||||
connections: ClientConnectionManager
|
|
||||||
channel: PluginChannelManager
|
|
||||||
logger: Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VoiceModeManager {
|
|
||||||
private readonly enabledConnectionsByInstance = new Map<string, Set<string>>()
|
|
||||||
private readonly aggregateByInstance = new Map<string, boolean>()
|
|
||||||
|
|
||||||
constructor(private readonly options: VoiceModeManagerOptions) {
|
|
||||||
this.options.connections.subscribe((event) => {
|
|
||||||
if (event.type !== "disconnected") return
|
|
||||||
this.clearConnection(event.connection)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setEnabled(instanceId: string, connection: ClientConnectionRef, enabled: boolean): void {
|
|
||||||
if (enabled && !this.options.connections.isConnected(connection)) {
|
|
||||||
this.options.logger.debug(
|
|
||||||
{ instanceId, clientId: connection.clientId, connectionId: connection.connectionId },
|
|
||||||
"Ignoring voice mode enable for disconnected client connection",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = getConnectionKey(connection)
|
|
||||||
const current = this.enabledConnectionsByInstance.get(instanceId) ?? new Set<string>()
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
current.add(key)
|
|
||||||
this.enabledConnectionsByInstance.set(instanceId, current)
|
|
||||||
} else if (current.delete(key)) {
|
|
||||||
if (current.size === 0) {
|
|
||||||
this.enabledConnectionsByInstance.delete(instanceId)
|
|
||||||
} else {
|
|
||||||
this.enabledConnectionsByInstance.set(instanceId, current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options.logger.debug({ instanceId, clientId: connection.clientId, connectionId: connection.connectionId, enabled }, "Voice mode updated for client connection")
|
|
||||||
this.publishIfChanged(instanceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
syncInstance(instanceId: string): void {
|
|
||||||
this.options.channel.send(instanceId, buildVoiceModeEvent(this.isEnabled(instanceId)))
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled(instanceId: string): boolean {
|
|
||||||
return this.aggregateByInstance.get(instanceId) === true
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearConnection(connection: ClientConnectionRef): void {
|
|
||||||
const key = getConnectionKey(connection)
|
|
||||||
for (const [instanceId, enabledConnections] of Array.from(this.enabledConnectionsByInstance.entries())) {
|
|
||||||
if (!enabledConnections.delete(key)) continue
|
|
||||||
if (enabledConnections.size === 0) {
|
|
||||||
this.enabledConnectionsByInstance.delete(instanceId)
|
|
||||||
}
|
|
||||||
this.publishIfChanged(instanceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private publishIfChanged(instanceId: string): void {
|
|
||||||
const enabled = (this.enabledConnectionsByInstance.get(instanceId)?.size ?? 0) > 0
|
|
||||||
const previous = this.aggregateByInstance.get(instanceId) === true
|
|
||||||
if (enabled === previous) return
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
this.aggregateByInstance.set(instanceId, true)
|
|
||||||
} else {
|
|
||||||
this.aggregateByInstance.delete(instanceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options.logger.debug({ instanceId, enabled }, "Broadcasting aggregate voice mode")
|
|
||||||
this.options.channel.send(instanceId, buildVoiceModeEvent(enabled))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildVoiceModeEvent(enabled: boolean) {
|
|
||||||
return {
|
|
||||||
type: "codenomad.voiceMode",
|
|
||||||
properties: {
|
|
||||||
enabled,
|
|
||||||
formatVersion: "v1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConnectionKey(connection: ClientConnectionRef): string {
|
|
||||||
return `${connection.clientId}:${connection.connectionId}`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import os from "node:os"
|
||||||
|
import { describe, it } from "node:test"
|
||||||
|
|
||||||
|
import { resolveNetworkAddresses, resolveRemoteAddresses } from "../network-addresses"
|
||||||
|
|
||||||
|
describe("resolveNetworkAddresses", () => {
|
||||||
|
it("preserves interface order among external addresses", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "172.24.0.1", family: "IPv4", internal: false },
|
||||||
|
{ address: "192.168.1.128", family: "IPv4", internal: false },
|
||||||
|
{ address: "10.0.0.8", family: 4, internal: false },
|
||||||
|
{ address: "127.0.0.1", family: "IPv4", internal: true },
|
||||||
|
{ address: "169.254.10.20", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveNetworkAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
result.map((entry) => entry.ip),
|
||||||
|
["172.24.0.1", "192.168.1.128", "10.0.0.8", "169.254.10.20", "127.0.0.1"],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolveRemoteAddresses", () => {
|
||||||
|
it("keeps all external addresses user-visible while preferring non-link-local addresses for the primary URL", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "169.254.10.20", family: "IPv4", internal: false },
|
||||||
|
{ address: "192.168.1.128", family: "IPv4", internal: false },
|
||||||
|
{ address: "172.24.0.1", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveRemoteAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
result.userVisible.map((entry) => entry.ip),
|
||||||
|
["192.168.1.128", "172.24.0.1", "169.254.10.20"],
|
||||||
|
)
|
||||||
|
assert.equal(result.primaryRemoteUrl, "https://192.168.1.128:9898")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("prefers private LAN addresses over public addresses", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "203.0.113.40", family: "IPv4", internal: false },
|
||||||
|
{ address: "192.168.1.128", family: "IPv4", internal: false },
|
||||||
|
{ address: "8.8.8.8", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveRemoteAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
result.userVisible.map((entry) => entry.ip),
|
||||||
|
["192.168.1.128", "203.0.113.40", "8.8.8.8"],
|
||||||
|
)
|
||||||
|
assert.equal(result.primaryRemoteUrl, "https://192.168.1.128:9898")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses a public address when no private LAN address is available", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "169.254.10.20", family: "IPv4", internal: false },
|
||||||
|
{ address: "203.0.113.40", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveRemoteAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(result.userVisible.map((entry) => entry.ip), ["203.0.113.40", "169.254.10.20"])
|
||||||
|
assert.equal(result.primaryRemoteUrl, "https://203.0.113.40:9898")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function usingMockedNetworkInterfaces(
|
||||||
|
addresses: Array<{ address: string; family: string | number; internal: boolean }>,
|
||||||
|
callback: () => void,
|
||||||
|
) {
|
||||||
|
const original = os.networkInterfaces
|
||||||
|
os.networkInterfaces = (() => ({
|
||||||
|
ethernet0: addresses as unknown as ReturnType<typeof os.networkInterfaces>[string],
|
||||||
|
})) as typeof os.networkInterfaces
|
||||||
|
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} finally {
|
||||||
|
os.networkInterfaces = original
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,9 +29,6 @@ import type { AuthManager } from "../auth/manager"
|
|||||||
import { registerAuthRoutes } from "./routes/auth"
|
import { registerAuthRoutes } from "./routes/auth"
|
||||||
import { sendUnauthorized, wantsHtml } from "../auth/http-auth"
|
import { sendUnauthorized, wantsHtml } from "../auth/http-auth"
|
||||||
import type { SpeechService } from "../speech/service"
|
import type { SpeechService } from "../speech/service"
|
||||||
import { ClientConnectionManager } from "../clients/connection-manager"
|
|
||||||
import { PluginChannelManager } from "../plugins/channel"
|
|
||||||
import { VoiceModeManager } from "../plugins/voice-mode"
|
|
||||||
|
|
||||||
interface HttpServerDeps {
|
interface HttpServerDeps {
|
||||||
bindHost: string
|
bindHost: string
|
||||||
@@ -176,13 +173,6 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
eventBus: deps.eventBus,
|
eventBus: deps.eventBus,
|
||||||
logger: deps.logger.child({ component: "background-processes" }),
|
logger: deps.logger.child({ component: "background-processes" }),
|
||||||
})
|
})
|
||||||
const clientConnectionManager = new ClientConnectionManager(deps.logger.child({ component: "client-connections" }))
|
|
||||||
const pluginChannel = new PluginChannelManager(deps.logger.child({ component: "plugin-channel" }))
|
|
||||||
const voiceModeManager = new VoiceModeManager({
|
|
||||||
connections: clientConnectionManager,
|
|
||||||
channel: pluginChannel,
|
|
||||||
logger: deps.logger.child({ component: "voice-mode" }),
|
|
||||||
})
|
|
||||||
|
|
||||||
registerAuthRoutes(app, { authManager: deps.authManager })
|
registerAuthRoutes(app, { authManager: deps.authManager })
|
||||||
|
|
||||||
@@ -258,12 +248,7 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
registerSettingsRoutes(app, { settings: deps.settings, logger: apiLogger })
|
registerSettingsRoutes(app, { settings: deps.settings, logger: apiLogger })
|
||||||
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser })
|
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser })
|
||||||
registerMetaRoutes(app, { serverMeta: deps.serverMeta })
|
registerMetaRoutes(app, { serverMeta: deps.serverMeta })
|
||||||
registerEventRoutes(app, {
|
registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient, logger: sseLogger })
|
||||||
eventBus: deps.eventBus,
|
|
||||||
registerClient: registerSseClient,
|
|
||||||
logger: sseLogger,
|
|
||||||
connectionManager: clientConnectionManager,
|
|
||||||
})
|
|
||||||
registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager })
|
registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager })
|
||||||
registerStorageRoutes(app, {
|
registerStorageRoutes(app, {
|
||||||
instanceStore: deps.instanceStore,
|
instanceStore: deps.instanceStore,
|
||||||
@@ -271,13 +256,7 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
workspaceManager: deps.workspaceManager,
|
workspaceManager: deps.workspaceManager,
|
||||||
})
|
})
|
||||||
registerSpeechRoutes(app, { speechService: deps.speechService })
|
registerSpeechRoutes(app, { speechService: deps.speechService })
|
||||||
registerPluginRoutes(app, {
|
registerPluginRoutes(app, { workspaceManager: deps.workspaceManager, eventBus: deps.eventBus, logger: proxyLogger })
|
||||||
workspaceManager: deps.workspaceManager,
|
|
||||||
eventBus: deps.eventBus,
|
|
||||||
logger: proxyLogger,
|
|
||||||
channel: pluginChannel,
|
|
||||||
voiceModeManager,
|
|
||||||
})
|
|
||||||
registerBackgroundProcessRoutes(app, { backgroundProcessManager })
|
registerBackgroundProcessRoutes(app, { backgroundProcessManager })
|
||||||
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
|
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
|
||||||
|
|
||||||
@@ -342,7 +321,6 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
closeSseClients()
|
closeSseClients()
|
||||||
clientConnectionManager.shutdown()
|
|
||||||
return app.close()
|
return app.close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import type { NetworkAddress } from "../api-types"
|
import type { NetworkAddress } from "../api-types"
|
||||||
|
|
||||||
|
export interface ResolvedRemoteAddresses {
|
||||||
|
all: NetworkAddress[]
|
||||||
|
userVisible: NetworkAddress[]
|
||||||
|
primaryRemoteUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveNetworkAddresses(args: {
|
export function resolveNetworkAddresses(args: {
|
||||||
host: string
|
host: string
|
||||||
protocol: "http" | "https"
|
protocol: "http" | "https"
|
||||||
@@ -58,10 +64,57 @@ export function resolveNetworkAddresses(args: {
|
|||||||
return results.sort((a, b) => {
|
return results.sort((a, b) => {
|
||||||
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
|
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
|
||||||
if (scopeDelta !== 0) return scopeDelta
|
if (scopeDelta !== 0) return scopeDelta
|
||||||
return a.ip.localeCompare(b.ip)
|
|
||||||
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveRemoteAddresses(args: {
|
||||||
|
host: string
|
||||||
|
protocol: "http" | "https"
|
||||||
|
port: number
|
||||||
|
}): ResolvedRemoteAddresses {
|
||||||
|
const all = resolveNetworkAddresses(args)
|
||||||
|
const userVisible = sortUserVisibleAddresses(all.filter((address) => address.scope === "external"))
|
||||||
|
return {
|
||||||
|
all,
|
||||||
|
userVisible,
|
||||||
|
primaryRemoteUrl: userVisible[0]?.remoteUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortUserVisibleAddresses(addresses: NetworkAddress[]): NetworkAddress[] {
|
||||||
|
return [...addresses].sort((left, right) => getUserVisiblePriority(left.ip) - getUserVisiblePriority(right.ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserVisiblePriority(ip: string): number {
|
||||||
|
if (isPrivateIPv4(ip)) return 0
|
||||||
|
if (isLinkLocalIPv4(ip)) return 2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLinkLocalIPv4(ip: string): boolean {
|
||||||
|
const octets = parseIPv4(ip)
|
||||||
|
if (!octets) return false
|
||||||
|
const [first, second] = octets
|
||||||
|
return first === 169 && second === 254
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateIPv4(ip: string): boolean {
|
||||||
|
const octets = parseIPv4(ip)
|
||||||
|
if (!octets) return false
|
||||||
|
const [first, second] = octets
|
||||||
|
|
||||||
|
if (first === 10) return true
|
||||||
|
if (first === 192 && second === 168) return true
|
||||||
|
return first === 172 && second >= 16 && second <= 31
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIPv4(value: string): number[] | null {
|
||||||
|
if (!isIPv4Address(value)) return null
|
||||||
|
return value.split(".").map((part) => Number(part))
|
||||||
|
}
|
||||||
|
|
||||||
function isIPv4Address(value: string | undefined): value is string {
|
function isIPv4Address(value: string | undefined): value is string {
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
const parts = value.split(".")
|
const parts = value.split(".")
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { z } from "zod"
|
|
||||||
import { EventBus } from "../../events/bus"
|
import { EventBus } from "../../events/bus"
|
||||||
import { WorkspaceEventPayload } from "../../api-types"
|
import { WorkspaceEventPayload } from "../../api-types"
|
||||||
import type { ClientConnectionManager } from "../../clients/connection-manager"
|
|
||||||
import { Logger } from "../../logger"
|
import { Logger } from "../../logger"
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
eventBus: EventBus
|
eventBus: EventBus
|
||||||
registerClient: (cleanup: () => void) => () => void
|
registerClient: (cleanup: () => void) => () => void
|
||||||
logger: Logger
|
logger: Logger
|
||||||
connectionManager: ClientConnectionManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextClientId = 0
|
let nextClientId = 0
|
||||||
|
|
||||||
const ConnectionQuerySchema = z.object({
|
|
||||||
clientId: z.string().trim().min(1),
|
|
||||||
connectionId: z.string().trim().min(1),
|
|
||||||
})
|
|
||||||
|
|
||||||
const PongBodySchema = ConnectionQuerySchema.extend({
|
|
||||||
pingTs: z.number().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
app.get("/api/events", (request, reply) => {
|
app.get("/api/events", (request, reply) => {
|
||||||
const clientId = ++nextClientId
|
const clientId = ++nextClientId
|
||||||
const connection = ConnectionQuerySchema.parse(request.query ?? {})
|
|
||||||
deps.logger.debug({ clientId }, "SSE client connected")
|
deps.logger.debug({ clientId }, "SSE client connected")
|
||||||
|
|
||||||
const origin = request.headers.origin ?? "*"
|
const origin = request.headers.origin ?? "*"
|
||||||
@@ -48,8 +35,7 @@ export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
|
|
||||||
const unsubscribe = deps.eventBus.onEvent(send)
|
const unsubscribe = deps.eventBus.onEvent(send)
|
||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
const ping = { ts: Date.now() }
|
reply.raw.write(`:hb ${Date.now()}\n\n`)
|
||||||
reply.raw.write(`event: codenomad.client.ping\ndata: ${JSON.stringify(ping)}\n\n`)
|
|
||||||
}, 15000)
|
}, 15000)
|
||||||
|
|
||||||
let closed = false
|
let closed = false
|
||||||
@@ -63,27 +49,13 @@ export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unregister = deps.registerClient(close)
|
const unregister = deps.registerClient(close)
|
||||||
const unregisterConnection = deps.connectionManager.register({
|
|
||||||
...connection,
|
|
||||||
close,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
close()
|
close()
|
||||||
unregister()
|
unregister()
|
||||||
unregisterConnection()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.raw.on("close", handleClose)
|
request.raw.on("close", handleClose)
|
||||||
request.raw.on("error", handleClose)
|
request.raw.on("error", handleClose)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post("/api/client-connections/pong", (request, reply) => {
|
|
||||||
const body = PongBodySchema.parse(request.body ?? {})
|
|
||||||
if (!deps.connectionManager.pong(body)) {
|
|
||||||
reply.code(404).send({ error: "Client connection not found" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reply.code(204).send()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { ServerMeta } from "../../api-types"
|
import { ServerMeta } from "../../api-types"
|
||||||
import { resolveNetworkAddresses } from "../network-addresses"
|
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
serverMeta: ServerMeta
|
serverMeta: ServerMeta
|
||||||
@@ -13,14 +13,12 @@ export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
function buildMetaResponse(meta: ServerMeta): ServerMeta {
|
function buildMetaResponse(meta: ServerMeta): ServerMeta {
|
||||||
const localPort = resolveLocalPort(meta)
|
const localPort = resolveLocalPort(meta)
|
||||||
const remote = resolveRemote(meta)
|
const remote = resolveRemote(meta)
|
||||||
const addresses = remote && remote.port > 0 ? resolveNetworkAddresses({ host: meta.host, protocol: remote.protocol, port: remote.port }) : []
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
localPort,
|
localPort,
|
||||||
remotePort: remote?.port,
|
remotePort: remote?.port,
|
||||||
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
|
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
|
||||||
addresses,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import type { VoiceModeStateResponse } from "../../api-types"
|
|
||||||
import type { WorkspaceManager } from "../../workspaces/manager"
|
import type { WorkspaceManager } from "../../workspaces/manager"
|
||||||
import type { EventBus } from "../../events/bus"
|
import type { EventBus } from "../../events/bus"
|
||||||
import type { Logger } from "../../logger"
|
import type { Logger } from "../../logger"
|
||||||
import { PluginChannelManager } from "../../plugins/channel"
|
import { PluginChannelManager } from "../../plugins/channel"
|
||||||
import { buildPingEvent, handlePluginEvent } from "../../plugins/handlers"
|
import { buildPingEvent, handlePluginEvent } from "../../plugins/handlers"
|
||||||
import { VoiceModeManager } from "../../plugins/voice-mode"
|
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
workspaceManager: WorkspaceManager
|
workspaceManager: WorkspaceManager
|
||||||
eventBus: EventBus
|
eventBus: EventBus
|
||||||
logger: Logger
|
logger: Logger
|
||||||
channel: PluginChannelManager
|
|
||||||
voiceModeManager: VoiceModeManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluginEventSchema = z.object({
|
const PluginEventSchema = z.object({
|
||||||
@@ -21,13 +17,9 @@ const PluginEventSchema = z.object({
|
|||||||
properties: z.record(z.unknown()).optional(),
|
properties: z.record(z.unknown()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const VoiceModeStateSchema = z.object({
|
|
||||||
enabled: z.boolean(),
|
|
||||||
clientId: z.string().trim().min(1),
|
|
||||||
connectionId: z.string().trim().min(1),
|
|
||||||
})
|
|
||||||
|
|
||||||
export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
|
const channel = new PluginChannelManager(deps.logger.child({ component: "plugin-channel" }))
|
||||||
|
|
||||||
app.get<{ Params: { id: string } }>("/workspaces/:id/plugin/events", (request, reply) => {
|
app.get<{ Params: { id: string } }>("/workspaces/:id/plugin/events", (request, reply) => {
|
||||||
const workspace = deps.workspaceManager.get(request.params.id)
|
const workspace = deps.workspaceManager.get(request.params.id)
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
@@ -41,11 +33,10 @@ export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
reply.raw.flushHeaders?.()
|
reply.raw.flushHeaders?.()
|
||||||
reply.hijack()
|
reply.hijack()
|
||||||
|
|
||||||
const registration = deps.channel.register(request.params.id, reply)
|
const registration = channel.register(request.params.id, reply)
|
||||||
deps.voiceModeManager.syncInstance(request.params.id)
|
|
||||||
|
|
||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
deps.channel.send(request.params.id, buildPingEvent())
|
channel.send(request.params.id, buildPingEvent())
|
||||||
}, 15000)
|
}, 15000)
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
@@ -58,22 +49,6 @@ export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
request.raw.on("error", close)
|
request.raw.on("error", close)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post<{ Params: { id: string }; Body: VoiceModeStateResponse }>("/workspaces/:id/plugin/voice-mode", (request, reply) => {
|
|
||||||
const workspace = deps.workspaceManager.get(request.params.id)
|
|
||||||
if (!workspace) {
|
|
||||||
reply.code(404).send({ error: "Workspace not found" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = VoiceModeStateSchema.parse(request.body ?? {})
|
|
||||||
deps.voiceModeManager.setEnabled(
|
|
||||||
request.params.id,
|
|
||||||
{ clientId: payload.clientId, connectionId: payload.connectionId },
|
|
||||||
payload.enabled,
|
|
||||||
)
|
|
||||||
return { enabled: payload.enabled }
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleWildcard = async (request: any, reply: any) => {
|
const handleWildcard = async (request: any, reply: any) => {
|
||||||
const workspaceId = request.params.id as string
|
const workspaceId = request.params.id as string
|
||||||
const workspace = deps.workspaceManager.get(workspaceId)
|
const workspace = deps.workspaceManager.get(workspaceId)
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ const WorkspaceFileContentQuerySchema = z.object({
|
|||||||
path: z.string(),
|
path: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const WorkspaceFileContentBodySchema = z.object({
|
|
||||||
contents: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const WorkspaceFileSearchQuerySchema = z.object({
|
const WorkspaceFileSearchQuerySchema = z.object({
|
||||||
q: z.string().trim().min(1, "Query is required"),
|
q: z.string().trim().min(1, "Query is required"),
|
||||||
limit: z.coerce.number().int().positive().max(200).optional(),
|
limit: z.coerce.number().int().positive().max(200).optional(),
|
||||||
@@ -104,20 +100,6 @@ export function registerWorkspaceRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
return handleWorkspaceError(error, reply)
|
return handleWorkspaceError(error, reply)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put<{
|
|
||||||
Params: { id: string }
|
|
||||||
Querystring: { path?: string }
|
|
||||||
}>("/api/workspaces/:id/files/content", async (request, reply) => {
|
|
||||||
try {
|
|
||||||
const query = WorkspaceFileContentQuerySchema.parse(request.query ?? {})
|
|
||||||
const body = WorkspaceFileContentBodySchema.parse(request.body ?? {})
|
|
||||||
deps.workspaceManager.writeFile(request.params.id, query.path, body.contents)
|
|
||||||
reply.code(204)
|
|
||||||
} catch (error) {
|
|
||||||
return handleWorkspaceError(error, reply)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -147,49 +147,19 @@ export class OpenAICompatibleSpeechProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = new URL("audio/speech", ensureTrailingSlash(settings.baseUrl ?? "https://api.openai.com/v1"))
|
const endpoint = new URL("audio/speech", ensureTrailingSlash(settings.baseUrl ?? "https://api.openai.com/v1"))
|
||||||
let response: Response
|
const response = await fetch(endpoint, {
|
||||||
try {
|
method: "POST",
|
||||||
response = await fetch(endpoint, {
|
headers: {
|
||||||
method: "POST",
|
Authorization: `Bearer ${settings.apiKey}`,
|
||||||
headers: {
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${settings.apiKey}`,
|
},
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify({
|
||||||
},
|
model: settings.ttsModel,
|
||||||
body: JSON.stringify({
|
voice: settings.ttsVoice,
|
||||||
model: settings.ttsModel,
|
input: text,
|
||||||
voice: settings.ttsVoice,
|
response_format: format,
|
||||||
input: text,
|
}),
|
||||||
response_format: format,
|
})
|
||||||
}),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
const detailedError = error as Error & {
|
|
||||||
cause?: unknown
|
|
||||||
code?: string
|
|
||||||
errno?: number | string
|
|
||||||
syscall?: string
|
|
||||||
address?: string
|
|
||||||
port?: number
|
|
||||||
}
|
|
||||||
this.options.logger.error(
|
|
||||||
{
|
|
||||||
err: error,
|
|
||||||
endpoint: endpoint.toString(),
|
|
||||||
baseUrl: settings.baseUrl,
|
|
||||||
model: settings.ttsModel,
|
|
||||||
voice: settings.ttsVoice,
|
|
||||||
format,
|
|
||||||
cause: detailedError.cause,
|
|
||||||
code: detailedError.code,
|
|
||||||
errno: detailedError.errno,
|
|
||||||
syscall: detailedError.syscall,
|
|
||||||
address: detailedError.address,
|
|
||||||
port: detailedError.port,
|
|
||||||
},
|
|
||||||
"speech.synthesize fetch failed",
|
|
||||||
)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const detail = await response.text()
|
const detail = await response.text()
|
||||||
|
|||||||
@@ -55,31 +55,4 @@ describe("resolveUi local version preference", () => {
|
|||||||
assert.equal(result.uiStaticDir, bundledDir)
|
assert.equal(result.uiStaticDir, bundledDir)
|
||||||
assert.equal(result.uiVersion, "0.8.1")
|
assert.equal(result.uiVersion, "0.8.1")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("prefers bundled when bundled and downloaded versions are equal", async () => {
|
|
||||||
const bundledDir = path.join(tempRoot, "bundled")
|
|
||||||
const configDir = path.join(tempRoot, "config")
|
|
||||||
const currentDir = path.join(configDir, "ui", "current")
|
|
||||||
|
|
||||||
await mkdir(bundledDir, { recursive: true })
|
|
||||||
await mkdir(currentDir, { recursive: true })
|
|
||||||
|
|
||||||
writeFileSync(path.join(bundledDir, "index.html"), "<html>bundled</html>")
|
|
||||||
writeFileSync(path.join(bundledDir, "ui-version.json"), JSON.stringify({ uiVersion: "0.8.1" }))
|
|
||||||
|
|
||||||
writeFileSync(path.join(currentDir, "index.html"), "<html>current</html>")
|
|
||||||
writeFileSync(path.join(currentDir, "ui-version.json"), JSON.stringify({ uiVersion: "0.8.1" }))
|
|
||||||
|
|
||||||
const result = await resolveUi({
|
|
||||||
serverVersion: "0.8.1",
|
|
||||||
bundledUiDir: bundledDir,
|
|
||||||
autoUpdate: false,
|
|
||||||
configDir,
|
|
||||||
logger: noopLogger,
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(result.source, "bundled")
|
|
||||||
assert.equal(result.uiStaticDir, bundledDir)
|
|
||||||
assert.equal(result.uiVersion, "0.8.1")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ async function pickBestLocalUi(args: {
|
|||||||
uiStaticDir: currentResolved,
|
uiStaticDir: currentResolved,
|
||||||
source: "downloaded",
|
source: "downloaded",
|
||||||
uiVersion: await readUiVersion(currentResolved),
|
uiVersion: await readUiVersion(currentResolved),
|
||||||
priority: 1,
|
priority: 2,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ async function pickBestLocalUi(args: {
|
|||||||
uiStaticDir: bundledResolved,
|
uiStaticDir: bundledResolved,
|
||||||
source: "bundled",
|
source: "bundled",
|
||||||
uiVersion: await readUiVersion(bundledResolved),
|
uiVersion: await readUiVersion(bundledResolved),
|
||||||
priority: 2,
|
priority: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,12 +83,6 @@ export class WorkspaceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(workspaceId: string, relativePath: string, contents: string): void {
|
|
||||||
const workspace = this.requireWorkspace(workspaceId)
|
|
||||||
const browser = new FileSystemBrowser({ rootDir: workspace.path })
|
|
||||||
browser.writeFile(relativePath, contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(folder: string, name?: string): Promise<WorkspaceDescriptor> {
|
async create(folder: string, name?: string): Promise<WorkspaceDescriptor> {
|
||||||
|
|
||||||
const id = `${Date.now().toString(36)}`
|
const id = `${Date.now().toString(36)}`
|
||||||
|
|||||||
2
packages/tauri-app/Cargo.lock
generated
2
packages/tauri-app/Cargo.lock
generated
@@ -458,7 +458,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codenomad-tauri"
|
name = "codenomad-tauri"
|
||||||
version = "0.13.3"
|
version = "0.12.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
"dev:ui": "npm run dev --workspace @codenomad/ui",
|
"dev:ui": "npm run dev --workspace @codenomad/ui",
|
||||||
"dev:prep": "node ./scripts/dev-prep.js",
|
"dev:prep": "node ./scripts/dev-prep.js",
|
||||||
"dev:bootstrap": "npm run dev:prep && npm run dev:ui",
|
"dev:bootstrap": "npm run dev:prep && npm run dev:ui",
|
||||||
"sync:version": "node ./scripts/sync-tauri-version.js",
|
|
||||||
"prebuild": "node ./scripts/prebuild.js",
|
"prebuild": "node ./scripts/prebuild.js",
|
||||||
"bundle:server": "npm run prebuild",
|
"bundle:server": "npm run prebuild",
|
||||||
"build": "tauri build"
|
"build": "tauri build"
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ async function ensureMonacoAssets() {
|
|||||||
function ensureServerBuild() {
|
function ensureServerBuild() {
|
||||||
const distPath = path.join(serverRoot, "dist")
|
const distPath = path.join(serverRoot, "dist")
|
||||||
const publicPath = path.join(serverRoot, "public")
|
const publicPath = path.join(serverRoot, "public")
|
||||||
console.log("[prebuild] rebuilding server workspace for desktop packaging...")
|
if (fs.existsSync(distPath) && fs.existsSync(publicPath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[prebuild] server build missing; running workspace build...")
|
||||||
execSync("npm --workspace @neuralnomads/codenomad run build", {
|
execSync("npm --workspace @neuralnomads/codenomad run build", {
|
||||||
cwd: workspaceRoot,
|
cwd: workspaceRoot,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const fs = require("fs")
|
|
||||||
const path = require("path")
|
|
||||||
|
|
||||||
const root = path.resolve(__dirname, "..")
|
|
||||||
const packageJsonPath = path.join(root, "package.json")
|
|
||||||
const cargoTomlPath = path.join(root, "src-tauri", "Cargo.toml")
|
|
||||||
const cargoLockPath = path.join(root, "Cargo.lock")
|
|
||||||
const tauriConfigPath = path.join(root, "src-tauri", "tauri.conf.json")
|
|
||||||
|
|
||||||
function readPackageVersion() {
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
|
|
||||||
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
||||||
throw new Error("Missing version in packages/tauri-app/package.json")
|
|
||||||
}
|
|
||||||
return packageJson.version
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncCargoToml(version) {
|
|
||||||
const current = fs.readFileSync(cargoTomlPath, "utf8")
|
|
||||||
const packageVersionPattern = /(\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/m
|
|
||||||
const match = current.match(packageVersionPattern)
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
throw new Error("Unable to find [package] version in packages/tauri-app/src-tauri/Cargo.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match[2] === version) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = current.replace(packageVersionPattern, (_, prefix, __, suffix) => `${prefix}${version}${suffix}`)
|
|
||||||
fs.writeFileSync(cargoTomlPath, updated)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncCargoLock(version) {
|
|
||||||
if (!fs.existsSync(cargoLockPath)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = fs.readFileSync(cargoLockPath, "utf8")
|
|
||||||
const packageVersionPattern = /(\[\[package\]\]\r?\nname = "codenomad-tauri"\r?\nversion = ")([^"]+)(")/
|
|
||||||
const match = current.match(packageVersionPattern)
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
throw new Error("Unable to find codenomad-tauri version in packages/tauri-app/Cargo.lock")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match[2] === version) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = current.replace(packageVersionPattern, (_, prefix, __, suffix) => `${prefix}${version}${suffix}`)
|
|
||||||
fs.writeFileSync(cargoLockPath, updated)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncTauriConfig(version) {
|
|
||||||
const current = fs.readFileSync(tauriConfigPath, "utf8")
|
|
||||||
const config = JSON.parse(current)
|
|
||||||
if (config.version === version) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
config.version = version
|
|
||||||
fs.writeFileSync(tauriConfigPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
const version = readPackageVersion()
|
|
||||||
const changed = []
|
|
||||||
|
|
||||||
if (syncCargoToml(version)) {
|
|
||||||
changed.push(path.relative(root, cargoTomlPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncCargoLock(version)) {
|
|
||||||
changed.push(path.relative(root, cargoLockPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncTauriConfig(version)) {
|
|
||||||
changed.push(path.relative(root, tauriConfigPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed.length === 0) {
|
|
||||||
console.log(`[sync-tauri-version] already aligned to ${version}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[sync-tauri-version] synced ${version} -> ${changed.join(", ")}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
main()
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
|
||||||
console.error(`[sync-tauri-version] failed: ${message}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "codenomad-tauri"
|
name = "codenomad-tauri"
|
||||||
version = "0.13.3"
|
version = "0.12.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>CodeNomad needs microphone access for speech-to-text prompt input.</string>
|
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
|
||||||
<string>CodeNomad needs local network access to connect to locally hosted AI and speech services.</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -16,7 +16,7 @@ use std::process::{Child, Command, Stdio};
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
|
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -48,7 +48,7 @@ fn workspace_root() -> Option<PathBuf> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const SESSION_COOKIE_NAME: &str = "codenomad_session";
|
const SESSION_COOKIE_NAME_PREFIX: &str = "codenomad_session";
|
||||||
|
|
||||||
const CLI_STOP_GRACE_SECS: u64 = 30;
|
const CLI_STOP_GRACE_SECS: u64 = 30;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -124,7 +124,11 @@ fn extract_cookie_value(set_cookie: &str, name: &str) -> Option<String> {
|
|||||||
Some(value.to_string())
|
Some(value.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exchange_bootstrap_token(base_url: &str, token: &str) -> anyhow::Result<Option<String>> {
|
fn exchange_bootstrap_token(
|
||||||
|
base_url: &str,
|
||||||
|
token: &str,
|
||||||
|
cookie_name: &str,
|
||||||
|
) -> anyhow::Result<Option<String>> {
|
||||||
let parsed = Url::parse(base_url)?;
|
let parsed = Url::parse(base_url)?;
|
||||||
let host = parsed.host_str().unwrap_or("127.0.0.1");
|
let host = parsed.host_str().unwrap_or("127.0.0.1");
|
||||||
let port = parsed.port_or_known_default().unwrap_or(80);
|
let port = parsed.port_or_known_default().unwrap_or(80);
|
||||||
@@ -159,11 +163,11 @@ fn exchange_bootstrap_token(base_url: &str, token: &str) -> anyhow::Result<Optio
|
|||||||
for line in lines {
|
for line in lines {
|
||||||
// handle case-insensitive header name
|
// handle case-insensitive header name
|
||||||
if let Some(value) = line.strip_prefix("Set-Cookie:") {
|
if let Some(value) = line.strip_prefix("Set-Cookie:") {
|
||||||
if let Some(session_id) = extract_cookie_value(value.trim(), SESSION_COOKIE_NAME) {
|
if let Some(session_id) = extract_cookie_value(value.trim(), cookie_name) {
|
||||||
return Ok(Some(session_id));
|
return Ok(Some(session_id));
|
||||||
}
|
}
|
||||||
} else if let Some(value) = line.strip_prefix("set-cookie:") {
|
} else if let Some(value) = line.strip_prefix("set-cookie:") {
|
||||||
if let Some(session_id) = extract_cookie_value(value.trim(), SESSION_COOKIE_NAME) {
|
if let Some(session_id) = extract_cookie_value(value.trim(), cookie_name) {
|
||||||
return Ok(Some(session_id));
|
return Ok(Some(session_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,11 +176,16 @@ fn exchange_bootstrap_token(base_url: &str, token: &str) -> anyhow::Result<Optio
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_session_cookie(app: &AppHandle, base_url: &str, session_id: &str) -> anyhow::Result<()> {
|
fn set_session_cookie(
|
||||||
|
app: &AppHandle,
|
||||||
|
base_url: &str,
|
||||||
|
cookie_name: &str,
|
||||||
|
session_id: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let parsed = Url::parse(base_url)?;
|
let parsed = Url::parse(base_url)?;
|
||||||
let domain = parsed.host_str().unwrap_or("127.0.0.1").to_string();
|
let domain = parsed.host_str().unwrap_or("127.0.0.1").to_string();
|
||||||
|
|
||||||
let cookie = Cookie::build((SESSION_COOKIE_NAME, session_id))
|
let cookie = Cookie::build((cookie_name.to_string(), session_id.to_string()))
|
||||||
.domain(domain)
|
.domain(domain)
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
@@ -190,6 +199,16 @@ fn set_session_cookie(app: &AppHandle, base_url: &str, session_id: &str) -> anyh
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_auth_cookie_name() -> String {
|
||||||
|
let pid = std::process::id();
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
format!("{SESSION_COOKIE_NAME_PREFIX}_{pid}_{timestamp}")
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";
|
const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -503,7 +522,8 @@ impl CliProcessManager {
|
|||||||
"resolved CLI entry runner={:?} entry={} host={}",
|
"resolved CLI entry runner={:?} entry={} host={}",
|
||||||
resolution.runner, resolution.entry, host
|
resolution.runner, resolution.entry, host
|
||||||
));
|
));
|
||||||
let args = resolution.build_args(dev, &host);
|
let auth_cookie_name = Arc::new(generate_auth_cookie_name());
|
||||||
|
let args = resolution.build_args(dev, &host, auth_cookie_name.as_str());
|
||||||
log_line(&format!("CLI args: {:?}", args));
|
log_line(&format!("CLI args: {:?}", args));
|
||||||
if dev {
|
if dev {
|
||||||
log_line("development mode: will prefer tsx + source if present");
|
log_line("development mode: will prefer tsx + source if present");
|
||||||
@@ -584,6 +604,7 @@ impl CliProcessManager {
|
|||||||
let app_clone = app.clone();
|
let app_clone = app.clone();
|
||||||
let ready_clone = ready.clone();
|
let ready_clone = ready.clone();
|
||||||
let token_clone = bootstrap_token.clone();
|
let token_clone = bootstrap_token.clone();
|
||||||
|
let auth_cookie_name_clone = auth_cookie_name.clone();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let stdout = child_clone
|
let stdout = child_clone
|
||||||
@@ -605,6 +626,7 @@ impl CliProcessManager {
|
|||||||
&status_clone,
|
&status_clone,
|
||||||
&ready_clone,
|
&ready_clone,
|
||||||
&token_clone,
|
&token_clone,
|
||||||
|
auth_cookie_name_clone.as_str(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(reader) = stderr {
|
if let Some(reader) = stderr {
|
||||||
@@ -615,6 +637,7 @@ impl CliProcessManager {
|
|||||||
&status_clone,
|
&status_clone,
|
||||||
&ready_clone,
|
&ready_clone,
|
||||||
&token_clone,
|
&token_clone,
|
||||||
|
auth_cookie_name_clone.as_str(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -731,6 +754,7 @@ impl CliProcessManager {
|
|||||||
status: &Arc<Mutex<CliStatus>>,
|
status: &Arc<Mutex<CliStatus>>,
|
||||||
ready: &Arc<AtomicBool>,
|
ready: &Arc<AtomicBool>,
|
||||||
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
||||||
|
auth_cookie_name: &str,
|
||||||
) {
|
) {
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
let local_url_regex = Regex::new(r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)").ok();
|
let local_url_regex = Regex::new(r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)").ok();
|
||||||
@@ -766,7 +790,14 @@ impl CliProcessManager {
|
|||||||
.and_then(|re| re.captures(line).and_then(|c| c.get(1)))
|
.and_then(|re| re.captures(line).and_then(|c| c.get(1)))
|
||||||
.map(|m| m.as_str().to_string())
|
.map(|m| m.as_str().to_string())
|
||||||
{
|
{
|
||||||
Self::mark_ready(app, status, ready, bootstrap_token, url);
|
Self::mark_ready(
|
||||||
|
app,
|
||||||
|
status,
|
||||||
|
ready,
|
||||||
|
bootstrap_token,
|
||||||
|
auth_cookie_name,
|
||||||
|
url,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,6 +812,7 @@ impl CliProcessManager {
|
|||||||
status,
|
status,
|
||||||
ready,
|
ready,
|
||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
|
auth_cookie_name,
|
||||||
format!("http://localhost:{port}"),
|
format!("http://localhost:{port}"),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -793,6 +825,7 @@ impl CliProcessManager {
|
|||||||
status,
|
status,
|
||||||
ready,
|
ready,
|
||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
|
auth_cookie_name,
|
||||||
format!("http://localhost:{}", port),
|
format!("http://localhost:{}", port),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -811,6 +844,7 @@ impl CliProcessManager {
|
|||||||
status: &Arc<Mutex<CliStatus>>,
|
status: &Arc<Mutex<CliStatus>>,
|
||||||
ready: &Arc<AtomicBool>,
|
ready: &Arc<AtomicBool>,
|
||||||
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
||||||
|
auth_cookie_name: &str,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) {
|
) {
|
||||||
ready.store(true, Ordering::SeqCst);
|
ready.store(true, Ordering::SeqCst);
|
||||||
@@ -834,9 +868,11 @@ impl CliProcessManager {
|
|||||||
if scheme.as_deref() != Some("http") {
|
if scheme.as_deref() != Some("http") {
|
||||||
navigate_main(app, &base_url);
|
navigate_main(app, &base_url);
|
||||||
} else {
|
} else {
|
||||||
match exchange_bootstrap_token(&base_url, &token) {
|
match exchange_bootstrap_token(&base_url, &token, &auth_cookie_name) {
|
||||||
Ok(Some(session_id)) => {
|
Ok(Some(session_id)) => {
|
||||||
if let Err(err) = set_session_cookie(app, &base_url, &session_id) {
|
if let Err(err) =
|
||||||
|
set_session_cookie(app, &base_url, &auth_cookie_name, &session_id)
|
||||||
|
{
|
||||||
log_line(&format!("failed to set session cookie: {err}"));
|
log_line(&format!("failed to set session cookie: {err}"));
|
||||||
navigate_main(app, &format!("{base_url}/login"));
|
navigate_main(app, &format!("{base_url}/login"));
|
||||||
} else {
|
} else {
|
||||||
@@ -932,11 +968,13 @@ impl CliEntry {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_args(&self, dev: bool, host: &str) -> Vec<String> {
|
fn build_args(&self, dev: bool, host: &str, auth_cookie_name: &str) -> Vec<String> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"serve".to_string(),
|
"serve".to_string(),
|
||||||
"--host".to_string(),
|
"--host".to_string(),
|
||||||
host.to_string(),
|
host.to_string(),
|
||||||
|
"--auth-cookie-name".to_string(),
|
||||||
|
auth_cookie_name.to_string(),
|
||||||
"--generate-token".to_string(),
|
"--generate-token".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "CodeNomad",
|
"productName": "CodeNomad",
|
||||||
"version": "0.13.3",
|
"version": "0.12.3",
|
||||||
"identifier": "ai.neuralnomads.codenomad.client",
|
"identifier": "ai.neuralnomads.codenomad.client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev:bootstrap",
|
"beforeDevCommand": "npm run dev:bootstrap",
|
||||||
"beforeBuildCommand": "npm run bundle:server",
|
"beforeBuildCommand": "npm run bundle:server",
|
||||||
"frontendDist": "resources/ui-loading"
|
"frontendDist": "resources/ui-loading"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [
|
"windows": [
|
||||||
@@ -30,13 +33,9 @@
|
|||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"scope": [
|
"scope": ["**"]
|
||||||
"**"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": ["main-window-native-dialogs"]
|
||||||
"main-window-native-dialogs"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
@@ -45,17 +44,7 @@
|
|||||||
"resources/server",
|
"resources/server",
|
||||||
"resources/ui-loading"
|
"resources/ui-loading"
|
||||||
],
|
],
|
||||||
"icon": [
|
"icon": ["icon.icns", "icon.ico", "icon.png"],
|
||||||
"icon.icns",
|
"targets": ["app", "appimage", "deb", "rpm", "nsis"]
|
||||||
"icon.ico",
|
|
||||||
"icon.png"
|
|
||||||
],
|
|
||||||
"targets": [
|
|
||||||
"app",
|
|
||||||
"appimage",
|
|
||||||
"deb",
|
|
||||||
"rpm",
|
|
||||||
"nsis"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.13.3",
|
"version": "0.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -108,15 +108,15 @@ const AlertDialog: Component = () => {
|
|||||||
open
|
open
|
||||||
modal
|
modal
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
// Only handle dismiss if dialog is dismissible (default: true)
|
if (!open) {
|
||||||
if (!open && payload.dismissible !== false) {
|
|
||||||
dismiss(false, payload)
|
dismiss(false, payload)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay class="modal-overlay z-[60]" />
|
<Dialog.Overlay class="modal-overlay" />
|
||||||
<Dialog.Content class="modal-surface fixed left-1/2 top-1/2 z-[1310] w-full max-w-sm -translate-x-1/2 -translate-y-1/2 p-6 border border-base shadow-2xl" tabIndex={-1}>
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<Dialog.Content class="modal-surface w-full max-w-sm p-6 border border-base shadow-2xl" tabIndex={-1}>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div
|
<div
|
||||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
||||||
@@ -140,11 +140,10 @@ const AlertDialog: Component = () => {
|
|||||||
|
|
||||||
<Show when={isPrompt}>
|
<Show when={isPrompt}>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label for="prompt-input" class="text-sm font-medium text-secondary">
|
<label class="text-sm font-medium text-secondary">
|
||||||
{payload.inputLabel || t("alertDialog.prompt.inputLabel")}
|
{payload.inputLabel || t("alertDialog.prompt.inputLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="prompt-input"
|
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
promptInputRef = el
|
promptInputRef = el
|
||||||
}}
|
}}
|
||||||
@@ -185,10 +184,11 @@ const AlertDialog: Component = () => {
|
|||||||
>
|
>
|
||||||
{confirmLabel}
|
{confirmLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</div>
|
||||||
</Dialog>
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ interface MonacoFileViewerProps {
|
|||||||
scopeKey: string
|
scopeKey: string
|
||||||
path: string
|
path: string
|
||||||
content: string
|
content: string
|
||||||
onSave?: (content: string) => void
|
|
||||||
onContentChange?: (content: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
||||||
@@ -35,11 +33,6 @@ export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
|||||||
editor = null
|
editor = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveContent = () => {
|
|
||||||
if (!editor || !props.onSave) return
|
|
||||||
props.onSave(editor.getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@@ -51,7 +44,7 @@ export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
|||||||
editor = monaco.editor.create(host, {
|
editor = monaco.editor.create(host, {
|
||||||
value: "",
|
value: "",
|
||||||
language: "plaintext",
|
language: "plaintext",
|
||||||
readOnly: false,
|
readOnly: true,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
lineNumbers: "on",
|
lineNumbers: "on",
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
@@ -61,14 +54,6 @@ export function MonacoFileViewer(props: MonacoFileViewerProps) {
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, saveContent)
|
|
||||||
|
|
||||||
editor.onDidChangeModelContent(() => {
|
|
||||||
if (props.onContentChange) {
|
|
||||||
props.onContentChange(editor.getValue())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setReady(true)
|
setReady(true)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="selector-button selector-button-secondary w-auto px-3 py-1.5 inline-flex items-center justify-center gap-1.5"
|
class="selector-button selector-button-secondary w-auto px-3 py-1.5 inline-flex items-center justify-center gap-1.5"
|
||||||
aria-label={t("folderSelection.links.githubStars")}
|
aria-label={t("folderSelection.links.githubStars")}
|
||||||
title={githubStars() !== null ? `${t("folderSelection.links.githubStars")}: ${githubStars()!.toLocaleString()}` : t("folderSelection.links.githubStars")}
|
title={t("folderSelection.links.githubStars")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
void openExternalUrl(GITHUB_URL, "folder-selection")
|
void openExternalUrl(GITHUB_URL, "folder-selection")
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
variant: "warning",
|
variant: "warning",
|
||||||
confirmLabel: t("infoView.dispose.confirm.confirmLabel"),
|
confirmLabel: t("infoView.dispose.confirm.confirmLabel"),
|
||||||
cancelLabel: t("infoView.dispose.confirm.cancelLabel"),
|
cancelLabel: t("infoView.dispose.confirm.cancelLabel"),
|
||||||
dismissible: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ import { serverApi } from "../../lib/api-client"
|
|||||||
import { loadBackgroundProcesses } from "../../stores/background-processes"
|
import { loadBackgroundProcesses } from "../../stores/background-processes"
|
||||||
import { BackgroundProcessOutputDialog } from "../background-process-output-dialog"
|
import { BackgroundProcessOutputDialog } from "../background-process-output-dialog"
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
import { getPermissionQueue, getPermissionQueueLength, getQuestionQueueLength, sendPermissionResponse } from "../../stores/instances"
|
import { getPermissionQueueLength, getQuestionQueueLength } from "../../stores/instances"
|
||||||
import SessionSidebar from "./shell/SessionSidebar"
|
import SessionSidebar from "./shell/SessionSidebar"
|
||||||
import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
|
import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
|
||||||
import RightPanel from "./shell/right-panel/RightPanel"
|
import RightPanel from "./shell/right-panel/RightPanel"
|
||||||
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
||||||
import { getRetrySeconds, getSessionRetry, getSessionStatus } from "../../stores/session-status"
|
import { getSessionStatus } from "../../stores/session-status"
|
||||||
import { Maximize2, ShieldAlert } from "lucide-solid"
|
import { Maximize2, ShieldAlert } from "lucide-solid"
|
||||||
|
|
||||||
import type { LayoutMode } from "./shell/types"
|
import type { LayoutMode } from "./shell/types"
|
||||||
@@ -57,13 +57,6 @@ import { useDrawerHostMeasure } from "./shell/useDrawerHostMeasure"
|
|||||||
import { useDrawerResize } from "./shell/useDrawerResize"
|
import { useDrawerResize } from "./shell/useDrawerResize"
|
||||||
import { useSessionCache } from "./shell/useSessionCache"
|
import { useSessionCache } from "./shell/useSessionCache"
|
||||||
import { useInstanceSessionContext } from "./shell/useInstanceSessionContext"
|
import { useInstanceSessionContext } from "./shell/useInstanceSessionContext"
|
||||||
import { getPermissionSessionId } from "../../types/permission"
|
|
||||||
import {
|
|
||||||
canAutoRespondPermission,
|
|
||||||
finishAutoRespondPermission,
|
|
||||||
getPermissionAutoAcceptInFlightVersion,
|
|
||||||
isPermissionAutoAcceptEnabled,
|
|
||||||
} from "../../stores/permission-auto-accept"
|
|
||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
@@ -104,7 +97,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
||||||
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
||||||
const [permissionModalOpen, setPermissionModalOpen] = createSignal(false)
|
const [permissionModalOpen, setPermissionModalOpen] = createSignal(false)
|
||||||
const [now, setNow] = createSignal(Date.now())
|
|
||||||
|
|
||||||
// Worktree selector manages its own dialogs.
|
// Worktree selector manages its own dialogs.
|
||||||
const [showSessionSearch, setShowSessionSearch] = createSignal(false)
|
const [showSessionSearch, setShowSessionSearch] = createSignal(false)
|
||||||
@@ -238,12 +230,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString())
|
window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString())
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
const timer = window.setInterval(() => setNow(Date.now()), 1000)
|
|
||||||
onCleanup(() => window.clearInterval(timer))
|
|
||||||
})
|
|
||||||
|
|
||||||
const connectionStatus = () => sseManager.getStatus(props.instance.id)
|
const connectionStatus = () => sseManager.getStatus(props.instance.id)
|
||||||
const connectionStatusClass = () => {
|
const connectionStatusClass = () => {
|
||||||
const status = connectionStatus()
|
const status = connectionStatus()
|
||||||
@@ -266,33 +252,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
return permissions + questions > 0
|
return permissions + questions > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const permissionQueue = createMemo(() => getPermissionQueue(props.instance.id))
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
getPermissionAutoAcceptInFlightVersion()
|
|
||||||
|
|
||||||
for (const permission of permissionQueue()) {
|
|
||||||
const sessionId = getPermissionSessionId(permission)
|
|
||||||
if (!sessionId) continue
|
|
||||||
if (!permission?.id) continue
|
|
||||||
if (!canAutoRespondPermission(props.instance.id, sessionId, permission.id)) continue
|
|
||||||
|
|
||||||
void sendPermissionResponse(props.instance.id, sessionId, permission.id, "once")
|
|
||||||
.catch((error) => {
|
|
||||||
log.error("Failed to auto-accept permission", error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
finishAutoRespondPermission(props.instance.id, sessionId, permission.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const yoloModeEnabled = createMemo(() => {
|
|
||||||
const session = activeSessionForInstance()
|
|
||||||
if (!session) return false
|
|
||||||
return isPermissionAutoAcceptEnabled(props.instance.id, session.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
const activeSessionStatusPill = createMemo(() => {
|
const activeSessionStatusPill = createMemo(() => {
|
||||||
const activeSessionId = activeSessionIdForInstance()
|
const activeSessionId = activeSessionIdForInstance()
|
||||||
if (!activeSessionId || activeSessionId === "info") return null
|
if (!activeSessionId || activeSessionId === "info") return null
|
||||||
@@ -313,28 +272,17 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const status = getSessionStatus(props.instance.id, activeSessionId)
|
const status = getSessionStatus(props.instance.id, activeSessionId)
|
||||||
const retry = getSessionRetry(props.instance.id, activeSessionId)
|
const text =
|
||||||
const text = retry
|
status === "working"
|
||||||
? (() => {
|
|
||||||
const seconds = getRetrySeconds(retry.next, now())
|
|
||||||
return seconds > 0 ? t("sessionList.status.retryingIn", { seconds: String(seconds) }) : t("sessionList.status.retrying")
|
|
||||||
})()
|
|
||||||
: status === "working"
|
|
||||||
? t("sessionList.status.working")
|
? t("sessionList.status.working")
|
||||||
: status === "compacting"
|
: status === "compacting"
|
||||||
? t("sessionList.status.compacting")
|
? t("sessionList.status.compacting")
|
||||||
: t("sessionList.status.idle")
|
: t("sessionList.status.idle")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
className: `session-${retry ? "retrying" : status}`,
|
className: `session-${status}`,
|
||||||
text,
|
text,
|
||||||
showAlertIcon: false,
|
showAlertIcon: false,
|
||||||
title: retry
|
|
||||||
? t("sessionList.status.retryTooltip", {
|
|
||||||
message: retry.message,
|
|
||||||
attempt: String(retry.attempt),
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -342,39 +290,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const pill = activeSessionStatusPill()
|
const pill = activeSessionStatusPill()
|
||||||
if (!pill) return null
|
if (!pill) return null
|
||||||
return (
|
return (
|
||||||
<span class={`status-indicator session-status session-status-list ${pill.className}`} title={pill.title}>
|
<span class={`status-indicator session-status session-status-list ${pill.className}`}>
|
||||||
{pill.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
{pill.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||||
{pill.text}
|
{pill.text}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderYoloModePill = () => {
|
|
||||||
if (!yoloModeEnabled()) return null
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
class="status-indicator session-status session-status-list session-yolo-mode"
|
|
||||||
aria-label={t("instanceShell.yoloMode.badgeAriaLabel")}
|
|
||||||
title={t("instanceShell.yoloMode.badgeAriaLabel")}
|
|
||||||
>
|
|
||||||
<span class="status-dot" />
|
|
||||||
{t("instanceShell.yoloMode.badge")}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderSessionHeaderIndicators = () => (
|
|
||||||
<div class="flex items-center flex-wrap justify-center gap-2">
|
|
||||||
{renderYoloModePill()}
|
|
||||||
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
|
||||||
<PermissionNotificationBanner
|
|
||||||
instanceId={props.instance.id}
|
|
||||||
onClick={() => setPermissionModalOpen(true)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleCommandPaletteClick = () => {
|
const handleCommandPaletteClick = () => {
|
||||||
showCommandPalette(props.instance.id)
|
showCommandPalette(props.instance.id)
|
||||||
}
|
}
|
||||||
@@ -498,7 +420,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
onClose={closeLeftDrawer}
|
onClose={closeLeftDrawer}
|
||||||
ModalProps={modalProps}
|
ModalProps={modalProps}
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: 60,
|
|
||||||
"& .MuiDrawer-paper": {
|
"& .MuiDrawer-paper": {
|
||||||
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
|
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
@@ -609,7 +530,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
onClose={closeRightDrawer}
|
onClose={closeRightDrawer}
|
||||||
ModalProps={modalProps}
|
ModalProps={modalProps}
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: 60,
|
|
||||||
"& .MuiDrawer-paper": {
|
"& .MuiDrawer-paper": {
|
||||||
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
|
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
@@ -700,7 +620,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||||
{renderSessionHeaderIndicators()}
|
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
||||||
|
<PermissionNotificationBanner
|
||||||
|
instanceId={props.instance.id}
|
||||||
|
onClick={() => setPermissionModalOpen(true)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||||
@@ -792,7 +717,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center session-header-hints">
|
<div class="ml-auto flex items-center session-header-hints">
|
||||||
{renderSessionHeaderIndicators()}
|
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
||||||
|
<PermissionNotificationBanner
|
||||||
|
instanceId={props.instance.id}
|
||||||
|
onClick={() => setPermissionModalOpen(true)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -48,103 +48,104 @@ interface SessionSidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
||||||
<div class="flex flex-col h-full min-h-0" ref={props.setContentEl}>
|
<div class="flex flex-col h-full min-h-0" ref={props.setContentEl}>
|
||||||
<div class="flex flex-col gap-2 px-4 py-3 border-b border-base">
|
<div class="flex flex-col gap-2 px-4 py-3 border-b border-base">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
||||||
{props.t("instanceShell.leftPanel.sessionsTitle")}
|
{props.t("instanceShell.leftPanel.sessionsTitle")}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-2 text-primary">
|
<div class="flex items-center gap-2 text-primary">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="inherit"
|
||||||
|
aria-label={props.t("sessionList.actions.newSession.ariaLabel")}
|
||||||
|
title={props.t("sessionList.actions.newSession.title")}
|
||||||
|
onClick={() => {
|
||||||
|
const result = props.onNewSession()
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
void result.catch((error) => log.error("Failed to create session:", error))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusSquare class="w-5 h-5" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="inherit"
|
||||||
|
aria-label={props.t("sessionList.filter.ariaLabel")}
|
||||||
|
title={props.t("sessionList.filter.ariaLabel")}
|
||||||
|
aria-pressed={props.showSearch()}
|
||||||
|
onClick={props.onToggleSearch}
|
||||||
|
sx={{
|
||||||
|
color: props.showSearch() ? "var(--text-primary)" : "inherit",
|
||||||
|
backgroundColor: props.showSearch() ? "var(--surface-hover)" : "transparent",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "var(--surface-hover)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search class="w-5 h-5" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="inherit"
|
||||||
|
aria-label={props.t("instanceShell.leftPanel.instanceInfo")}
|
||||||
|
title={props.t("instanceShell.leftPanel.instanceInfo")}
|
||||||
|
onClick={() => props.onSelectSession("info")}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<Show when={!props.isPhoneLayout()}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label={props.t("sessionList.actions.newSession.ariaLabel")}
|
aria-label={props.leftPinned() ? props.t("instanceShell.leftDrawer.unpin") : props.t("instanceShell.leftDrawer.pin")}
|
||||||
title={props.t("sessionList.actions.newSession.title")}
|
onClick={() => (props.leftPinned() ? props.onUnpinLeftDrawer() : props.onPinLeftDrawer())}
|
||||||
onClick={() => {
|
|
||||||
const result = props.onNewSession()
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
void result.catch((error) => log.error("Failed to create session:", error))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<PlusSquare class="w-5 h-5" />
|
{props.leftPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Show>
|
||||||
|
<Show when={props.drawerState() === "floating-open"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label={props.t("sessionList.filter.ariaLabel")}
|
aria-label={props.t("instanceShell.leftDrawer.toggle.close")}
|
||||||
title={props.t("sessionList.filter.ariaLabel")}
|
title={props.t("instanceShell.leftDrawer.toggle.close")}
|
||||||
aria-pressed={props.showSearch()}
|
onClick={props.onCloseLeftDrawer}
|
||||||
onClick={props.onToggleSearch}
|
|
||||||
sx={{
|
|
||||||
color: props.showSearch() ? "var(--text-primary)" : "inherit",
|
|
||||||
backgroundColor: props.showSearch() ? "var(--surface-hover)" : "transparent",
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "var(--surface-hover)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Search class="w-5 h-5" />
|
<MenuOpenIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
aria-label={props.t("instanceShell.leftPanel.instanceInfo")}
|
|
||||||
title={props.t("instanceShell.leftPanel.instanceInfo")}
|
|
||||||
onClick={() => props.onSelectSession("info")}
|
|
||||||
>
|
|
||||||
<InfoOutlinedIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<Show when={!props.isPhoneLayout()}>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
aria-label={props.leftPinned() ? props.t("instanceShell.leftDrawer.unpin") : props.t("instanceShell.leftDrawer.pin")}
|
|
||||||
onClick={() => (props.leftPinned() ? props.onUnpinLeftDrawer() : props.onPinLeftDrawer())}
|
|
||||||
>
|
|
||||||
{props.leftPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
|
||||||
</IconButton>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.drawerState() === "floating-open"}>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
aria-label={props.t("instanceShell.leftDrawer.toggle.close")}
|
|
||||||
title={props.t("instanceShell.leftDrawer.toggle.close")}
|
|
||||||
onClick={props.onCloseLeftDrawer}
|
|
||||||
>
|
|
||||||
<MenuOpenIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="session-sidebar-shortcuts">
|
|
||||||
<Show when={props.keyboardShortcuts().length}>
|
|
||||||
<KeyboardHint shortcuts={props.keyboardShortcuts()} separator=" " showDescription={false} />
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="session-sidebar-shortcuts">
|
||||||
|
<Show when={props.keyboardShortcuts().length}>
|
||||||
|
<KeyboardHint shortcuts={props.keyboardShortcuts()} separator=" " showDescription={false} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
||||||
<SessionList
|
<SessionList
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
threads={props.threads()}
|
threads={props.threads()}
|
||||||
activeSessionId={props.activeSessionId()}
|
activeSessionId={props.activeSessionId()}
|
||||||
onSelect={props.onSelectSession}
|
onSelect={props.onSelectSession}
|
||||||
onNew={() => {
|
onNew={() => {
|
||||||
const result = props.onNewSession()
|
const result = props.onNewSession()
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
void result.catch((error) => log.error("Failed to create session:", error))
|
void result.catch((error) => log.error("Failed to create session:", error))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
enableFilterBar={props.showSearch()}
|
enableFilterBar={props.showSearch()}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
showFooter={false}
|
showFooter={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="session-sidebar-separator" />
|
<div class="session-sidebar-separator" />
|
||||||
<Show when={props.activeSession()}>
|
<Show when={props.activeSession()}>
|
||||||
{(activeSession) => (
|
{(activeSession) => (
|
||||||
|
<>
|
||||||
<div class="session-sidebar-controls px-4 py-4 border-t border-base flex flex-col gap-3">
|
<div class="session-sidebar-controls px-4 py-4 border-t border-base flex flex-col gap-3">
|
||||||
<WorktreeSelector instanceId={props.instanceId} sessionId={activeSession().id} />
|
<WorktreeSelector instanceId={props.instanceId} sessionId={activeSession().id} />
|
||||||
|
|
||||||
@@ -176,10 +177,11 @@ const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
|||||||
showDescription={false}
|
showDescription={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
</Show>
|
)}
|
||||||
</div>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
export default SessionSidebar
|
export default SessionSidebar
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } f
|
|||||||
|
|
||||||
import { getDefaultWorktreeSlug, getOrCreateWorktreeClient, getWorktreeSlugForSession } from "../../../../stores/worktrees"
|
import { getDefaultWorktreeSlug, getOrCreateWorktreeClient, getWorktreeSlugForSession } from "../../../../stores/worktrees"
|
||||||
import { requestData } from "../../../../lib/opencode-api"
|
import { requestData } from "../../../../lib/opencode-api"
|
||||||
import { serverApi } from "../../../../lib/api-client"
|
|
||||||
import { showConfirmDialog } from "../../../../stores/alerts"
|
|
||||||
import { showToastNotification } from "../../../../lib/notifications"
|
|
||||||
import { buildUnifiedDiffFromSdkPatch, tryReverseApplyUnifiedDiff } from "../../../../lib/unified-diff-reverse"
|
import { buildUnifiedDiffFromSdkPatch, tryReverseApplyUnifiedDiff } from "../../../../lib/unified-diff-reverse"
|
||||||
import { useGlobalPointerDrag } from "../useGlobalPointerDrag"
|
import { useGlobalPointerDrag } from "../useGlobalPointerDrag"
|
||||||
import {
|
import {
|
||||||
@@ -89,7 +86,6 @@ interface RightPanelProps {
|
|||||||
const RightPanel: Component<RightPanelProps> = (props) => {
|
const RightPanel: Component<RightPanelProps> = (props) => {
|
||||||
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("changes"))
|
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("changes"))
|
||||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
||||||
"yolo-mode",
|
|
||||||
"plan",
|
"plan",
|
||||||
"background-processes",
|
"background-processes",
|
||||||
"mcp",
|
"mcp",
|
||||||
@@ -106,9 +102,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
const [browserSelectedContent, setBrowserSelectedContent] = createSignal<string | null>(null)
|
const [browserSelectedContent, setBrowserSelectedContent] = createSignal<string | null>(null)
|
||||||
const [browserSelectedLoading, setBrowserSelectedLoading] = createSignal(false)
|
const [browserSelectedLoading, setBrowserSelectedLoading] = createSignal(false)
|
||||||
const [browserSelectedError, setBrowserSelectedError] = createSignal<string | null>(null)
|
const [browserSelectedError, setBrowserSelectedError] = createSignal<string | null>(null)
|
||||||
const [browserSelectedDirty, setBrowserSelectedDirty] = createSignal(false)
|
|
||||||
const [browserSelectedSaving, setBrowserSelectedSaving] = createSignal(false)
|
|
||||||
const [browserSelectedOriginalContent, setBrowserSelectedOriginalContent] = createSignal<string | null>(null)
|
|
||||||
|
|
||||||
const [diffViewMode, setDiffViewMode] = createSignal<DiffViewMode>(
|
const [diffViewMode, setDiffViewMode] = createSignal<DiffViewMode>(
|
||||||
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY, ["split", "unified"] as const) ?? "unified",
|
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY, ["split", "unified"] as const) ?? "unified",
|
||||||
@@ -546,8 +539,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
setBrowserSelectedLoading(true)
|
setBrowserSelectedLoading(true)
|
||||||
setBrowserSelectedError(null)
|
setBrowserSelectedError(null)
|
||||||
setBrowserSelectedContent(null)
|
setBrowserSelectedContent(null)
|
||||||
setBrowserSelectedDirty(false)
|
|
||||||
setBrowserSelectedOriginalContent(null)
|
|
||||||
|
|
||||||
// Phone: treat file selection as a commit action and close the overlay.
|
// Phone: treat file selection as a commit action and close the overlay.
|
||||||
if (props.isPhoneLayout()) {
|
if (props.isPhoneLayout()) {
|
||||||
@@ -568,7 +559,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
throw new Error("Unsupported file type")
|
throw new Error("Unsupported file type")
|
||||||
}
|
}
|
||||||
setBrowserSelectedContent(text)
|
setBrowserSelectedContent(text)
|
||||||
setBrowserSelectedOriginalContent(text) // Track original content for conflict detection
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to read file")
|
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to read file")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -576,95 +566,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveBrowserFile = async (content: string): Promise<boolean> => {
|
|
||||||
const path = browserSelectedPath()
|
|
||||||
if (!path) return false
|
|
||||||
|
|
||||||
// Check for conflict: agent edited file while user was editing
|
|
||||||
const originalContent = browserSelectedOriginalContent()
|
|
||||||
if (originalContent !== null) {
|
|
||||||
try {
|
|
||||||
const currentDiskContent = await requestData<FileContent>(
|
|
||||||
browserClient().file.read({ path }),
|
|
||||||
"file.read",
|
|
||||||
)
|
|
||||||
const diskContent = (currentDiskContent as any)?.content
|
|
||||||
|
|
||||||
// If disk content differs from what we originally loaded (agent edit)
|
|
||||||
// AND differs from user's current edits, we have a conflict
|
|
||||||
if (diskContent !== originalContent && diskContent !== content) {
|
|
||||||
const confirmed = await showConfirmDialog(
|
|
||||||
props.t("instanceShell.rightPanel.actions.conflict.message", { path }),
|
|
||||||
{
|
|
||||||
variant: "warning",
|
|
||||||
confirmLabel: props.t("instanceShell.rightPanel.actions.conflict.confirmLabel"),
|
|
||||||
cancelLabel: props.t("instanceShell.rightPanel.actions.conflict.cancelLabel"),
|
|
||||||
dismissible: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (!confirmed) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// User chose to overwrite, proceed with save
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// If we can't check for conflict, proceed with save
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setBrowserSelectedSaving(true)
|
|
||||||
try {
|
|
||||||
await serverApi.writeWorkspaceFile(props.instanceId, path, content)
|
|
||||||
setBrowserSelectedContent(content)
|
|
||||||
setBrowserSelectedOriginalContent(content) // Update original to match saved
|
|
||||||
setBrowserSelectedDirty(false)
|
|
||||||
showToastNotification({
|
|
||||||
message: props.t("instanceShell.rightPanel.toast.saveSuccess"),
|
|
||||||
variant: "success",
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to save file")
|
|
||||||
showToastNotification({
|
|
||||||
message: props.t("instanceShell.rightPanel.toast.saveError"),
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
setBrowserSelectedSaving(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBrowserFileChange = (content: string) => {
|
|
||||||
setBrowserSelectedContent(content)
|
|
||||||
setBrowserSelectedDirty(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpenBrowserFileRequest = async (path: string) => {
|
|
||||||
if (browserSelectedDirty()) {
|
|
||||||
const confirmed = await showConfirmDialog(
|
|
||||||
props.t("instanceShell.rightPanel.actions.saveConfirm.message", { path: browserSelectedPath() || "" }),
|
|
||||||
{
|
|
||||||
variant: "warning",
|
|
||||||
confirmLabel: props.t("instanceShell.rightPanel.actions.saveConfirm.confirmLabel"),
|
|
||||||
cancelLabel: props.t("instanceShell.rightPanel.actions.saveConfirm.cancelLabel"),
|
|
||||||
dismissible: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (confirmed) {
|
|
||||||
const saveSuccess = await saveBrowserFile(browserSelectedContent() || "")
|
|
||||||
if (!saveSuccess) {
|
|
||||||
// Save failed - stay on current file, error toast already shown
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// User chose not to save - clear dirty state and discard edits
|
|
||||||
setBrowserSelectedDirty(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await openBrowserFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (rightPanelTab() !== "files") return
|
if (rightPanelTab() !== "files") return
|
||||||
if (browserLoading()) return
|
if (browserLoading()) return
|
||||||
@@ -677,7 +578,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
setBrowserSelectedContent(null)
|
setBrowserSelectedContent(null)
|
||||||
setBrowserSelectedLoading(false)
|
setBrowserSelectedLoading(false)
|
||||||
setBrowserSelectedError(null)
|
setBrowserSelectedError(null)
|
||||||
setBrowserSelectedDirty(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -730,22 +630,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refreshFilesTab = async () => {
|
const refreshFilesTab = async () => {
|
||||||
// Prompt for confirmation if file has unsaved changes
|
|
||||||
if (browserSelectedDirty()) {
|
|
||||||
const confirmed = await showConfirmDialog(
|
|
||||||
props.t("instanceShell.rightPanel.actions.refreshDirty.message"),
|
|
||||||
{
|
|
||||||
variant: "warning",
|
|
||||||
confirmLabel: props.t("instanceShell.rightPanel.actions.refreshDirty.confirmLabel"),
|
|
||||||
cancelLabel: props.t("instanceShell.rightPanel.actions.refreshDirty.cancelLabel"),
|
|
||||||
dismissible: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (!confirmed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadBrowserEntries(browserPath())
|
void loadBrowserEntries(browserPath())
|
||||||
const selected = browserSelectedPath()
|
const selected = browserSelectedPath()
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -767,8 +651,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
throw new Error("Unsupported file type")
|
throw new Error("Unsupported file type")
|
||||||
}
|
}
|
||||||
setBrowserSelectedContent(text)
|
setBrowserSelectedContent(text)
|
||||||
setBrowserSelectedOriginalContent(text) // Update original content after refresh
|
|
||||||
setBrowserSelectedDirty(false) // Clear dirty after refresh
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to read file")
|
setBrowserSelectedError(error instanceof Error ? error.message : "Failed to read file")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -788,7 +670,7 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
setRightPanelTab("changes")
|
setRightPanelTab("changes")
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusSectionIds = ["yolo-mode", "session-changes", "plan", "background-processes", "mcp", "lsp", "plugins"]
|
const statusSectionIds = ["session-changes", "plan", "background-processes", "mcp", "lsp", "plugins"]
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const currentExpanded = new Set(rightPanelExpandedItems())
|
const currentExpanded = new Set(rightPanelExpandedItems())
|
||||||
@@ -948,15 +830,11 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
browserSelectedContent={browserSelectedContent}
|
browserSelectedContent={browserSelectedContent}
|
||||||
browserSelectedLoading={browserSelectedLoading}
|
browserSelectedLoading={browserSelectedLoading}
|
||||||
browserSelectedError={browserSelectedError}
|
browserSelectedError={browserSelectedError}
|
||||||
browserSelectedDirty={browserSelectedDirty}
|
|
||||||
browserSelectedSaving={browserSelectedSaving}
|
|
||||||
parentPath={browserParentPath}
|
parentPath={browserParentPath}
|
||||||
scopeKey={browserScopeKey}
|
scopeKey={browserScopeKey}
|
||||||
onLoadEntries={(path: string) => void loadBrowserEntries(path)}
|
onLoadEntries={(path: string) => void loadBrowserEntries(path)}
|
||||||
onRequestOpenFile={(path: string) => void handleOpenBrowserFileRequest(path)}
|
onOpenFile={(path: string) => void openBrowserFile(path)}
|
||||||
onRefresh={() => void refreshFilesTab()}
|
onRefresh={() => void refreshFilesTab()}
|
||||||
onSave={(content: string) => void saveBrowserFile(content)}
|
|
||||||
onContentChange={(content: string) => handleBrowserFileChange(content)}
|
|
||||||
listOpen={filesListOpen}
|
listOpen={filesListOpen}
|
||||||
onToggleList={toggleFilesList}
|
onToggleList={toggleFilesList}
|
||||||
splitWidth={filesSplitWidth}
|
splitWidth={filesSplitWidth}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { For, Show, Suspense, lazy, type Accessor, type Component, type JSX } from "solid-js"
|
import { For, Show, Suspense, lazy, type Accessor, type Component, type JSX } from "solid-js"
|
||||||
import type { FileNode } from "@opencode-ai/sdk/v2/client"
|
import type { FileNode } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
import { RefreshCw, Save } from "lucide-solid"
|
import { RefreshCw } from "lucide-solid"
|
||||||
|
|
||||||
import SplitFilePanel from "../components/SplitFilePanel"
|
import SplitFilePanel from "../components/SplitFilePanel"
|
||||||
|
|
||||||
@@ -21,17 +21,13 @@ interface FilesTabProps {
|
|||||||
browserSelectedContent: Accessor<string | null>
|
browserSelectedContent: Accessor<string | null>
|
||||||
browserSelectedLoading: Accessor<boolean>
|
browserSelectedLoading: Accessor<boolean>
|
||||||
browserSelectedError: Accessor<string | null>
|
browserSelectedError: Accessor<string | null>
|
||||||
browserSelectedDirty: Accessor<boolean>
|
|
||||||
browserSelectedSaving: Accessor<boolean>
|
|
||||||
|
|
||||||
parentPath: Accessor<string | null>
|
parentPath: Accessor<string | null>
|
||||||
scopeKey: Accessor<string>
|
scopeKey: Accessor<string>
|
||||||
|
|
||||||
onLoadEntries: (path: string) => void
|
onLoadEntries: (path: string) => void
|
||||||
onRequestOpenFile: (path: string) => void
|
onOpenFile: (path: string) => void
|
||||||
onRefresh: () => void
|
onRefresh: () => void
|
||||||
onSave: (content: string) => void
|
|
||||||
onContentChange: (content: string) => void
|
|
||||||
|
|
||||||
listOpen: Accessor<boolean>
|
listOpen: Accessor<boolean>
|
||||||
onToggleList: () => void
|
onToggleList: () => void
|
||||||
@@ -42,13 +38,6 @@ interface FilesTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FilesTab: Component<FilesTabProps> = (props) => {
|
const FilesTab: Component<FilesTabProps> = (props) => {
|
||||||
const handleSave = () => {
|
|
||||||
const content = props.browserSelectedContent()
|
|
||||||
if (content !== undefined && content !== null) {
|
|
||||||
props.onSave(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderContent = (): JSX.Element => {
|
const renderContent = (): JSX.Element => {
|
||||||
const entriesValue = props.browserEntries()
|
const entriesValue = props.browserEntries()
|
||||||
const entries = entriesValue || []
|
const entries = entriesValue || []
|
||||||
@@ -97,13 +86,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LazyMonacoFileViewer
|
<LazyMonacoFileViewer scopeKey={props.scopeKey()} path={payload().path} content={payload().content} />
|
||||||
scopeKey={props.scopeKey()}
|
|
||||||
path={payload().path}
|
|
||||||
content={payload().content}
|
|
||||||
onSave={props.onSave}
|
|
||||||
onContentChange={props.onContentChange}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -152,7 +135,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
props.onLoadEntries(item.path)
|
props.onLoadEntries(item.path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
props.onRequestOpenFile(item.path)
|
props.onOpenFile(item.path)
|
||||||
}}
|
}}
|
||||||
title={item.path}
|
title={item.path}
|
||||||
>
|
>
|
||||||
@@ -185,25 +168,14 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={props.browserError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
<Show when={props.browserError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="files-header-icon-button"
|
|
||||||
title={props.t("instanceShell.rightPanel.actions.save") || "Save (Ctrl+S)"}
|
|
||||||
aria-label={props.t("instanceShell.rightPanel.actions.save") || "Save"}
|
|
||||||
disabled={props.browserSelectedSaving() || !props.browserSelectedDirty()}
|
|
||||||
style={{ "margin-inline-start": "auto" }}
|
|
||||||
onClick={handleSave}
|
|
||||||
>
|
|
||||||
<Show when={props.browserSelectedSaving()} fallback={<Save class="h-4 w-4" />}>
|
|
||||||
<RefreshCw class="h-4 w-4 animate-spin" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="files-header-icon-button"
|
class="files-header-icon-button"
|
||||||
title={props.t("instanceShell.rightPanel.actions.refresh")}
|
title={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||||
aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
|
aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||||
disabled={props.browserLoading()}
|
disabled={props.browserLoading()}
|
||||||
|
style={{ "margin-inline-start": "auto" }}
|
||||||
onClick={() => props.onRefresh()}
|
onClick={() => props.onRefresh()}
|
||||||
>
|
>
|
||||||
<RefreshCw class={`h-4 w-4${props.browserLoading() ? " animate-spin" : ""}`} />
|
<RefreshCw class={`h-4 w-4${props.browserLoading() ? " animate-spin" : ""}`} />
|
||||||
@@ -226,4 +198,4 @@ const FilesTab: Component<FilesTabProps> = (props) => {
|
|||||||
return <>{renderContent()}</>
|
return <>{renderContent()}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilesTab
|
export default FilesTab
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { For, Show, type Accessor, type Component } from "solid-js"
|
|||||||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import { Accordion } from "@kobalte/core"
|
import { Accordion } from "@kobalte/core"
|
||||||
import { Tooltip } from "@kobalte/core/tooltip"
|
import { Tooltip } from "@kobalte/core/tooltip"
|
||||||
import Switch from "@suid/material/Switch"
|
|
||||||
|
|
||||||
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ import type { Session } from "../../../../../types/session"
|
|||||||
import ContextUsagePanel from "../../../../session/context-usage-panel"
|
import ContextUsagePanel from "../../../../session/context-usage-panel"
|
||||||
import { TodoListView } from "../../../../tool-call/renderers/todo"
|
import { TodoListView } from "../../../../tool-call/renderers/todo"
|
||||||
import InstanceServiceStatus from "../../../../instance-service-status"
|
import InstanceServiceStatus from "../../../../instance-service-status"
|
||||||
import { isPermissionAutoAcceptEnabled, togglePermissionAutoAccept } from "../../../../../stores/permission-auto-accept"
|
|
||||||
|
|
||||||
interface StatusTabProps {
|
interface StatusTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
@@ -41,35 +39,6 @@ interface StatusTabProps {
|
|||||||
const StatusTab: Component<StatusTabProps> = (props) => {
|
const StatusTab: Component<StatusTabProps> = (props) => {
|
||||||
const isSectionExpanded = (id: string) => props.expandedItems().includes(id)
|
const isSectionExpanded = (id: string) => props.expandedItems().includes(id)
|
||||||
|
|
||||||
const renderYoloModeSection = () => {
|
|
||||||
const session = props.activeSession()
|
|
||||||
if (!session) {
|
|
||||||
return (
|
|
||||||
<div class="right-panel-empty right-panel-empty--left">
|
|
||||||
<span class="text-xs">{props.t("instanceShell.yoloMode.noSessionSelected")}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="rounded-md border border-base bg-surface-secondary px-3 py-2">
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div class="min-w-0">
|
|
||||||
<div class="text-sm font-medium text-primary">{props.t("instanceShell.yoloMode.title")}</div>
|
|
||||||
<p class="mt-1 text-xs text-secondary">{props.t("instanceShell.yoloMode.description")}</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={isPermissionAutoAcceptEnabled(props.instanceId, session.id)}
|
|
||||||
color="warning"
|
|
||||||
size="small"
|
|
||||||
inputProps={{ "aria-label": props.t("instanceShell.yoloMode.title") }}
|
|
||||||
onChange={() => togglePermissionAutoAccept(props.instanceId, session.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderStatusSessionChanges = () => {
|
const renderStatusSessionChanges = () => {
|
||||||
const sessionId = props.activeSessionId()
|
const sessionId = props.activeSessionId()
|
||||||
if (!sessionId || sessionId === "info") {
|
if (!sessionId || sessionId === "info") {
|
||||||
@@ -235,12 +204,6 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statusSections = [
|
const statusSections = [
|
||||||
{
|
|
||||||
id: "yolo-mode",
|
|
||||||
labelKey: "instanceShell.rightPanel.sections.yoloMode",
|
|
||||||
tooltipKey: "instanceShell.rightPanel.sections.yoloMode.tooltip",
|
|
||||||
render: renderYoloModeSection,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "session-changes",
|
id: "session-changes",
|
||||||
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
||||||
@@ -318,23 +281,29 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
<For each={statusSections}>
|
<For each={statusSections}>
|
||||||
{(section) => (
|
{(section) => (
|
||||||
<Accordion.Item value={section.id} class="right-panel-accordion-item">
|
<Accordion.Item value={section.id} class="right-panel-accordion-item">
|
||||||
<Accordion.Header class="right-panel-accordion-header-row">
|
<Accordion.Header>
|
||||||
<Accordion.Trigger class="right-panel-accordion-trigger">
|
<Accordion.Trigger class="right-panel-accordion-trigger">
|
||||||
<span class="section-left">
|
<span class="section-left">
|
||||||
|
<Tooltip openDelay={200} gutter={4} placement="top">
|
||||||
|
<Tooltip.Trigger
|
||||||
|
class="section-info-trigger"
|
||||||
|
aria-label={props.t(section.tooltipKey)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Info class="section-info-icon" />
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content class="section-info-tooltip">
|
||||||
|
{props.t(section.tooltipKey)}
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip>
|
||||||
<span class="section-label">{props.t(section.labelKey)}</span>
|
<span class="section-label">{props.t(section.labelKey)}</span>
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
||||||
/>
|
/>
|
||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
<Tooltip openDelay={200} gutter={4} placement="top">
|
|
||||||
<Tooltip.Trigger as="button" type="button" class="section-info-trigger" aria-label={props.t(section.tooltipKey)}>
|
|
||||||
<Info class="section-info-icon" />
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Portal>
|
|
||||||
<Tooltip.Content class="section-info-tooltip">{props.t(section.tooltipKey)}</Tooltip.Content>
|
|
||||||
</Tooltip.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Accordion.Header>
|
</Accordion.Header>
|
||||||
<Accordion.Content class="right-panel-accordion-content">{section.render()}</Accordion.Content>
|
<Accordion.Content class="right-panel-accordion-content">{section.render()}</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ interface MarkdownProps {
|
|||||||
isDark?: boolean
|
isDark?: boolean
|
||||||
size?: "base" | "sm" | "tight"
|
size?: "base" | "sm" | "tight"
|
||||||
disableHighlight?: boolean
|
disableHighlight?: boolean
|
||||||
escapeRawHtml?: boolean
|
|
||||||
onRendered?: () => void
|
onRendered?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,12 +103,11 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
const text = decodeHtmlEntitiesLocally(rawText)
|
const text = decodeHtmlEntitiesLocally(rawText)
|
||||||
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
||||||
const highlightEnabled = !props.disableHighlight
|
const highlightEnabled = !props.disableHighlight
|
||||||
const escapeRawHtml = Boolean(props.escapeRawHtml)
|
|
||||||
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : undefined
|
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : undefined
|
||||||
const cacheId = resolvePartCacheId(part, text)
|
const cacheId = resolvePartCacheId(part, text)
|
||||||
const version = resolvePartVersion(part, text)
|
const version = resolvePartVersion(part, text)
|
||||||
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${escapeRawHtml ? 1 : 0}:${version}`
|
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${version}`
|
||||||
return { part, text, themeKey, highlightEnabled, escapeRawHtml, partId, cacheId, version, requestKey }
|
return { part, text, themeKey, highlightEnabled, partId, cacheId, version, requestKey }
|
||||||
})
|
})
|
||||||
|
|
||||||
const cacheHandle = useGlobalCache({
|
const cacheHandle = useGlobalCache({
|
||||||
@@ -118,26 +116,20 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
scope: "markdown",
|
scope: "markdown",
|
||||||
cacheId: () => {
|
cacheId: () => {
|
||||||
const { cacheId, themeKey, highlightEnabled } = resolved()
|
const { cacheId, themeKey, highlightEnabled } = resolved()
|
||||||
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${resolved().escapeRawHtml ? 1 : 0}`
|
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}`
|
||||||
},
|
},
|
||||||
version: () => resolved().version,
|
version: () => resolved().version,
|
||||||
})
|
})
|
||||||
|
|
||||||
const commitCacheEntry = (
|
const commitCacheEntry = (snapshot: ReturnType<typeof resolved>, renderedHtml: string) => {
|
||||||
snapshot: ReturnType<typeof resolved>,
|
|
||||||
renderedHtml: string,
|
|
||||||
options?: { cache?: boolean },
|
|
||||||
) => {
|
|
||||||
const cacheEntry: RenderCache = {
|
const cacheEntry: RenderCache = {
|
||||||
text: snapshot.text,
|
text: snapshot.text,
|
||||||
html: renderedHtml,
|
html: renderedHtml,
|
||||||
theme: snapshot.themeKey,
|
theme: snapshot.themeKey,
|
||||||
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
mode: snapshot.version,
|
||||||
}
|
}
|
||||||
setHtml(renderedHtml)
|
setHtml(renderedHtml)
|
||||||
if (options?.cache ?? true) {
|
cacheHandle.set(cacheEntry)
|
||||||
cacheHandle.set(cacheEntry)
|
|
||||||
}
|
|
||||||
notifyRendered()
|
notifyRendered()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,23 +138,20 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
|
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
|
||||||
const rendered = await markdown.renderMarkdown(snapshot.text, {
|
const rendered = await markdown.renderMarkdown(snapshot.text, {
|
||||||
suppressHighlight: !snapshot.highlightEnabled,
|
suppressHighlight: !snapshot.highlightEnabled,
|
||||||
escapeRawHtml: snapshot.escapeRawHtml,
|
|
||||||
})
|
})
|
||||||
const shouldCache = !snapshot.highlightEnabled || !markdown.hasPendingCodeHighlight(snapshot.text)
|
|
||||||
|
|
||||||
if (latestRequestKey === snapshot.requestKey) {
|
if (latestRequestKey === snapshot.requestKey) {
|
||||||
commitCacheEntry(snapshot, rendered, { cache: shouldCache })
|
commitCacheEntry(snapshot, rendered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const snapshot = resolved()
|
const snapshot = resolved()
|
||||||
latestRequestKey = snapshot.requestKey
|
latestRequestKey = snapshot.requestKey
|
||||||
const cacheMode = `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`
|
|
||||||
|
|
||||||
const cacheMatches = (cache: RenderCache | undefined) => {
|
const cacheMatches = (cache: RenderCache | undefined) => {
|
||||||
if (!cache) return false
|
if (!cache) return false
|
||||||
return cache.theme === snapshot.themeKey && cache.mode === cacheMode
|
return cache.theme === snapshot.themeKey && cache.mode === snapshot.version
|
||||||
}
|
}
|
||||||
|
|
||||||
const localCache = snapshot.part.renderCache
|
const localCache = snapshot.part.renderCache
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ export default function MessagePart(props: MessagePartProps) {
|
|||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
isDark={isDark()}
|
isDark={isDark()}
|
||||||
size={isAssistantMessage() ? "tight" : "base"}
|
size={isAssistantMessage() ? "tight" : "base"}
|
||||||
escapeRawHtml={props.messageType === "user"}
|
|
||||||
onRendered={props.onRendered}
|
onRendered={props.onRendered}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ import { usePromptAttachments } from "./prompt-input/usePromptAttachments"
|
|||||||
import { usePromptPicker } from "./prompt-input/usePromptPicker"
|
import { usePromptPicker } from "./prompt-input/usePromptPicker"
|
||||||
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
||||||
import { usePromptVoiceInput } from "./prompt-input/usePromptVoiceInput"
|
import { usePromptVoiceInput } from "./prompt-input/usePromptVoiceInput"
|
||||||
import {
|
import { canUseConversationMode, isConversationModeEnabled, toggleConversationMode } from "../stores/conversation-speech"
|
||||||
canUseConversationMode,
|
|
||||||
clearConversationPlaybackForInstance,
|
|
||||||
isConversationModeEnabled,
|
|
||||||
toggleConversationMode,
|
|
||||||
} from "../stores/conversation-speech"
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
||||||
|
|
||||||
@@ -497,8 +492,6 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const beginVoicePress = (event?: PointerEvent | KeyboardEvent) => {
|
const beginVoicePress = (event?: PointerEvent | KeyboardEvent) => {
|
||||||
if (voiceButtonPressed || props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput()) return
|
if (voiceButtonPressed || props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput()) return
|
||||||
voiceButtonPressed = true
|
voiceButtonPressed = true
|
||||||
// Treat a mic press as barge-in: stop any active assistant speech before listening.
|
|
||||||
clearConversationPlaybackForInstance(props.instanceId)
|
|
||||||
|
|
||||||
if (event instanceof PointerEvent) {
|
if (event instanceof PointerEvent) {
|
||||||
const target = event.currentTarget
|
const target = event.currentTarget
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { showAlertDialog } from "../../stores/alerts"
|
|||||||
import { loadSpeechCapabilities, speechCapabilities } from "../../stores/speech"
|
import { loadSpeechCapabilities, speechCapabilities } from "../../stores/speech"
|
||||||
import { serverApi } from "../../lib/api-client"
|
import { serverApi } from "../../lib/api-client"
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
import { isElectronHost } from "../../lib/runtime-env"
|
|
||||||
|
|
||||||
interface UsePromptVoiceInputOptions {
|
interface UsePromptVoiceInputOptions {
|
||||||
prompt: Accessor<string>
|
prompt: Accessor<string>
|
||||||
@@ -89,14 +88,6 @@ export function usePromptVoiceInput(options: UsePromptVoiceInputOptions) {
|
|||||||
try {
|
try {
|
||||||
recordedChunks = []
|
recordedChunks = []
|
||||||
shouldTranscribe = true
|
shouldTranscribe = true
|
||||||
|
|
||||||
if (isElectronHost()) {
|
|
||||||
const granted = await (window as Window & { electronAPI?: ElectronAPI }).electronAPI?.requestMicrophoneAccess?.()
|
|
||||||
if (granted && !granted.granted) {
|
|
||||||
throw new Error(t("promptInput.voiceInput.error.permissionDenied"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
mediaRecorder = createRecorder(mediaStream)
|
mediaRecorder = createRecorder(mediaStream)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Dialog } from "@kobalte/core/dialog"
|
|||||||
import { Switch } from "@kobalte/core/switch"
|
import { Switch } from "@kobalte/core/switch"
|
||||||
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
||||||
import { toDataURL } from "qrcode"
|
import { toDataURL } from "qrcode"
|
||||||
import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
import { ChevronDown, ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
||||||
import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types"
|
import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types"
|
||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
import { restartCli } from "../lib/native/cli"
|
import { restartCli } from "../lib/native/cli"
|
||||||
@@ -10,6 +10,7 @@ import { serverSettings, setListeningMode } from "../stores/preferences"
|
|||||||
import { showConfirmDialog } from "../stores/alerts"
|
import { showConfirmDialog } from "../stores/alerts"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import { splitRemoteAddresses, type RemoteAddressGroups } from "../lib/remote-access-addresses"
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
|
|
||||||
@@ -32,17 +33,17 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
||||||
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
||||||
const [savingPassword, setSavingPassword] = createSignal(false)
|
const [savingPassword, setSavingPassword] = createSignal(false)
|
||||||
|
const [showAllAddresses, setShowAllAddresses] = createSignal(false)
|
||||||
|
|
||||||
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
||||||
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
||||||
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
||||||
const displayAddresses = createMemo(() => {
|
const displayAddresses = createMemo<RemoteAddressGroups>(() => {
|
||||||
const list = addresses()
|
const list = addresses()
|
||||||
if (!allowExternalConnections()) {
|
if (!allowExternalConnections()) {
|
||||||
return []
|
return { recommended: null, hidden: [] }
|
||||||
}
|
}
|
||||||
// Local URL is displayed separately; list only remote-friendly addresses.
|
return splitRemoteAddresses(list)
|
||||||
return list.filter((address) => address.scope !== "loopback")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshMeta = async () => {
|
const refreshMeta = async () => {
|
||||||
@@ -53,6 +54,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
||||||
setMeta(metaResult)
|
setMeta(metaResult)
|
||||||
setAuthStatus(authResult)
|
setAuthStatus(authResult)
|
||||||
|
setShowAllAddresses(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err))
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -98,7 +100,6 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
variant: "warning",
|
variant: "warning",
|
||||||
confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"),
|
confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"),
|
||||||
cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"),
|
cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"),
|
||||||
dismissible: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
@@ -326,7 +327,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
|
|
||||||
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
||||||
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
||||||
<Show when={displayAddresses().length > 0} fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}>
|
<Show when={displayAddresses().recommended || meta()?.localUrl} fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}>
|
||||||
<div class="remote-address-list">
|
<div class="remote-address-list">
|
||||||
<Show when={meta()?.localUrl}>
|
<Show when={meta()?.localUrl}>
|
||||||
{(url) => {
|
{(url) => {
|
||||||
@@ -373,8 +374,9 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
<For each={displayAddresses()}>
|
<Show when={displayAddresses().recommended}>
|
||||||
{(address) => {
|
{(addressAccessor) => {
|
||||||
|
const address = addressAccessor()
|
||||||
const url = address.remoteUrl
|
const url = address.remoteUrl
|
||||||
const expandedState = () => expandedUrl() === url
|
const expandedState = () => expandedUrl() === url
|
||||||
const qr = () => qrCodes()[url]
|
const qr = () => qrCodes()[url]
|
||||||
@@ -384,13 +386,14 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
: address.scope === "loopback"
|
: address.scope === "loopback"
|
||||||
? t("remoteAccess.address.scope.loopback")
|
? t("remoteAccess.address.scope.loopback")
|
||||||
: t("remoteAccess.address.scope.internal")
|
: t("remoteAccess.address.scope.internal")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="remote-address">
|
<div class="remote-address">
|
||||||
<div class="remote-address-main">
|
<div class="remote-address-main">
|
||||||
<div>
|
<div>
|
||||||
<p class="remote-address-url">{url}</p>
|
<p class="remote-address-url">{url}</p>
|
||||||
<p class="remote-address-meta">
|
<p class="remote-address-meta">
|
||||||
{address.family.toUpperCase()} • {scopeLabel()} • {address.ip}
|
{address.family.toUpperCase()} - {scopeLabel()} - {address.ip}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="remote-actions">
|
<div class="remote-actions">
|
||||||
@@ -425,7 +428,83 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={displayAddresses().hidden.length > 0}>
|
||||||
|
<div class="remote-address-disclosure" data-expanded={showAllAddresses()}>
|
||||||
|
<button
|
||||||
|
class="remote-address-disclosure-trigger"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAllAddresses(!showAllAddresses())}
|
||||||
|
aria-expanded={showAllAddresses()}
|
||||||
|
>
|
||||||
|
<span class="remote-address-disclosure-label">
|
||||||
|
{showAllAddresses()
|
||||||
|
? t("remoteAccess.addresses.actions.hideOther")
|
||||||
|
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })}
|
||||||
|
</span>
|
||||||
|
<ChevronDown class={`remote-address-disclosure-chevron ${showAllAddresses() ? "is-expanded" : ""}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={showAllAddresses()}>
|
||||||
|
<div class="remote-address-disclosure-content">
|
||||||
|
<For each={displayAddresses().hidden}>
|
||||||
|
{(address) => {
|
||||||
|
const url = address.remoteUrl
|
||||||
|
const expandedState = () => expandedUrl() === url
|
||||||
|
const qr = () => qrCodes()[url]
|
||||||
|
const scopeLabel = () =>
|
||||||
|
address.scope === "external"
|
||||||
|
? t("remoteAccess.address.scope.network")
|
||||||
|
: address.scope === "loopback"
|
||||||
|
? t("remoteAccess.address.scope.loopback")
|
||||||
|
: t("remoteAccess.address.scope.internal")
|
||||||
|
return (
|
||||||
|
<div class="remote-address">
|
||||||
|
<div class="remote-address-main">
|
||||||
|
<div>
|
||||||
|
<p class="remote-address-url">{url}</p>
|
||||||
|
<p class="remote-address-meta">
|
||||||
|
{address.family.toUpperCase()} • {scopeLabel()} • {address.ip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="remote-actions">
|
||||||
|
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}>
|
||||||
|
<ExternalLink class="remote-icon" />
|
||||||
|
{t("remoteAccess.address.open")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="remote-pill"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void toggleExpanded(url)}
|
||||||
|
aria-expanded={expandedState()}
|
||||||
|
>
|
||||||
|
<Link2 class="remote-icon" />
|
||||||
|
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={expandedState()}>
|
||||||
|
<div class="remote-qr">
|
||||||
|
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||||
|
{(dataUrl) => (
|
||||||
|
<img
|
||||||
|
src={dataUrl()}
|
||||||
|
alt={t("remoteAccess.address.qrAlt", { url })}
|
||||||
|
class="remote-qr-img"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
||||||
import type { SessionStatus } from "../types/session"
|
import type { SessionStatus } from "../types/session"
|
||||||
import type { SessionThread } from "../stores/session-state"
|
import type { SessionThread } from "../stores/session-state"
|
||||||
import { getRetrySeconds, getSessionRetry, getSessionStatus } from "../stores/session-status"
|
import { getSessionStatus } from "../stores/session-status"
|
||||||
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare, Split, RotateCw } from "lucide-solid"
|
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare, Split } from "lucide-solid"
|
||||||
import KeyboardHint from "./keyboard-hint"
|
import KeyboardHint from "./keyboard-hint"
|
||||||
import SessionRenameDialog from "./session-rename-dialog"
|
import SessionRenameDialog from "./session-rename-dialog"
|
||||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
ensureSessionParentExpanded,
|
ensureSessionParentExpanded,
|
||||||
getVisibleSessionIds,
|
getVisibleSessionIds,
|
||||||
isSessionParentExpanded,
|
isSessionParentExpanded,
|
||||||
loadMessages,
|
|
||||||
loading,
|
loading,
|
||||||
renameSession,
|
renameSession,
|
||||||
sessions as sessionStateSessions,
|
sessions as sessionStateSessions,
|
||||||
@@ -54,14 +53,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const normalizedQuery = createMemo(() => (props.enableFilterBar ? filterQuery().trim().toLowerCase() : ""))
|
const normalizedQuery = createMemo(() => (props.enableFilterBar ? filterQuery().trim().toLowerCase() : ""))
|
||||||
|
|
||||||
const [selectedSessionIds, setSelectedSessionIds] = createSignal<Set<string>>(new Set())
|
const [selectedSessionIds, setSelectedSessionIds] = createSignal<Set<string>>(new Set())
|
||||||
const [reloadingSessionIds, setReloadingSessionIds] = createSignal<Set<string>>(new Set())
|
|
||||||
const [now, setNow] = createSignal(Date.now())
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
const timer = window.setInterval(() => setNow(Date.now()), 1000)
|
|
||||||
onCleanup(() => window.clearInterval(timer))
|
|
||||||
})
|
|
||||||
|
|
||||||
const normalizeSessionLabel = (sessionId: string) => {
|
const normalizeSessionLabel = (sessionId: string) => {
|
||||||
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||||
@@ -166,7 +157,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
variant: "warning",
|
variant: "warning",
|
||||||
confirmLabel: t("sessionList.delete.confirmLabel"),
|
confirmLabel: t("sessionList.delete.confirmLabel"),
|
||||||
cancelLabel: t("sessionList.delete.cancelLabel"),
|
cancelLabel: t("sessionList.delete.cancelLabel"),
|
||||||
dismissible: false,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
@@ -222,32 +212,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
setRenameTarget({ id: sessionId, title: session.title ?? "", label })
|
setRenameTarget({ id: sessionId, title: session.title ?? "", label })
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSessionReloading = (sessionId: string) => reloadingSessionIds().has(sessionId)
|
|
||||||
|
|
||||||
const handleReloadSession = async (event: MouseEvent, sessionId: string) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
if (isSessionReloading(sessionId)) return
|
|
||||||
|
|
||||||
setReloadingSessionIds((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.add(sessionId)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await loadMessages(props.instanceId, sessionId, true)
|
|
||||||
} catch (error) {
|
|
||||||
log.error(`Failed to reload session ${sessionId}:`, error)
|
|
||||||
showToastNotification({ message: t("sessionList.reload.error"), variant: "error" })
|
|
||||||
} finally {
|
|
||||||
setReloadingSessionIds((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.delete(sessionId)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeRenameDialog = () => {
|
const closeRenameDialog = () => {
|
||||||
setRenameTarget(null)
|
setRenameTarget(null)
|
||||||
}
|
}
|
||||||
@@ -321,7 +285,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
variant: "warning",
|
variant: "warning",
|
||||||
confirmLabel: t("sessionList.bulkDelete.confirmLabel"),
|
confirmLabel: t("sessionList.bulkDelete.confirmLabel"),
|
||||||
cancelLabel: t("sessionList.bulkDelete.cancelLabel"),
|
cancelLabel: t("sessionList.bulkDelete.cancelLabel"),
|
||||||
dismissible: false,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -407,13 +370,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const isActive = () => props.activeSessionId === rowProps.sessionId
|
const isActive = () => props.activeSessionId === rowProps.sessionId
|
||||||
const title = () => session()?.title || t("sessionList.session.untitled")
|
const title = () => session()?.title || t("sessionList.session.untitled")
|
||||||
const status = () => getSessionStatus(props.instanceId, rowProps.sessionId)
|
const status = () => getSessionStatus(props.instanceId, rowProps.sessionId)
|
||||||
const retry = () => getSessionRetry(props.instanceId, rowProps.sessionId)
|
|
||||||
const statusLabel = () => {
|
const statusLabel = () => {
|
||||||
const retryState = retry()
|
|
||||||
if (retryState) {
|
|
||||||
const seconds = getRetrySeconds(retryState.next, now())
|
|
||||||
return seconds > 0 ? t("sessionList.status.retryingIn", { seconds: String(seconds) }) : t("sessionList.status.retrying")
|
|
||||||
}
|
|
||||||
switch (formatSessionStatus(status())) {
|
switch (formatSessionStatus(status())) {
|
||||||
case "working":
|
case "working":
|
||||||
return t("sessionList.status.working")
|
return t("sessionList.status.working")
|
||||||
@@ -426,21 +383,13 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const needsPermission = () => Boolean(session()?.pendingPermission)
|
const needsPermission = () => Boolean(session()?.pendingPermission)
|
||||||
const needsQuestion = () => Boolean((session() as any)?.pendingQuestion)
|
const needsQuestion = () => Boolean((session() as any)?.pendingQuestion)
|
||||||
const needsInput = () => needsPermission() || needsQuestion()
|
const needsInput = () => needsPermission() || needsQuestion()
|
||||||
const statusClassName = () => (needsInput() ? "session-permission" : `session-${retry() ? "retrying" : status()}`)
|
const statusClassName = () => (needsInput() ? "session-permission" : `session-${status()}`)
|
||||||
const statusText = () =>
|
const statusText = () =>
|
||||||
needsPermission()
|
needsPermission()
|
||||||
? t("sessionList.status.needsPermission")
|
? t("sessionList.status.needsPermission")
|
||||||
: needsQuestion()
|
: needsQuestion()
|
||||||
? t("sessionList.status.needsInput")
|
? t("sessionList.status.needsInput")
|
||||||
: statusLabel()
|
: statusLabel()
|
||||||
const statusTooltip = () => {
|
|
||||||
const retryState = retry()
|
|
||||||
if (!retryState) return undefined
|
|
||||||
return t("sessionList.status.retryTooltip", {
|
|
||||||
message: retryState.message,
|
|
||||||
attempt: String(retryState.attempt),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = () => selectedSessionIds().has(rowProps.sessionId)
|
const isSelected = () => selectedSessionIds().has(rowProps.sessionId)
|
||||||
|
|
||||||
@@ -520,7 +469,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
<span class={`status-indicator session-status session-status-list ${statusClassName()}`} title={statusTooltip()}>
|
<span class={`status-indicator session-status session-status-list ${statusClassName()}`}>
|
||||||
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||||
{statusText()}
|
{statusText()}
|
||||||
</span>
|
</span>
|
||||||
@@ -542,21 +491,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Copy class="w-3 h-3" />
|
<Copy class="w-3 h-3" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
|
||||||
onClick={(event) => handleReloadSession(event, rowProps.sessionId)}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={t("sessionList.actions.reload.ariaLabel")}
|
|
||||||
title={t("sessionList.actions.reload.title")}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={!isSessionReloading(rowProps.sessionId)}
|
|
||||||
fallback={<RotateCw class="w-3 h-3 animate-spin" />}
|
|
||||||
>
|
|
||||||
<RotateCw class="w-3 h-3" />
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Switch } from "@kobalte/core/switch"
|
import { Switch } from "@kobalte/core/switch"
|
||||||
import { For, Show, createMemo, createSignal, type Component, onMount } from "solid-js"
|
import { For, Show, createMemo, createSignal, type Component, onMount } from "solid-js"
|
||||||
import { toDataURL } from "qrcode"
|
import { toDataURL } from "qrcode"
|
||||||
import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
import { ChevronDown, ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
||||||
import type { NetworkAddress, ServerMeta } from "../../../../server/src/api-types"
|
import type { NetworkAddress, ServerMeta } from "../../../../server/src/api-types"
|
||||||
import { serverApi } from "../../lib/api-client"
|
import { serverApi } from "../../lib/api-client"
|
||||||
import { restartCli } from "../../lib/native/cli"
|
import { restartCli } from "../../lib/native/cli"
|
||||||
@@ -9,6 +9,7 @@ import { serverSettings, setListeningMode } from "../../stores/preferences"
|
|||||||
import { showConfirmDialog } from "../../stores/alerts"
|
import { showConfirmDialog } from "../../stores/alerts"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
|
import { splitRemoteAddresses, type RemoteAddressGroups } from "../../lib/remote-access-addresses"
|
||||||
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
@@ -30,14 +31,15 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
||||||
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
||||||
const [savingPassword, setSavingPassword] = createSignal(false)
|
const [savingPassword, setSavingPassword] = createSignal(false)
|
||||||
|
const [showAllAddresses, setShowAllAddresses] = createSignal(false)
|
||||||
|
|
||||||
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
||||||
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
||||||
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
||||||
const displayAddresses = createMemo(() => {
|
const displayAddresses = createMemo<RemoteAddressGroups>(() => {
|
||||||
const list = addresses()
|
const list = addresses()
|
||||||
if (!allowExternalConnections()) return []
|
if (!allowExternalConnections()) return { recommended: null, hidden: [] }
|
||||||
return list.filter((address) => address.scope !== "loopback")
|
return splitRemoteAddresses(list)
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshMeta = async () => {
|
const refreshMeta = async () => {
|
||||||
@@ -48,6 +50,7 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
||||||
setMeta(metaResult)
|
setMeta(metaResult)
|
||||||
setAuthStatus(authResult)
|
setAuthStatus(authResult)
|
||||||
|
setShowAllAddresses(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err))
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -86,7 +89,6 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
variant: "warning",
|
variant: "warning",
|
||||||
confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"),
|
confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"),
|
||||||
cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"),
|
cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"),
|
||||||
dismissible: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
@@ -218,31 +220,35 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
fallback={<div class="settings-card-message">{t("remoteAccess.authStatus.unavailable")}</div>}
|
fallback={<div class="settings-card-message">{t("remoteAccess.authStatus.unavailable")}</div>}
|
||||||
>
|
>
|
||||||
<div class="settings-card-content">
|
<div class="settings-card-content">
|
||||||
<p class="settings-help-text">{t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })}</p>
|
<div class="settings-password-summary-row">
|
||||||
<p class="settings-help-text">
|
<div class="settings-password-summary-copy">
|
||||||
{authStatus()!.passwordUserProvided
|
<p class="settings-help-text">{t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })}</p>
|
||||||
? t("remoteAccess.password.status.set")
|
<p class="settings-help-text">
|
||||||
: t("remoteAccess.password.status.unset")}
|
{authStatus()!.passwordUserProvided
|
||||||
</p>
|
? t("remoteAccess.password.status.set")
|
||||||
|
: t("remoteAccess.password.status.unset")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-password-actions">
|
<div class="settings-password-actions">
|
||||||
<button
|
<button
|
||||||
class="settings-pill-button"
|
class="settings-pill-button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPasswordFormOpen(!passwordFormOpen())
|
setPasswordFormOpen(!passwordFormOpen())
|
||||||
setPasswordError(null)
|
setPasswordError(null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{passwordFormOpen()
|
{passwordFormOpen()
|
||||||
? t("remoteAccess.password.actions.cancel")
|
? t("remoteAccess.password.actions.cancel")
|
||||||
: authStatus()!.passwordUserProvided
|
: authStatus()!.passwordUserProvided
|
||||||
? t("remoteAccess.password.actions.change")
|
? t("remoteAccess.password.actions.change")
|
||||||
: t("remoteAccess.password.actions.set")}
|
: t("remoteAccess.password.actions.set")}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={passwordFormOpen()}>
|
<Show when={passwordFormOpen()}>
|
||||||
<div class="settings-form-group">
|
<div class="settings-form-group">
|
||||||
<label class="settings-form-label">{t("remoteAccess.password.form.newPassword")}</label>
|
<label class="settings-form-label">{t("remoteAccess.password.form.newPassword")}</label>
|
||||||
<input
|
<input
|
||||||
@@ -292,7 +298,7 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
||||||
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
||||||
<Show
|
<Show
|
||||||
when={displayAddresses().length > 0 || meta()?.localUrl}
|
when={Boolean(displayAddresses().recommended) || meta()?.localUrl}
|
||||||
fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}
|
fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}
|
||||||
>
|
>
|
||||||
<div class="remote-address-list">
|
<div class="remote-address-list">
|
||||||
@@ -342,8 +348,9 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<For each={displayAddresses()}>
|
<Show when={displayAddresses().recommended}>
|
||||||
{(address) => {
|
{(addressAccessor) => {
|
||||||
|
const address = addressAccessor()
|
||||||
const url = address.remoteUrl
|
const url = address.remoteUrl
|
||||||
const expandedState = () => expandedUrl() === url
|
const expandedState = () => expandedUrl() === url
|
||||||
const qr = () => qrCodes()[url]
|
const qr = () => qrCodes()[url]
|
||||||
@@ -383,7 +390,11 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
<div class="remote-qr">
|
<div class="remote-qr">
|
||||||
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||||
{(dataUrl) => (
|
{(dataUrl) => (
|
||||||
<img src={dataUrl()} alt={t("remoteAccess.address.qrAlt", { url })} class="remote-qr-img" />
|
<img
|
||||||
|
src={dataUrl()}
|
||||||
|
alt={t("remoteAccess.address.qrAlt", { url })}
|
||||||
|
class="remote-qr-img"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,7 +402,80 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={displayAddresses().hidden.length > 0}>
|
||||||
|
<div class="remote-address-disclosure" data-expanded={showAllAddresses()}>
|
||||||
|
<button
|
||||||
|
class="remote-address-disclosure-trigger"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAllAddresses(!showAllAddresses())}
|
||||||
|
aria-expanded={showAllAddresses()}
|
||||||
|
>
|
||||||
|
<span class="remote-address-disclosure-label">
|
||||||
|
{showAllAddresses()
|
||||||
|
? t("remoteAccess.addresses.actions.hideOther")
|
||||||
|
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })}
|
||||||
|
</span>
|
||||||
|
<ChevronDown class={`remote-address-disclosure-chevron ${showAllAddresses() ? "is-expanded" : ""}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={showAllAddresses()}>
|
||||||
|
<div class="remote-address-disclosure-content">
|
||||||
|
<For each={displayAddresses().hidden}>
|
||||||
|
{(address) => {
|
||||||
|
const url = address.remoteUrl
|
||||||
|
const expandedState = () => expandedUrl() === url
|
||||||
|
const qr = () => qrCodes()[url]
|
||||||
|
const scopeLabel = () =>
|
||||||
|
address.scope === "external"
|
||||||
|
? t("remoteAccess.address.scope.network")
|
||||||
|
: address.scope === "loopback"
|
||||||
|
? t("remoteAccess.address.scope.loopback")
|
||||||
|
: t("remoteAccess.address.scope.internal")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="remote-address">
|
||||||
|
<div class="remote-address-main">
|
||||||
|
<div>
|
||||||
|
<p class="remote-address-url">{url}</p>
|
||||||
|
<p class="remote-address-meta">
|
||||||
|
{address.family.toUpperCase()} - {scopeLabel()} - {address.ip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="remote-actions">
|
||||||
|
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}>
|
||||||
|
<ExternalLink class="remote-icon" />
|
||||||
|
{t("remoteAccess.address.open")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="remote-pill"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void toggleExpanded(url)}
|
||||||
|
aria-expanded={expandedState()}
|
||||||
|
>
|
||||||
|
<Link2 class="remote-icon" />
|
||||||
|
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={expandedState()}>
|
||||||
|
<div class="remote-qr">
|
||||||
|
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||||
|
{(dataUrl) => (
|
||||||
|
<img src={dataUrl()} alt={t("remoteAccess.address.qrAlt", { url })} class="remote-qr-img" />
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import type {
|
|||||||
SpeechSynthesisResponse,
|
SpeechSynthesisResponse,
|
||||||
SpeechTranscriptionResponse,
|
SpeechTranscriptionResponse,
|
||||||
ServerMeta,
|
ServerMeta,
|
||||||
VoiceModeStateResponse,
|
|
||||||
WorkspaceCreateRequest,
|
WorkspaceCreateRequest,
|
||||||
WorkspaceDescriptor,
|
WorkspaceDescriptor,
|
||||||
WorkspaceFileResponse,
|
WorkspaceFileResponse,
|
||||||
@@ -24,7 +23,6 @@ import type {
|
|||||||
WorktreeMap,
|
WorktreeMap,
|
||||||
WorktreeCreateRequest,
|
WorktreeCreateRequest,
|
||||||
} from "../../../server/src/api-types"
|
} from "../../../server/src/api-types"
|
||||||
import { getClientIdentity } from "./client-identity"
|
|
||||||
import { getLogger } from "./logger"
|
import { getLogger } from "./logger"
|
||||||
|
|
||||||
const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined
|
const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined
|
||||||
@@ -236,16 +234,6 @@ export const serverApi = {
|
|||||||
`/api/workspaces/${encodeURIComponent(id)}/files/content?${params.toString()}`,
|
`/api/workspaces/${encodeURIComponent(id)}/files/content?${params.toString()}`,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
writeWorkspaceFile(id: string, relativePath: string, contents: string): Promise<void> {
|
|
||||||
const params = new URLSearchParams({ path: relativePath })
|
|
||||||
return request(
|
|
||||||
`/api/workspaces/${encodeURIComponent(id)}/files/content?${params.toString()}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({ contents }),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchConfigOwner<T extends Record<string, any> = Record<string, any>>(owner: string): Promise<T> {
|
fetchConfigOwner<T extends Record<string, any> = Record<string, any>>(owner: string): Promise<T> {
|
||||||
return request<T>(`/api/storage/config/${encodeURIComponent(owner)}`)
|
return request<T>(`/api/storage/config/${encodeURIComponent(owner)}`)
|
||||||
@@ -350,19 +338,6 @@ export const serverApi = {
|
|||||||
{ method: "POST" },
|
{ method: "POST" },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateVoiceMode(instanceId: string, enabled: boolean): Promise<VoiceModeStateResponse> {
|
|
||||||
const identity = getClientIdentity()
|
|
||||||
return request<VoiceModeStateResponse>(`/workspaces/${encodeURIComponent(instanceId)}/plugin/voice-mode`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ ...identity, enabled }),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
sendClientConnectionPong(payload: { clientId: string; connectionId: string; pingTs?: number }): Promise<void> {
|
|
||||||
return request<void>("/api/client-connections/pong", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fetchBackgroundProcessOutput(
|
fetchBackgroundProcessOutput(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
processId: string,
|
processId: string,
|
||||||
@@ -387,15 +362,9 @@ export const serverApi = {
|
|||||||
`/workspaces/${encodeURIComponent(instanceId)}/plugin/background-processes/${encodeURIComponent(processId)}/output${suffix}`,
|
`/workspaces/${encodeURIComponent(instanceId)}/plugin/background-processes/${encodeURIComponent(processId)}/output${suffix}`,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
connectEvents(
|
connectEvents(onEvent: (event: WorkspaceEventPayload) => void, onError?: () => void) {
|
||||||
onEvent: (event: WorkspaceEventPayload) => void,
|
sseLogger.info(`Connecting to ${EVENTS_URL}`)
|
||||||
onError?: () => void,
|
const source = new EventSource(EVENTS_URL, { withCredentials: true } as any)
|
||||||
onPing?: (payload: { ts?: number }) => void,
|
|
||||||
) {
|
|
||||||
const identity = getClientIdentity()
|
|
||||||
const url = buildClientEventsUrl(identity)
|
|
||||||
sseLogger.info(`Connecting to ${url}`)
|
|
||||||
const source = new EventSource(url, { withCredentials: true } as any)
|
|
||||||
source.onmessage = (event) => {
|
source.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(event.data) as WorkspaceEventPayload
|
const payload = JSON.parse(event.data) as WorkspaceEventPayload
|
||||||
@@ -408,26 +377,8 @@ export const serverApi = {
|
|||||||
sseLogger.warn("EventSource error, closing stream")
|
sseLogger.warn("EventSource error, closing stream")
|
||||||
onError?.()
|
onError?.()
|
||||||
}
|
}
|
||||||
source.addEventListener("codenomad.client.ping", (event: MessageEvent) => {
|
|
||||||
try {
|
|
||||||
const payload = event.data ? (JSON.parse(event.data) as { ts?: number }) : {}
|
|
||||||
onPing?.(payload)
|
|
||||||
} catch (error) {
|
|
||||||
sseLogger.error("Failed to parse ping event", error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return source
|
return source
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildClientEventsUrl(identity: { clientId: string; connectionId: string }): string {
|
|
||||||
const url = new URL(EVENTS_URL, typeof window !== "undefined" ? window.location.origin : "http://localhost")
|
|
||||||
url.searchParams.set("clientId", identity.clientId)
|
|
||||||
url.searchParams.set("connectionId", identity.connectionId)
|
|
||||||
if (EVENTS_URL.startsWith("http://") || EVENTS_URL.startsWith("https://")) {
|
|
||||||
return url.toString()
|
|
||||||
}
|
|
||||||
return `${url.pathname}${url.search}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { WorkspaceDescriptor, WorkspaceLogEntry, WorkspaceEventPayload, WorkspaceEventType }
|
export type { WorkspaceDescriptor, WorkspaceLogEntry, WorkspaceEventPayload, WorkspaceEventType }
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
const CLIENT_ID_STORAGE_KEY = "codenomad.client-id"
|
|
||||||
const CONNECTION_ID_STORAGE_KEY = "codenomad.connection-id"
|
|
||||||
|
|
||||||
let cachedClientId: string | null = null
|
|
||||||
let cachedConnectionId: string | null = null
|
|
||||||
|
|
||||||
export function getClientIdentity(): { clientId: string; connectionId: string } {
|
|
||||||
return {
|
|
||||||
clientId: getOrCreateClientId(),
|
|
||||||
connectionId: getOrCreateConnectionId(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrCreateClientId(): string {
|
|
||||||
if (cachedClientId) return cachedClientId
|
|
||||||
cachedClientId = getOrCreateStoredValue(CLIENT_ID_STORAGE_KEY, window.localStorage)
|
|
||||||
return cachedClientId
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrCreateConnectionId(): string {
|
|
||||||
if (cachedConnectionId) return cachedConnectionId
|
|
||||||
cachedConnectionId = getOrCreateStoredValue(CONNECTION_ID_STORAGE_KEY, window.sessionStorage)
|
|
||||||
return cachedConnectionId
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrCreateStoredValue(key: string, storage: Storage): string {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return generateUUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existing = storage.getItem(key)
|
|
||||||
if (existing && existing.trim()) {
|
|
||||||
return existing.trim()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return generateUUID()
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = generateUUID()
|
|
||||||
try {
|
|
||||||
storage.setItem(key, next)
|
|
||||||
} catch {
|
|
||||||
// Ignore storage failures and fall back to the in-memory value.
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateUUID(): string {
|
|
||||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
||||||
return crypto.randomUUID()
|
|
||||||
}
|
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
|
||||||
const random = (Math.random() * 16) | 0
|
|
||||||
const value = char === "x" ? random : (random & 0x3) | 0x8
|
|
||||||
return value.toString(16)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,9 @@ export function formatCompactCount(value: number): string {
|
|||||||
return `${(value / 1_000_000).toFixed(1)}M`
|
return `${(value / 1_000_000).toFixed(1)}M`
|
||||||
}
|
}
|
||||||
if (value >= 10_000) {
|
if (value >= 10_000) {
|
||||||
|
return `${Math.round(value / 1_000)}K`
|
||||||
|
}
|
||||||
|
if (value >= 1_000) {
|
||||||
const label = `${(value / 1_000).toFixed(1)}K`
|
const label = `${(value / 1_000).toFixed(1)}K`
|
||||||
return label.replace(/\.0K$/, "K")
|
return label.replace(/\.0K$/, "K")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Instance Info",
|
"instanceShell.leftPanel.instanceInfo": "Instance Info",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Pin left drawer",
|
"instanceShell.leftDrawer.pin": "Pin left drawer",
|
||||||
"instanceShell.leftDrawer.unpin": "Unpin left drawer",
|
"instanceShell.leftDrawer.unpin": "Unpin left drawer",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Left drawer pinned",
|
"instanceShell.leftDrawer.toggle.pinned": "Left drawer pinned",
|
||||||
@@ -94,20 +95,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "Status",
|
"instanceShell.rightPanel.tabs.status": "Status",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
||||||
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
||||||
"instanceShell.rightPanel.actions.save": "Save (Ctrl+S)",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.message": "Do you want to save changes to \"{path}\" before switching?",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Save",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Discard Changes",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.message": "File was modified by the agent. Overwrite agent's changes?",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Overwrite",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Cancel",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.message": "File has unsaved changes. Refresh will discard your edits. Continue?",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Refresh",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancel",
|
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "File saved successfully",
|
|
||||||
"instanceShell.rightPanel.toast.saveError": "Failed to save file",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode": "Yolo Mode",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Automatically approves permission requests for the current session. Use it only when you trust the tools being run.",
|
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
@@ -151,12 +138,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
||||||
"instanceShell.plan.empty": "Nothing planned yet.",
|
"instanceShell.plan.empty": "Nothing planned yet.",
|
||||||
|
|
||||||
"instanceShell.yoloMode.noSessionSelected": "Select a session to configure Yolo mode.",
|
|
||||||
"instanceShell.yoloMode.title": "Yolo mode",
|
|
||||||
"instanceShell.yoloMode.description": "Automatically approve permission requests for this session. Disabled by default.",
|
|
||||||
"instanceShell.yoloMode.badge": "Yolo mode",
|
|
||||||
"instanceShell.yoloMode.badgeAriaLabel": "Yolo mode enabled",
|
|
||||||
|
|
||||||
"instanceShell.backgroundProcesses.empty": "No background processes.",
|
"instanceShell.backgroundProcesses.empty": "No background processes.",
|
||||||
"instanceShell.backgroundProcesses.status": "Status: {status}",
|
"instanceShell.backgroundProcesses.status": "Status: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Output: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "Output: {sizeKb}KB",
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ export const messagingMessages = {
|
|||||||
"promptInput.voiceInput.transcribing.title": "Transcribing audio",
|
"promptInput.voiceInput.transcribing.title": "Transcribing audio",
|
||||||
"promptInput.voiceInput.error.title": "Voice input failed",
|
"promptInput.voiceInput.error.title": "Voice input failed",
|
||||||
"promptInput.voiceInput.error.permission": "Microphone access is required to record voice input.",
|
"promptInput.voiceInput.error.permission": "Microphone access is required to record voice input.",
|
||||||
"promptInput.voiceInput.error.permissionDenied": "Microphone access was denied by macOS.",
|
|
||||||
"promptInput.voiceInput.error.unsupported": "Voice input is not supported in this browser.",
|
"promptInput.voiceInput.error.unsupported": "Voice input is not supported in this browser.",
|
||||||
"promptInput.voiceInput.error.transcribe": "Unable to transcribe the recorded audio.",
|
"promptInput.voiceInput.error.transcribe": "Unable to transcribe the recorded audio.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Launch or scan from another machine to hand over control.",
|
"remoteAccess.sections.addresses.help": "Launch or scan from another machine to hand over control.",
|
||||||
"remoteAccess.addresses.loading": "Loading addresses…",
|
"remoteAccess.addresses.loading": "Loading addresses…",
|
||||||
"remoteAccess.addresses.none": "No addresses available yet.",
|
"remoteAccess.addresses.none": "No addresses available yet.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Show {count} other addresses",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Hide other addresses",
|
||||||
"remoteAccess.address.scope.network": "Network",
|
"remoteAccess.address.scope.network": "Network",
|
||||||
"remoteAccess.address.scope.loopback": "Loopback",
|
"remoteAccess.address.scope.loopback": "Loopback",
|
||||||
"remoteAccess.address.scope.internal": "Internal",
|
"remoteAccess.address.scope.internal": "Internal",
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "Working",
|
"sessionList.status.working": "Working",
|
||||||
"sessionList.status.compacting": "Compacting",
|
"sessionList.status.compacting": "Compacting",
|
||||||
"sessionList.status.idle": "Idle",
|
"sessionList.status.idle": "Idle",
|
||||||
"sessionList.status.retrying": "Retrying",
|
|
||||||
"sessionList.status.retryingIn": "Retrying in {seconds}s",
|
|
||||||
"sessionList.status.retryTooltip": "{message} (Attempt {attempt})",
|
|
||||||
"sessionList.status.retryToast": "{countdown}: {message} (Attempt {attempt})",
|
|
||||||
"sessionList.status.needsPermission": "Needs Permission",
|
"sessionList.status.needsPermission": "Needs Permission",
|
||||||
"sessionList.status.needsInput": "Needs Input",
|
"sessionList.status.needsInput": "Needs Input",
|
||||||
"sessionList.expand.collapseAriaLabel": "Collapse session",
|
"sessionList.expand.collapseAriaLabel": "Collapse session",
|
||||||
@@ -29,15 +25,12 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "New session",
|
"sessionList.actions.newSession.title": "New session",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Copy session ID",
|
"sessionList.actions.copyId.ariaLabel": "Copy session ID",
|
||||||
"sessionList.actions.copyId.title": "Copy session ID",
|
"sessionList.actions.copyId.title": "Copy session ID",
|
||||||
"sessionList.actions.reload.ariaLabel": "Reload session",
|
|
||||||
"sessionList.actions.reload.title": "Reload session",
|
|
||||||
"sessionList.actions.rename.ariaLabel": "Rename session",
|
"sessionList.actions.rename.ariaLabel": "Rename session",
|
||||||
"sessionList.actions.rename.title": "Rename session",
|
"sessionList.actions.rename.title": "Rename session",
|
||||||
"sessionList.actions.delete.ariaLabel": "Delete session",
|
"sessionList.actions.delete.ariaLabel": "Delete session",
|
||||||
"sessionList.actions.delete.title": "Delete session",
|
"sessionList.actions.delete.title": "Delete session",
|
||||||
"sessionList.copyId.success": "Session ID copied",
|
"sessionList.copyId.success": "Session ID copied",
|
||||||
"sessionList.copyId.error": "Unable to copy session ID",
|
"sessionList.copyId.error": "Unable to copy session ID",
|
||||||
"sessionList.reload.error": "Unable to reload session",
|
|
||||||
"sessionList.delete.error": "Unable to delete session",
|
"sessionList.delete.error": "Unable to delete session",
|
||||||
"sessionList.delete.title": "Delete session",
|
"sessionList.delete.title": "Delete session",
|
||||||
"sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.",
|
"sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Sesiones",
|
"instanceShell.leftPanel.sessionsTitle": "Sesiones",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Info de la instancia",
|
"instanceShell.leftPanel.instanceInfo": "Info de la instancia",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Fijar panel izquierdo",
|
"instanceShell.leftDrawer.pin": "Fijar panel izquierdo",
|
||||||
"instanceShell.leftDrawer.unpin": "Desfijar panel izquierdo",
|
"instanceShell.leftDrawer.unpin": "Desfijar panel izquierdo",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Panel izquierdo fijado",
|
"instanceShell.leftDrawer.toggle.pinned": "Panel izquierdo fijado",
|
||||||
@@ -93,21 +94,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.files": "Archivos",
|
"instanceShell.rightPanel.tabs.files": "Archivos",
|
||||||
"instanceShell.rightPanel.tabs.status": "Estado",
|
"instanceShell.rightPanel.tabs.status": "Estado",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
||||||
"instanceShell.rightPanel.actions.refresh": "Actualizar",
|
|
||||||
"instanceShell.rightPanel.actions.save": "Guardar (Ctrl+S)",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.message": "¿Deseas guardar los cambios en \"{path}\" antes de cambiar?",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Guardar",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Descartar cambios",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.message": "El archivo fue modificado por el agente. ¿Sobrescribir los cambios del agente?",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Sobrescribir",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Cancelar",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.message": "El archivo tiene cambios sin guardar. Actualizar discardará tus ediciones. ¿Continuar?",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Actualizar",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancelar",
|
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "Archivo guardado exitosamente",
|
|
||||||
"instanceShell.rightPanel.toast.saveError": "Error al guardar el archivo",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode": "Modo yolo",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Aprueba automaticamente las solicitudes de permiso de la sesion actual. Usalo solo si confias en las herramientas que se estan ejecutando.",
|
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
|
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
@@ -141,12 +127,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "Selecciona una sesión para ver el plan.",
|
"instanceShell.plan.noSessionSelected": "Selecciona una sesión para ver el plan.",
|
||||||
"instanceShell.plan.empty": "Aún no hay nada planificado.",
|
"instanceShell.plan.empty": "Aún no hay nada planificado.",
|
||||||
|
|
||||||
"instanceShell.yoloMode.noSessionSelected": "Selecciona una sesion para configurar el modo yolo.",
|
|
||||||
"instanceShell.yoloMode.title": "Modo yolo",
|
|
||||||
"instanceShell.yoloMode.description": "Aprueba automaticamente las solicitudes de permiso de esta sesion. Esta desactivado por defecto.",
|
|
||||||
"instanceShell.yoloMode.badge": "Modo yolo",
|
|
||||||
"instanceShell.yoloMode.badgeAriaLabel": "Modo yolo activado",
|
|
||||||
|
|
||||||
"instanceShell.backgroundProcesses.empty": "No hay procesos en segundo plano.",
|
"instanceShell.backgroundProcesses.empty": "No hay procesos en segundo plano.",
|
||||||
"instanceShell.backgroundProcesses.status": "Estado: {status}",
|
"instanceShell.backgroundProcesses.status": "Estado: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Salida: {sizeKb} KB",
|
"instanceShell.backgroundProcesses.output": "Salida: {sizeKb} KB",
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export const messagingMessages = {
|
|||||||
"promptInput.voiceInput.transcribing.title": "Transcribiendo audio",
|
"promptInput.voiceInput.transcribing.title": "Transcribiendo audio",
|
||||||
"promptInput.voiceInput.error.title": "La entrada de voz falló",
|
"promptInput.voiceInput.error.title": "La entrada de voz falló",
|
||||||
"promptInput.voiceInput.error.permission": "Se requiere acceso al micrófono para grabar la entrada de voz.",
|
"promptInput.voiceInput.error.permission": "Se requiere acceso al micrófono para grabar la entrada de voz.",
|
||||||
"promptInput.voiceInput.error.permissionDenied": "macOS denegó el acceso al micrófono.",
|
|
||||||
"promptInput.voiceInput.error.unsupported": "La entrada de voz no es compatible con este navegador.",
|
"promptInput.voiceInput.error.unsupported": "La entrada de voz no es compatible con este navegador.",
|
||||||
"promptInput.voiceInput.error.transcribe": "No se pudo transcribir el audio grabado.",
|
"promptInput.voiceInput.error.transcribe": "No se pudo transcribir el audio grabado.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Abre o escanea desde otra máquina para transferir el control.",
|
"remoteAccess.sections.addresses.help": "Abre o escanea desde otra máquina para transferir el control.",
|
||||||
"remoteAccess.addresses.loading": "Cargando direcciones…",
|
"remoteAccess.addresses.loading": "Cargando direcciones…",
|
||||||
"remoteAccess.addresses.none": "Aún no hay direcciones disponibles.",
|
"remoteAccess.addresses.none": "Aún no hay direcciones disponibles.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Mostrar {count} direcciones más",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Ocultar otras direcciones",
|
||||||
"remoteAccess.address.scope.network": "Red",
|
"remoteAccess.address.scope.network": "Red",
|
||||||
"remoteAccess.address.scope.loopback": "Loopback",
|
"remoteAccess.address.scope.loopback": "Loopback",
|
||||||
"remoteAccess.address.scope.internal": "Interna",
|
"remoteAccess.address.scope.internal": "Interna",
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "Trabajando",
|
"sessionList.status.working": "Trabajando",
|
||||||
"sessionList.status.compacting": "Compactando",
|
"sessionList.status.compacting": "Compactando",
|
||||||
"sessionList.status.idle": "Inactiva",
|
"sessionList.status.idle": "Inactiva",
|
||||||
"sessionList.status.retrying": "Reintentando",
|
|
||||||
"sessionList.status.retryingIn": "Reintentando en {seconds}s",
|
|
||||||
"sessionList.status.retryTooltip": "{message} (Intento {attempt})",
|
|
||||||
"sessionList.status.retryToast": "{countdown}: {message} (Intento {attempt})",
|
|
||||||
"sessionList.status.needsPermission": "Requiere permiso",
|
"sessionList.status.needsPermission": "Requiere permiso",
|
||||||
"sessionList.status.needsInput": "Requiere entrada",
|
"sessionList.status.needsInput": "Requiere entrada",
|
||||||
"sessionList.expand.collapseAriaLabel": "Colapsar sesión",
|
"sessionList.expand.collapseAriaLabel": "Colapsar sesión",
|
||||||
@@ -29,15 +25,12 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "Nueva sesión",
|
"sessionList.actions.newSession.title": "Nueva sesión",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Copiar ID de sesión",
|
"sessionList.actions.copyId.ariaLabel": "Copiar ID de sesión",
|
||||||
"sessionList.actions.copyId.title": "Copiar ID de sesión",
|
"sessionList.actions.copyId.title": "Copiar ID de sesión",
|
||||||
"sessionList.actions.reload.ariaLabel": "Recargar sesión",
|
|
||||||
"sessionList.actions.reload.title": "Recargar sesión",
|
|
||||||
"sessionList.actions.rename.ariaLabel": "Renombrar sesión",
|
"sessionList.actions.rename.ariaLabel": "Renombrar sesión",
|
||||||
"sessionList.actions.rename.title": "Renombrar sesión",
|
"sessionList.actions.rename.title": "Renombrar sesión",
|
||||||
"sessionList.actions.delete.ariaLabel": "Eliminar sesión",
|
"sessionList.actions.delete.ariaLabel": "Eliminar sesión",
|
||||||
"sessionList.actions.delete.title": "Eliminar sesión",
|
"sessionList.actions.delete.title": "Eliminar sesión",
|
||||||
"sessionList.copyId.success": "ID de sesión copiado",
|
"sessionList.copyId.success": "ID de sesión copiado",
|
||||||
"sessionList.copyId.error": "No se pudo copiar el ID de sesión",
|
"sessionList.copyId.error": "No se pudo copiar el ID de sesión",
|
||||||
"sessionList.reload.error": "No se pudo recargar la sesión",
|
|
||||||
"sessionList.delete.error": "No se pudo eliminar la sesión",
|
"sessionList.delete.error": "No se pudo eliminar la sesión",
|
||||||
"sessionList.delete.title": "Eliminar sesión",
|
"sessionList.delete.title": "Eliminar sesión",
|
||||||
"sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.",
|
"sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Infos de l'instance",
|
"instanceShell.leftPanel.instanceInfo": "Infos de l'instance",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Épingler le tiroir gauche",
|
"instanceShell.leftDrawer.pin": "Épingler le tiroir gauche",
|
||||||
"instanceShell.leftDrawer.unpin": "Désépingler le tiroir gauche",
|
"instanceShell.leftDrawer.unpin": "Désépingler le tiroir gauche",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Tiroir gauche épinglé",
|
"instanceShell.leftDrawer.toggle.pinned": "Tiroir gauche épinglé",
|
||||||
@@ -93,21 +94,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.files": "Fichiers",
|
"instanceShell.rightPanel.tabs.files": "Fichiers",
|
||||||
"instanceShell.rightPanel.tabs.status": "Statut",
|
"instanceShell.rightPanel.tabs.status": "Statut",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
||||||
"instanceShell.rightPanel.actions.refresh": "Actualiser",
|
|
||||||
"instanceShell.rightPanel.actions.save": "Enregistrer (Ctrl+S)",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.message": "Voulez-vous enregistrer les modifications de \"{path}\" avant de changer ?",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Enregistrer",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Annuler les modifications",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.message": "Le fichier a été modifié par l'agent. Écraser les modifications de l'agent ?",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Écraser",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Annuler",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.message": "Le fichier a des modifications non enregistrées. Actualiser supprimera vos modifications. Continuer ?",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Actualiser",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Annuler",
|
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "Fichier enregistré avec succès",
|
|
||||||
"instanceShell.rightPanel.toast.saveError": "Échec de l'enregistrement du fichier",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode": "Mode yolo",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Approuve automatiquement les demandes d'autorisation pour la session actuelle. A utiliser seulement si vous faites confiance aux outils executes.",
|
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
@@ -141,12 +127,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "Sélectionnez une session pour voir le plan.",
|
"instanceShell.plan.noSessionSelected": "Sélectionnez une session pour voir le plan.",
|
||||||
"instanceShell.plan.empty": "Aucun plan pour l'instant.",
|
"instanceShell.plan.empty": "Aucun plan pour l'instant.",
|
||||||
|
|
||||||
"instanceShell.yoloMode.noSessionSelected": "Selectionnez une session pour configurer le mode yolo.",
|
|
||||||
"instanceShell.yoloMode.title": "Mode yolo",
|
|
||||||
"instanceShell.yoloMode.description": "Approuve automatiquement les demandes d'autorisation pour cette session. Desactive par defaut.",
|
|
||||||
"instanceShell.yoloMode.badge": "Mode yolo",
|
|
||||||
"instanceShell.yoloMode.badgeAriaLabel": "Mode yolo active",
|
|
||||||
|
|
||||||
"instanceShell.backgroundProcesses.empty": "Aucun processus en arrière-plan.",
|
"instanceShell.backgroundProcesses.empty": "Aucun processus en arrière-plan.",
|
||||||
"instanceShell.backgroundProcesses.status": "Statut : {status}",
|
"instanceShell.backgroundProcesses.status": "Statut : {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Sortie : {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "Sortie : {sizeKb}KB",
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export const messagingMessages = {
|
|||||||
"promptInput.voiceInput.transcribing.title": "Transcription de l'audio",
|
"promptInput.voiceInput.transcribing.title": "Transcription de l'audio",
|
||||||
"promptInput.voiceInput.error.title": "Échec de la saisie vocale",
|
"promptInput.voiceInput.error.title": "Échec de la saisie vocale",
|
||||||
"promptInput.voiceInput.error.permission": "L'accès au microphone est requis pour enregistrer la saisie vocale.",
|
"promptInput.voiceInput.error.permission": "L'accès au microphone est requis pour enregistrer la saisie vocale.",
|
||||||
"promptInput.voiceInput.error.permissionDenied": "macOS a refusé l'accès au microphone.",
|
|
||||||
"promptInput.voiceInput.error.unsupported": "La saisie vocale n'est pas prise en charge dans ce navigateur.",
|
"promptInput.voiceInput.error.unsupported": "La saisie vocale n'est pas prise en charge dans ce navigateur.",
|
||||||
"promptInput.voiceInput.error.transcribe": "Impossible de transcrire l'audio enregistré.",
|
"promptInput.voiceInput.error.transcribe": "Impossible de transcrire l'audio enregistré.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Lancez ou scannez depuis une autre machine pour passer le contrôle.",
|
"remoteAccess.sections.addresses.help": "Lancez ou scannez depuis une autre machine pour passer le contrôle.",
|
||||||
"remoteAccess.addresses.loading": "Chargement des adresses…",
|
"remoteAccess.addresses.loading": "Chargement des adresses…",
|
||||||
"remoteAccess.addresses.none": "Aucune adresse disponible pour le moment.",
|
"remoteAccess.addresses.none": "Aucune adresse disponible pour le moment.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Afficher {count} autres adresses",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Masquer les autres adresses",
|
||||||
"remoteAccess.address.scope.network": "Réseau",
|
"remoteAccess.address.scope.network": "Réseau",
|
||||||
"remoteAccess.address.scope.loopback": "Boucle locale",
|
"remoteAccess.address.scope.loopback": "Boucle locale",
|
||||||
"remoteAccess.address.scope.internal": "Interne",
|
"remoteAccess.address.scope.internal": "Interne",
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "En cours",
|
"sessionList.status.working": "En cours",
|
||||||
"sessionList.status.compacting": "Compactage",
|
"sessionList.status.compacting": "Compactage",
|
||||||
"sessionList.status.idle": "Inactif",
|
"sessionList.status.idle": "Inactif",
|
||||||
"sessionList.status.retrying": "Nouvelle tentative",
|
|
||||||
"sessionList.status.retryingIn": "Nouvelle tentative dans {seconds}s",
|
|
||||||
"sessionList.status.retryTooltip": "{message} (Tentative {attempt})",
|
|
||||||
"sessionList.status.retryToast": "{countdown} : {message} (Tentative {attempt})",
|
|
||||||
"sessionList.status.needsPermission": "Autorisation requise",
|
"sessionList.status.needsPermission": "Autorisation requise",
|
||||||
"sessionList.status.needsInput": "Entrée requise",
|
"sessionList.status.needsInput": "Entrée requise",
|
||||||
"sessionList.expand.collapseAriaLabel": "Réduire la session",
|
"sessionList.expand.collapseAriaLabel": "Réduire la session",
|
||||||
@@ -29,15 +25,12 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "Nouvelle session",
|
"sessionList.actions.newSession.title": "Nouvelle session",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Copier l'ID de session",
|
"sessionList.actions.copyId.ariaLabel": "Copier l'ID de session",
|
||||||
"sessionList.actions.copyId.title": "Copier l'ID de session",
|
"sessionList.actions.copyId.title": "Copier l'ID de session",
|
||||||
"sessionList.actions.reload.ariaLabel": "Recharger la session",
|
|
||||||
"sessionList.actions.reload.title": "Recharger la session",
|
|
||||||
"sessionList.actions.rename.ariaLabel": "Renommer la session",
|
"sessionList.actions.rename.ariaLabel": "Renommer la session",
|
||||||
"sessionList.actions.rename.title": "Renommer la session",
|
"sessionList.actions.rename.title": "Renommer la session",
|
||||||
"sessionList.actions.delete.ariaLabel": "Supprimer la session",
|
"sessionList.actions.delete.ariaLabel": "Supprimer la session",
|
||||||
"sessionList.actions.delete.title": "Supprimer la session",
|
"sessionList.actions.delete.title": "Supprimer la session",
|
||||||
"sessionList.copyId.success": "ID de session copié",
|
"sessionList.copyId.success": "ID de session copié",
|
||||||
"sessionList.copyId.error": "Impossible de copier l'ID de session",
|
"sessionList.copyId.error": "Impossible de copier l'ID de session",
|
||||||
"sessionList.reload.error": "Impossible de recharger la session",
|
|
||||||
"sessionList.delete.error": "Impossible de supprimer la session",
|
"sessionList.delete.error": "Impossible de supprimer la session",
|
||||||
"sessionList.delete.title": "Supprimer la session",
|
"sessionList.delete.title": "Supprimer la session",
|
||||||
"sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.",
|
"sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "סשנים",
|
"instanceShell.leftPanel.sessionsTitle": "סשנים",
|
||||||
"instanceShell.leftPanel.instanceInfo": "מידע על המופע",
|
"instanceShell.leftPanel.instanceInfo": "מידע על המופע",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "נעץ מגירה שמאלית",
|
"instanceShell.leftDrawer.pin": "נעץ מגירה שמאלית",
|
||||||
"instanceShell.leftDrawer.unpin": "שחרר נעיצת מגירה שמאלית",
|
"instanceShell.leftDrawer.unpin": "שחרר נעיצת מגירה שמאלית",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "המגירה השמאלית נעוצה",
|
"instanceShell.leftDrawer.toggle.pinned": "המגירה השמאלית נעוצה",
|
||||||
@@ -94,20 +95,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.status": "סטטוס",
|
"instanceShell.rightPanel.tabs.status": "סטטוס",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "לשוניות לוח ימני",
|
"instanceShell.rightPanel.tabs.ariaLabel": "לשוניות לוח ימני",
|
||||||
"instanceShell.rightPanel.actions.refresh": "רענן",
|
"instanceShell.rightPanel.actions.refresh": "רענן",
|
||||||
"instanceShell.rightPanel.actions.save": "שמור (Ctrl+S)",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.message": "האם ברצונך לשמור את השינויים לפני המעבר?",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "שמור",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "בטל שינויים",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.message": "הקובץ שונה על ידי הסוכן. לדרוס את שינויי הסוכן?",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "דרוס",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "בטל",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.message": "לקובץ יש שינויים שלא נשמרו. רענון יבטל את העריכות שלך. להמשיך?",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "רענן",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "בטל",
|
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "הקובץ נשמר בהצלחה",
|
|
||||||
"instanceShell.rightPanel.toast.saveError": "כשלון בשמירת הקובץ",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode": "מצב Yolo",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "מאשר אוטומטית בקשות הרשאה עבור הסשן הנוכחי. השתמשו בזה רק אם אתם סומכים על הכלים שרצים.",
|
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
|
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
|
||||||
"instanceShell.rightPanel.sections.plan": "תוכנית",
|
"instanceShell.rightPanel.sections.plan": "תוכנית",
|
||||||
@@ -149,12 +136,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
||||||
"instanceShell.plan.empty": "עדיין לא תוכנן דבר.",
|
"instanceShell.plan.empty": "עדיין לא תוכנן דבר.",
|
||||||
|
|
||||||
"instanceShell.yoloMode.noSessionSelected": "בחרו סשן כדי להגדיר מצב Yolo.",
|
|
||||||
"instanceShell.yoloMode.title": "מצב Yolo",
|
|
||||||
"instanceShell.yoloMode.description": "מאשר אוטומטית בקשות הרשאה עבור הסשן הזה. כבוי כברירת מחדל.",
|
|
||||||
"instanceShell.yoloMode.badge": "Yolo",
|
|
||||||
"instanceShell.yoloMode.badgeAriaLabel": "מצב Yolo פעיל",
|
|
||||||
|
|
||||||
"instanceShell.backgroundProcesses.empty": "אין תהליכי רקע.",
|
"instanceShell.backgroundProcesses.empty": "אין תהליכי רקע.",
|
||||||
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ export const messagingMessages = {
|
|||||||
"promptInput.voiceInput.transcribing.title": "מתמלל אודיו",
|
"promptInput.voiceInput.transcribing.title": "מתמלל אודיו",
|
||||||
"promptInput.voiceInput.error.title": "קלט קולי נכשל",
|
"promptInput.voiceInput.error.title": "קלט קולי נכשל",
|
||||||
"promptInput.voiceInput.error.permission": "נדרשת גישה למיקרופון כדי להקליט קלט קולי.",
|
"promptInput.voiceInput.error.permission": "נדרשת גישה למיקרופון כדי להקליט קלט קולי.",
|
||||||
"promptInput.voiceInput.error.permissionDenied": "הגישה למיקרופון נדחתה על ידי macOS.",
|
|
||||||
"promptInput.voiceInput.error.unsupported": "קלט קולי אינו נתמך בדפדפן זה.",
|
"promptInput.voiceInput.error.unsupported": "קלט קולי אינו נתמך בדפדפן זה.",
|
||||||
"promptInput.voiceInput.error.transcribe": "לא ניתן היה לתמלל את האודיו שהוקלט.",
|
"promptInput.voiceInput.error.transcribe": "לא ניתן היה לתמלל את האודיו שהוקלט.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "הפעל או סרוק ממכונה אחרת להעברת שליטה.",
|
"remoteAccess.sections.addresses.help": "הפעל או סרוק ממכונה אחרת להעברת שליטה.",
|
||||||
"remoteAccess.addresses.loading": "טוען כתובות…",
|
"remoteAccess.addresses.loading": "טוען כתובות…",
|
||||||
"remoteAccess.addresses.none": "אין כתובות זמינות עדיין.",
|
"remoteAccess.addresses.none": "אין כתובות זמינות עדיין.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "הצג עוד {count} כתובות",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "הסתר כתובות נוספות",
|
||||||
"remoteAccess.address.scope.network": "רשת",
|
"remoteAccess.address.scope.network": "רשת",
|
||||||
"remoteAccess.address.scope.loopback": "לולאה מקומית",
|
"remoteAccess.address.scope.loopback": "לולאה מקומית",
|
||||||
"remoteAccess.address.scope.internal": "פנימי",
|
"remoteAccess.address.scope.internal": "פנימי",
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "עובד",
|
"sessionList.status.working": "עובד",
|
||||||
"sessionList.status.compacting": "מסכם",
|
"sessionList.status.compacting": "מסכם",
|
||||||
"sessionList.status.idle": "מוכן",
|
"sessionList.status.idle": "מוכן",
|
||||||
"sessionList.status.retrying": "מנסה שוב",
|
|
||||||
"sessionList.status.retryingIn": "מנסה שוב בעוד {seconds}ש׳",
|
|
||||||
"sessionList.status.retryTooltip": "{message} (ניסיון {attempt})",
|
|
||||||
"sessionList.status.retryToast": "{countdown}: {message} (ניסיון {attempt})",
|
|
||||||
"sessionList.status.needsPermission": "נדרש אישור",
|
"sessionList.status.needsPermission": "נדרש אישור",
|
||||||
"sessionList.status.needsInput": "נדרש קלט",
|
"sessionList.status.needsInput": "נדרש קלט",
|
||||||
"sessionList.expand.collapseAriaLabel": "כווץ סשן",
|
"sessionList.expand.collapseAriaLabel": "כווץ סשן",
|
||||||
@@ -29,15 +25,12 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "סשן חדש",
|
"sessionList.actions.newSession.title": "סשן חדש",
|
||||||
"sessionList.actions.copyId.ariaLabel": "העתק מזהה סשן",
|
"sessionList.actions.copyId.ariaLabel": "העתק מזהה סשן",
|
||||||
"sessionList.actions.copyId.title": "העתק מזהה סשן",
|
"sessionList.actions.copyId.title": "העתק מזהה סשן",
|
||||||
"sessionList.actions.reload.ariaLabel": "טען מחדש סשן",
|
|
||||||
"sessionList.actions.reload.title": "טען מחדש סשן",
|
|
||||||
"sessionList.actions.rename.ariaLabel": "שנה שם סשן",
|
"sessionList.actions.rename.ariaLabel": "שנה שם סשן",
|
||||||
"sessionList.actions.rename.title": "שנה שם סשן",
|
"sessionList.actions.rename.title": "שנה שם סשן",
|
||||||
"sessionList.actions.delete.ariaLabel": "מחק סשן",
|
"sessionList.actions.delete.ariaLabel": "מחק סשן",
|
||||||
"sessionList.actions.delete.title": "מחק סשן",
|
"sessionList.actions.delete.title": "מחק סשן",
|
||||||
"sessionList.copyId.success": "מזהה סשן הועתק",
|
"sessionList.copyId.success": "מזהה סשן הועתק",
|
||||||
"sessionList.copyId.error": "לא ניתן להעתיק מזהה סשן",
|
"sessionList.copyId.error": "לא ניתן להעתיק מזהה סשן",
|
||||||
"sessionList.reload.error": "לא ניתן לטעון מחדש את הסשן",
|
|
||||||
"sessionList.delete.error": "לא ניתן למחוק סשן",
|
"sessionList.delete.error": "לא ניתן למחוק סשן",
|
||||||
"sessionList.delete.title": "מחק סשן",
|
"sessionList.delete.title": "מחק סשן",
|
||||||
"sessionList.delete.confirmMessage": "למחוק את \"{label}\"? לא ניתן לבטל פעולה זו.",
|
"sessionList.delete.confirmMessage": "למחוק את \"{label}\"? לא ניתן לבטל פעולה זו.",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "セッション",
|
"instanceShell.leftPanel.sessionsTitle": "セッション",
|
||||||
"instanceShell.leftPanel.instanceInfo": "インスタンス情報",
|
"instanceShell.leftPanel.instanceInfo": "インスタンス情報",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "左ドロワーを固定",
|
"instanceShell.leftDrawer.pin": "左ドロワーを固定",
|
||||||
"instanceShell.leftDrawer.unpin": "左ドロワーの固定を解除",
|
"instanceShell.leftDrawer.unpin": "左ドロワーの固定を解除",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "左ドロワーを固定しました",
|
"instanceShell.leftDrawer.toggle.pinned": "左ドロワーを固定しました",
|
||||||
@@ -93,21 +94,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.files": "ファイル",
|
"instanceShell.rightPanel.tabs.files": "ファイル",
|
||||||
"instanceShell.rightPanel.tabs.status": "ステータス",
|
"instanceShell.rightPanel.tabs.status": "ステータス",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
||||||
"instanceShell.rightPanel.actions.refresh": "更新",
|
|
||||||
"instanceShell.rightPanel.actions.save": "保存 (Ctrl+S)",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.message": "「{path}」への変更を切り替え前に保存しますか?",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "保存",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "変更を破棄",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.message": "ファイルはエージェントによって変更されました。上書きしますか?",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "上書き",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "キャンセル",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.message": "ファイルには未保存の変更があります。更新すると編集が破棄されます。続行しますか?",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "更新",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "キャンセル",
|
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "ファイルを保存しました",
|
|
||||||
"instanceShell.rightPanel.toast.saveError": "ファイルの保存に失敗しました",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode": "Yoloモード",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "現在のセッションの権限リクエストを自動承認します。実行中のツールを信頼できる場合にのみ使用してください。",
|
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
||||||
"instanceShell.rightPanel.sections.plan": "計画",
|
"instanceShell.rightPanel.sections.plan": "計画",
|
||||||
@@ -141,12 +127,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
||||||
"instanceShell.plan.empty": "まだ計画はありません。",
|
"instanceShell.plan.empty": "まだ計画はありません。",
|
||||||
|
|
||||||
"instanceShell.yoloMode.noSessionSelected": "Yoloモードを設定するにはセッションを選択してください。",
|
|
||||||
"instanceShell.yoloMode.title": "Yoloモード",
|
|
||||||
"instanceShell.yoloMode.description": "このセッションの権限リクエストを自動承認します。デフォルトでは無効です。",
|
|
||||||
"instanceShell.yoloMode.badge": "Yolo",
|
|
||||||
"instanceShell.yoloMode.badgeAriaLabel": "Yoloモードが有効",
|
|
||||||
|
|
||||||
"instanceShell.backgroundProcesses.empty": "バックグラウンドプロセスはありません。",
|
"instanceShell.backgroundProcesses.empty": "バックグラウンドプロセスはありません。",
|
||||||
"instanceShell.backgroundProcesses.status": "状態: {status}",
|
"instanceShell.backgroundProcesses.status": "状態: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "出力: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "出力: {sizeKb}KB",
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export const messagingMessages = {
|
|||||||
"promptInput.voiceInput.transcribing.title": "音声を文字起こし中",
|
"promptInput.voiceInput.transcribing.title": "音声を文字起こし中",
|
||||||
"promptInput.voiceInput.error.title": "音声入力に失敗しました",
|
"promptInput.voiceInput.error.title": "音声入力に失敗しました",
|
||||||
"promptInput.voiceInput.error.permission": "音声入力を録音するにはマイクへのアクセスが必要です。",
|
"promptInput.voiceInput.error.permission": "音声入力を録音するにはマイクへのアクセスが必要です。",
|
||||||
"promptInput.voiceInput.error.permissionDenied": "macOS によりマイクへのアクセスが拒否されました。",
|
|
||||||
"promptInput.voiceInput.error.unsupported": "このブラウザーでは音声入力はサポートされていません。",
|
"promptInput.voiceInput.error.unsupported": "このブラウザーでは音声入力はサポートされていません。",
|
||||||
"promptInput.voiceInput.error.transcribe": "録音した音声を文字起こしできませんでした。",
|
"promptInput.voiceInput.error.transcribe": "録音した音声を文字起こしできませんでした。",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "別の端末から起動またはスキャンして操作を引き継ぎます。",
|
"remoteAccess.sections.addresses.help": "別の端末から起動またはスキャンして操作を引き継ぎます。",
|
||||||
"remoteAccess.addresses.loading": "アドレスを読み込み中…",
|
"remoteAccess.addresses.loading": "アドレスを読み込み中…",
|
||||||
"remoteAccess.addresses.none": "まだ利用可能なアドレスがありません。",
|
"remoteAccess.addresses.none": "まだ利用可能なアドレスがありません。",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "他の {count} 件のアドレスを表示",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "他のアドレスを隠す",
|
||||||
"remoteAccess.address.scope.network": "ネットワーク",
|
"remoteAccess.address.scope.network": "ネットワーク",
|
||||||
"remoteAccess.address.scope.loopback": "ループバック",
|
"remoteAccess.address.scope.loopback": "ループバック",
|
||||||
"remoteAccess.address.scope.internal": "内部",
|
"remoteAccess.address.scope.internal": "内部",
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "作業中",
|
"sessionList.status.working": "作業中",
|
||||||
"sessionList.status.compacting": "圧縮中",
|
"sessionList.status.compacting": "圧縮中",
|
||||||
"sessionList.status.idle": "待機中",
|
"sessionList.status.idle": "待機中",
|
||||||
"sessionList.status.retrying": "再試行中",
|
|
||||||
"sessionList.status.retryingIn": "{seconds}秒後に再試行",
|
|
||||||
"sessionList.status.retryTooltip": "{message}({attempt}回目)",
|
|
||||||
"sessionList.status.retryToast": "{countdown}: {message}({attempt}回目)",
|
|
||||||
"sessionList.status.needsPermission": "許可待ち",
|
"sessionList.status.needsPermission": "許可待ち",
|
||||||
"sessionList.status.needsInput": "入力待ち",
|
"sessionList.status.needsInput": "入力待ち",
|
||||||
"sessionList.expand.collapseAriaLabel": "セッションを折りたたむ",
|
"sessionList.expand.collapseAriaLabel": "セッションを折りたたむ",
|
||||||
@@ -29,15 +25,12 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "新しいセッション",
|
"sessionList.actions.newSession.title": "新しいセッション",
|
||||||
"sessionList.actions.copyId.ariaLabel": "セッション ID をコピー",
|
"sessionList.actions.copyId.ariaLabel": "セッション ID をコピー",
|
||||||
"sessionList.actions.copyId.title": "セッション ID をコピー",
|
"sessionList.actions.copyId.title": "セッション ID をコピー",
|
||||||
"sessionList.actions.reload.ariaLabel": "セッションを再読み込み",
|
|
||||||
"sessionList.actions.reload.title": "セッションを再読み込み",
|
|
||||||
"sessionList.actions.rename.ariaLabel": "セッション名を変更",
|
"sessionList.actions.rename.ariaLabel": "セッション名を変更",
|
||||||
"sessionList.actions.rename.title": "セッション名を変更",
|
"sessionList.actions.rename.title": "セッション名を変更",
|
||||||
"sessionList.actions.delete.ariaLabel": "セッションを削除",
|
"sessionList.actions.delete.ariaLabel": "セッションを削除",
|
||||||
"sessionList.actions.delete.title": "セッションを削除",
|
"sessionList.actions.delete.title": "セッションを削除",
|
||||||
"sessionList.copyId.success": "セッション ID をコピーしました",
|
"sessionList.copyId.success": "セッション ID をコピーしました",
|
||||||
"sessionList.copyId.error": "セッション ID をコピーできません",
|
"sessionList.copyId.error": "セッション ID をコピーできません",
|
||||||
"sessionList.reload.error": "セッションを再読み込みできません",
|
|
||||||
"sessionList.delete.error": "セッションを削除できません",
|
"sessionList.delete.error": "セッションを削除できません",
|
||||||
"sessionList.delete.title": "セッションを削除",
|
"sessionList.delete.title": "セッションを削除",
|
||||||
"sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。",
|
"sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Сессии",
|
"instanceShell.leftPanel.sessionsTitle": "Сессии",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Информация об экземпляре",
|
"instanceShell.leftPanel.instanceInfo": "Информация об экземпляре",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Закрепить левую панель",
|
"instanceShell.leftDrawer.pin": "Закрепить левую панель",
|
||||||
"instanceShell.leftDrawer.unpin": "Открепить левую панель",
|
"instanceShell.leftDrawer.unpin": "Открепить левую панель",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Левая панель закреплена",
|
"instanceShell.leftDrawer.toggle.pinned": "Левая панель закреплена",
|
||||||
@@ -93,21 +94,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.files": "Файлы",
|
"instanceShell.rightPanel.tabs.files": "Файлы",
|
||||||
"instanceShell.rightPanel.tabs.status": "Статус",
|
"instanceShell.rightPanel.tabs.status": "Статус",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
||||||
"instanceShell.rightPanel.actions.refresh": "Обновить",
|
|
||||||
"instanceShell.rightPanel.actions.save": "Сохранить (Ctrl+S)",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.message": "Сохранить изменения в \"{path}\" перед переключением?",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Сохранить",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Отменить изменения",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.message": "Файл был изменён агентом. Перезаписать изменения агента?",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Перезаписать",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Отмена",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.message": "Файл имеет несохранённые изменения. Обновление отменит ваши правки. Продолжить?",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Обновить",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Отмена",
|
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "Файл успешно сохранён",
|
|
||||||
"instanceShell.rightPanel.toast.saveError": "Не удалось сохранить файл",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode": "Режим Yolo",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Автоматически одобряет запросы разрешений для текущей сессии. Включайте только если доверяете запускаемым инструментам.",
|
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
||||||
"instanceShell.rightPanel.sections.plan": "План",
|
"instanceShell.rightPanel.sections.plan": "План",
|
||||||
@@ -141,12 +127,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
|
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
|
||||||
"instanceShell.plan.empty": "Пока ничего не запланировано.",
|
"instanceShell.plan.empty": "Пока ничего не запланировано.",
|
||||||
|
|
||||||
"instanceShell.yoloMode.noSessionSelected": "Выберите сессию, чтобы настроить режим Yolo.",
|
|
||||||
"instanceShell.yoloMode.title": "Режим Yolo",
|
|
||||||
"instanceShell.yoloMode.description": "Автоматически одобряет запросы разрешений для этой сессии. По умолчанию выключен.",
|
|
||||||
"instanceShell.yoloMode.badge": "Yolo",
|
|
||||||
"instanceShell.yoloMode.badgeAriaLabel": "Режим Yolo включен",
|
|
||||||
|
|
||||||
"instanceShell.backgroundProcesses.empty": "Нет фоновых процессов.",
|
"instanceShell.backgroundProcesses.empty": "Нет фоновых процессов.",
|
||||||
"instanceShell.backgroundProcesses.status": "Статус: {status}",
|
"instanceShell.backgroundProcesses.status": "Статус: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Вывод: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "Вывод: {sizeKb}KB",
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export const messagingMessages = {
|
|||||||
"promptInput.voiceInput.transcribing.title": "Идёт расшифровка аудио",
|
"promptInput.voiceInput.transcribing.title": "Идёт расшифровка аудио",
|
||||||
"promptInput.voiceInput.error.title": "Сбой голосового ввода",
|
"promptInput.voiceInput.error.title": "Сбой голосового ввода",
|
||||||
"promptInput.voiceInput.error.permission": "Для записи голосового ввода требуется доступ к микрофону.",
|
"promptInput.voiceInput.error.permission": "Для записи голосового ввода требуется доступ к микрофону.",
|
||||||
"promptInput.voiceInput.error.permissionDenied": "macOS запретила доступ к микрофону.",
|
|
||||||
"promptInput.voiceInput.error.unsupported": "Голосовой ввод не поддерживается в этом браузере.",
|
"promptInput.voiceInput.error.unsupported": "Голосовой ввод не поддерживается в этом браузере.",
|
||||||
"promptInput.voiceInput.error.transcribe": "Не удалось расшифровать записанное аудио.",
|
"promptInput.voiceInput.error.transcribe": "Не удалось расшифровать записанное аудио.",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Откройте или отсканируйте с другой машины, чтобы передать управление.",
|
"remoteAccess.sections.addresses.help": "Откройте или отсканируйте с другой машины, чтобы передать управление.",
|
||||||
"remoteAccess.addresses.loading": "Загрузка адресов…",
|
"remoteAccess.addresses.loading": "Загрузка адресов…",
|
||||||
"remoteAccess.addresses.none": "Пока нет доступных адресов.",
|
"remoteAccess.addresses.none": "Пока нет доступных адресов.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Показать еще {count} адресов",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Скрыть остальные адреса",
|
||||||
"remoteAccess.address.scope.network": "Сеть",
|
"remoteAccess.address.scope.network": "Сеть",
|
||||||
"remoteAccess.address.scope.loopback": "Loopback",
|
"remoteAccess.address.scope.loopback": "Loopback",
|
||||||
"remoteAccess.address.scope.internal": "Внутренний",
|
"remoteAccess.address.scope.internal": "Внутренний",
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "Работает",
|
"sessionList.status.working": "Работает",
|
||||||
"sessionList.status.compacting": "Компактация",
|
"sessionList.status.compacting": "Компактация",
|
||||||
"sessionList.status.idle": "Простой",
|
"sessionList.status.idle": "Простой",
|
||||||
"sessionList.status.retrying": "Повтор",
|
|
||||||
"sessionList.status.retryingIn": "Повтор через {seconds}с",
|
|
||||||
"sessionList.status.retryTooltip": "{message} (Попытка {attempt})",
|
|
||||||
"sessionList.status.retryToast": "{countdown}: {message} (Попытка {attempt})",
|
|
||||||
"sessionList.status.needsPermission": "Требуется разрешение",
|
"sessionList.status.needsPermission": "Требуется разрешение",
|
||||||
"sessionList.status.needsInput": "Требуется ввод",
|
"sessionList.status.needsInput": "Требуется ввод",
|
||||||
"sessionList.expand.collapseAriaLabel": "Свернуть сессию",
|
"sessionList.expand.collapseAriaLabel": "Свернуть сессию",
|
||||||
@@ -29,15 +25,12 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "Новая сессия",
|
"sessionList.actions.newSession.title": "Новая сессия",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Скопировать ID сессии",
|
"sessionList.actions.copyId.ariaLabel": "Скопировать ID сессии",
|
||||||
"sessionList.actions.copyId.title": "Скопировать ID сессии",
|
"sessionList.actions.copyId.title": "Скопировать ID сессии",
|
||||||
"sessionList.actions.reload.ariaLabel": "Обновить сессию",
|
|
||||||
"sessionList.actions.reload.title": "Обновить сессию",
|
|
||||||
"sessionList.actions.rename.ariaLabel": "Переименовать сессию",
|
"sessionList.actions.rename.ariaLabel": "Переименовать сессию",
|
||||||
"sessionList.actions.rename.title": "Переименовать сессию",
|
"sessionList.actions.rename.title": "Переименовать сессию",
|
||||||
"sessionList.actions.delete.ariaLabel": "Удалить сессию",
|
"sessionList.actions.delete.ariaLabel": "Удалить сессию",
|
||||||
"sessionList.actions.delete.title": "Удалить сессию",
|
"sessionList.actions.delete.title": "Удалить сессию",
|
||||||
"sessionList.copyId.success": "ID сессии скопирован",
|
"sessionList.copyId.success": "ID сессии скопирован",
|
||||||
"sessionList.copyId.error": "Не удалось скопировать ID сессии",
|
"sessionList.copyId.error": "Не удалось скопировать ID сессии",
|
||||||
"sessionList.reload.error": "Не удалось обновить сессию",
|
|
||||||
"sessionList.delete.error": "Не удалось удалить сессию",
|
"sessionList.delete.error": "Не удалось удалить сессию",
|
||||||
"sessionList.delete.title": "Удалить сессию",
|
"sessionList.delete.title": "Удалить сессию",
|
||||||
"sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.",
|
"sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "会话",
|
"instanceShell.leftPanel.sessionsTitle": "会话",
|
||||||
"instanceShell.leftPanel.instanceInfo": "实例信息",
|
"instanceShell.leftPanel.instanceInfo": "实例信息",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "固定左侧抽屉",
|
"instanceShell.leftDrawer.pin": "固定左侧抽屉",
|
||||||
"instanceShell.leftDrawer.unpin": "取消固定左侧抽屉",
|
"instanceShell.leftDrawer.unpin": "取消固定左侧抽屉",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "左侧抽屉已固定",
|
"instanceShell.leftDrawer.toggle.pinned": "左侧抽屉已固定",
|
||||||
@@ -93,21 +94,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.tabs.files": "文件",
|
"instanceShell.rightPanel.tabs.files": "文件",
|
||||||
"instanceShell.rightPanel.tabs.status": "状态",
|
"instanceShell.rightPanel.tabs.status": "状态",
|
||||||
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
||||||
"instanceShell.rightPanel.actions.refresh": "刷新",
|
|
||||||
"instanceShell.rightPanel.actions.save": "保存 (Ctrl+S)",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.message": "切换前是否保存对 \"{path}\" 的更改?",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "保存",
|
|
||||||
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "放弃更改",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.message": "文件已被代理修改。是否覆盖代理的更改?",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.confirmLabel": "覆盖",
|
|
||||||
"instanceShell.rightPanel.actions.conflict.cancelLabel": "取消",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.message": "文件有未保存的更改。刷新将放弃您的编辑。继续?",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "刷新",
|
|
||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "取消",
|
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "文件保存成功",
|
|
||||||
"instanceShell.rightPanel.toast.saveError": "保存文件失败",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode": "Yolo 模式",
|
|
||||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "自动批准当前会话的权限请求。仅在你信任正在运行的工具时启用。",
|
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
||||||
"instanceShell.rightPanel.sections.plan": "计划",
|
"instanceShell.rightPanel.sections.plan": "计划",
|
||||||
@@ -141,12 +127,6 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
|
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
|
||||||
"instanceShell.plan.empty": "暂无计划。",
|
"instanceShell.plan.empty": "暂无计划。",
|
||||||
|
|
||||||
"instanceShell.yoloMode.noSessionSelected": "请选择一个会话来配置 Yolo 模式。",
|
|
||||||
"instanceShell.yoloMode.title": "Yolo 模式",
|
|
||||||
"instanceShell.yoloMode.description": "自动批准此会话的权限请求。默认关闭。",
|
|
||||||
"instanceShell.yoloMode.badge": "Yolo",
|
|
||||||
"instanceShell.yoloMode.badgeAriaLabel": "Yolo 模式已启用",
|
|
||||||
|
|
||||||
"instanceShell.backgroundProcesses.empty": "没有后台进程。",
|
"instanceShell.backgroundProcesses.empty": "没有后台进程。",
|
||||||
"instanceShell.backgroundProcesses.status": "状态:{status}",
|
"instanceShell.backgroundProcesses.status": "状态:{status}",
|
||||||
"instanceShell.backgroundProcesses.output": "输出:{sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "输出:{sizeKb}KB",
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export const messagingMessages = {
|
|||||||
"promptInput.voiceInput.transcribing.title": "正在转写音频",
|
"promptInput.voiceInput.transcribing.title": "正在转写音频",
|
||||||
"promptInput.voiceInput.error.title": "语音输入失败",
|
"promptInput.voiceInput.error.title": "语音输入失败",
|
||||||
"promptInput.voiceInput.error.permission": "录制语音输入需要麦克风访问权限。",
|
"promptInput.voiceInput.error.permission": "录制语音输入需要麦克风访问权限。",
|
||||||
"promptInput.voiceInput.error.permissionDenied": "macOS 已拒绝麦克风访问。",
|
|
||||||
"promptInput.voiceInput.error.unsupported": "此浏览器不支持语音输入。",
|
"promptInput.voiceInput.error.unsupported": "此浏览器不支持语音输入。",
|
||||||
"promptInput.voiceInput.error.transcribe": "无法转写录制的音频。",
|
"promptInput.voiceInput.error.transcribe": "无法转写录制的音频。",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "从另一台设备打开或扫描,以接管控制权。",
|
"remoteAccess.sections.addresses.help": "从另一台设备打开或扫描,以接管控制权。",
|
||||||
"remoteAccess.addresses.loading": "正在加载地址…",
|
"remoteAccess.addresses.loading": "正在加载地址…",
|
||||||
"remoteAccess.addresses.none": "暂时没有可用地址。",
|
"remoteAccess.addresses.none": "暂时没有可用地址。",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "显示另外 {count} 个地址",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "隐藏其他地址",
|
||||||
"remoteAccess.address.scope.network": "网络",
|
"remoteAccess.address.scope.network": "网络",
|
||||||
"remoteAccess.address.scope.loopback": "回环",
|
"remoteAccess.address.scope.loopback": "回环",
|
||||||
"remoteAccess.address.scope.internal": "内部",
|
"remoteAccess.address.scope.internal": "内部",
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "工作中",
|
"sessionList.status.working": "工作中",
|
||||||
"sessionList.status.compacting": "压缩中",
|
"sessionList.status.compacting": "压缩中",
|
||||||
"sessionList.status.idle": "空闲",
|
"sessionList.status.idle": "空闲",
|
||||||
"sessionList.status.retrying": "重试中",
|
|
||||||
"sessionList.status.retryingIn": "{seconds} 秒后重试",
|
|
||||||
"sessionList.status.retryTooltip": "{message}(第 {attempt} 次尝试)",
|
|
||||||
"sessionList.status.retryToast": "{countdown}: {message}(第 {attempt} 次尝试)",
|
|
||||||
"sessionList.status.needsPermission": "需要权限",
|
"sessionList.status.needsPermission": "需要权限",
|
||||||
"sessionList.status.needsInput": "需要输入",
|
"sessionList.status.needsInput": "需要输入",
|
||||||
"sessionList.expand.collapseAriaLabel": "折叠会话",
|
"sessionList.expand.collapseAriaLabel": "折叠会话",
|
||||||
@@ -29,15 +25,12 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "新建会话",
|
"sessionList.actions.newSession.title": "新建会话",
|
||||||
"sessionList.actions.copyId.ariaLabel": "复制会话 ID",
|
"sessionList.actions.copyId.ariaLabel": "复制会话 ID",
|
||||||
"sessionList.actions.copyId.title": "复制会话 ID",
|
"sessionList.actions.copyId.title": "复制会话 ID",
|
||||||
"sessionList.actions.reload.ariaLabel": "重新加载会话",
|
|
||||||
"sessionList.actions.reload.title": "重新加载会话",
|
|
||||||
"sessionList.actions.rename.ariaLabel": "重命名会话",
|
"sessionList.actions.rename.ariaLabel": "重命名会话",
|
||||||
"sessionList.actions.rename.title": "重命名会话",
|
"sessionList.actions.rename.title": "重命名会话",
|
||||||
"sessionList.actions.delete.ariaLabel": "删除会话",
|
"sessionList.actions.delete.ariaLabel": "删除会话",
|
||||||
"sessionList.actions.delete.title": "删除会话",
|
"sessionList.actions.delete.title": "删除会话",
|
||||||
"sessionList.copyId.success": "已复制会话 ID",
|
"sessionList.copyId.success": "已复制会话 ID",
|
||||||
"sessionList.copyId.error": "无法复制会话 ID",
|
"sessionList.copyId.error": "无法复制会话 ID",
|
||||||
"sessionList.reload.error": "无法重新加载会话",
|
|
||||||
"sessionList.delete.error": "无法删除会话",
|
"sessionList.delete.error": "无法删除会话",
|
||||||
"sessionList.delete.title": "删除会话",
|
"sessionList.delete.title": "删除会话",
|
||||||
"sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。",
|
"sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ let highlighterPromise: Promise<Highlighter> | null = null
|
|||||||
let currentTheme: "light" | "dark" = "light"
|
let currentTheme: "light" | "dark" = "light"
|
||||||
let isInitialized = false
|
let isInitialized = false
|
||||||
let highlightSuppressed = false
|
let highlightSuppressed = false
|
||||||
let escapeRawHtmlEnabled = false
|
|
||||||
let rendererSetup = false
|
let rendererSetup = false
|
||||||
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
||||||
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | null = null
|
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | null = null
|
||||||
@@ -120,7 +119,14 @@ function resolveLanguage(token: string): { canonical: string | null; raw: string
|
|||||||
return { canonical: null, raw: normalized }
|
return { canonical: null, raw: normalized }
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectCodeFenceLanguages(content: string): string[] {
|
async function ensureLanguages(content: string) {
|
||||||
|
if (highlightSuppressed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
||||||
|
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
||||||
|
// to miss these and prevent languages from loading.
|
||||||
const foundLanguages = new Set<string>()
|
const foundLanguages = new Set<string>()
|
||||||
try {
|
try {
|
||||||
const tokens = marked.lexer(content) as any
|
const tokens = marked.lexer(content) as any
|
||||||
@@ -132,44 +138,10 @@ function collectCodeFenceLanguages(content: string): string[] {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
// If tokenization fails for any reason, skip language preloading.
|
||||||
}
|
|
||||||
|
|
||||||
return [...foundLanguages]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasPendingCodeHighlight(content: string): boolean {
|
|
||||||
const languages = collectCodeFenceLanguages(content)
|
|
||||||
for (const token of languages) {
|
|
||||||
const rawToken = normalizeLanguageToken(token)
|
|
||||||
if (!rawToken || rawToken === "text") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const { canonical, raw } = resolveLanguage(token)
|
|
||||||
const langKey = canonical || raw
|
|
||||||
if (langKey === "text" || raw === "text") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!highlighter || !loadedLanguages.has(langKey)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureLanguages(content: string) {
|
|
||||||
if (highlightSuppressed) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
|
||||||
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
|
||||||
// to miss these and prevent languages from loading.
|
|
||||||
const foundLanguages = collectCodeFenceLanguages(content)
|
|
||||||
|
|
||||||
// Queue language loading tasks
|
// Queue language loading tasks
|
||||||
for (const token of foundLanguages) {
|
for (const token of foundLanguages) {
|
||||||
const rawToken = normalizeLanguageToken(token)
|
const rawToken = normalizeLanguageToken(token)
|
||||||
@@ -313,14 +285,6 @@ function setupRenderer(isDark: boolean) {
|
|||||||
return `<code class="inline-code">${escapeHtml(decoded)}</code>`
|
return `<code class="inline-code">${escapeHtml(decoded)}</code>`
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.html = (html: string) => {
|
|
||||||
if (!escapeRawHtmlEnabled) {
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
return escapeHtml(decodeHtmlEntities(html))
|
|
||||||
}
|
|
||||||
|
|
||||||
marked.use({ renderer })
|
marked.use({ renderer })
|
||||||
rendererSetup = true
|
rendererSetup = true
|
||||||
}
|
}
|
||||||
@@ -344,7 +308,6 @@ export async function renderMarkdown(
|
|||||||
content: string,
|
content: string,
|
||||||
options?: {
|
options?: {
|
||||||
suppressHighlight?: boolean
|
suppressHighlight?: boolean
|
||||||
escapeRawHtml?: boolean
|
|
||||||
},
|
},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
@@ -353,7 +316,6 @@ export async function renderMarkdown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const suppressHighlight = options?.suppressHighlight ?? false
|
const suppressHighlight = options?.suppressHighlight ?? false
|
||||||
const escapeRawHtml = options?.escapeRawHtml ?? false
|
|
||||||
const decoded = decodeHtmlEntities(content)
|
const decoded = decodeHtmlEntities(content)
|
||||||
|
|
||||||
if (!suppressHighlight) {
|
if (!suppressHighlight) {
|
||||||
@@ -362,16 +324,13 @@ export async function renderMarkdown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousSuppressed = highlightSuppressed
|
const previousSuppressed = highlightSuppressed
|
||||||
const previousEscapeRawHtml = escapeRawHtmlEnabled
|
|
||||||
highlightSuppressed = suppressHighlight
|
highlightSuppressed = suppressHighlight
|
||||||
escapeRawHtmlEnabled = escapeRawHtml
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Proceed to parse immediately - highlighting will be available on next render
|
// Proceed to parse immediately - highlighting will be available on next render
|
||||||
return marked.parse(decoded) as Promise<string>
|
return marked.parse(decoded) as Promise<string>
|
||||||
} finally {
|
} finally {
|
||||||
highlightSuppressed = previousSuppressed
|
highlightSuppressed = previousSuppressed
|
||||||
escapeRawHtmlEnabled = previousEscapeRawHtml
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,11 +102,9 @@ export function showToastNotification(payload: ToastPayload): ToastHandle {
|
|||||||
</button>
|
</button>
|
||||||
<div class="flex items-start gap-3 pr-6">
|
<div class="flex items-start gap-3 pr-6">
|
||||||
<span class={`mt-1 inline-block h-2.5 w-2.5 rounded-full ${accent.badge}`} />
|
<span class={`mt-1 inline-block h-2.5 w-2.5 rounded-full ${accent.badge}`} />
|
||||||
<div class="min-w-0 flex-1 text-sm leading-snug">
|
<div class="flex-1 text-sm leading-snug">
|
||||||
{payload.title && <p class={`break-words ${accent.headline} font-semibold`}>{payload.title}</p>}
|
{payload.title && <p class={`font-semibold ${accent.headline}`}>{payload.title}</p>}
|
||||||
<p class={`${accent.body} ${payload.title ? "mt-1" : ""} whitespace-pre-wrap break-words [overflow-wrap:anywhere]`}>
|
<p class={`${accent.body} ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
||||||
{payload.message}
|
|
||||||
</p>
|
|
||||||
{payload.action && (
|
{payload.action && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
17
packages/ui/src/lib/remote-access-addresses.test.ts
Normal file
17
packages/ui/src/lib/remote-access-addresses.test.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { describe, it } from "node:test"
|
||||||
|
|
||||||
|
import { splitRemoteAddresses } from "./remote-access-addresses"
|
||||||
|
|
||||||
|
describe("splitRemoteAddresses", () => {
|
||||||
|
it("keeps the first remote address visible and collapses the rest", () => {
|
||||||
|
const result = splitRemoteAddresses([
|
||||||
|
{ ip: "127.0.0.1", family: "ipv4", scope: "loopback", remoteUrl: "https://127.0.0.1:9898" },
|
||||||
|
{ ip: "192.168.1.128", family: "ipv4", scope: "external", remoteUrl: "https://192.168.1.128:9898" },
|
||||||
|
{ ip: "172.24.96.1", family: "ipv4", scope: "external", remoteUrl: "https://172.24.96.1:9898" },
|
||||||
|
])
|
||||||
|
|
||||||
|
assert.equal(result.recommended?.ip, "192.168.1.128")
|
||||||
|
assert.deepEqual(result.hidden.map((address) => address.ip), ["172.24.96.1"])
|
||||||
|
})
|
||||||
|
})
|
||||||
14
packages/ui/src/lib/remote-access-addresses.ts
Normal file
14
packages/ui/src/lib/remote-access-addresses.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { NetworkAddress } from "../../../server/src/api-types"
|
||||||
|
|
||||||
|
export interface RemoteAddressGroups {
|
||||||
|
recommended: NetworkAddress | null
|
||||||
|
hidden: NetworkAddress[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitRemoteAddresses(addresses: NetworkAddress[]): RemoteAddressGroups {
|
||||||
|
const remoteAddresses = addresses.filter((address) => address.scope !== "loopback")
|
||||||
|
return {
|
||||||
|
recommended: remoteAddresses[0] ?? null,
|
||||||
|
hidden: remoteAddresses.slice(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/src/api-types"
|
import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/src/api-types"
|
||||||
import { serverApi } from "./api-client"
|
import { serverApi } from "./api-client"
|
||||||
import { getClientIdentity } from "./client-identity"
|
|
||||||
import { getLogger } from "./logger"
|
import { getLogger } from "./logger"
|
||||||
|
|
||||||
const RETRY_BASE_DELAY = 1000
|
const RETRY_BASE_DELAY = 1000
|
||||||
@@ -17,7 +16,6 @@ function logSse(message: string, context?: Record<string, unknown>) {
|
|||||||
|
|
||||||
class ServerEvents {
|
class ServerEvents {
|
||||||
private handlers = new Map<WorkspaceEventType | "*", Set<(event: WorkspaceEventPayload) => void>>()
|
private handlers = new Map<WorkspaceEventType | "*", Set<(event: WorkspaceEventPayload) => void>>()
|
||||||
private openHandlers = new Set<() => void>()
|
|
||||||
private source: EventSource | null = null
|
private source: EventSource | null = null
|
||||||
private retryDelay = RETRY_BASE_DELAY
|
private retryDelay = RETRY_BASE_DELAY
|
||||||
|
|
||||||
@@ -30,24 +28,10 @@ class ServerEvents {
|
|||||||
this.source.close()
|
this.source.close()
|
||||||
}
|
}
|
||||||
logSse("Connecting to backend events stream")
|
logSse("Connecting to backend events stream")
|
||||||
this.source = serverApi.connectEvents(
|
this.source = serverApi.connectEvents((event) => this.dispatch(event), () => this.scheduleReconnect())
|
||||||
(event) => this.dispatch(event),
|
|
||||||
() => this.scheduleReconnect(),
|
|
||||||
(payload) => {
|
|
||||||
void serverApi
|
|
||||||
.sendClientConnectionPong({
|
|
||||||
...getClientIdentity(),
|
|
||||||
pingTs: payload.ts,
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
log.error("Failed to send client connection pong", error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
this.source.onopen = () => {
|
this.source.onopen = () => {
|
||||||
logSse("Events stream connected")
|
logSse("Events stream connected")
|
||||||
this.retryDelay = RETRY_BASE_DELAY
|
this.retryDelay = RETRY_BASE_DELAY
|
||||||
this.openHandlers.forEach((handler) => handler())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +61,6 @@ class ServerEvents {
|
|||||||
bucket.add(handler)
|
bucket.add(handler)
|
||||||
return () => bucket.delete(handler)
|
return () => bucket.delete(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen(handler: () => void): () => void {
|
|
||||||
this.openHandlers.add(handler)
|
|
||||||
return () => this.openHandlers.delete(handler)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serverEvents = new ServerEvents()
|
export const serverEvents = new ServerEvents()
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ export type AlertDialogState = {
|
|||||||
variant?: AlertVariant
|
variant?: AlertVariant
|
||||||
confirmLabel?: string
|
confirmLabel?: string
|
||||||
cancelLabel?: string
|
cancelLabel?: string
|
||||||
/** When false, prevents dismissal via Escape key or backdrop click. Default: true */
|
|
||||||
dismissible?: boolean
|
|
||||||
onConfirm?: () => void
|
onConfirm?: () => void
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { showToastNotification } from "../lib/notifications"
|
|||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { formatToMimeType, getSpeechPlaybackSupport } from "../lib/speech-playback-support"
|
import { formatToMimeType, getSpeechPlaybackSupport } from "../lib/speech-playback-support"
|
||||||
import { serverEvents } from "../lib/server-events"
|
|
||||||
import { serverSettings } from "./preferences"
|
import { serverSettings } from "./preferences"
|
||||||
import { loadSpeechCapabilities, speechCapabilities } from "./speech"
|
import { loadSpeechCapabilities, speechCapabilities } from "./speech"
|
||||||
import { getActiveSession, sessions } from "./session-state"
|
import { getActiveSession, sessions } from "./session-state"
|
||||||
@@ -31,7 +30,6 @@ interface PlaybackHandle {
|
|||||||
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
const [conversationModeInstances, setConversationModeInstances] = createSignal<Map<string, boolean>>(new Map())
|
const [conversationModeInstances, setConversationModeInstances] = createSignal<Map<string, boolean>>(new Map())
|
||||||
const LEADING_SPOKEN_BLOCK_REGEX = /^\s*```spoken[ \t]*\r?\n([\s\S]*?)\r?\n```(?:\r?\n|$)/i
|
|
||||||
|
|
||||||
const queuedKeys = new Set<string>()
|
const queuedKeys = new Set<string>()
|
||||||
const spokenKeysBySession = new Map<string, Set<string>>()
|
const spokenKeysBySession = new Map<string, Set<string>>()
|
||||||
@@ -45,10 +43,6 @@ let currentPlayback:
|
|||||||
let queueRunner: Promise<void> | null = null
|
let queueRunner: Promise<void> | null = null
|
||||||
let playbackErrorShown = false
|
let playbackErrorShown = false
|
||||||
|
|
||||||
serverEvents.onOpen(() => {
|
|
||||||
void syncConversationModesToServer()
|
|
||||||
})
|
|
||||||
|
|
||||||
function getEntryKey(instanceId: string, sessionId: string, messageId: string, partId: string): string {
|
function getEntryKey(instanceId: string, sessionId: string, messageId: string, partId: string): string {
|
||||||
return `${instanceId}:${sessionId}:${messageId}:${partId}`
|
return `${instanceId}:${sessionId}:${messageId}:${partId}`
|
||||||
}
|
}
|
||||||
@@ -113,9 +107,6 @@ export function canUseConversationMode(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setConversationModeEnabled(instanceId: string, enabled: boolean): void {
|
export function setConversationModeEnabled(instanceId: string, enabled: boolean): void {
|
||||||
const previous = isConversationModeEnabled(instanceId)
|
|
||||||
if (previous === enabled) return
|
|
||||||
|
|
||||||
setConversationModeInstances((prev) => {
|
setConversationModeInstances((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@@ -129,23 +120,6 @@ export function setConversationModeEnabled(instanceId: string, enabled: boolean)
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
clearConversationPlaybackForInstance(instanceId)
|
clearConversationPlaybackForInstance(instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
void serverApi.updateVoiceMode(instanceId, enabled).catch((error) => {
|
|
||||||
log.error("Failed to update conversation mode", error)
|
|
||||||
setConversationModeInstances((prev) => {
|
|
||||||
const next = new Map(prev)
|
|
||||||
if (previous) {
|
|
||||||
next.set(instanceId, true)
|
|
||||||
} else {
|
|
||||||
next.delete(instanceId)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!previous) {
|
|
||||||
clearConversationPlaybackForInstance(instanceId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleConversationMode(instanceId: string): void {
|
export function toggleConversationMode(instanceId: string): void {
|
||||||
@@ -214,7 +188,7 @@ export function handleConversationAssistantPartUpdated(instanceId: string, part:
|
|||||||
if (!isConversationModeEnabled(instanceId)) return
|
if (!isConversationModeEnabled(instanceId)) return
|
||||||
if (!isSpeakableSession(instanceId, sessionId)) return
|
if (!isSpeakableSession(instanceId, sessionId)) return
|
||||||
|
|
||||||
const text = extractLeadingSpokenBlock(resolveTextPartContent(part))
|
const text = resolveTextPartContent(part).trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
|
||||||
const key = getEntryKey(instanceId, sessionId, messageId, partId)
|
const key = getEntryKey(instanceId, sessionId, messageId, partId)
|
||||||
@@ -531,18 +505,3 @@ function createObjectUrlFromBase64(audioBase64: string, mimeType: string): strin
|
|||||||
}
|
}
|
||||||
return URL.createObjectURL(new Blob([bytes], { type: mimeType || "audio/mpeg" }))
|
return URL.createObjectURL(new Blob([bytes], { type: mimeType || "audio/mpeg" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractLeadingSpokenBlock(text: string): string {
|
|
||||||
const match = text.match(LEADING_SPOKEN_BLOCK_REGEX)
|
|
||||||
if (!match?.[1]) return ""
|
|
||||||
return match[1].trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncConversationModesToServer(): Promise<void> {
|
|
||||||
const updates: Promise<unknown>[] = []
|
|
||||||
for (const [instanceId, enabled] of conversationModeInstances()) {
|
|
||||||
if (!enabled) continue
|
|
||||||
updates.push(serverApi.updateVoiceMode(instanceId, true))
|
|
||||||
}
|
|
||||||
await Promise.allSettled(updates)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { createSignal } from "solid-js"
|
|
||||||
|
|
||||||
const STORAGE_KEY = "codenomad:permission-auto-accept:v1"
|
|
||||||
|
|
||||||
function makeKey(instanceId: string, sessionId: string) {
|
|
||||||
return `${instanceId}:${sessionId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function readInitialState() {
|
|
||||||
if (typeof window === "undefined" || !window.localStorage) {
|
|
||||||
return new Map<string, boolean>()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
|
||||||
if (!raw) return new Map<string, boolean>()
|
|
||||||
const parsed = JSON.parse(raw) as Record<string, boolean>
|
|
||||||
return new Map(Object.entries(parsed).filter((entry): entry is [string, boolean] => entry[1] === true))
|
|
||||||
} catch {
|
|
||||||
return new Map<string, boolean>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function persist(next: Map<string, boolean>) {
|
|
||||||
if (typeof window === "undefined" || !window.localStorage) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(Object.fromEntries(next)))
|
|
||||||
} catch {
|
|
||||||
// ignore persistence failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [autoAcceptState, setAutoAcceptState] = createSignal(readInitialState())
|
|
||||||
const [inFlightVersion, setInFlightVersion] = createSignal(0)
|
|
||||||
|
|
||||||
const inFlight = new Set<string>()
|
|
||||||
|
|
||||||
export function isPermissionAutoAcceptEnabled(instanceId: string, sessionId: string) {
|
|
||||||
return autoAcceptState().get(makeKey(instanceId, sessionId)) ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setPermissionAutoAcceptEnabled(instanceId: string, sessionId: string, enabled: boolean) {
|
|
||||||
const key = makeKey(instanceId, sessionId)
|
|
||||||
setAutoAcceptState((prev) => {
|
|
||||||
const next = new Map(prev)
|
|
||||||
if (enabled) {
|
|
||||||
next.set(key, true)
|
|
||||||
} else {
|
|
||||||
next.delete(key)
|
|
||||||
}
|
|
||||||
persist(next)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function togglePermissionAutoAccept(instanceId: string, sessionId: string) {
|
|
||||||
setPermissionAutoAcceptEnabled(instanceId, sessionId, !isPermissionAutoAcceptEnabled(instanceId, sessionId))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function canAutoRespondPermission(instanceId: string, sessionId: string, requestId: string) {
|
|
||||||
const key = makeKey(instanceId, sessionId)
|
|
||||||
if (!autoAcceptState().get(key)) return false
|
|
||||||
const requestKey = `${key}:${requestId}`
|
|
||||||
if (inFlight.has(requestKey)) return false
|
|
||||||
inFlight.add(requestKey)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPermissionAutoAcceptInFlightVersion() {
|
|
||||||
return inFlightVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function finishAutoRespondPermission(instanceId: string, sessionId: string, requestId: string) {
|
|
||||||
if (!inFlight.delete(`${makeKey(instanceId, sessionId)}:${requestId}`)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setInFlightVersion((value) => value + 1)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user