feat: add workspace import UI flow
This commit is contained in:
parent
0c87a02b87
commit
357f6b0f40
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "agent-orchestrator",
|
||||
"version": "0.0.0",
|
||||
"version": "0.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agent-orchestrator",
|
||||
"version": "0.0.0",
|
||||
"version": "0.10.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.16",
|
||||
|
|
@ -156,6 +156,7 @@
|
|||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
|
|
@ -493,6 +494,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
|
|
@ -541,6 +543,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
|
|
@ -2777,33 +2780,10 @@
|
|||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
|
@ -3056,6 +3036,7 @@
|
|||
"integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^3.0.1",
|
||||
"@inquirer/confirm": "^4.0.1",
|
||||
|
|
@ -3472,6 +3453,7 @@
|
|||
"integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^4.0.0",
|
||||
"@octokit/graphql": "^7.1.0",
|
||||
|
|
@ -5676,6 +5658,40 @@
|
|||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
|
|
@ -6066,6 +6082,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.15.tgz",
|
||||
"integrity": "sha512-GawYz7HEjj8rTUUDoT/SemDEVm63pZUO+2mOcXHY9Jl3EwMS5gFBnPu/2UvcrwRm1jN1k79fokc0d4aFmrLatg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.162.0",
|
||||
"@tanstack/react-store": "^0.9.3",
|
||||
|
|
@ -6358,8 +6375,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
|
|
@ -6476,6 +6492,7 @@
|
|||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -6486,6 +6503,7 @@
|
|||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
|
|
@ -6894,7 +6912,8 @@
|
|||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
|
|
@ -6926,6 +6945,7 @@
|
|||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -6989,6 +7009,7 @@
|
|||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -7535,6 +7556,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
|
|
@ -8686,7 +8708,6 @@
|
|||
"integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.15.3",
|
||||
"builder-util": "26.15.3",
|
||||
|
|
@ -8700,7 +8721,6 @@
|
|||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
|
|
@ -8716,7 +8736,6 @@
|
|||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
|
|
@ -8730,7 +8749,6 @@
|
|||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
|
|
@ -8740,8 +8758,7 @@
|
|||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.11",
|
||||
|
|
@ -9355,7 +9372,6 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
|
|
@ -9376,7 +9392,6 @@
|
|||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
|
|
@ -9393,31 +9408,6 @@
|
|||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
|
|
@ -11843,7 +11833,6 @@
|
|||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
|
|
@ -12324,7 +12313,6 @@
|
|||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
|
|
@ -13275,7 +13263,6 @@
|
|||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
|
|
@ -13291,7 +13278,6 @@
|
|||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -13522,6 +13508,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -13531,6 +13518,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -13543,8 +13531,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.7.2",
|
||||
|
|
@ -13919,7 +13906,6 @@
|
|||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
|
|
@ -14116,6 +14102,7 @@
|
|||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz",
|
||||
"integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
|
|
@ -14595,7 +14582,6 @@
|
|||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
|
|
@ -14955,6 +14941,7 @@
|
|||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -15176,6 +15163,7 @@
|
|||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
|
|
@ -15225,6 +15213,7 @@
|
|||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,6 +7,7 @@ nodeLinker: hoisted
|
|||
# Electron's postinstall downloads the runtime binary into node_modules/electron/dist;
|
||||
# without this approval pnpm skips it and `electron-forge start` cannot launch.
|
||||
allowBuilds:
|
||||
core-js: false
|
||||
electron: true
|
||||
electron-winstaller: true
|
||||
# @electron/rebuild (via electron-forge) depends on @electron/node-gyp as a git
|
||||
|
|
|
|||
|
|
@ -26,12 +26,13 @@ import {
|
|||
type UpdateSettings,
|
||||
type UpdateStatus,
|
||||
} from "./main/update-settings";
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { execFile, spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { readdir, readFile, rm, stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { type DaemonLaunchSpec, resolveDaemonLaunch } from "./shared/daemon-launch";
|
||||
import { createListenPortScanner, defaultRunFilePath, parseRunFile } from "./shared/daemon-discovery";
|
||||
import type { DaemonStatus } from "./shared/daemon-status";
|
||||
|
|
@ -96,6 +97,22 @@ let browserViewHost: BrowserViewHost | null = null;
|
|||
// Held for the app lifetime. Dropping it (on any exit) triggers daemon self-stop.
|
||||
let supervisorLink: SupervisorLinkHandle | null = null;
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
type GitRepoScanResult = {
|
||||
name: string;
|
||||
path: string;
|
||||
relativePath: string;
|
||||
branch: string;
|
||||
remote: string;
|
||||
hasRemote: boolean;
|
||||
};
|
||||
|
||||
type ImportFolderScanResult = {
|
||||
path: string;
|
||||
repos: GitRepoScanResult[];
|
||||
};
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
const RENDERER_SCHEME = "app";
|
||||
|
|
@ -825,15 +842,90 @@ ipcMain.handle("app:getVersion", () => app.getVersion());
|
|||
ipcMain.handle("telemetry:getBootstrap", () =>
|
||||
buildTelemetryBootstrap(process.env, app.getVersion(), process.platform),
|
||||
);
|
||||
ipcMain.handle("app:chooseDirectory", async () => {
|
||||
async function chooseDirectory(title: string): Promise<string | null> {
|
||||
const options: OpenDialogOptions = {
|
||||
properties: ["openDirectory"],
|
||||
title: "Choose a git repository",
|
||||
title,
|
||||
};
|
||||
const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options);
|
||||
|
||||
if (result.canceled) return null;
|
||||
return result.filePaths[0] ?? null;
|
||||
}
|
||||
|
||||
async function gitOutput(cwd: string, args: string[]): Promise<string> {
|
||||
const { stdout } = await execFileAsync("git", args, { cwd, timeout: 5000 });
|
||||
return String(stdout).trim();
|
||||
}
|
||||
|
||||
async function isGitRepo(repoPath: string): Promise<boolean> {
|
||||
try {
|
||||
await stat(path.join(repoPath, ".git"));
|
||||
await gitOutput(repoPath, ["rev-parse", "--show-toplevel"]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDefaultBranch(repoPath: string): Promise<string> {
|
||||
try {
|
||||
const ref = await gitOutput(repoPath, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
||||
if (ref) return ref.replace(/^origin\//, "");
|
||||
} catch {
|
||||
// Fall back to the checked-out branch when origin/HEAD is unavailable.
|
||||
}
|
||||
try {
|
||||
const branch = await gitOutput(repoPath, ["branch", "--show-current"]);
|
||||
if (branch) return branch;
|
||||
} catch {
|
||||
// Detached or unreadable HEAD is represented below.
|
||||
}
|
||||
return "HEAD";
|
||||
}
|
||||
|
||||
async function scanGitRepo(repoPath: string, rootPath: string): Promise<GitRepoScanResult | null> {
|
||||
if (!(await isGitRepo(repoPath))) return null;
|
||||
const [branchResult, remoteResult] = await Promise.allSettled([
|
||||
resolveDefaultBranch(repoPath),
|
||||
gitOutput(repoPath, ["remote", "get-url", "origin"]),
|
||||
]);
|
||||
const relativePath = repoPath === rootPath ? "." : path.relative(rootPath, repoPath);
|
||||
return {
|
||||
name: path.basename(repoPath),
|
||||
path: repoPath,
|
||||
relativePath,
|
||||
branch: branchResult.status === "fulfilled" && branchResult.value ? branchResult.value : "HEAD",
|
||||
remote: remoteResult.status === "fulfilled" ? remoteResult.value : "",
|
||||
hasRemote: remoteResult.status === "fulfilled" && remoteResult.value.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function scanImportFolder(rootPath: string, mode: "project" | "workspace"): Promise<ImportFolderScanResult> {
|
||||
if (mode === "project") {
|
||||
const repo = await scanGitRepo(rootPath, rootPath);
|
||||
return { path: rootPath, repos: repo ? [repo] : [] };
|
||||
}
|
||||
|
||||
const entries = await readdir(rootPath, { withFileTypes: true });
|
||||
const repos = await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory() && entry.name !== ".git")
|
||||
.map((entry) => scanGitRepo(path.join(rootPath, entry.name), rootPath)),
|
||||
);
|
||||
return {
|
||||
path: rootPath,
|
||||
repos: repos
|
||||
.filter((repo): repo is GitRepoScanResult => repo !== null)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
};
|
||||
}
|
||||
|
||||
ipcMain.handle("app:chooseDirectory", async (_event, title?: string) => {
|
||||
return chooseDirectory(typeof title === "string" && title.trim() ? title : "Choose a git repository");
|
||||
});
|
||||
ipcMain.handle("app:scanImportFolder", async (_event, input: { path: string; mode: "project" | "workspace" }) => {
|
||||
return scanImportFolder(input.path, input.mode);
|
||||
});
|
||||
ipcMain.handle("clipboard:writeText", (_event, text: string) => {
|
||||
clipboard.writeText(text, "clipboard");
|
||||
|
|
|
|||
|
|
@ -16,10 +16,28 @@ export type BrowserNavigateInput = {
|
|||
url: string;
|
||||
};
|
||||
|
||||
export type ImportFolderMode = "project" | "workspace";
|
||||
|
||||
export type ImportRepoScan = {
|
||||
name: string;
|
||||
path: string;
|
||||
relativePath: string;
|
||||
branch: string;
|
||||
remote: string;
|
||||
hasRemote: boolean;
|
||||
};
|
||||
|
||||
export type ImportFolderScan = {
|
||||
path: string;
|
||||
repos: ImportRepoScan[];
|
||||
};
|
||||
|
||||
const api = {
|
||||
app: {
|
||||
getVersion: () => ipcRenderer.invoke("app:getVersion") as Promise<string>,
|
||||
chooseDirectory: () => ipcRenderer.invoke("app:chooseDirectory") as Promise<string | null>,
|
||||
chooseDirectory: (title?: string) => ipcRenderer.invoke("app:chooseDirectory", title) as Promise<string | null>,
|
||||
scanImportFolder: (input: { path: string; mode: ImportFolderMode }) =>
|
||||
ipcRenderer.invoke("app:scanImportFolder", input) as Promise<ImportFolderScan>,
|
||||
},
|
||||
clipboard: {
|
||||
writeText: (text: string) => ipcRenderer.invoke("clipboard:writeText", text) as Promise<void>,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { components } from "../../api/schema";
|
|||
import { agentsQueryKey, agentsQueryOptions, refreshAgents } from "../hooks/useAgentsQuery";
|
||||
import { AGENT_OPTIONS } from "../lib/agent-options";
|
||||
import { buildIntake, type IntakeForm, IntakeFields, intakeNeedsRule } from "./IntakeFields";
|
||||
import type { ProjectKind } from "../types/workspace";
|
||||
import { Button } from "./ui/button";
|
||||
import { Label } from "./ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
|
|
@ -25,6 +26,7 @@ const EMPTY_INTAKE: IntakeForm = { enabled: false, repo: "", assignee: "" };
|
|||
type CreateProjectAgentSheetProps = {
|
||||
error?: string | null;
|
||||
isCreating: boolean;
|
||||
kind: ProjectKind;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (selection: CreateProjectAgentSelection) => Promise<void>;
|
||||
open: boolean;
|
||||
|
|
@ -34,6 +36,7 @@ type CreateProjectAgentSheetProps = {
|
|||
export function CreateProjectAgentSheet({
|
||||
error,
|
||||
isCreating,
|
||||
kind,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
open,
|
||||
|
|
@ -85,7 +88,9 @@ export function CreateProjectAgentSheet({
|
|||
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-[min(420px,calc(100vw-32px))] -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-popover p-0 text-popover-foreground shadow-xl data-[state=open]:animate-modal-in">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
|
||||
<div className="min-w-0">
|
||||
<Dialog.Title className="text-[15px] font-semibold text-foreground">Project agents</Dialog.Title>
|
||||
<Dialog.Title className="text-[15px] font-semibold text-foreground">
|
||||
{kind === "workspace" ? "Workspace agents" : "Project agents"}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="mt-1 break-all text-[12px] text-muted-foreground">
|
||||
{path ?? ""}
|
||||
</Dialog.Description>
|
||||
|
|
@ -177,7 +182,7 @@ export function CreateProjectAgentSheet({
|
|||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" disabled={!canSubmit}>
|
||||
{isCreating ? "Creating..." : "Create and start"}
|
||||
{isCreating ? "Creating..." : kind === "workspace" ? "Create workspace and start" : "Create and start"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -195,11 +195,38 @@ function SettingsBody({ project, projectId, onSaved }: { project: Project; proje
|
|||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2 font-mono text-[12px] text-muted-foreground">
|
||||
<ReadonlyRow label="id" value={project.id} />
|
||||
<ReadonlyRow label="kind" value={project.kind === "workspace" ? "workspace" : "single repo"} />
|
||||
<ReadonlyRow label="path" value={project.path} />
|
||||
<ReadonlyRow label="repo" value={project.repo || "—"} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{project.kind === "workspace" && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[13px]">Workspace repos</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{project.workspaceRepos?.length ? (
|
||||
project.workspaceRepos.map((repo) => (
|
||||
<div
|
||||
key={repo.name}
|
||||
className="grid grid-cols-[minmax(0,120px)_minmax(0,1fr)] gap-3 rounded-md border border-border px-3 py-2 font-mono text-[12px]"
|
||||
>
|
||||
<span className="truncate text-foreground">{repo.name}</span>
|
||||
<span className="min-w-0 truncate text-muted-foreground">
|
||||
{repo.relativePath}
|
||||
{repo.repo ? ` · ${repo.repo}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-[12px] text-muted-foreground">No child repositories are registered.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[13px]">Worktrees</CardTitle>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,13 @@ const session: WorkspaceSession = {
|
|||
prs: [],
|
||||
};
|
||||
|
||||
type CreateProjectHandler = (input: { path: string; workerAgent: string; orchestratorAgent: string }) => Promise<void>;
|
||||
type CreateProjectInput = {
|
||||
path: string;
|
||||
workerAgent: string;
|
||||
orchestratorAgent: string;
|
||||
asWorkspace?: boolean;
|
||||
};
|
||||
type CreateProjectHandler = (input: CreateProjectInput) => Promise<void>;
|
||||
type RemoveProjectHandler = (projectId: string) => Promise<void>;
|
||||
|
||||
function renderSidebar({
|
||||
|
|
@ -190,6 +196,12 @@ describe("Sidebar", () => {
|
|||
renderSidebar({ onCreateProject });
|
||||
|
||||
await user.click(screen.getByLabelText("New project"));
|
||||
expect(screen.getByRole("dialog", { name: "Import to Agent Orchestrator" })).toBeInTheDocument();
|
||||
expect(window.ao!.app.chooseDirectory).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole("button", { name: /^Project/i }));
|
||||
expect(await screen.findByRole("dialog", { name: "Import project" })).toBeInTheDocument();
|
||||
expect(window.ao!.app.chooseDirectory).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole("button", { name: /Choose a project folder/i }));
|
||||
|
||||
expect(await screen.findByText("/repo/new-project")).toBeInTheDocument();
|
||||
const dialog = screen.getByRole("dialog", { name: "Project agents" });
|
||||
|
|
@ -203,10 +215,95 @@ describe("Sidebar", () => {
|
|||
path: "/repo/new-project",
|
||||
workerAgent: "codex",
|
||||
orchestratorAgent: "claude-code",
|
||||
asWorkspace: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can create a workspace project from the project add flow", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCreateProject = vi.fn().mockResolvedValue(undefined) as CreateProjectHandler;
|
||||
window.ao!.app.chooseDirectory = vi.fn().mockResolvedValue("/repo/workspace");
|
||||
renderSidebar({ onCreateProject });
|
||||
|
||||
await user.click(screen.getByLabelText("New project"));
|
||||
await user.click(screen.getByRole("button", { name: /^Workspace/i }));
|
||||
expect(await screen.findByRole("dialog", { name: "Import workspace" })).toBeInTheDocument();
|
||||
expect(window.ao!.app.chooseDirectory).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole("button", { name: /Choose a folder/i }));
|
||||
|
||||
expect(await screen.findByText("/repo/workspace")).toBeInTheDocument();
|
||||
expect(screen.getByRole("dialog", { name: "Workspace agents" })).toBeInTheDocument();
|
||||
await chooseOption(screen.getByRole("combobox", { name: "Worker agent" }), "codex");
|
||||
await chooseOption(screen.getByRole("combobox", { name: "Orchestrator agent" }), "claude-code");
|
||||
await user.click(screen.getByRole("button", { name: "Create workspace and start" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onCreateProject).toHaveBeenCalledWith({
|
||||
path: "/repo/workspace",
|
||||
workerAgent: "codex",
|
||||
orchestratorAgent: "claude-code",
|
||||
asWorkspace: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows detected repository validation when workspace import fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCreateProject = vi.fn().mockRejectedValue(new Error("workspace not registered")) as CreateProjectHandler;
|
||||
window.ao!.app.chooseDirectory = vi.fn().mockResolvedValue("/Users/test/dev/acme");
|
||||
window.ao!.app.scanImportFolder = vi.fn().mockResolvedValue({
|
||||
path: "/Users/test/dev/acme",
|
||||
repos: [
|
||||
{
|
||||
name: "web",
|
||||
path: "/Users/test/dev/acme/web",
|
||||
relativePath: "web",
|
||||
branch: "HEAD",
|
||||
remote: "",
|
||||
hasRemote: false,
|
||||
},
|
||||
{
|
||||
name: "api",
|
||||
path: "/Users/test/dev/acme/api",
|
||||
relativePath: "api",
|
||||
branch: "main",
|
||||
remote: "git@github.com:acme/api.git",
|
||||
hasRemote: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
renderSidebar({ onCreateProject });
|
||||
|
||||
await user.click(screen.getByLabelText("New project"));
|
||||
await user.click(screen.getByRole("button", { name: /^Workspace/i }));
|
||||
await user.click(await screen.findByRole("button", { name: /Choose a folder/i }));
|
||||
await screen.findByRole("dialog", { name: "Workspace agents" });
|
||||
await chooseOption(screen.getByRole("combobox", { name: "Worker agent" }), "codex");
|
||||
await chooseOption(screen.getByRole("combobox", { name: "Orchestrator agent" }), "claude-code");
|
||||
await user.click(screen.getByRole("button", { name: "Create workspace and start" }));
|
||||
|
||||
expect(await screen.findByText(/Validation failed · workspace not registered/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("web")).toBeInTheDocument();
|
||||
expect(screen.getByText("No remote configured")).toBeInTheDocument();
|
||||
expect(screen.getByText("api")).toBeInTheDocument();
|
||||
expect(screen.getByText("main github.com/acme/api")).toBeInTheDocument();
|
||||
expect(screen.getByText("Resolve 1 failed repository to continue")).toBeInTheDocument();
|
||||
expect(window.ao!.app.scanImportFolder).toHaveBeenCalledWith({
|
||||
path: "/Users/test/dev/acme",
|
||||
mode: "workspace",
|
||||
});
|
||||
});
|
||||
|
||||
it("opens global settings from the footer menu when no project is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /project actions/i }));
|
||||
|
||||
expect(await screen.findByRole("menuitem", { name: /settings/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows needs-auth agents as unavailable while keeping authorized agents selectable", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCreateProject = vi.fn().mockResolvedValue(undefined) as CreateProjectHandler;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, useParams, useRouterState } from "@tanstack/react-router";
|
||||
import {
|
||||
ChevronRight,
|
||||
CheckCircle2,
|
||||
Folder,
|
||||
FolderPlus,
|
||||
GitPullRequest,
|
||||
LayoutDashboard,
|
||||
Moon,
|
||||
|
|
@ -12,12 +16,16 @@ import {
|
|||
Settings,
|
||||
Sun,
|
||||
Trash2,
|
||||
X,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useRef, useState, type ReactNode } from "react";
|
||||
import type { ImportFolderScan } from "../../preload";
|
||||
import {
|
||||
attentionZone,
|
||||
newestActiveOrchestrator,
|
||||
sessionIsActive,
|
||||
type ProjectKind,
|
||||
type WorkspaceSession,
|
||||
type WorkspaceSummary,
|
||||
workerSessions,
|
||||
|
|
@ -58,6 +66,7 @@ import aoLogo from "../assets/ao-logo.png";
|
|||
import { cn } from "../lib/utils";
|
||||
import { useUiStore } from "../stores/ui-store";
|
||||
import { CreateProjectAgentSheet, type CreateProjectAgentSelection } from "./CreateProjectAgentSheet";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
// The macOS hiddenInset traffic lights and the fixed TitlebarNav overlay live
|
||||
// in the full-width topbar's left inset (_shell renders the bar above the
|
||||
|
|
@ -253,7 +262,7 @@ export function Sidebar({
|
|||
<div className="px-2 py-3 group-data-[collapsible=icon]:hidden">
|
||||
<p className="text-[12px] text-passive">No projects yet.</p>
|
||||
<p className="mt-1 text-[11px] text-passive">
|
||||
Click <span className="text-foreground">+</span> above to register a git repo.
|
||||
Click <span className="text-foreground">+</span> above to register a repo or workspace.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -769,16 +778,34 @@ function CreateProjectFlow({
|
|||
children: (state: { choosePath: () => void; disabled: boolean; label: string }) => ReactNode;
|
||||
}) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [modePickerOpen, setModePickerOpen] = useState(false);
|
||||
const [folderPickerOpen, setFolderPickerOpen] = useState(false);
|
||||
const [selectedKind, setSelectedKind] = useState<ProjectKind>("single_repo");
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
const [validationScan, setValidationScan] = useState<ImportFolderScan | null>(null);
|
||||
const [isChoosingPath, setIsChoosingPath] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const openFolderStep = (kind: ProjectKind) => {
|
||||
setError(null);
|
||||
setValidationScan(null);
|
||||
setSelectedKind(kind);
|
||||
setModePickerOpen(false);
|
||||
window.requestAnimationFrame(() => setFolderPickerOpen(true));
|
||||
};
|
||||
|
||||
const choosePath = async () => {
|
||||
setError(null);
|
||||
setIsChoosingPath(true);
|
||||
try {
|
||||
const path = await aoBridge.app.chooseDirectory();
|
||||
if (path) setSelectedPath(path);
|
||||
const path = await aoBridge.app.chooseDirectory(
|
||||
selectedKind === "workspace" ? "Choose a workspace folder" : "Choose a project repository",
|
||||
);
|
||||
if (path) {
|
||||
setValidationScan(null);
|
||||
setSelectedPath(path);
|
||||
setFolderPickerOpen(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Could not add project");
|
||||
} finally {
|
||||
|
|
@ -791,10 +818,22 @@ function CreateProjectFlow({
|
|||
setError(null);
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await onCreateProject({ path: selectedPath, ...selection });
|
||||
await onCreateProject({ path: selectedPath, asWorkspace: selectedKind === "workspace", ...selection });
|
||||
setSelectedPath(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Could not add project");
|
||||
const message = err instanceof Error ? err.message : "Could not add project";
|
||||
setError(message);
|
||||
try {
|
||||
const scan = await aoBridge.app.scanImportFolder({
|
||||
path: selectedPath,
|
||||
mode: selectedKind === "workspace" ? "workspace" : "project",
|
||||
});
|
||||
setValidationScan(scan);
|
||||
} catch {
|
||||
setValidationScan({ path: selectedPath, repos: [] });
|
||||
}
|
||||
setSelectedPath(null);
|
||||
setFolderPickerOpen(true);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
|
|
@ -804,14 +843,44 @@ function CreateProjectFlow({
|
|||
|
||||
return (
|
||||
<>
|
||||
{children({ choosePath: () => void choosePath(), disabled: isChoosingPath || isCreating, label })}
|
||||
{children({ choosePath: () => setModePickerOpen(true), disabled: isChoosingPath || isCreating, label })}
|
||||
<CreateProjectModeDialog
|
||||
disabled={isChoosingPath || isCreating}
|
||||
open={modePickerOpen}
|
||||
onOpenChange={(open) => !isChoosingPath && !isCreating && setModePickerOpen(open)}
|
||||
onSelect={openFolderStep}
|
||||
/>
|
||||
<CreateProjectFolderDialog
|
||||
disabled={isChoosingPath || isCreating}
|
||||
error={error}
|
||||
kind={selectedKind}
|
||||
open={folderPickerOpen}
|
||||
scan={validationScan}
|
||||
onBack={() => {
|
||||
setError(null);
|
||||
setValidationScan(null);
|
||||
setFolderPickerOpen(false);
|
||||
window.requestAnimationFrame(() => setModePickerOpen(true));
|
||||
}}
|
||||
onChooseFolder={() => void choosePath()}
|
||||
onOpenChange={(open) => {
|
||||
if (!isChoosingPath && !isCreating) {
|
||||
setFolderPickerOpen(open);
|
||||
if (!open) {
|
||||
setError(null);
|
||||
setValidationScan(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CreateProjectAgentSheet
|
||||
error={error}
|
||||
isCreating={isCreating}
|
||||
kind={selectedKind}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setSelectedPath(null);
|
||||
setError(null);
|
||||
if (!folderPickerOpen) setError(null);
|
||||
}
|
||||
}}
|
||||
onSubmit={createProject}
|
||||
|
|
@ -826,3 +895,317 @@ function CreateProjectFlow({
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateProjectModeDialog({
|
||||
disabled,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
open,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelect: (kind: ProjectKind) => void;
|
||||
open: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/55 data-[state=open]:animate-overlay-in" />
|
||||
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 flex max-h-[min(720px,calc(100svh-24px))] w-[min(680px,calc(100vw-24px))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-popover p-0 text-popover-foreground shadow-xl data-[state=open]:animate-modal-in">
|
||||
<div className="flex shrink-0 items-start justify-between gap-4 px-4 pb-3 pt-4 sm:px-6 sm:pb-4 sm:pt-5">
|
||||
<div className="min-w-0">
|
||||
<Dialog.Title className="text-[18px] font-semibold text-foreground">
|
||||
Import to Agent Orchestrator
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="mt-1 text-[13px] font-medium text-muted-foreground">
|
||||
What are you importing?
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-7 shrink-0 place-items-center rounded-md text-muted-foreground transition hover:bg-surface hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
||||
aria-label="Close new project dialog"
|
||||
disabled={disabled}
|
||||
>
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<div className="grid min-h-0 gap-3 overflow-y-auto px-4 pb-4 sm:grid-cols-2 sm:px-6 sm:pb-6">
|
||||
<ProjectModeButton
|
||||
description="Several Git repos that live under one parent folder."
|
||||
disabled={disabled}
|
||||
kind="workspace"
|
||||
onClick={() => onSelect("workspace")}
|
||||
/>
|
||||
<ProjectModeButton
|
||||
description="A single Git repository — one codebase, tracked in one repo."
|
||||
disabled={disabled}
|
||||
kind="single_repo"
|
||||
onClick={() => onSelect("single_repo")}
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectModeButton({
|
||||
description,
|
||||
disabled,
|
||||
kind,
|
||||
onClick,
|
||||
}: {
|
||||
description: string;
|
||||
disabled: boolean;
|
||||
kind: ProjectKind;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const isWorkspace = kind === "workspace";
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isWorkspace ? "Workspace" : "Project"}
|
||||
className="flex min-h-[176px] w-full flex-col justify-end rounded-lg border border-border bg-card px-4 py-4 text-left transition-colors hover:bg-background focus-visible:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:pointer-events-none disabled:opacity-50 sm:min-h-[220px] sm:px-5 sm:py-5"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="mb-3 flex min-h-[70px] w-full items-center justify-center sm:mb-4 sm:min-h-[92px]">
|
||||
{isWorkspace ? (
|
||||
<span className="mx-auto w-[min(210px,100%)] rounded-lg border border-dashed border-border px-3 py-3">
|
||||
<span className="mx-auto mb-2 flex w-[min(160px,100%)] items-center gap-2 font-mono text-[11px] font-semibold text-muted-foreground">
|
||||
<Folder className="size-3.5" aria-hidden="true" />
|
||||
my-workspace/
|
||||
</span>
|
||||
{["web-app", "api-server", "shared-libs"].map((repo) => (
|
||||
<span
|
||||
key={repo}
|
||||
className="mx-auto mb-1.5 flex w-[min(170px,100%)] items-center gap-2 rounded-md bg-background px-2.5 py-1.5 font-mono text-[12px] font-semibold text-foreground last:mb-0"
|
||||
>
|
||||
<span className="size-1.5 rounded-full bg-success" aria-hidden="true" />
|
||||
{repo}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
) : (
|
||||
<span className="mx-auto max-w-full rounded-lg border border-border bg-background px-4 py-3 font-mono text-[12px] font-semibold text-foreground sm:px-5 sm:py-3.5 sm:text-[13px]">
|
||||
<span className="mr-2 inline-block size-1.5 rounded-full bg-success" aria-hidden="true" />
|
||||
web-app <span className="px-2 text-muted-foreground">·</span>
|
||||
<span className="text-muted-foreground">main</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="block text-[15px] font-semibold text-foreground sm:text-[16px]">
|
||||
{isWorkspace ? "Workspace" : "Project"}
|
||||
</span>
|
||||
<span className="mt-2 block text-[12px] leading-5 text-muted-foreground sm:min-h-[40px] sm:text-[13px]">
|
||||
{description}
|
||||
</span>
|
||||
<span className="mt-3 font-mono text-[12px] font-semibold text-passive">
|
||||
<span className="mr-2 text-passive">•</span>
|
||||
{isWorkspace ? "Multiple repositories" : "One repository"}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateProjectFolderDialog({
|
||||
disabled,
|
||||
error,
|
||||
kind,
|
||||
onBack,
|
||||
onChooseFolder,
|
||||
onOpenChange,
|
||||
open,
|
||||
scan,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
error: string | null;
|
||||
kind: ProjectKind;
|
||||
onBack: () => void;
|
||||
onChooseFolder: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
open: boolean;
|
||||
scan: ImportFolderScan | null;
|
||||
}) {
|
||||
const isWorkspace = kind === "workspace";
|
||||
const failedRepos = scan?.repos.filter((repo) => !repo.hasRemote) ?? [];
|
||||
const hasScan = scan !== null;
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/55 data-[state=open]:animate-overlay-in" />
|
||||
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 flex max-h-[min(640px,calc(100svh-24px))] w-[min(640px,calc(100vw-24px))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-border bg-popover p-0 text-popover-foreground shadow-xl data-[state=open]:animate-modal-in">
|
||||
<div className="flex shrink-0 items-start gap-3 border-b border-border px-4 py-4 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-8 shrink-0 place-items-center rounded-lg border border-border text-muted-foreground transition hover:bg-surface hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
||||
aria-label="Back to import type"
|
||||
disabled={disabled}
|
||||
onClick={onBack}
|
||||
>
|
||||
<ChevronRight className="size-4 rotate-180" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Dialog.Title className="text-[18px] font-semibold text-foreground">
|
||||
{isWorkspace ? "Import workspace" : "Import project"}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="mt-1 max-w-[520px] text-[13px] font-medium leading-5 text-muted-foreground">
|
||||
{isWorkspace
|
||||
? "Pick a folder that contains your Git repositories. Each repo inside it joins the workspace."
|
||||
: "Import a single Git repository as one project."}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-7 shrink-0 place-items-center rounded-md text-muted-foreground transition hover:bg-surface hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
||||
aria-label="Close import dialog"
|
||||
disabled={disabled}
|
||||
>
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<div className="min-h-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-6">
|
||||
{hasScan ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-border bg-background px-4 py-3">
|
||||
<Folder className="size-5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-mono text-[14px] font-semibold text-foreground">
|
||||
{displayImportPath(scan.path)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[12px] text-muted-foreground">
|
||||
{isWorkspace ? "Workspace root" : "Project folder"}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="button" variant="outline" disabled={disabled} onClick={onChooseFolder}>
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/40 bg-destructive/10">
|
||||
<div className="border-b border-destructive/30 px-4 py-3 font-mono text-[12px] font-semibold uppercase tracking-[0.12em] text-destructive">
|
||||
<span className="mr-2 inline-block size-2 rounded-full bg-destructive" aria-hidden="true" />
|
||||
Validation failed · {isWorkspace ? "workspace" : "project"} not registered
|
||||
</div>
|
||||
{failedRepos.length > 0 ? (
|
||||
failedRepos.map((repo) => <ImportRepoRow key={repo.path} repo={repo} failed />)
|
||||
) : (
|
||||
<div className="px-4 py-3 text-[12px] leading-5 text-destructive">{error}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scan.repos
|
||||
.filter((repo) => repo.hasRemote)
|
||||
.map((repo) => (
|
||||
<div key={repo.path} className="rounded-lg border border-border bg-background">
|
||||
<ImportRepoRow repo={repo} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{scan.repos.length === 0 && (
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-4 text-[12px] text-muted-foreground">
|
||||
No repositories detected in this folder.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-h-[132px] w-full flex-col items-center justify-center rounded-lg border border-dashed border-border bg-background px-4 py-5 text-center transition-colors hover:bg-surface disabled:pointer-events-none disabled:opacity-50 sm:min-h-[160px] sm:px-5 sm:py-6"
|
||||
disabled={disabled}
|
||||
onClick={onChooseFolder}
|
||||
>
|
||||
<span className="mb-4 grid size-11 place-items-center rounded-xl bg-card text-muted-foreground">
|
||||
<FolderPlus className="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="text-[15px] font-semibold text-foreground">
|
||||
{isWorkspace ? "Choose a folder" : "Choose a project folder"}
|
||||
</span>
|
||||
<span className="mt-2 max-w-full text-pretty text-[12px] text-muted-foreground sm:text-[13px]">
|
||||
{isWorkspace
|
||||
? "Opens your system file picker — pick the folder that holds your repos"
|
||||
: "Opens your system file picker — select one repo folder"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{error && (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-4 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[12px] leading-5 text-destructive",
|
||||
hasScan && "sr-only",
|
||||
)}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-3 border-t border-border px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||
<p className="text-[12px] font-medium text-muted-foreground">
|
||||
{hasScan && failedRepos.length > 0
|
||||
? `Resolve ${failedRepos.length} failed ${failedRepos.length === 1 ? "repository" : "repositories"} to continue`
|
||||
: isWorkspace
|
||||
? "No repositories to import"
|
||||
: "No project selected"}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 sm:gap-3">
|
||||
<Button type="button" variant="outline" disabled={disabled} onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" variant="primary" disabled>
|
||||
{isWorkspace ? "Import workspace" : "Import project"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ImportRepoRow({ failed = false, repo }: { failed?: boolean; repo: ImportFolderScan["repos"][number] }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
{failed ? (
|
||||
<XCircle className="size-5 shrink-0 text-destructive" aria-hidden="true" />
|
||||
) : (
|
||||
<CheckCircle2 className="size-5 shrink-0 text-success" aria-hidden="true" />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[14px] font-semibold text-foreground">{repo.name}</div>
|
||||
<div className="mt-0.5 truncate font-mono text-[12px] text-muted-foreground">
|
||||
{displayImportPath(repo.path)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"hidden max-w-[260px] shrink-0 truncate text-right font-mono text-[12px] sm:block",
|
||||
failed ? "text-muted-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{repo.hasRemote ? `${repo.branch} ${remoteDisplay(repo.remote)}` : "No remote configured"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function displayImportPath(value: string) {
|
||||
return value.replace(/^\/Users\/[^/]+/, "~");
|
||||
}
|
||||
|
||||
function remoteDisplay(remote: string) {
|
||||
const ssh = remote.match(/^[^@]+@([^:]+):(.+)$/);
|
||||
if (ssh?.[1] && ssh[2]) return `${ssh[1]}/${ssh[2].replace(/\.git$/, "")}`;
|
||||
try {
|
||||
const url = new URL(remote);
|
||||
return `${url.host}${url.pathname.replace(/\.git$/, "")}`;
|
||||
} catch {
|
||||
return remote.replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ async function fetchWorkspaces(): Promise<WorkspaceSummary[]> {
|
|||
return (projectsData?.projects ?? []).map((project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
kind: project.kind === "workspace" ? "workspace" : "single_repo",
|
||||
path: project.path,
|
||||
orchestratorAgent: project.orchestratorAgent ? toAgentProvider(project.orchestratorAgent) : undefined,
|
||||
sessions: (sessionsData?.sessions ?? [])
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const aoBridge: AoBridge =
|
|||
app: {
|
||||
getVersion: async () => "0.0.0-preview",
|
||||
chooseDirectory: async () => null,
|
||||
scanImportFolder: async ({ path }) => ({ path, repos: [] }),
|
||||
},
|
||||
clipboard: {
|
||||
writeText: async (text: string) => {
|
||||
|
|
|
|||
|
|
@ -8,189 +8,191 @@
|
|||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as ShellRouteImport } from "./routes/_shell";
|
||||
import { Route as ShellIndexRouteImport } from "./routes/_shell.index";
|
||||
import { Route as ShellSettingsRouteImport } from "./routes/_shell.settings";
|
||||
import { Route as ShellPrsRouteImport } from "./routes/_shell.prs";
|
||||
import { Route as ShellSessionsSessionIdRouteImport } from "./routes/_shell.sessions.$sessionId";
|
||||
import { Route as ShellProjectsProjectIdRouteImport } from "./routes/_shell.projects.$projectId";
|
||||
import { Route as ShellProjectsProjectIdSettingsRouteImport } from "./routes/_shell.projects.$projectId_.settings";
|
||||
import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from "./routes/_shell.projects.$projectId_.sessions.$sessionId";
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ShellRouteImport } from './routes/_shell'
|
||||
import { Route as ShellIndexRouteImport } from './routes/_shell.index'
|
||||
import { Route as ShellSettingsRouteImport } from './routes/_shell.settings'
|
||||
import { Route as ShellPrsRouteImport } from './routes/_shell.prs'
|
||||
import { Route as ShellSessionsSessionIdRouteImport } from './routes/_shell.sessions.$sessionId'
|
||||
import { Route as ShellProjectsProjectIdRouteImport } from './routes/_shell.projects.$projectId'
|
||||
import { Route as ShellProjectsProjectIdSettingsRouteImport } from './routes/_shell.projects.$projectId_.settings'
|
||||
import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from './routes/_shell.projects.$projectId_.sessions.$sessionId'
|
||||
|
||||
const ShellRoute = ShellRouteImport.update({
|
||||
id: "/_shell",
|
||||
id: '/_shell',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const ShellIndexRoute = ShellIndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const ShellSettingsRoute = ShellSettingsRouteImport.update({
|
||||
id: "/settings",
|
||||
path: "/settings",
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const ShellPrsRoute = ShellPrsRouteImport.update({
|
||||
id: "/prs",
|
||||
path: "/prs",
|
||||
id: '/prs',
|
||||
path: '/prs',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const ShellSessionsSessionIdRoute = ShellSessionsSessionIdRouteImport.update({
|
||||
id: "/sessions/$sessionId",
|
||||
path: "/sessions/$sessionId",
|
||||
id: '/sessions/$sessionId',
|
||||
path: '/sessions/$sessionId',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
const ShellProjectsProjectIdRoute = ShellProjectsProjectIdRouteImport.update({
|
||||
id: "/projects/$projectId",
|
||||
path: "/projects/$projectId",
|
||||
id: '/projects/$projectId',
|
||||
path: '/projects/$projectId',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
const ShellProjectsProjectIdSettingsRoute = ShellProjectsProjectIdSettingsRouteImport.update({
|
||||
id: "/projects/$projectId_/settings",
|
||||
path: "/projects/$projectId/settings",
|
||||
} as any)
|
||||
const ShellProjectsProjectIdSettingsRoute =
|
||||
ShellProjectsProjectIdSettingsRouteImport.update({
|
||||
id: '/projects/$projectId_/settings',
|
||||
path: '/projects/$projectId/settings',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
const ShellProjectsProjectIdSessionsSessionIdRoute = ShellProjectsProjectIdSessionsSessionIdRouteImport.update({
|
||||
id: "/projects/$projectId_/sessions/$sessionId",
|
||||
path: "/projects/$projectId/sessions/$sessionId",
|
||||
} as any)
|
||||
const ShellProjectsProjectIdSessionsSessionIdRoute =
|
||||
ShellProjectsProjectIdSessionsSessionIdRouteImport.update({
|
||||
id: '/projects/$projectId_/sessions/$sessionId',
|
||||
path: '/projects/$projectId/sessions/$sessionId',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof ShellIndexRoute;
|
||||
"/prs": typeof ShellPrsRoute;
|
||||
"/settings": typeof ShellSettingsRoute;
|
||||
"/projects/$projectId": typeof ShellProjectsProjectIdRoute;
|
||||
"/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
|
||||
"/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
|
||||
"/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
'/': typeof ShellIndexRoute
|
||||
'/prs': typeof ShellPrsRoute
|
||||
'/settings': typeof ShellSettingsRoute
|
||||
'/projects/$projectId': typeof ShellProjectsProjectIdRoute
|
||||
'/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
|
||||
'/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute
|
||||
'/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/prs": typeof ShellPrsRoute;
|
||||
"/settings": typeof ShellSettingsRoute;
|
||||
"/": typeof ShellIndexRoute;
|
||||
"/projects/$projectId": typeof ShellProjectsProjectIdRoute;
|
||||
"/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
|
||||
"/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
|
||||
"/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
'/prs': typeof ShellPrsRoute
|
||||
'/settings': typeof ShellSettingsRoute
|
||||
'/': typeof ShellIndexRoute
|
||||
'/projects/$projectId': typeof ShellProjectsProjectIdRoute
|
||||
'/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
|
||||
'/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute
|
||||
'/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/_shell": typeof ShellRouteWithChildren;
|
||||
"/_shell/prs": typeof ShellPrsRoute;
|
||||
"/_shell/settings": typeof ShellSettingsRoute;
|
||||
"/_shell/": typeof ShellIndexRoute;
|
||||
"/_shell/projects/$projectId": typeof ShellProjectsProjectIdRoute;
|
||||
"/_shell/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
|
||||
"/_shell/projects/$projectId_/settings": typeof ShellProjectsProjectIdSettingsRoute;
|
||||
"/_shell/projects/$projectId_/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
__root__: typeof rootRouteImport
|
||||
'/_shell': typeof ShellRouteWithChildren
|
||||
'/_shell/prs': typeof ShellPrsRoute
|
||||
'/_shell/settings': typeof ShellSettingsRoute
|
||||
'/_shell/': typeof ShellIndexRoute
|
||||
'/_shell/projects/$projectId': typeof ShellProjectsProjectIdRoute
|
||||
'/_shell/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
|
||||
'/_shell/projects/$projectId_/settings': typeof ShellProjectsProjectIdSettingsRoute
|
||||
'/_shell/projects/$projectId_/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| "/"
|
||||
| "/prs"
|
||||
| "/settings"
|
||||
| "/projects/$projectId"
|
||||
| "/sessions/$sessionId"
|
||||
| "/projects/$projectId/settings"
|
||||
| "/projects/$projectId/sessions/$sessionId";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
| '/'
|
||||
| '/prs'
|
||||
| '/settings'
|
||||
| '/projects/$projectId'
|
||||
| '/sessions/$sessionId'
|
||||
| '/projects/$projectId/settings'
|
||||
| '/projects/$projectId/sessions/$sessionId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| "/prs"
|
||||
| "/settings"
|
||||
| "/"
|
||||
| "/projects/$projectId"
|
||||
| "/sessions/$sessionId"
|
||||
| "/projects/$projectId/settings"
|
||||
| "/projects/$projectId/sessions/$sessionId";
|
||||
| '/prs'
|
||||
| '/settings'
|
||||
| '/'
|
||||
| '/projects/$projectId'
|
||||
| '/sessions/$sessionId'
|
||||
| '/projects/$projectId/settings'
|
||||
| '/projects/$projectId/sessions/$sessionId'
|
||||
id:
|
||||
| "__root__"
|
||||
| "/_shell"
|
||||
| "/_shell/prs"
|
||||
| "/_shell/settings"
|
||||
| "/_shell/"
|
||||
| "/_shell/projects/$projectId"
|
||||
| "/_shell/sessions/$sessionId"
|
||||
| "/_shell/projects/$projectId_/settings"
|
||||
| "/_shell/projects/$projectId_/sessions/$sessionId";
|
||||
fileRoutesById: FileRoutesById;
|
||||
| '__root__'
|
||||
| '/_shell'
|
||||
| '/_shell/prs'
|
||||
| '/_shell/settings'
|
||||
| '/_shell/'
|
||||
| '/_shell/projects/$projectId'
|
||||
| '/_shell/sessions/$sessionId'
|
||||
| '/_shell/projects/$projectId_/settings'
|
||||
| '/_shell/projects/$projectId_/sessions/$sessionId'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
ShellRoute: typeof ShellRouteWithChildren;
|
||||
ShellRoute: typeof ShellRouteWithChildren
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
"/_shell": {
|
||||
id: "/_shell";
|
||||
path: "";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof ShellRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/_shell/": {
|
||||
id: "/_shell/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof ShellIndexRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/settings": {
|
||||
id: "/_shell/settings";
|
||||
path: "/settings";
|
||||
fullPath: "/settings";
|
||||
preLoaderRoute: typeof ShellSettingsRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/prs": {
|
||||
id: "/_shell/prs";
|
||||
path: "/prs";
|
||||
fullPath: "/prs";
|
||||
preLoaderRoute: typeof ShellPrsRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/sessions/$sessionId": {
|
||||
id: "/_shell/sessions/$sessionId";
|
||||
path: "/sessions/$sessionId";
|
||||
fullPath: "/sessions/$sessionId";
|
||||
preLoaderRoute: typeof ShellSessionsSessionIdRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/projects/$projectId": {
|
||||
id: "/_shell/projects/$projectId";
|
||||
path: "/projects/$projectId";
|
||||
fullPath: "/projects/$projectId";
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/projects/$projectId_/settings": {
|
||||
id: "/_shell/projects/$projectId_/settings";
|
||||
path: "/projects/$projectId/settings";
|
||||
fullPath: "/projects/$projectId/settings";
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/projects/$projectId_/sessions/$sessionId": {
|
||||
id: "/_shell/projects/$projectId_/sessions/$sessionId";
|
||||
path: "/projects/$projectId/sessions/$sessionId";
|
||||
fullPath: "/projects/$projectId/sessions/$sessionId";
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
'/_shell': {
|
||||
id: '/_shell'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof ShellRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_shell/': {
|
||||
id: '/_shell/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof ShellIndexRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/settings': {
|
||||
id: '/_shell/settings'
|
||||
path: '/settings'
|
||||
fullPath: '/settings'
|
||||
preLoaderRoute: typeof ShellSettingsRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/prs': {
|
||||
id: '/_shell/prs'
|
||||
path: '/prs'
|
||||
fullPath: '/prs'
|
||||
preLoaderRoute: typeof ShellPrsRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/sessions/$sessionId': {
|
||||
id: '/_shell/sessions/$sessionId'
|
||||
path: '/sessions/$sessionId'
|
||||
fullPath: '/sessions/$sessionId'
|
||||
preLoaderRoute: typeof ShellSessionsSessionIdRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/projects/$projectId': {
|
||||
id: '/_shell/projects/$projectId'
|
||||
path: '/projects/$projectId'
|
||||
fullPath: '/projects/$projectId'
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/projects/$projectId_/settings': {
|
||||
id: '/_shell/projects/$projectId_/settings'
|
||||
path: '/projects/$projectId/settings'
|
||||
fullPath: '/projects/$projectId/settings'
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/projects/$projectId_/sessions/$sessionId': {
|
||||
id: '/_shell/projects/$projectId_/sessions/$sessionId'
|
||||
path: '/projects/$projectId/sessions/$sessionId'
|
||||
fullPath: '/projects/$projectId/sessions/$sessionId'
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ShellRouteChildren {
|
||||
ShellPrsRoute: typeof ShellPrsRoute;
|
||||
ShellSettingsRoute: typeof ShellSettingsRoute;
|
||||
ShellIndexRoute: typeof ShellIndexRoute;
|
||||
ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute;
|
||||
ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute;
|
||||
ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute;
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
ShellPrsRoute: typeof ShellPrsRoute
|
||||
ShellSettingsRoute: typeof ShellSettingsRoute
|
||||
ShellIndexRoute: typeof ShellIndexRoute
|
||||
ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute
|
||||
ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute
|
||||
ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
}
|
||||
|
||||
const ShellRouteChildren: ShellRouteChildren = {
|
||||
|
|
@ -200,12 +202,15 @@ const ShellRouteChildren: ShellRouteChildren = {
|
|||
ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute,
|
||||
ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute,
|
||||
ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute,
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute: ShellProjectsProjectIdSessionsSessionIdRoute,
|
||||
};
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute:
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute,
|
||||
}
|
||||
|
||||
const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren);
|
||||
const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
ShellRoute: ShellRouteWithChildren,
|
||||
};
|
||||
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ function ShellLayout() {
|
|||
workerAgent: string;
|
||||
orchestratorAgent: string;
|
||||
trackerIntake?: components["schemas"]["TrackerIntakeConfig"];
|
||||
asWorkspace?: boolean;
|
||||
}) => {
|
||||
void addRendererExceptionStep("Project add requested", {
|
||||
source: "project-add",
|
||||
|
|
@ -83,6 +84,7 @@ function ShellLayout() {
|
|||
const { data, error } = await apiClient.POST("/api/v1/projects", {
|
||||
body: {
|
||||
path: input.path,
|
||||
asWorkspace: input.asWorkspace || undefined,
|
||||
config: {
|
||||
worker: { agent: input.workerAgent },
|
||||
orchestrator: { agent: input.orchestratorAgent },
|
||||
|
|
@ -104,7 +106,9 @@ function ShellLayout() {
|
|||
const workspace: WorkspaceSummary = {
|
||||
id: data.project.id,
|
||||
name: data.project.name,
|
||||
kind: data.project.kind === "workspace" ? "workspace" : "single_repo",
|
||||
path: data.project.path,
|
||||
workspaceRepos: data.project.workspaceRepos,
|
||||
type: "main",
|
||||
orchestratorAgent: input.orchestratorAgent as WorkspaceSummary["orchestratorAgent"],
|
||||
sessions: [],
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ if (typeof window !== "undefined") {
|
|||
app: {
|
||||
getVersion: async () => "0.0.0-test",
|
||||
chooseDirectory: async () => null,
|
||||
scanImportFolder: async ({ path }: { path: string }) => ({ path, repos: [] }),
|
||||
},
|
||||
clipboard: {
|
||||
writeText: async () => undefined,
|
||||
|
|
|
|||
|
|
@ -174,6 +174,14 @@ export function canonicalTrackerIssueId(issueId?: string): string | undefined {
|
|||
return TRACKER_PROVIDER_PREFIXES.some((prefix) => issueId.startsWith(prefix)) ? issueId : undefined;
|
||||
}
|
||||
|
||||
export type ProjectKind = "single_repo" | "workspace";
|
||||
|
||||
export type WorkspaceRepoSummary = {
|
||||
name: string;
|
||||
relativePath: string;
|
||||
repo: string;
|
||||
};
|
||||
|
||||
/** Glanceable worker status. Maps 1:1 to the accent colors in DESIGN.md. */
|
||||
export type WorkerDisplayStatus =
|
||||
"working" | "needs_you" | "mergeable" | "ci_failed" | "no_signal" | "done" | "unknown";
|
||||
|
|
@ -355,7 +363,9 @@ export function attentionZone(session: WorkspaceSession): AttentionZone {
|
|||
export type WorkspaceSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind?: ProjectKind;
|
||||
path: string;
|
||||
workspaceRepos?: WorkspaceRepoSummary[];
|
||||
type?: "main" | "worktree";
|
||||
orchestratorAgent?: AgentProvider;
|
||||
accentColor?: string;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,11 @@ describe("buildDaemonEnv", () => {
|
|||
const env = buildDaemonEnv({ ...minimalProcessEnv, TERM: "screen-256color" }, null, {});
|
||||
expect(env.TERM).toBe("screen-256color");
|
||||
});
|
||||
|
||||
it("replaces TERM=dumb with a tmux-usable default", () => {
|
||||
const env = buildDaemonEnv({ ...minimalProcessEnv, TERM: "dumb" }, null, {});
|
||||
expect(env.TERM).toBe("xterm-256color");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveShellPath", () => {
|
||||
|
|
|
|||
|
|
@ -63,8 +63,15 @@ export function withFallbackPath(currentPath: string | undefined): string {
|
|||
return result.join(":");
|
||||
}
|
||||
|
||||
function normalizeTerm(term: string | undefined): string {
|
||||
const trimmed = term?.trim();
|
||||
if (!trimmed || trimmed === "dumb") return "xterm-256color";
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// Base = shell env, overlaid by processEnv so Electron/AO runtime vars win, then
|
||||
// PATH forced to the shell's PATH (with floor), then explicit overrides.
|
||||
// PATH forced to the shell's PATH (with floor), TERM forced to a tmux-usable
|
||||
// value, then explicit overrides.
|
||||
//
|
||||
// TERM defaults to xterm-256color (what the renderer's xterm.js emulates): a
|
||||
// Finder/Dock launch starts under launchd with no controlling tty, so TERM is
|
||||
|
|
@ -78,6 +85,7 @@ export function buildDaemonEnv(
|
|||
): NodeJS.ProcessEnv {
|
||||
const merged: NodeJS.ProcessEnv = { TERM: "xterm-256color", ...(shellEnv ?? {}), ...processEnv };
|
||||
merged.PATH = withFallbackPath(shellEnv?.PATH ?? processEnv.PATH);
|
||||
merged.TERM = normalizeTerm(merged.TERM);
|
||||
return { ...merged, ...overrides };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue