From 4cd84a47349681c23110fc2ed7308794a6fa805c Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:37:49 +0530 Subject: [PATCH 01/38] fix: add API proxy to Vite dev server for web mode CORS When running in web mode (npm run dev:web), the frontend on localhost:3007 was making cross-origin requests to the backend on localhost:3008, causing CORS errors. Added Vite proxy configuration to forward /api requests from the dev server to the backend. This makes all API calls appear same-origin to the browser, eliminating CORS blocks during development. Now web mode users can access http://localhost:3007 without CORS errors. Fixes: CORS "Not allowed by CORS" errors in web mode Co-Authored-By: Claude Haiku 4.5 --- apps/ui/vite.config.mts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/ui/vite.config.mts b/apps/ui/vite.config.mts index 0d18997e..81d74391 100644 --- a/apps/ui/vite.config.mts +++ b/apps/ui/vite.config.mts @@ -68,6 +68,12 @@ export default defineConfig(({ command }) => { host: process.env.HOST || '0.0.0.0', port: parseInt(process.env.TEST_PORT || '3007', 10), allowedHosts: true, + proxy: { + '/api': { + target: 'http://localhost:3008', + changeOrigin: true, + }, + }, }, build: { outDir: 'dist', From 7eae0215f286c3636cbba7c784ccda85fcf10219 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:38:09 +0530 Subject: [PATCH 02/38] chore: update package-lock.json --- package-lock.json | 88 ++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fc7b149..97a2c4fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "tree-kill": "1.2.2" }, "devDependencies": { - "dmg-license": "^1.0.11", "husky": "9.1.7", "lint-staged": "16.2.7", "prettier": "3.7.4", @@ -26,6 +25,9 @@ }, "engines": { "node": ">=22.0.0 <23.0.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" } }, "apps/server": { @@ -6114,7 +6116,7 @@ "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -6124,15 +6126,15 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" @@ -6156,7 +6158,6 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6166,7 +6167,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -6213,8 +6214,8 @@ "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -6719,7 +6720,7 @@ "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6921,7 +6922,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -7003,7 +7004,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -7013,7 +7014,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -7237,8 +7238,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.8" } @@ -7289,8 +7290,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8" } @@ -7363,7 +7364,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -7537,7 +7538,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -8033,8 +8034,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" @@ -8128,7 +8129,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -8141,7 +8142,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/colorette": { @@ -8309,8 +8310,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/cors": { "version": "2.8.5", @@ -8329,8 +8330,8 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "buffer": "^5.1.0" } @@ -8377,7 +8378,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/d3-color": { @@ -8792,8 +8792,8 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -9057,7 +9057,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -9682,11 +9682,11 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, "engines": [ "node >=0.6.0" ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -9698,7 +9698,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -10648,8 +10648,8 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -10678,7 +10678,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -10866,7 +10866,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11132,7 +11132,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-schema-typed": { @@ -11253,6 +11253,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11318,6 +11319,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -13077,8 +13079,8 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/node-api-version": { "version": "0.2.1", @@ -13677,7 +13679,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -13793,7 +13795,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -14593,8 +14595,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -14608,7 +14610,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -14805,7 +14807,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14850,7 +14852,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15609,7 +15611,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -15709,8 +15711,8 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, "license": "MIT", + "optional": true, "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -16153,7 +16155,7 @@ "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.0" From 4186b80a82a3c324a336fee0f7bf0ae9ed845d12 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:41:21 +0530 Subject: [PATCH 03/38] fix: use relative URLs in web mode to leverage Vite proxy In web mode, the API client was hardcoding localhost:3008, which bypassed the Vite proxy and caused CORS errors. Now it uses relative URLs (just /api) in web mode, allowing the proxy to handle routing and making requests appear same-origin. - Web mode: Use relative URLs for proxy routing (no CORS issues) - Electron mode: Continue using hardcoded localhost:3008 This allows the Vite proxy configuration to actually work in web mode. Fixes: Persistent CORS errors in web mode development Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/lib/http-api-client.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index cd0e6739..f069af1c 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -156,6 +156,12 @@ const getServerUrl = (): string => { if (typeof window !== 'undefined') { const envUrl = import.meta.env.VITE_SERVER_URL; if (envUrl) return envUrl; + + // In web mode (not Electron), use relative URL to leverage Vite proxy + // This avoids CORS issues since requests appear same-origin + if (!window.electron) { + return ''; + } } // Use VITE_HOSTNAME if set, otherwise default to localhost const hostname = import.meta.env.VITE_HOSTNAME || 'localhost'; From b8875f71a50085ee253166bf60104248033bbb09 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:45:10 +0530 Subject: [PATCH 04/38] fix: improve CORS configuration to handle localhost and private IPs The CORS check was too strict for local development. Changed to: - Parse origin URL properly to extract hostname - Allow all localhost origins (any port) - Allow all 127.0.0.1 origins (loopback IP) - Allow all private network IPs (192.168.x.x, 10.x.x.x, 172.x.x.x) - Keep security by rejecting unknown origins This fixes CORS errors when accessing from http://localhost:3007 or other local addresses during web mode development. Fixes: "Not allowed by CORS" errors in web mode Co-Authored-By: Claude Haiku 4.5 --- apps/server/src/index.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index d90c7a36..4219dc9e 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -175,11 +175,17 @@ app.use( return; } - // For local development, allow localhost origins + // For local development, allow all localhost/loopback origins (any port) + const url = new URL(origin); + const hostname = url.hostname; if ( - origin.startsWith('http://localhost:') || - origin.startsWith('http://127.0.0.1:') || - origin.startsWith('http://[::1]:') + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '::1' || + hostname === '0.0.0.0' || + hostname.startsWith('192.168.') || + hostname.startsWith('10.') || + hostname.startsWith('172.') ) { callback(null, origin); return; From e10cb83adcf16dc7edacb801abf280c46b78eabd Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:47:53 +0530 Subject: [PATCH 05/38] debug: add CORS logging to diagnose origin rejection Added detailed logging to see: - What origin is being sent - How the hostname is parsed - Why origins are being accepted/rejected This will help us understand why CORS is still failing in web mode. Co-Authored-By: Claude Haiku 4.5 --- apps/server/src/index.ts | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 4219dc9e..06575282 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -164,9 +164,12 @@ app.use( return; } + console.log(`[CORS] Checking origin: ${origin}`); + // If CORS_ORIGIN is set, use it (can be comma-separated list) const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()); if (allowedOrigins && allowedOrigins.length > 0 && allowedOrigins[0] !== '*') { + console.log(`[CORS] CORS_ORIGIN env var is set: ${allowedOrigins.join(', ')}`); if (allowedOrigins.includes(origin)) { callback(null, origin); } else { @@ -176,22 +179,30 @@ app.use( } // For local development, allow all localhost/loopback origins (any port) - const url = new URL(origin); - const hostname = url.hostname; - if ( - hostname === 'localhost' || - hostname === '127.0.0.1' || - hostname === '::1' || - hostname === '0.0.0.0' || - hostname.startsWith('192.168.') || - hostname.startsWith('10.') || - hostname.startsWith('172.') - ) { - callback(null, origin); - return; + try { + const url = new URL(origin); + const hostname = url.hostname; + console.log(`[CORS] Parsed hostname: ${hostname}`); + + if ( + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '::1' || + hostname === '0.0.0.0' || + hostname.startsWith('192.168.') || + hostname.startsWith('10.') || + hostname.startsWith('172.') + ) { + console.log(`[CORS] ✓ Allowing origin: ${origin}`); + callback(null, origin); + return; + } + } catch (err) { + console.error(`[CORS] Error parsing URL: ${origin}`, err); } // Reject other origins by default for security + console.log(`[CORS] ✗ Rejecting origin: ${origin}`); callback(new Error('Not allowed by CORS')); }, credentials: true, From b0b49764b98c8bd4e15d57a4e4719ac1be9a6d46 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:50:41 +0530 Subject: [PATCH 06/38] fix: add localhost to CORS_ORIGIN for web mode development The web mode launcher was setting CORS_ORIGIN to only include the system hostname and 127.0.0.1, but users access via http://localhost:3007 which wasn't in the allowed list. Now includes: - http://localhost:3007 (primary dev URL) - http://$HOSTNAME:3007 (system hostname if needed) - http://127.0.0.1:3007 (loopback IP) Also cleaned up debug logging from CORS check since root cause is now clear. Fixes: Persistent "Not allowed by CORS" errors in web mode Co-Authored-By: Claude Haiku 4.5 --- apps/server/src/index.ts | 8 +------- start-automaker.sh | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 06575282..70cf9318 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -164,12 +164,9 @@ app.use( return; } - console.log(`[CORS] Checking origin: ${origin}`); - // If CORS_ORIGIN is set, use it (can be comma-separated list) const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()); if (allowedOrigins && allowedOrigins.length > 0 && allowedOrigins[0] !== '*') { - console.log(`[CORS] CORS_ORIGIN env var is set: ${allowedOrigins.join(', ')}`); if (allowedOrigins.includes(origin)) { callback(null, origin); } else { @@ -182,7 +179,6 @@ app.use( try { const url = new URL(origin); const hostname = url.hostname; - console.log(`[CORS] Parsed hostname: ${hostname}`); if ( hostname === 'localhost' || @@ -193,16 +189,14 @@ app.use( hostname.startsWith('10.') || hostname.startsWith('172.') ) { - console.log(`[CORS] ✓ Allowing origin: ${origin}`); callback(null, origin); return; } } catch (err) { - console.error(`[CORS] Error parsing URL: ${origin}`, err); + // Ignore URL parsing errors } // Reject other origins by default for security - console.log(`[CORS] ✗ Rejecting origin: ${origin}`); callback(new Error('Not allowed by CORS')); }, credentials: true, diff --git a/start-automaker.sh b/start-automaker.sh index a2d3e54c..86be391c 100755 --- a/start-automaker.sh +++ b/start-automaker.sh @@ -1075,7 +1075,7 @@ case $MODE in export TEST_PORT="$WEB_PORT" export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT" export PORT="$SERVER_PORT" - export CORS_ORIGIN="http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT" + export CORS_ORIGIN="http://localhost:$WEB_PORT,http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT" export VITE_APP_MODE="1" if [ "$PRODUCTION_MODE" = true ]; then From fdad82bf8887494ae8b4d934868b66ddc77c06ea Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 01:52:11 +0530 Subject: [PATCH 07/38] fix: enable WebSocket proxying in Vite dev server Enables ws: true for /api proxy to properly forward WebSocket connections through the development server in web mode. This ensures real-time features work correctly when developing in browser mode. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/vite.config.mts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ui/vite.config.mts b/apps/ui/vite.config.mts index 81d74391..1a378d56 100644 --- a/apps/ui/vite.config.mts +++ b/apps/ui/vite.config.mts @@ -72,6 +72,7 @@ export default defineConfig(({ command }) => { '/api': { target: 'http://localhost:3008', changeOrigin: true, + ws: true, }, }, }, From a7f7898ee4a4b868c6934a119c2323b7ebd558ad Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:02:10 +0530 Subject: [PATCH 08/38] fix: persist session token to localStorage for web mode page reload survival Web mode sessions were being lost on page reload because the session token was stored only in memory (cachedSessionToken). When the page reloaded, the token was cleared and verifySession() would fail, redirecting users to login. This commit adds localStorage persistence for the session token, ensuring: 1. Token survives page reloads in web mode 2. verifySession() can use the persisted token from localStorage 3. Token is cleared properly on logout 4. Graceful fallback if localStorage is unavailable (SSR, disabled storage) The HTTP-only cookie alone isn't sufficient for web mode due to SameSite cookie restrictions and potential proxy issues with credentials forwarding. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/lib/http-api-client.ts | 36 ++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index f069af1c..2943f3e2 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -179,8 +179,24 @@ let apiKeyInitialized = false; let apiKeyInitPromise: Promise | null = null; // Cached session token for authentication (Web mode - explicit header auth) -// Only used in-memory after fresh login; on refresh we rely on HTTP-only cookies +// Persisted to localStorage to survive page reloads let cachedSessionToken: string | null = null; +const SESSION_TOKEN_KEY = 'automaker:sessionToken'; + +// Initialize cached session token from localStorage on module load +// This ensures web mode survives page reloads with valid authentication +const initSessionToken = (): void => { + if (typeof window === 'undefined') return; // Skip in SSR + try { + cachedSessionToken = window.localStorage.getItem(SESSION_TOKEN_KEY); + } catch { + // localStorage might be disabled or unavailable + cachedSessionToken = null; + } +}; + +// Initialize on module load +initSessionToken(); // Get API key for Electron mode (returns cached value after initialization) // Exported for use in WebSocket connections that need auth @@ -200,14 +216,30 @@ export const waitForApiKeyInit = (): Promise => { // Get session token for Web mode (returns cached value after login) export const getSessionToken = (): string | null => cachedSessionToken; -// Set session token (called after login) +// Set session token (called after login) - persists to localStorage for page reload survival export const setSessionToken = (token: string | null): void => { cachedSessionToken = token; + if (typeof window === 'undefined') return; // Skip in SSR + try { + if (token) { + window.localStorage.setItem(SESSION_TOKEN_KEY, token); + } else { + window.localStorage.removeItem(SESSION_TOKEN_KEY); + } + } catch { + // localStorage might be disabled; continue with in-memory cache + } }; // Clear session token (called on logout) export const clearSessionToken = (): void => { cachedSessionToken = null; + if (typeof window === 'undefined') return; // Skip in SSR + try { + window.localStorage.removeItem(SESSION_TOKEN_KEY); + } catch { + // localStorage might be disabled + } }; /** From 174c02cb79f66e806f95246ef0e88a754315fe16 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:09:28 +0530 Subject: [PATCH 09/38] fix: automatically remove projects with non-existent paths When a project fails to initialize because the directory no longer exists (e.g., test artifacts, deleted folders), automatically remove it from the project list instead of showing the error repeatedly on every reload. This prevents users from being stuck with broken project references in their settings after testing or when project directories are moved/deleted. The user is notified with a toast message explaining the removal. Co-Authored-By: Claude Haiku 4.5 --- .../src/components/views/dashboard-view.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/components/views/dashboard-view.tsx b/apps/ui/src/components/views/dashboard-view.tsx index 7e657c80..df962916 100644 --- a/apps/ui/src/components/views/dashboard-view.tsx +++ b/apps/ui/src/components/views/dashboard-view.tsx @@ -124,6 +124,19 @@ export function DashboardView() { const initResult = await initializeProject(path); if (!initResult.success) { + // If the project directory doesn't exist, automatically remove it from the project list + if (initResult.error?.includes('does not exist')) { + const projectToRemove = projects.find((p) => p.path === path); + if (projectToRemove) { + logger.warn(`[Dashboard] Removing project with non-existent path: ${path}`); + moveProjectToTrash(projectToRemove.id); + toast.error('Project directory not found', { + description: `Removed ${name} from your projects list since the directory no longer exists.`, + }); + return; + } + } + toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); @@ -151,7 +164,15 @@ export function DashboardView() { setIsOpening(false); } }, - [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject, navigate] + [ + projects, + trashedProjects, + currentProject, + globalTheme, + upsertAndSetCurrentProject, + navigate, + moveProjectToTrash, + ] ); const handleOpenProject = useCallback(async () => { From 2a8706e714a0f2ad9bd8cc595379286990de35ca Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:21:47 +0530 Subject: [PATCH 10/38] fix: add session token to image URLs for web mode authentication In web mode, image loads may not send session cookies due to proxy/CORS restrictions. This adds the session token as a query parameter to ensure images load correctly with proper authentication in web mode. Fixes custom project icons and images not loading in web mode. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/lib/api-fetch.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/lib/api-fetch.ts b/apps/ui/src/lib/api-fetch.ts index b544c993..f8959c8f 100644 --- a/apps/ui/src/lib/api-fetch.ts +++ b/apps/ui/src/lib/api-fetch.ts @@ -185,7 +185,13 @@ export function getAuthenticatedImageUrl( if (apiKey) { params.set('apiKey', apiKey); } - // Note: Session token auth relies on cookies which are sent automatically by the browser + + // Web mode: also add session token as query param for image loads + // This ensures images load correctly even if cookies aren't sent (e.g., cross-origin proxy scenarios) + const sessionToken = getSessionToken(); + if (sessionToken) { + params.set('token', sessionToken); + } return `${serverUrl}/api/fs/image?${params.toString()}`; } From b66efae5b7e4ca3652004b5cc48c408d131cc46d Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:30:16 +0530 Subject: [PATCH 11/38] fix: sync projects immediately instead of debouncing Projects are critical data that must persist across mode switches (Electron/web). Previously, project changes were debounced by 1 second, which could cause data loss if: 1. User switched from Electron to web mode quickly 2. App closed before debounce timer fired 3. Network temporarily unavailable during debounce window This change makes project array changes sync immediately (syncNow) instead of using the 1-second debounce, ensuring projects are always persisted to the server right away and visible in both Electron and web modes. Fixes issue where projects opened in Electron didn't appear in web mode. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/hooks/use-settings-sync.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index ea865566..80fd00a8 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -340,9 +340,22 @@ export function useSettingsSync(): SettingsSyncState { return; } - // Check if any synced field changed + // If projects array changed (by reference, meaning content changed), sync immediately + // This is critical - projects list changes must sync right away to prevent loss + // when switching between Electron and web modes or closing the app + if (newState.projects !== prevState.projects) { + logger.debug('Projects array changed, syncing immediately', { + prevCount: prevState.projects?.length ?? 0, + newCount: newState.projects?.length ?? 0, + }); + syncNow(); + return; + } + + // Check if any other synced field changed let changed = false; for (const field of SETTINGS_FIELDS_TO_SYNC) { + if (field === 'projects') continue; // Already handled above if (hasSettingsFieldChanged(field, newState, prevState)) { changed = true; break; From 9137f0e75fca14db458020e7dddde193570f2189 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 02:46:31 +0530 Subject: [PATCH 12/38] fix: keep localStorage cache in sync with server settings When switching between Electron and web modes or when the server temporarily stops, web mode was falling back to stale localStorage data instead of fresh server data. This fix: 1. Updates localStorage cache whenever fresh server settings are fetched 2. Updates localStorage cache whenever settings are synced to server 3. Prioritizes fresh settings cache over old Zustand persisted storage This ensures that: - Web mode always sees the latest projects even after mode switches - Switching from Electron to web mode immediately shows new projects - Server restarts don't cause web mode to use stale cached data Fixes issue where projects opened in Electron didn't appear in web mode after stopping and restarting the server. Co-Authored-By: Claude Haiku 4.5 --- apps/ui/src/hooks/use-settings-migration.ts | 23 +++++++++++++++++++++ apps/ui/src/hooks/use-settings-sync.ts | 9 ++++++++ 2 files changed, 32 insertions(+) diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 07119b85..20824a30 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -114,6 +114,20 @@ export function resetMigrationState(): void { */ export function parseLocalStorageSettings(): Partial | null { try { + // First, check for fresh server settings cache (updated whenever server settings are fetched) + // This prevents stale data when switching between modes + const settingsCache = getItem('automaker-settings-cache'); + if (settingsCache) { + try { + const cached = JSON.parse(settingsCache) as GlobalSettings; + logger.debug('Using fresh settings cache from localStorage'); + return cached; + } catch (e) { + logger.warn('Failed to parse settings cache, falling back to old storage'); + } + } + + // Fall back to old Zustand persisted storage const automakerStorage = getItem('automaker-storage'); if (!automakerStorage) { return null; @@ -412,6 +426,15 @@ export function useSettingsMigration(): MigrationState { if (global.success && global.settings) { serverSettings = global.settings as unknown as GlobalSettings; logger.info(`Server has ${serverSettings.projects?.length ?? 0} projects`); + + // Update localStorage with fresh server data to keep cache in sync + // This prevents stale localStorage data from being used when switching between modes + try { + localStorage.setItem('automaker-settings-cache', JSON.stringify(serverSettings)); + logger.debug('Updated localStorage with fresh server settings'); + } catch (storageError) { + logger.warn('Failed to update localStorage cache:', storageError); + } } } catch (error) { logger.error('Failed to fetch server settings:', error); diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 80fd00a8..e1346a91 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -215,6 +215,15 @@ export function useSettingsSync(): SettingsSyncState { if (result.success) { lastSyncedRef.current = updateHash; logger.debug('Settings synced to server'); + + // Update localStorage cache with synced settings to keep it fresh + // This prevents stale data when switching between Electron and web modes + try { + setItem('automaker-settings-cache', JSON.stringify(updates)); + logger.debug('Updated localStorage cache after sync'); + } catch (storageError) { + logger.warn('Failed to update localStorage cache after sync:', storageError); + } } else { logger.error('Failed to sync settings:', result.error); } From 7b7ac72c14856f06130beab1d7143fcfe277d5fe Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 18 Jan 2026 03:06:09 +0530 Subject: [PATCH 13/38] fix: use shared data directory for Electron and web modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL FIX: Electron and web mode were using DIFFERENT data directories: - Electron: Docker volume 'automaker-data' (isolated from host) - Web: Local ./data directory (host filesystem) This caused projects opened in Electron to never appear in web mode because they were synced to a completely separate Docker volume. Solution: Mount the host's ./data directory into both containers This ensures Electron and web mode always share the same data directory and all projects are immediately visible across modes. Now when you: 1. Open projects in Electron → synced to ./data 2. Switch to web mode → loads from same ./data 3. Restart server → both see the same projects Fixes issue where projects opened in Electron don't appear in web mode. Co-Authored-By: Claude Haiku 4.5 --- docker-compose.dev-server.yml | 9 ++++----- docker-compose.dev.yml | 8 +++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/docker-compose.dev-server.yml b/docker-compose.dev-server.yml index 9ff0972e..ea44fffc 100644 --- a/docker-compose.dev-server.yml +++ b/docker-compose.dev-server.yml @@ -59,8 +59,10 @@ services: # This ensures native modules are built for the container's architecture - automaker-dev-node-modules:/app/node_modules - # Persist data across restarts - - automaker-data:/data + # IMPORTANT: Mount local ./data directory (not a Docker volume) + # This ensures Electron and web mode share the same data directory + # and projects opened in either mode are visible in both + - ./data:/data # Persist CLI configurations - automaker-claude-config:/home/automaker/.claude @@ -97,9 +99,6 @@ volumes: name: automaker-dev-node-modules # Named volume for container-specific node_modules - automaker-data: - name: automaker-data - automaker-claude-config: name: automaker-claude-config diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index de4ebb11..d9cf830f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -60,8 +60,9 @@ services: # This ensures native modules are built for the container's architecture - automaker-dev-node-modules:/app/node_modules - # Persist data across restarts - - automaker-data:/data + # IMPORTANT: Mount local ./data directory (not a Docker volume) + # This ensures data is consistent across Electron and web modes + - ./data:/data # Persist CLI configurations - automaker-claude-config:/home/automaker/.claude @@ -141,9 +142,6 @@ volumes: name: automaker-dev-node-modules # Named volume for container-specific node_modules - automaker-data: - name: automaker-data - automaker-claude-config: name: automaker-claude-config From 832d10e133f7cf83e2d5321740f757c14601b2d4 Mon Sep 17 00:00:00 2001 From: webdevcody Date: Sat, 17 Jan 2026 17:58:16 -0500 Subject: [PATCH 14/38] refactor: replace Loader2 with Spinner component across the application This update standardizes the loading indicators by replacing all instances of Loader2 with the new Spinner component. The Spinner component provides a consistent look and feel for loading states throughout the UI, enhancing the user experience. Changes include: - Updated loading indicators in various components such as popovers, modals, and views. - Ensured that the Spinner component is used with appropriate sizes for different contexts. No functional changes were made; this is purely a visual and structural improvement. --- .../src/components/claude-usage-popover.tsx | 3 +- .../ui/src/components/codex-usage-popover.tsx | 3 +- .../dialogs/board-background-modal.tsx | 7 ++-- .../components/dialogs/new-project-modal.tsx | 14 ++----- .../dialogs/workspace-picker-modal.tsx | 5 ++- .../sidebar/components/sidebar-navigation.tsx | 7 ++-- apps/ui/src/components/session-manager.tsx | 4 +- apps/ui/src/components/ui/button.tsx | 4 +- .../ui/description-image-dropzone.tsx | 5 ++- .../components/ui/feature-image-upload.tsx | 5 ++- apps/ui/src/components/ui/git-diff-panel.tsx | 4 +- apps/ui/src/components/ui/image-drop-zone.tsx | 5 ++- apps/ui/src/components/ui/loading-state.tsx | 10 ++--- apps/ui/src/components/ui/log-viewer.tsx | 4 +- apps/ui/src/components/ui/spinner.tsx | 32 +++++++++++++++ .../src/components/ui/task-progress-panel.tsx | 5 ++- apps/ui/src/components/usage-popover.tsx | 5 ++- .../src/components/views/agent-tools-view.tsx | 8 ++-- .../components/thinking-indicator.tsx | 16 +------- .../ui/src/components/views/analysis-view.tsx | 10 ++--- apps/ui/src/components/views/board-view.tsx | 4 +- .../views/board-view/board-search-bar.tsx | 5 ++- .../kanban-card/agent-info-panel.tsx | 14 ++----- .../components/kanban-card/card-header.tsx | 6 +-- .../board-view/dialogs/agent-output-modal.tsx | 9 +++-- .../dialogs/backlog-plan-dialog.tsx | 19 +++------ .../dialogs/commit-worktree-dialog.tsx | 5 ++- .../dialogs/create-branch-dialog.tsx | 5 ++- .../board-view/dialogs/create-pr-dialog.tsx | 5 ++- .../dialogs/create-worktree-dialog.tsx | 5 ++- .../dialogs/delete-worktree-dialog.tsx | 5 ++- .../dialogs/merge-worktree-dialog.tsx | 5 ++- .../dialogs/plan-approval-dialog.tsx | 7 ++-- .../board-view/init-script-indicator.tsx | 5 ++- .../views/board-view/mobile-usage-bar.tsx | 9 +++-- .../board-view/shared/model-selector.tsx | 4 +- .../shared/planning-mode-selector.tsx | 4 +- .../components/branch-switch-dropdown.tsx | 5 ++- .../components/dev-server-logs-panel.tsx | 8 ++-- .../components/worktree-mobile-dropdown.tsx | 7 ++-- .../components/worktree-tab.tsx | 11 ++--- .../worktree-panel/worktree-panel.tsx | 5 ++- apps/ui/src/components/views/code-view.tsx | 5 ++- apps/ui/src/components/views/context-view.tsx | 11 +++-- .../src/components/views/dashboard-view.tsx | 4 +- .../components/issue-detail-panel.tsx | 10 ++--- .../components/issue-row.tsx | 4 +- .../components/issues-list-header.tsx | 3 +- .../src/components/views/github-prs-view.tsx | 7 ++-- .../src/components/views/graph-view-page.tsx | 4 +- .../components/ideation-dashboard.tsx | 11 ++--- .../components/prompt-category-grid.tsx | 4 +- .../ideation-view/components/prompt-list.tsx | 7 ++-- .../components/views/ideation-view/index.tsx | 9 ++--- .../src/components/views/interview-view.tsx | 7 ++-- apps/ui/src/components/views/login-view.tsx | 9 +++-- apps/ui/src/components/views/memory-view.tsx | 3 +- .../components/views/notifications-view.tsx | 5 ++- .../worktree-preferences-section.tsx | 16 ++------ .../components/views/running-agents-view.tsx | 11 +++-- .../settings-view/account/account-section.tsx | 3 +- .../settings-view/api-keys/api-key-field.tsx | 5 ++- .../api-keys/api-keys-section.tsx | 7 ++-- .../api-keys/claude-usage-section.tsx | 3 +- .../cli-status/claude-cli-status.tsx | 3 +- .../cli-status/cli-status-card.tsx | 3 +- .../cli-status/codex-cli-status.tsx | 3 +- .../cli-status/cursor-cli-status.tsx | 3 +- .../cli-status/opencode-cli-status.tsx | 3 +- .../codex/codex-usage-section.tsx | 3 +- .../event-hooks/event-history-view.tsx | 7 +++- .../components/mcp-server-card.tsx | 5 ++- .../components/mcp-server-header.tsx | 3 +- .../views/settings-view/mcp-servers/utils.tsx | 5 ++- .../claude-settings-tab/subagents-section.tsx | 18 ++------- .../providers/cursor-permissions-section.tsx | 3 +- .../opencode-model-configuration.tsx | 5 ++- .../components/cli-installation-card.tsx | 5 ++- .../setup-view/components/status-badge.tsx | 5 ++- .../setup-view/steps/claude-setup-step.tsx | 22 +++++----- .../views/setup-view/steps/cli-setup-step.tsx | 22 +++++----- .../setup-view/steps/cursor-setup-step.tsx | 8 ++-- .../setup-view/steps/github-setup-step.tsx | 6 +-- .../setup-view/steps/opencode-setup-step.tsx | 8 ++-- .../setup-view/steps/providers-setup-step.tsx | 40 +++++++++---------- apps/ui/src/components/views/spec-view.tsx | 4 +- .../spec-view/components/spec-empty-state.tsx | 7 ++-- .../spec-view/components/spec-header.tsx | 9 +++-- .../spec-view/dialogs/create-spec-dialog.tsx | 5 ++- .../dialogs/regenerate-spec-dialog.tsx | 5 ++- .../ui/src/components/views/terminal-view.tsx | 6 +-- .../views/terminal-view/terminal-panel.tsx | 6 +-- apps/ui/src/components/views/welcome-view.tsx | 6 +-- 93 files changed, 351 insertions(+), 333 deletions(-) create mode 100644 apps/ui/src/components/ui/spinner.tsx diff --git a/apps/ui/src/components/claude-usage-popover.tsx b/apps/ui/src/components/claude-usage-popover.tsx index d51e316c..fa3d5c94 100644 --- a/apps/ui/src/components/claude-usage-popover.tsx +++ b/apps/ui/src/components/claude-usage-popover.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { getElectronAPI } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; @@ -279,7 +280,7 @@ export function ClaudeUsagePopover() { ) : !claudeUsage ? ( // Loading state
- +

Loading usage data...

) : ( diff --git a/apps/ui/src/components/codex-usage-popover.tsx b/apps/ui/src/components/codex-usage-popover.tsx index f6005b6a..0fee4226 100644 --- a/apps/ui/src/components/codex-usage-popover.tsx +++ b/apps/ui/src/components/codex-usage-popover.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { getElectronAPI } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; @@ -333,7 +334,7 @@ export function CodexUsagePopover() { ) : !codexUsage ? ( // Loading state
- +

Loading usage data...

) : codexUsage.rateLimits ? ( diff --git a/apps/ui/src/components/dialogs/board-background-modal.tsx b/apps/ui/src/components/dialogs/board-background-modal.tsx index 89ab44da..e381c366 100644 --- a/apps/ui/src/components/dialogs/board-background-modal.tsx +++ b/apps/ui/src/components/dialogs/board-background-modal.tsx @@ -1,6 +1,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; -import { ImageIcon, Upload, Loader2, Trash2 } from 'lucide-react'; +import { ImageIcon, Upload, Trash2 } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; const logger = createLogger('BoardBackgroundModal'); import { @@ -313,7 +314,7 @@ export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModa /> {isProcessing && (
- +
)} @@ -353,7 +354,7 @@ export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModa )} > {isProcessing ? ( - + ) : ( )} diff --git a/apps/ui/src/components/dialogs/new-project-modal.tsx b/apps/ui/src/components/dialogs/new-project-modal.tsx index dd114bf9..55df0a1c 100644 --- a/apps/ui/src/components/dialogs/new-project-modal.tsx +++ b/apps/ui/src/components/dialogs/new-project-modal.tsx @@ -14,16 +14,8 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; -import { - FolderPlus, - FolderOpen, - Rocket, - ExternalLink, - Check, - Loader2, - Link, - Folder, -} from 'lucide-react'; +import { FolderPlus, FolderOpen, Rocket, ExternalLink, Check, Link, Folder } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { starterTemplates, type StarterTemplate } from '@/lib/templates'; import { getElectronAPI } from '@/lib/electron'; import { cn } from '@/lib/utils'; @@ -451,7 +443,7 @@ export function NewProjectModal({ > {isCreating ? ( <> - + {activeTab === 'template' ? 'Cloning...' : 'Creating...'} ) : ( diff --git a/apps/ui/src/components/dialogs/workspace-picker-modal.tsx b/apps/ui/src/components/dialogs/workspace-picker-modal.tsx index 4f287465..84e723fc 100644 --- a/apps/ui/src/components/dialogs/workspace-picker-modal.tsx +++ b/apps/ui/src/components/dialogs/workspace-picker-modal.tsx @@ -8,7 +8,8 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Folder, Loader2, FolderOpen, AlertCircle } from 'lucide-react'; +import { Folder, FolderOpen, AlertCircle } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { getHttpApiClient } from '@/lib/http-api-client'; interface WorkspaceDirectory { @@ -74,7 +75,7 @@ export function WorkspacePickerModal({ open, onOpenChange, onSelect }: Workspace
{isLoading && (
- +

Loading projects...

)} diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index 3cda8229..c4956159 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -1,9 +1,9 @@ import type { NavigateOptions } from '@tanstack/react-router'; -import { Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { formatShortcut } from '@/store/app-store'; import type { NavSection } from '../types'; import type { Project } from '@/lib/electron'; +import { Spinner } from '@/components/ui/spinner'; interface SidebarNavigationProps { currentProject: Project | null; @@ -93,9 +93,10 @@ export function SidebarNavigation({ >
{item.isLoading ? ( - diff --git a/apps/ui/src/components/session-manager.tsx b/apps/ui/src/components/session-manager.tsx index 88c31acc..f0fa9a45 100644 --- a/apps/ui/src/components/session-manager.tsx +++ b/apps/ui/src/components/session-manager.tsx @@ -16,8 +16,8 @@ import { Check, X, ArchiveRestore, - Loader2, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import type { SessionListItem } from '@/types/electron'; import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; @@ -466,7 +466,7 @@ export function SessionManager({ {/* Show loading indicator if this session is running (either current session thinking or any session in runningSessions) */} {(currentSessionId === session.id && isCurrentSessionThinking) || runningSessions.has(session.id) ? ( - + ) : ( )} diff --git a/apps/ui/src/components/ui/button.tsx b/apps/ui/src/components/ui/button.tsx index fa970a52..a7163ed3 100644 --- a/apps/ui/src/components/ui/button.tsx +++ b/apps/ui/src/components/ui/button.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; -import { Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { Spinner } from '@/components/ui/spinner'; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.98]", @@ -39,7 +39,7 @@ const buttonVariants = cva( // Loading spinner component function ButtonSpinner({ className }: { className?: string }) { - return
) : !claudeUsage ? (
- +

Loading usage data...

) : ( @@ -568,7 +569,7 @@ export function UsagePopover() {
) : !codexUsage ? (
- +

Loading usage data...

) : codexUsage.rateLimits ? ( diff --git a/apps/ui/src/components/views/agent-tools-view.tsx b/apps/ui/src/components/views/agent-tools-view.tsx index 4485f165..48c3f92d 100644 --- a/apps/ui/src/components/views/agent-tools-view.tsx +++ b/apps/ui/src/components/views/agent-tools-view.tsx @@ -11,12 +11,12 @@ import { Terminal, CheckCircle, XCircle, - Loader2, Play, File, Pencil, Wrench, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { getElectronAPI } from '@/lib/electron'; @@ -236,7 +236,7 @@ export function AgentToolsView() { > {isReadingFile ? ( <> - + Reading... ) : ( @@ -315,7 +315,7 @@ export function AgentToolsView() { > {isWritingFile ? ( <> - + Writing... ) : ( @@ -383,7 +383,7 @@ export function AgentToolsView() { > {isRunningCommand ? ( <> - + Running... ) : ( diff --git a/apps/ui/src/components/views/agent-view/components/thinking-indicator.tsx b/apps/ui/src/components/views/agent-view/components/thinking-indicator.tsx index facd4fc5..ff2965d5 100644 --- a/apps/ui/src/components/views/agent-view/components/thinking-indicator.tsx +++ b/apps/ui/src/components/views/agent-view/components/thinking-indicator.tsx @@ -1,4 +1,5 @@ import { Bot } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; export function ThinkingIndicator() { return ( @@ -8,20 +9,7 @@ export function ThinkingIndicator() {
-
- - - -
+ Thinking...
diff --git a/apps/ui/src/components/views/analysis-view.tsx b/apps/ui/src/components/views/analysis-view.tsx index e235a9e9..2143d390 100644 --- a/apps/ui/src/components/views/analysis-view.tsx +++ b/apps/ui/src/components/views/analysis-view.tsx @@ -14,12 +14,12 @@ import { RefreshCw, BarChart3, FileCode, - Loader2, FileText, CheckCircle, AlertCircle, ListChecks, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn, generateUUID } from '@/lib/utils'; const logger = createLogger('AnalysisView'); @@ -742,7 +742,7 @@ ${Object.entries(projectAnalysis.filesByExtension)
{children}
diff --git a/apps/ui/src/components/views/board-view/shared/model-selector.tsx b/apps/ui/src/components/views/board-view/shared/model-selector.tsx index 957fccc0..fb6deeae 100644 --- a/apps/ui/src/components/views/board-view/shared/model-selector.tsx +++ b/apps/ui/src/components/views/board-view/shared/model-selector.tsx @@ -11,7 +11,7 @@ import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@autom import type { ModelProvider } from '@automaker/types'; import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants'; import { useEffect } from 'react'; -import { RefreshCw } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; interface ModelSelectorProps { selectedModel: string; // Can be ModelAlias or "cursor-{id}" @@ -294,7 +294,7 @@ export function ModelSelector({ {/* Loading state */} {codexModelsLoading && dynamicCodexModels.length === 0 && (
- + Loading models...
)} diff --git a/apps/ui/src/components/views/board-view/shared/planning-mode-selector.tsx b/apps/ui/src/components/views/board-view/shared/planning-mode-selector.tsx index 66af8d13..5c9bb5db 100644 --- a/apps/ui/src/components/views/board-view/shared/planning-mode-selector.tsx +++ b/apps/ui/src/components/views/board-view/shared/planning-mode-selector.tsx @@ -6,12 +6,12 @@ import { ClipboardList, FileText, ScrollText, - Loader2, Check, Eye, RefreshCw, Sparkles, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; @@ -236,7 +236,7 @@ export function PlanningModeSelector({
{isGenerating ? ( <> - + Generating {mode === 'full' ? 'comprehensive spec' : 'spec'}... diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx index c7e7b7ef..0f6d2af3 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx @@ -8,7 +8,8 @@ import { DropdownMenuTrigger, DropdownMenuLabel, } from '@/components/ui/dropdown-menu'; -import { GitBranch, RefreshCw, GitBranchPlus, Check, Search } from 'lucide-react'; +import { GitBranch, GitBranchPlus, Check, Search } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import type { WorktreeInfo, BranchInfo } from '../types'; @@ -81,7 +82,7 @@ export function BranchSwitchDropdown({
{isLoadingBranches ? ( - + Loading branches... ) : filteredBranches.length === 0 ? ( diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx index 859ad34c..8405fbca 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx @@ -2,7 +2,6 @@ import { useEffect, useRef, useCallback, useState } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { - Loader2, Terminal, ArrowDown, ExternalLink, @@ -12,6 +11,7 @@ import { Clock, GitBranch, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { XtermLogViewer, type XtermLogViewerRef } from '@/components/ui/xterm-log-viewer'; import { useDevServerLogs } from '../hooks/use-dev-server-logs'; @@ -183,7 +183,7 @@ export function DevServerLogsPanel({ onClick={() => fetchLogs()} title="Refresh logs" > - + {isLoading ? : }
@@ -234,7 +234,7 @@ export function DevServerLogsPanel({ > {isLoading && !logs ? (
- + Loading logs...
) : !logs && !isRunning ? ( @@ -245,7 +245,7 @@ export function DevServerLogsPanel({ ) : !logs ? (
-
+

Waiting for output...

Logs will appear as the server generates output diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx index 52a07c96..079c9b11 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx @@ -7,7 +7,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { GitBranch, ChevronDown, Loader2, CircleDot, Check } from 'lucide-react'; +import { GitBranch, ChevronDown, CircleDot, Check } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import type { WorktreeInfo } from '../types'; @@ -44,7 +45,7 @@ export function WorktreeMobileDropdown({ {displayBranch} {isActivating ? ( - + ) : ( )} @@ -74,7 +75,7 @@ export function WorktreeMobileDropdown({ ) : (

)} - {isRunning && } + {isRunning && } {worktree.branch} diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx index 5cb379d3..212e6d89 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx @@ -1,6 +1,7 @@ import type { JSX } from 'react'; import { Button } from '@/components/ui/button'; -import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from 'lucide-react'; +import { Globe, CircleDot, GitPullRequest } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types'; @@ -197,8 +198,8 @@ export function WorktreeTab({ aria-label={worktree.branch} data-testid={`worktree-branch-${worktree.branch}`} > - {isRunning && } - {isActivating && !isRunning && } + {isRunning && } + {isActivating && !isRunning && } {worktree.branch} {cardCount !== undefined && cardCount > 0 && ( @@ -264,8 +265,8 @@ export function WorktreeTab({ : 'Click to switch to this branch' } > - {isRunning && } - {isActivating && !isRunning && } + {isRunning && } + {isActivating && !isRunning && } {worktree.branch} {cardCount !== undefined && cardCount > 0 && ( diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx index 2cc844f4..fbd54d73 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useCallback, useState } from 'react'; import { Button } from '@/components/ui/button'; import { GitBranch, Plus, RefreshCw } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn, pathsEqual } from '@/lib/utils'; import { toast } from 'sonner'; import { getHttpApiClient } from '@/lib/http-api-client'; @@ -285,7 +286,7 @@ export function WorktreePanel({ disabled={isLoading} title="Refresh worktrees" > - + {isLoading ? : } )} @@ -429,7 +430,7 @@ export function WorktreePanel({ disabled={isLoading} title="Refresh worktrees" > - + {isLoading ? : }
diff --git a/apps/ui/src/components/views/code-view.tsx b/apps/ui/src/components/views/code-view.tsx index 581a298b..ce80bc23 100644 --- a/apps/ui/src/components/views/code-view.tsx +++ b/apps/ui/src/components/views/code-view.tsx @@ -4,7 +4,8 @@ import { useAppStore } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { File, Folder, FolderOpen, ChevronRight, ChevronDown, RefreshCw, Code } from 'lucide-react'; +import { File, Folder, FolderOpen, ChevronRight, ChevronDown, Code } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; const logger = createLogger('CodeView'); @@ -206,7 +207,7 @@ export function CodeView() { if (isLoading) { return (
- +
); } diff --git a/apps/ui/src/components/views/context-view.tsx b/apps/ui/src/components/views/context-view.tsx index 024ee392..b186e0c1 100644 --- a/apps/ui/src/components/views/context-view.tsx +++ b/apps/ui/src/components/views/context-view.tsx @@ -12,7 +12,6 @@ import { HeaderActionsPanelTrigger, } from '@/components/ui/header-actions-panel'; import { - RefreshCw, FileText, Image as ImageIcon, Trash2, @@ -24,9 +23,9 @@ import { Pencil, FilePlus, FileUp, - Loader2, MoreVertical, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { useKeyboardShortcuts, useKeyboardShortcutsConfig, @@ -670,7 +669,7 @@ export function ContextView() { if (isLoading) { return (
- +
); } @@ -790,7 +789,7 @@ export function ContextView() { {isUploading && (
- + Uploading {uploadingFileName}...
@@ -838,7 +837,7 @@ export function ContextView() { {file.name} {isGenerating ? ( - + Generating description... ) : file.description ? ( @@ -955,7 +954,7 @@ export function ContextView() { {generatingDescriptions.has(selectedFile.name) ? (
- + Generating description with AI...
) : selectedFile.description ? ( diff --git a/apps/ui/src/components/views/dashboard-view.tsx b/apps/ui/src/components/views/dashboard-view.tsx index 7e657c80..f9582d00 100644 --- a/apps/ui/src/components/views/dashboard-view.tsx +++ b/apps/ui/src/components/views/dashboard-view.tsx @@ -18,7 +18,6 @@ import { Folder, Star, Clock, - Loader2, ChevronDown, MessageSquare, MoreVertical, @@ -28,6 +27,7 @@ import { type LucideIcon, } from 'lucide-react'; import * as LucideIcons from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Input } from '@/components/ui/input'; import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { @@ -992,7 +992,7 @@ export function DashboardView() { data-testid="project-opening-overlay" >
- +

Opening project...

diff --git a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx index 3ff836dc..cc62a7fe 100644 --- a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx +++ b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx @@ -4,7 +4,6 @@ import { X, Wand2, ExternalLink, - Loader2, CheckCircle, Clock, GitPullRequest, @@ -14,6 +13,7 @@ import { ChevronDown, ChevronUp, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; @@ -87,7 +87,7 @@ export function IssueDetailPanel({ if (isValidating) { return ( ); @@ -297,9 +297,7 @@ export function IssueDetailPanel({ Comments {totalCount > 0 && `(${totalCount})`} - {commentsLoading && ( - - )} + {commentsLoading && } {commentsExpanded ? ( ) : ( @@ -340,7 +338,7 @@ export function IssueDetailPanel({ > {loadingMore ? ( <> - + Loading... ) : ( diff --git a/apps/ui/src/components/views/github-issues-view/components/issue-row.tsx b/apps/ui/src/components/views/github-issues-view/components/issue-row.tsx index bf6496f1..01bf8316 100644 --- a/apps/ui/src/components/views/github-issues-view/components/issue-row.tsx +++ b/apps/ui/src/components/views/github-issues-view/components/issue-row.tsx @@ -2,12 +2,12 @@ import { Circle, CheckCircle2, ExternalLink, - Loader2, CheckCircle, Sparkles, GitPullRequest, User, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import type { IssueRowProps } from '../types'; @@ -97,7 +97,7 @@ export function IssueRow({ {/* Validating indicator */} {isValidating && ( - + Analyzing... )} diff --git a/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx b/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx index 1c58bbe4..5b599c4e 100644 --- a/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx +++ b/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx @@ -1,5 +1,6 @@ import { CircleDot, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import type { IssuesStateFilter } from '../types'; import { IssuesFilterControls } from './issues-filter-controls'; @@ -77,7 +78,7 @@ export function IssuesListHeader({
diff --git a/apps/ui/src/components/views/github-prs-view.tsx b/apps/ui/src/components/views/github-prs-view.tsx index 855d136c..fbbcb9eb 100644 --- a/apps/ui/src/components/views/github-prs-view.tsx +++ b/apps/ui/src/components/views/github-prs-view.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { createLogger } from '@automaker/utils/logger'; -import { GitPullRequest, Loader2, RefreshCw, ExternalLink, GitMerge, X } from 'lucide-react'; +import { GitPullRequest, RefreshCw, ExternalLink, GitMerge, X } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { getElectronAPI, GitHubPR } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; import { Button } from '@/components/ui/button'; @@ -86,7 +87,7 @@ export function GitHubPRsView() { if (loading) { return (
- +
); } @@ -134,7 +135,7 @@ export function GitHubPRsView() { diff --git a/apps/ui/src/components/views/graph-view-page.tsx b/apps/ui/src/components/views/graph-view-page.tsx index f8e9ba0a..47acf313 100644 --- a/apps/ui/src/components/views/graph-view-page.tsx +++ b/apps/ui/src/components/views/graph-view-page.tsx @@ -17,7 +17,7 @@ import { import { useWorktrees } from './board-view/worktree-panel/hooks'; import { useAutoMode } from '@/hooks/use-auto-mode'; import { pathsEqual } from '@/lib/utils'; -import { RefreshCw } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { getElectronAPI } from '@/lib/electron'; import { createLogger } from '@automaker/utils/logger'; import { toast } from 'sonner'; @@ -330,7 +330,7 @@ export function GraphViewPage() { if (isLoading) { return (
- +
); } diff --git a/apps/ui/src/components/views/ideation-view/components/ideation-dashboard.tsx b/apps/ui/src/components/views/ideation-view/components/ideation-dashboard.tsx index 41d12a34..8bf6d7bb 100644 --- a/apps/ui/src/components/views/ideation-view/components/ideation-dashboard.tsx +++ b/apps/ui/src/components/views/ideation-view/components/ideation-dashboard.tsx @@ -4,7 +4,8 @@ */ import { useState, useMemo, useEffect, useCallback } from 'react'; -import { Loader2, AlertCircle, Plus, X, Sparkles, Lightbulb, Trash2 } from 'lucide-react'; +import { AlertCircle, Plus, X, Sparkles, Lightbulb, Trash2 } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -109,7 +110,7 @@ function SuggestionCard({ )} > {isAdding ? ( - + ) : ( <> @@ -153,11 +154,7 @@ function GeneratingCard({ job }: { job: GenerationJob }) { isError ? 'bg-destructive/10 text-destructive' : 'bg-blue-500/10 text-blue-500' )} > - {isError ? ( - - ) : ( - - )} + {isError ? : }

{job.prompt.title}

diff --git a/apps/ui/src/components/views/ideation-view/components/prompt-category-grid.tsx b/apps/ui/src/components/views/ideation-view/components/prompt-category-grid.tsx index a4d3d505..c09548b0 100644 --- a/apps/ui/src/components/views/ideation-view/components/prompt-category-grid.tsx +++ b/apps/ui/src/components/views/ideation-view/components/prompt-category-grid.tsx @@ -13,8 +13,8 @@ import { Gauge, Accessibility, BarChart3, - Loader2, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Card, CardContent } from '@/components/ui/card'; import { useGuidedPrompts } from '@/hooks/use-guided-prompts'; import type { IdeaCategory } from '@automaker/types'; @@ -53,7 +53,7 @@ export function PromptCategoryGrid({ onSelect, onBack }: PromptCategoryGridProps {isLoading && (
- + Loading categories...
)} diff --git a/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx b/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx index a7e3fc8b..af52030b 100644 --- a/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx +++ b/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx @@ -3,7 +3,8 @@ */ import { useState, useMemo } from 'react'; -import { ArrowLeft, Lightbulb, Loader2, CheckCircle2 } from 'lucide-react'; +import { ArrowLeft, Lightbulb, CheckCircle2 } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Card, CardContent } from '@/components/ui/card'; import { useGuidedPrompts } from '@/hooks/use-guided-prompts'; import { useIdeationStore } from '@/store/ideation-store'; @@ -121,7 +122,7 @@ export function PromptList({ category, onBack }: PromptListProps) {
{isLoadingPrompts && (
- + Loading prompts...
)} @@ -162,7 +163,7 @@ export function PromptList({ category, onBack }: PromptListProps) { }`} > {isLoading || isGenerating ? ( - + ) : isStarted ? ( ) : ( diff --git a/apps/ui/src/components/views/ideation-view/index.tsx b/apps/ui/src/components/views/ideation-view/index.tsx index 0662c6ed..50cbd8d3 100644 --- a/apps/ui/src/components/views/ideation-view/index.tsx +++ b/apps/ui/src/components/views/ideation-view/index.tsx @@ -11,7 +11,8 @@ import { PromptList } from './components/prompt-list'; import { IdeationDashboard } from './components/ideation-dashboard'; import { useGuidedPrompts } from '@/hooks/use-guided-prompts'; import { Button } from '@/components/ui/button'; -import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Loader2, Trash2 } from 'lucide-react'; +import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Trash2 } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import type { IdeaCategory } from '@automaker/types'; import type { IdeationMode } from '@/store/ideation-store'; @@ -152,11 +153,7 @@ function IdeationHeader({ className="gap-2" disabled={isAcceptingAll} > - {isAcceptingAll ? ( - - ) : ( - - )} + {isAcceptingAll ? : } Accept All ({acceptAllCount}) )} diff --git a/apps/ui/src/components/views/interview-view.tsx b/apps/ui/src/components/views/interview-view.tsx index b9b9997e..b56971c1 100644 --- a/apps/ui/src/components/views/interview-view.tsx +++ b/apps/ui/src/components/views/interview-view.tsx @@ -5,7 +5,8 @@ import { useAppStore, Feature } from '@/store/app-store'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Bot, Send, User, Loader2, Sparkles, FileText, ArrowLeft, CheckCircle } from 'lucide-react'; +import { Bot, Send, User, Sparkles, FileText, ArrowLeft, CheckCircle } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn, generateUUID } from '@/lib/utils'; import { getElectronAPI } from '@/lib/electron'; import { Markdown } from '@/components/ui/markdown'; @@ -491,7 +492,7 @@ export function InterviewView() {
- + Generating specification...
@@ -571,7 +572,7 @@ export function InterviewView() { > {isGenerating ? ( <> - + Creating... ) : ( diff --git a/apps/ui/src/components/views/login-view.tsx b/apps/ui/src/components/views/login-view.tsx index faca109c..0ed259bf 100644 --- a/apps/ui/src/components/views/login-view.tsx +++ b/apps/ui/src/components/views/login-view.tsx @@ -24,7 +24,8 @@ import { } from '@/lib/http-api-client'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { KeyRound, AlertCircle, Loader2, RefreshCw, ServerCrash } from 'lucide-react'; +import { KeyRound, AlertCircle, RefreshCw, ServerCrash } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { useAuthStore } from '@/store/auth-store'; import { useSetupStore } from '@/store/setup-store'; @@ -349,7 +350,7 @@ export function LoginView() { return (
- +

Connecting to server {state.attempt > 1 ? ` (attempt ${state.attempt}/${MAX_RETRIES})` : '...'} @@ -385,7 +386,7 @@ export function LoginView() { return (

- +

{state.phase === 'checking_setup' ? 'Loading settings...' : 'Redirecting...'}

@@ -447,7 +448,7 @@ export function LoginView() { > {isLoggingIn ? ( <> - + Authenticating... ) : ( diff --git a/apps/ui/src/components/views/memory-view.tsx b/apps/ui/src/components/views/memory-view.tsx index 66533413..b6331602 100644 --- a/apps/ui/src/components/views/memory-view.tsx +++ b/apps/ui/src/components/views/memory-view.tsx @@ -19,6 +19,7 @@ import { FilePlus, MoreVertical, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Dialog, DialogContent, @@ -299,7 +300,7 @@ export function MemoryView() { if (isLoading) { return (
- +
); } diff --git a/apps/ui/src/components/views/notifications-view.tsx b/apps/ui/src/components/views/notifications-view.tsx index aaffb011..08386c55 100644 --- a/apps/ui/src/components/views/notifications-view.tsx +++ b/apps/ui/src/components/views/notifications-view.tsx @@ -9,7 +9,8 @@ import { useLoadNotifications, useNotificationEvents } from '@/hooks/use-notific import { getHttpApiClient } from '@/lib/http-api-client'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Bell, Check, CheckCheck, Trash2, ExternalLink, Loader2 } from 'lucide-react'; +import { Bell, Check, CheckCheck, Trash2, ExternalLink } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { useNavigate } from '@tanstack/react-router'; import type { Notification } from '@automaker/types'; @@ -146,7 +147,7 @@ export function NotificationsView() { if (isLoading) { return (
- +

Loading notifications...

); diff --git a/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx b/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx index c289d382..d6d0c247 100644 --- a/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx +++ b/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx @@ -10,9 +10,9 @@ import { Save, RotateCcw, Trash2, - Loader2, PanelBottomClose, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { apiGet, apiPut, apiDelete } from '@/lib/api-fetch'; import { toast } from 'sonner'; @@ -409,7 +409,7 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti {isLoading ? (
- +
) : ( <> @@ -448,11 +448,7 @@ npm install disabled={!scriptExists || isSaving || isDeleting} className="gap-1.5 text-destructive hover:text-destructive hover:bg-destructive/10" > - {isDeleting ? ( - - ) : ( - - )} + {isDeleting ? : } Delete
diff --git a/apps/ui/src/components/views/running-agents-view.tsx b/apps/ui/src/components/views/running-agents-view.tsx index d46729c1..b77518d0 100644 --- a/apps/ui/src/components/views/running-agents-view.tsx +++ b/apps/ui/src/components/views/running-agents-view.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { createLogger } from '@automaker/utils/logger'; -import { Bot, Folder, Loader2, RefreshCw, Square, Activity, FileText } from 'lucide-react'; +import { Bot, Folder, RefreshCw, Square, Activity, FileText } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { getElectronAPI, RunningAgent } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; import { Button } from '@/components/ui/button'; @@ -146,7 +147,7 @@ export function RunningAgentsView() { if (loading) { return (
- +
); } @@ -169,7 +170,11 @@ export function RunningAgentsView() {
diff --git a/apps/ui/src/components/views/settings-view/account/account-section.tsx b/apps/ui/src/components/views/settings-view/account/account-section.tsx index 901e5040..d10049fc 100644 --- a/apps/ui/src/components/views/settings-view/account/account-section.tsx +++ b/apps/ui/src/components/views/settings-view/account/account-section.tsx @@ -11,6 +11,7 @@ import { import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from 'sonner'; import { LogOut, User, Code2, RefreshCw } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { logout } from '@/lib/http-api-client'; import { useAuthStore } from '@/store/auth-store'; @@ -143,7 +144,7 @@ export function AccountSection() { disabled={isRefreshing || isLoadingEditors} className="shrink-0 h-9 w-9" > - + {isRefreshing ? : } diff --git a/apps/ui/src/components/views/settings-view/api-keys/api-key-field.tsx b/apps/ui/src/components/views/settings-view/api-keys/api-key-field.tsx index 6d044f6c..61b49a1c 100644 --- a/apps/ui/src/components/views/settings-view/api-keys/api-key-field.tsx +++ b/apps/ui/src/components/views/settings-view/api-keys/api-key-field.tsx @@ -1,7 +1,8 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Zap } from 'lucide-react'; +import { AlertCircle, CheckCircle2, Eye, EyeOff, Zap } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import type { ProviderConfig } from '@/config/api-providers'; interface ApiKeyFieldProps { @@ -70,7 +71,7 @@ export function ApiKeyField({ config }: ApiKeyFieldProps) { > {testButton.loading ? ( <> - + Testing... ) : ( diff --git a/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx b/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx index 088f3ddf..840c8e63 100644 --- a/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx +++ b/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx @@ -1,7 +1,8 @@ import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; import { Button } from '@/components/ui/button'; -import { Key, CheckCircle2, Trash2, Loader2 } from 'lucide-react'; +import { Key, CheckCircle2, Trash2 } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { ApiKeyField } from './api-key-field'; import { buildProviderConfigs } from '@/config/api-providers'; import { SecurityNotice } from './security-notice'; @@ -142,7 +143,7 @@ export function ApiKeysSection() { data-testid="delete-anthropic-key" > {isDeletingAnthropicKey ? ( - + ) : ( )} @@ -159,7 +160,7 @@ export function ApiKeysSection() { data-testid="delete-openai-key" > {isDeletingOpenaiKey ? ( - + ) : ( )} diff --git a/apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx b/apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx index 11912ec4..2aa1ff3c 100644 --- a/apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx +++ b/apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx @@ -4,6 +4,7 @@ import { getElectronAPI } from '@/lib/electron'; import { useSetupStore } from '@/store/setup-store'; import { useAppStore } from '@/store/app-store'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { RefreshCw, AlertCircle } from 'lucide-react'; const ERROR_NO_API = 'Claude usage API not available'; @@ -178,7 +179,7 @@ export function ClaudeUsageSection() { data-testid="refresh-claude-usage" title={CLAUDE_REFRESH_LABEL} > - + {isLoading ? : }

{CLAUDE_USAGE_SUBTITLE}

diff --git a/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx index 2457969b..a6474a7a 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { CliStatus } from '../shared/types'; @@ -172,7 +173,7 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C 'transition-all duration-200' )} > - + {isChecking ? : }

diff --git a/apps/ui/src/components/views/settings-view/cli-status/cli-status-card.tsx b/apps/ui/src/components/views/settings-view/cli-status/cli-status-card.tsx index dd194c1f..6e577787 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/cli-status-card.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/cli-status-card.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button'; import { CheckCircle2, AlertCircle, RefreshCw } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import type { CliStatus } from '../shared/types'; @@ -56,7 +57,7 @@ export function CliStatusCard({ 'transition-all duration-200' )} > - + {isChecking ? : }

{description}

diff --git a/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx index 86635264..3e0d8b53 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { CliStatus } from '../shared/types'; @@ -165,7 +166,7 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl 'transition-all duration-200' )} > - + {isChecking ? : }

diff --git a/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx index bc49270c..68c052fb 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import { CursorIcon } from '@/components/ui/provider-icon'; @@ -290,7 +291,7 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat 'transition-all duration-200' )} > - + {isChecking ? : }

diff --git a/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx b/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx index bfd9efe6..7d7577c5 100644 --- a/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx +++ b/apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { Spinner } from '@/components/ui/spinner'; import { CheckCircle2, AlertCircle, RefreshCw, Bot, Cloud } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { CliStatus } from '../shared/types'; @@ -221,7 +222,7 @@ export function OpencodeCliStatus({ 'transition-all duration-200' )} > - + {isChecking ? : }

diff --git a/apps/ui/src/components/views/settings-view/codex/codex-usage-section.tsx b/apps/ui/src/components/views/settings-view/codex/codex-usage-section.tsx index b879df4a..9012047d 100644 --- a/apps/ui/src/components/views/settings-view/codex/codex-usage-section.tsx +++ b/apps/ui/src/components/views/settings-view/codex/codex-usage-section.tsx @@ -1,6 +1,7 @@ // @ts-nocheck import { useCallback, useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { RefreshCw, AlertCircle } from 'lucide-react'; import { OpenAIIcon } from '@/components/ui/provider-icon'; import { cn } from '@/lib/utils'; @@ -168,7 +169,7 @@ export function CodexUsageSection() { data-testid="refresh-codex-usage" title={CODEX_REFRESH_LABEL} > - + {isLoading ? : }

{CODEX_USAGE_SUBTITLE}

diff --git a/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx b/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx index 780f5f98..e9c5a071 100644 --- a/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx +++ b/apps/ui/src/components/views/settings-view/event-hooks/event-history-view.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { History, @@ -184,7 +185,11 @@ export function EventHistoryView() {

{events.length > 0 && ( diff --git a/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-card.tsx b/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-card.tsx index babf4bda..752b06e7 100644 --- a/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-card.tsx +++ b/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-card.tsx @@ -1,4 +1,5 @@ -import { ChevronDown, ChevronRight, Code, Pencil, Trash2, PlayCircle, Loader2 } from 'lucide-react'; +import { ChevronDown, ChevronRight, Code, Pencil, Trash2, PlayCircle } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; @@ -111,7 +112,7 @@ export function MCPServerCard({ className="h-8 px-2" > {testState?.status === 'testing' ? ( - + ) : ( )} diff --git a/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-header.tsx b/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-header.tsx index a85fc305..8caf3bca 100644 --- a/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-header.tsx +++ b/apps/ui/src/components/views/settings-view/mcp-servers/components/mcp-server-header.tsx @@ -1,5 +1,6 @@ import { Plug, RefreshCw, Download, Code, FileJson, Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; interface MCPServerHeaderProps { @@ -43,7 +44,7 @@ export function MCPServerHeader({ disabled={isRefreshing} data-testid="refresh-mcp-servers-button" > - + {isRefreshing ? : } {hasServers && ( <> diff --git a/apps/ui/src/components/views/settings-view/mcp-servers/utils.tsx b/apps/ui/src/components/views/settings-view/mcp-servers/utils.tsx index 25102025..83687556 100644 --- a/apps/ui/src/components/views/settings-view/mcp-servers/utils.tsx +++ b/apps/ui/src/components/views/settings-view/mcp-servers/utils.tsx @@ -1,4 +1,5 @@ -import { Terminal, Globe, Loader2, CheckCircle2, XCircle } from 'lucide-react'; +import { Terminal, Globe, CheckCircle2, XCircle } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import type { ServerType, ServerTestState } from './types'; import { SENSITIVE_PARAM_PATTERNS } from './constants'; @@ -40,7 +41,7 @@ export function getServerIcon(type: ServerType = 'stdio') { export function getTestStatusIcon(status: ServerTestState['status']) { switch (status) { case 'testing': - return ; + return ; case 'success': return ; case 'error': diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagents-section.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagents-section.tsx index 08800331..d1f1bf76 100644 --- a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagents-section.tsx +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagents-section.tsx @@ -14,16 +14,8 @@ import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; -import { - Bot, - RefreshCw, - Loader2, - Users, - ExternalLink, - Globe, - FolderOpen, - Sparkles, -} from 'lucide-react'; +import { Bot, RefreshCw, Users, ExternalLink, Globe, FolderOpen, Sparkles } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { useSubagents } from './hooks/use-subagents'; import { useSubagentsSettings } from './hooks/use-subagents-settings'; import { SubagentCard } from './subagent-card'; @@ -178,11 +170,7 @@ export function SubagentsSection() { title="Refresh agents from disk" className="gap-1.5 h-7 px-2 text-xs" > - {isLoadingAgents ? ( - - ) : ( - - )} + {isLoadingAgents ? : } Refresh
diff --git a/apps/ui/src/components/views/settings-view/providers/cursor-permissions-section.tsx b/apps/ui/src/components/views/settings-view/providers/cursor-permissions-section.tsx index 29be25b3..133913b9 100644 --- a/apps/ui/src/components/views/settings-view/providers/cursor-permissions-section.tsx +++ b/apps/ui/src/components/views/settings-view/providers/cursor-permissions-section.tsx @@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Shield, ShieldCheck, ShieldAlert, ChevronDown, Copy, Check } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import type { CursorStatus } from '../hooks/use-cursor-status'; import type { PermissionsData } from '../hooks/use-cursor-permissions'; @@ -118,7 +119,7 @@ export function CursorPermissionsSection({ {isLoadingPermissions ? (
-
+
) : ( <> diff --git a/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx b/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx index 3d2d0fb6..6ecce79c 100644 --- a/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx +++ b/apps/ui/src/components/views/settings-view/providers/opencode-model-configuration.tsx @@ -9,7 +9,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Terminal, Cloud, Cpu, Brain, Github, Loader2, KeyRound, ShieldCheck } from 'lucide-react'; +import { Terminal, Cloud, Cpu, Brain, Github, KeyRound, ShieldCheck } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { Input } from '@/components/ui/input'; import type { @@ -500,7 +501,7 @@ export function OpencodeModelConfiguration({

{isLoadingDynamicModels && (
- + Discovering...
)} diff --git a/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx b/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx index ee32f231..4932ef29 100644 --- a/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx +++ b/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx @@ -1,6 +1,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Download, Loader2, AlertCircle } from 'lucide-react'; +import { Download, AlertCircle } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { CopyableCommandField } from './copyable-command-field'; import { TerminalOutput } from './terminal-output'; @@ -59,7 +60,7 @@ export function CliInstallationCard({ > {isInstalling ? ( <> - + Installing... ) : ( diff --git a/apps/ui/src/components/views/setup-view/components/status-badge.tsx b/apps/ui/src/components/views/setup-view/components/status-badge.tsx index 38692a0b..53869d07 100644 --- a/apps/ui/src/components/views/setup-view/components/status-badge.tsx +++ b/apps/ui/src/components/views/setup-view/components/status-badge.tsx @@ -1,4 +1,5 @@ -import { CheckCircle2, XCircle, Loader2, AlertCircle } from 'lucide-react'; +import { CheckCircle2, XCircle, AlertCircle } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; interface StatusBadgeProps { status: @@ -34,7 +35,7 @@ export function StatusBadge({ status, label }: StatusBadgeProps) { }; case 'checking': return { - icon: , + icon: , className: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20', }; case 'unverified': diff --git a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx index 8b56f49c..87bf6f77 100644 --- a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx @@ -14,7 +14,6 @@ import { useAppStore } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { CheckCircle2, - Loader2, Key, ArrowRight, ArrowLeft, @@ -27,6 +26,7 @@ import { XCircle, Trash2, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { toast } from 'sonner'; import { StatusBadge, TerminalOutput } from '../components'; import { useCliStatus, useCliInstallation, useTokenSave } from '../hooks'; @@ -330,7 +330,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps Authentication Methods
@@ -412,7 +412,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps > {isInstalling ? ( <> - + Installing... ) : ( @@ -435,7 +435,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps {/* CLI Verification Status */} {cliVerificationStatus === 'verifying' && (
- +

Verifying CLI authentication...

Running a test query

@@ -494,7 +494,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps > {cliVerificationStatus === 'verifying' ? ( <> - + Verifying... ) : cliVerificationStatus === 'error' ? ( @@ -574,7 +574,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps > {isSavingApiKey ? ( <> - + Saving... ) : ( @@ -589,11 +589,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps className="border-red-500/50 text-red-500 hover:bg-red-500/10 hover:text-red-400" data-testid="delete-anthropic-key-button" > - {isDeletingApiKey ? ( - - ) : ( - - )} + {isDeletingApiKey ? : } )}
@@ -602,7 +598,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps {/* API Key Verification Status */} {apiKeyVerificationStatus === 'verifying' && (
- +

Verifying API key...

Running a test query

@@ -642,7 +638,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps > {apiKeyVerificationStatus === 'verifying' ? ( <> - + Verifying... ) : apiKeyVerificationStatus === 'error' ? ( diff --git a/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx index cf581f8c..031d6815 100644 --- a/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx @@ -14,7 +14,6 @@ import { useAppStore } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { CheckCircle2, - Loader2, Key, ArrowRight, ArrowLeft, @@ -27,6 +26,7 @@ import { XCircle, Trash2, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { toast } from 'sonner'; import { StatusBadge, TerminalOutput } from '../components'; import { useCliStatus, useCliInstallation, useTokenSave } from '../hooks'; @@ -332,7 +332,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup Authentication Methods
Choose one of the following methods to authenticate: @@ -408,7 +408,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup > {isInstalling ? ( <> - + Installing... ) : ( @@ -427,7 +427,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup {cliVerificationStatus === 'verifying' && (
- +

Verifying CLI authentication...

Running a test query

@@ -605,7 +605,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup > {cliVerificationStatus === 'verifying' ? ( <> - + Verifying... ) : cliVerificationStatus === 'error' ? ( @@ -681,7 +681,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup > {isSavingApiKey ? ( <> - + Saving... ) : ( @@ -696,11 +696,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup className="border-red-500/50 text-red-500 hover:bg-red-500/10 hover:text-red-400" data-testid={config.testIds.deleteApiKeyButton} > - {isDeletingApiKey ? ( - - ) : ( - - )} + {isDeletingApiKey ? : } )}
@@ -708,7 +704,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup {apiKeyVerificationStatus === 'verifying' && (
- +

Verifying API key...

Running a test query

@@ -767,7 +763,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup > {apiKeyVerificationStatus === 'verifying' ? ( <> - + Verifying... ) : apiKeyVerificationStatus === 'error' ? ( diff --git a/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx index ff591f1a..e48057c4 100644 --- a/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx @@ -7,7 +7,6 @@ import { useSetupStore } from '@/store/setup-store'; import { getElectronAPI } from '@/lib/electron'; import { CheckCircle2, - Loader2, ArrowRight, ArrowLeft, ExternalLink, @@ -16,6 +15,7 @@ import { AlertTriangle, XCircle, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { toast } from 'sonner'; import { StatusBadge } from '../components'; import { CursorIcon } from '@/components/ui/provider-icon'; @@ -204,7 +204,7 @@ export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps
{getStatusBadge()}
@@ -318,7 +318,7 @@ export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps > {isLoggingIn ? ( <> - + Waiting for login... ) : ( @@ -332,7 +332,7 @@ export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps {/* Loading State */} {isChecking && (
- +

Checking Cursor CLI status...

diff --git a/apps/ui/src/components/views/setup-view/steps/github-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/github-setup-step.tsx index fcccb618..3a20ee24 100644 --- a/apps/ui/src/components/views/setup-view/steps/github-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/github-setup-step.tsx @@ -6,7 +6,6 @@ import { useSetupStore } from '@/store/setup-store'; import { getElectronAPI } from '@/lib/electron'; import { CheckCircle2, - Loader2, ArrowRight, ArrowLeft, ExternalLink, @@ -16,6 +15,7 @@ import { Github, XCircle, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { toast } from 'sonner'; import { StatusBadge } from '../components'; @@ -116,7 +116,7 @@ export function GitHubSetupStep({ onNext, onBack, onSkip }: GitHubSetupStepProps
{getStatusBadge()}
@@ -252,7 +252,7 @@ export function GitHubSetupStep({ onNext, onBack, onSkip }: GitHubSetupStepProps {/* Loading State */} {isChecking && (
- +

Checking GitHub CLI status...

diff --git a/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx index 5e7e29c0..58337851 100644 --- a/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx @@ -7,7 +7,6 @@ import { useSetupStore } from '@/store/setup-store'; import { getElectronAPI } from '@/lib/electron'; import { CheckCircle2, - Loader2, ArrowRight, ArrowLeft, ExternalLink, @@ -17,6 +16,7 @@ import { XCircle, Terminal, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { toast } from 'sonner'; import { StatusBadge } from '../components'; @@ -204,7 +204,7 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP
{getStatusBadge()}
@@ -316,7 +316,7 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP > {isLoggingIn ? ( <> - + Waiting for login... ) : ( @@ -330,7 +330,7 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP {/* Loading State */} {isChecking && (
- +

Checking OpenCode CLI status...

diff --git a/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx index b9ad3263..53b3ca0b 100644 --- a/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx @@ -17,7 +17,6 @@ import { ArrowRight, ArrowLeft, CheckCircle2, - Loader2, Key, ExternalLink, Copy, @@ -29,6 +28,7 @@ import { Terminal, AlertCircle, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon'; @@ -240,7 +240,7 @@ function ClaudeContent() { onClick={checkStatus} disabled={isChecking || isVerifying} > - + {isChecking || isVerifying ? : }
@@ -278,7 +278,7 @@ function ClaudeContent() { {/* Checking/Verifying State */} {(isChecking || isVerifying) && (
- +

{isChecking ? 'Checking Claude CLI status...' : 'Verifying authentication...'}

@@ -322,7 +322,7 @@ function ClaudeContent() { > {isInstalling ? ( <> - + Installing... ) : ( @@ -417,11 +417,7 @@ function ClaudeContent() { disabled={isSavingApiKey || !apiKey.trim()} className="flex-1 bg-brand-500 hover:bg-brand-600 text-white" > - {isSavingApiKey ? ( - - ) : ( - 'Save API Key' - )} + {isSavingApiKey ? : 'Save API Key'} {hasApiKey && (
@@ -658,7 +654,7 @@ function CursorContent() { > {isLoggingIn ? ( <> - + Waiting for login... ) : ( @@ -671,7 +667,7 @@ function CursorContent() { {isChecking && (
- +

Checking Cursor CLI status...

)} @@ -807,7 +803,7 @@ function CodexContent() { Codex CLI Status
@@ -915,7 +911,7 @@ function CodexContent() { > {isLoggingIn ? ( <> - + Waiting for login... ) : ( @@ -958,7 +954,7 @@ function CodexContent() { disabled={isSaving || !apiKey.trim()} className="w-full bg-brand-500 hover:bg-brand-600 text-white" > - {isSaving ? : 'Save API Key'} + {isSaving ? : 'Save API Key'} @@ -968,7 +964,7 @@ function CodexContent() { {isChecking && (
- +

Checking Codex CLI status...

)} @@ -1082,7 +1078,7 @@ function OpencodeContent() { OpenCode CLI Status
@@ -1191,7 +1187,7 @@ function OpencodeContent() { > {isLoggingIn ? ( <> - + Waiting for login... ) : ( @@ -1204,7 +1200,7 @@ function OpencodeContent() { {isChecking && (
- +

Checking OpenCode CLI status...

)} @@ -1416,7 +1412,7 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps) ); case 'verifying': return ( - + ); case 'installed_not_auth': return ( @@ -1436,7 +1432,7 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps) {isInitialChecking && (
- +

Checking provider status...

)} diff --git a/apps/ui/src/components/views/spec-view.tsx b/apps/ui/src/components/views/spec-view.tsx index 616dc4dd..88fec94b 100644 --- a/apps/ui/src/components/views/spec-view.tsx +++ b/apps/ui/src/components/views/spec-view.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { RefreshCw } from 'lucide-react'; import { useAppStore } from '@/store/app-store'; +import { Spinner } from '@/components/ui/spinner'; // Extracted hooks import { useSpecLoading, useSpecSave, useSpecGeneration } from './spec-view/hooks'; @@ -86,7 +86,7 @@ export function SpecView() { if (isLoading) { return (
- +
); } diff --git a/apps/ui/src/components/views/spec-view/components/spec-empty-state.tsx b/apps/ui/src/components/views/spec-view/components/spec-empty-state.tsx index fa1792b1..ce7c1667 100644 --- a/apps/ui/src/components/views/spec-view/components/spec-empty-state.tsx +++ b/apps/ui/src/components/views/spec-view/components/spec-empty-state.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button'; -import { FileText, FilePlus2, Loader2 } from 'lucide-react'; +import { FileText, FilePlus2 } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { PHASE_LABELS } from '../constants'; interface SpecEmptyStateProps { @@ -36,7 +37,7 @@ export function SpecEmptyState({ {isProcessing && (
- +
@@ -64,7 +65,7 @@ export function SpecEmptyState({
{isCreating ? ( - + ) : ( )} diff --git a/apps/ui/src/components/views/spec-view/components/spec-header.tsx b/apps/ui/src/components/views/spec-view/components/spec-header.tsx index b38a6579..72d879dd 100644 --- a/apps/ui/src/components/views/spec-view/components/spec-header.tsx +++ b/apps/ui/src/components/views/spec-view/components/spec-header.tsx @@ -3,7 +3,8 @@ import { HeaderActionsPanel, HeaderActionsPanelTrigger, } from '@/components/ui/header-actions-panel'; -import { Save, Sparkles, Loader2, FileText, AlertCircle, ListPlus, RefreshCcw } from 'lucide-react'; +import { Save, Sparkles, FileText, AlertCircle, ListPlus, RefreshCcw } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { PHASE_LABELS } from '../constants'; interface SpecHeaderProps { @@ -59,7 +60,7 @@ export function SpecHeader({ {isProcessing && (
- +
@@ -83,7 +84,7 @@ export function SpecHeader({ {/* Mobile processing indicator */} {isProcessing && (
- + Processing...
)} @@ -157,7 +158,7 @@ export function SpecHeader({ {/* Status messages in panel */} {isProcessing && (
- +
{isSyncing diff --git a/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx b/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx index 73389f78..f77b08ca 100644 --- a/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx +++ b/apps/ui/src/components/views/spec-view/dialogs/create-spec-dialog.tsx @@ -1,4 +1,5 @@ -import { Sparkles, Clock, Loader2 } from 'lucide-react'; +import { Sparkles, Clock } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Dialog, DialogContent, @@ -163,7 +164,7 @@ export function CreateSpecDialog({ > {isCreatingSpec ? ( <> - + Generating... ) : ( diff --git a/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx b/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx index fd534a58..c911fc94 100644 --- a/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx +++ b/apps/ui/src/components/views/spec-view/dialogs/regenerate-spec-dialog.tsx @@ -1,4 +1,5 @@ -import { Sparkles, Clock, Loader2 } from 'lucide-react'; +import { Sparkles, Clock } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { Dialog, DialogContent, @@ -158,7 +159,7 @@ export function RegenerateSpecDialog({ > {isRegenerating ? ( <> - + Regenerating... ) : ( diff --git a/apps/ui/src/components/views/terminal-view.tsx b/apps/ui/src/components/views/terminal-view.tsx index 328afc21..0287ca68 100644 --- a/apps/ui/src/components/views/terminal-view.tsx +++ b/apps/ui/src/components/views/terminal-view.tsx @@ -7,13 +7,13 @@ import { Unlock, SplitSquareHorizontal, SplitSquareVertical, - Loader2, AlertCircle, RefreshCw, X, SquarePlus, Settings, } from 'lucide-react'; +import { Spinner } from '@/components/ui/spinner'; import { getServerUrlSync } from '@/lib/http-api-client'; import { useAppStore, @@ -1279,7 +1279,7 @@ export function TerminalView() { if (loading) { return (
- +
); } @@ -1342,7 +1342,7 @@ export function TerminalView() { {authError &&

{authError}

} +
+ + ))} +
+ )} + +
+ ); +} diff --git a/apps/ui/src/components/views/spec-view/components/edit-mode/capabilities-section.tsx b/apps/ui/src/components/views/spec-view/components/edit-mode/capabilities-section.tsx new file mode 100644 index 00000000..cfec2d78 --- /dev/null +++ b/apps/ui/src/components/views/spec-view/components/edit-mode/capabilities-section.tsx @@ -0,0 +1,30 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Lightbulb } from 'lucide-react'; +import { ArrayFieldEditor } from './array-field-editor'; + +interface CapabilitiesSectionProps { + capabilities: string[]; + onChange: (capabilities: string[]) => void; +} + +export function CapabilitiesSection({ capabilities, onChange }: CapabilitiesSectionProps) { + return ( + + + + + Core Capabilities + + + + + + + ); +} diff --git a/apps/ui/src/components/views/spec-view/components/edit-mode/features-section.tsx b/apps/ui/src/components/views/spec-view/components/edit-mode/features-section.tsx new file mode 100644 index 00000000..1cdbac2f --- /dev/null +++ b/apps/ui/src/components/views/spec-view/components/edit-mode/features-section.tsx @@ -0,0 +1,261 @@ +import { Plus, X, ChevronDown, ChevronUp, FolderOpen } from 'lucide-react'; +import { useState, useRef, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { ListChecks } from 'lucide-react'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import type { SpecOutput } from '@automaker/spec-parser'; + +type Feature = SpecOutput['implemented_features'][number]; + +interface FeaturesSectionProps { + features: Feature[]; + onChange: (features: Feature[]) => void; +} + +interface FeatureWithId extends Feature { + _id: string; + _locationIds?: string[]; +} + +function generateId(): string { + return crypto.randomUUID(); +} + +function featureToInternal(feature: Feature): FeatureWithId { + return { + ...feature, + _id: generateId(), + _locationIds: feature.file_locations?.map(() => generateId()), + }; +} + +function internalToFeature(internal: FeatureWithId): Feature { + const { _id, _locationIds, ...feature } = internal; + return feature; +} + +interface FeatureCardProps { + feature: FeatureWithId; + index: number; + onChange: (feature: FeatureWithId) => void; + onRemove: () => void; +} + +function FeatureCard({ feature, index, onChange, onRemove }: FeatureCardProps) { + const [isOpen, setIsOpen] = useState(false); + + const handleNameChange = (name: string) => { + onChange({ ...feature, name }); + }; + + const handleDescriptionChange = (description: string) => { + onChange({ ...feature, description }); + }; + + const handleAddLocation = () => { + const locations = feature.file_locations || []; + const locationIds = feature._locationIds || []; + onChange({ + ...feature, + file_locations: [...locations, ''], + _locationIds: [...locationIds, generateId()], + }); + }; + + const handleRemoveLocation = (locId: string) => { + const locationIds = feature._locationIds || []; + const idx = locationIds.indexOf(locId); + if (idx === -1) return; + + const newLocations = feature.file_locations?.filter((_, i) => i !== idx); + const newLocationIds = locationIds.filter((id) => id !== locId); + onChange({ + ...feature, + file_locations: newLocations && newLocations.length > 0 ? newLocations : undefined, + _locationIds: newLocationIds.length > 0 ? newLocationIds : undefined, + }); + }; + + const handleLocationChange = (locId: string, value: string) => { + const locationIds = feature._locationIds || []; + const idx = locationIds.indexOf(locId); + if (idx === -1) return; + + const locations = [...(feature.file_locations || [])]; + locations[idx] = value; + onChange({ ...feature, file_locations: locations }); + }; + + return ( + + +
+ + + +
+ handleNameChange(e.target.value)} + placeholder="Feature name..." + className="font-medium" + /> +
+ + #{index + 1} + + +
+ +
+
+ +