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 && (
)}
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
;
+ return
;
}
function Button({
diff --git a/apps/ui/src/components/ui/description-image-dropzone.tsx b/apps/ui/src/components/ui/description-image-dropzone.tsx
index 42b2d588..7b67fd9b 100644
--- a/apps/ui/src/components/ui/description-image-dropzone.tsx
+++ b/apps/ui/src/components/ui/description-image-dropzone.tsx
@@ -3,7 +3,8 @@ import { createLogger } from '@automaker/utils/logger';
import { cn } from '@/lib/utils';
const logger = createLogger('DescriptionImageDropZone');
-import { ImageIcon, X, Loader2, FileText } from 'lucide-react';
+import { ImageIcon, X, FileText } from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
import { Textarea } from '@/components/ui/textarea';
import { getElectronAPI } from '@/lib/electron';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
@@ -431,7 +432,7 @@ export function DescriptionImageDropZone({
{/* Processing indicator */}
{isProcessing && (
-
+
Processing files...
)}
diff --git a/apps/ui/src/components/ui/feature-image-upload.tsx b/apps/ui/src/components/ui/feature-image-upload.tsx
index ec4ef205..23837cc1 100644
--- a/apps/ui/src/components/ui/feature-image-upload.tsx
+++ b/apps/ui/src/components/ui/feature-image-upload.tsx
@@ -3,7 +3,8 @@ import { createLogger } from '@automaker/utils/logger';
import { cn } from '@/lib/utils';
const logger = createLogger('FeatureImageUpload');
-import { ImageIcon, X, Upload } from 'lucide-react';
+import { ImageIcon, X } from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
import {
fileToBase64,
generateImageId,
@@ -196,7 +197,7 @@ export function FeatureImageUpload({
)}
>
{isProcessing ? (
-
+
) : (
)}
diff --git a/apps/ui/src/components/ui/git-diff-panel.tsx b/apps/ui/src/components/ui/git-diff-panel.tsx
index 803ff46c..e0e51ea0 100644
--- a/apps/ui/src/components/ui/git-diff-panel.tsx
+++ b/apps/ui/src/components/ui/git-diff-panel.tsx
@@ -9,11 +9,11 @@ import {
FilePen,
ChevronDown,
ChevronRight,
- Loader2,
RefreshCw,
GitBranch,
AlertCircle,
} from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
import { Button } from './button';
import type { FileStatus } from '@/types/electron';
@@ -484,7 +484,7 @@ export function GitDiffPanel({
{isLoading ? (
-
+
Loading changes...
) : error ? (
diff --git a/apps/ui/src/components/ui/image-drop-zone.tsx b/apps/ui/src/components/ui/image-drop-zone.tsx
index cdd7b396..dcaf892d 100644
--- a/apps/ui/src/components/ui/image-drop-zone.tsx
+++ b/apps/ui/src/components/ui/image-drop-zone.tsx
@@ -3,7 +3,8 @@ import { createLogger } from '@automaker/utils/logger';
import { cn } from '@/lib/utils';
const logger = createLogger('ImageDropZone');
-import { ImageIcon, X, Upload } from 'lucide-react';
+import { ImageIcon, X } from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
import type { ImageAttachment } from '@/store/app-store';
import {
fileToBase64,
@@ -204,7 +205,7 @@ export function ImageDropZone({
)}
>
{isProcessing ? (
-
+
) : (
)}
diff --git a/apps/ui/src/components/ui/loading-state.tsx b/apps/ui/src/components/ui/loading-state.tsx
index 9ae6ff3b..60695e4c 100644
--- a/apps/ui/src/components/ui/loading-state.tsx
+++ b/apps/ui/src/components/ui/loading-state.tsx
@@ -1,17 +1,15 @@
-import { Loader2 } from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
interface LoadingStateProps {
/** Optional custom message to display below the spinner */
message?: string;
- /** Optional custom size class for the spinner (default: h-8 w-8) */
- size?: string;
}
-export function LoadingState({ message, size = 'h-8 w-8' }: LoadingStateProps) {
+export function LoadingState({ message }: LoadingStateProps) {
return (
-
- {message &&
{message}
}
+
+ {message &&
{message}
}
);
}
diff --git a/apps/ui/src/components/ui/log-viewer.tsx b/apps/ui/src/components/ui/log-viewer.tsx
index 1d14a14e..65426f8b 100644
--- a/apps/ui/src/components/ui/log-viewer.tsx
+++ b/apps/ui/src/components/ui/log-viewer.tsx
@@ -22,8 +22,8 @@ import {
Filter,
Circle,
Play,
- Loader2,
} from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import {
parseLogOutput,
@@ -148,7 +148,7 @@ function TodoListRenderer({ todos }: { todos: TodoItem[] }) {
case 'completed':
return
;
case 'in_progress':
- return
;
+ return
;
case 'pending':
return
;
default:
diff --git a/apps/ui/src/components/ui/spinner.tsx b/apps/ui/src/components/ui/spinner.tsx
new file mode 100644
index 00000000..c66b7684
--- /dev/null
+++ b/apps/ui/src/components/ui/spinner.tsx
@@ -0,0 +1,32 @@
+import { Loader2 } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+const sizeClasses: Record
= {
+ xs: 'h-3 w-3',
+ sm: 'h-4 w-4',
+ md: 'h-5 w-5',
+ lg: 'h-6 w-6',
+ xl: 'h-8 w-8',
+};
+
+interface SpinnerProps {
+ /** Size of the spinner */
+ size?: SpinnerSize;
+ /** Additional class names */
+ className?: string;
+}
+
+/**
+ * Themed spinner component using the primary brand color.
+ * Use this for all loading indicators throughout the app for consistency.
+ */
+export function Spinner({ size = 'md', className }: SpinnerProps) {
+ return (
+
+ );
+}
diff --git a/apps/ui/src/components/ui/task-progress-panel.tsx b/apps/ui/src/components/ui/task-progress-panel.tsx
index 414be1e7..4fecefbc 100644
--- a/apps/ui/src/components/ui/task-progress-panel.tsx
+++ b/apps/ui/src/components/ui/task-progress-panel.tsx
@@ -5,7 +5,8 @@ import { createLogger } from '@automaker/utils/logger';
import { cn } from '@/lib/utils';
const logger = createLogger('TaskProgressPanel');
-import { Check, Loader2, Circle, ChevronDown, ChevronRight, FileCode } from 'lucide-react';
+import { Check, Circle, ChevronDown, ChevronRight, FileCode } from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
import { getElectronAPI } from '@/lib/electron';
import type { AutoModeEvent } from '@/types/electron';
import { Badge } from '@/components/ui/badge';
@@ -260,7 +261,7 @@ export function TaskProgressPanel({
)}
>
{isCompleted && }
- {isActive && }
+ {isActive && }
{isPending && }
diff --git a/apps/ui/src/components/usage-popover.tsx b/apps/ui/src/components/usage-popover.tsx
index ac15a519..26c100ce 100644
--- a/apps/ui/src/components/usage-popover.tsx
+++ b/apps/ui/src/components/usage-popover.tsx
@@ -3,6 +3,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
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';
@@ -449,7 +450,7 @@ export function UsagePopover() {
) : !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() {
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)
{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() {
-
+ {loading ? (
+
+ ) : (
+
+ )}
Refresh
{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
-
+ {isChecking ? : }
@@ -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
-
+ {isChecking ? : }
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()}
-
+ {isChecking ? : }
@@ -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()}
-
+ {isChecking ? : }
@@ -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()}
-
+ {isChecking ? : }
@@ -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 && (
{isDeletingApiKey ? (
-
+
) : (
)}
@@ -553,7 +549,7 @@ function CursorContent() {
Cursor CLI Status
-
+ {isChecking ? : }
@@ -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
-
+ {isChecking ? : }
@@ -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
-
+ {isChecking ? : }
@@ -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}
}
{authLoading ? (
-
+
) : (
)}
diff --git a/apps/ui/src/components/views/terminal-view/terminal-panel.tsx b/apps/ui/src/components/views/terminal-view/terminal-panel.tsx
index 481ee6b4..88f02591 100644
--- a/apps/ui/src/components/views/terminal-view/terminal-panel.tsx
+++ b/apps/ui/src/components/views/terminal-view/terminal-panel.tsx
@@ -13,7 +13,6 @@ import {
CheckSquare,
Trash2,
ImageIcon,
- Loader2,
Settings,
RotateCcw,
Search,
@@ -24,6 +23,7 @@ import {
ArrowDown,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
+import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Slider } from '@/components/ui/slider';
@@ -1743,7 +1743,7 @@ export function TerminalPanel({
{isProcessingImage ? (
<>
-
+
Processing...
>
) : (
@@ -1791,7 +1791,7 @@ export function TerminalPanel({
)}
{connectionStatus === 'reconnecting' && (
-
+
Reconnecting...
)}
diff --git a/apps/ui/src/components/views/welcome-view.tsx b/apps/ui/src/components/views/welcome-view.tsx
index b07c5188..46e4d88c 100644
--- a/apps/ui/src/components/views/welcome-view.tsx
+++ b/apps/ui/src/components/views/welcome-view.tsx
@@ -20,8 +20,8 @@ import {
Sparkles,
MessageSquare,
ChevronDown,
- Loader2,
} from 'lucide-react';
+import { Spinner } from '@/components/ui/spinner';
import {
DropdownMenu,
DropdownMenuContent,
@@ -758,7 +758,7 @@ export function WelcomeView() {
{isAnalyzing ? (
-
+
AI agent is analyzing your project structure...
@@ -802,7 +802,7 @@ export function WelcomeView() {
data-testid="project-opening-overlay"
>
-
+
Initializing project...
From 5b1e0105f4b6441d0e6e2d9e4b846c4583228a73 Mon Sep 17 00:00:00 2001
From: Shirone
Date: Sat, 17 Jan 2026 23:58:19 +0100
Subject: [PATCH 15/38] refactor: standardize PR state representation across
the application
Updated the PR state handling to use a consistent uppercase format ('OPEN', 'MERGED', 'CLOSED') throughout the codebase. This includes changes to the worktree metadata interface, PR creation logic, and related tests to ensure uniformity and prevent potential mismatches in state representation.
Additionally, modified the GitHub PR fetching logic to retrieve all PR states, allowing for better detection of state changes.
This refactor enhances clarity and consistency in how PR states are managed and displayed.
---
apps/server/src/lib/worktree-metadata.ts | 6 ++-
.../src/routes/worktree/routes/create-pr.ts | 9 ++--
.../server/src/routes/worktree/routes/list.ts | 54 +++++++++++++------
.../tests/unit/lib/worktree-metadata.test.ts | 14 ++---
.../views/board-view/worktree-panel/types.ts | 9 +++-
5 files changed, 63 insertions(+), 29 deletions(-)
diff --git a/apps/server/src/lib/worktree-metadata.ts b/apps/server/src/lib/worktree-metadata.ts
index 3f7ea60d..ab0ba67c 100644
--- a/apps/server/src/lib/worktree-metadata.ts
+++ b/apps/server/src/lib/worktree-metadata.ts
@@ -9,11 +9,15 @@ import * as path from 'path';
/** Maximum length for sanitized branch names in filesystem paths */
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;
+/** GitHub PR states as returned by the GitHub API */
+export type PRState = 'OPEN' | 'MERGED' | 'CLOSED';
+
export interface WorktreePRInfo {
number: number;
url: string;
title: string;
- state: string;
+ /** PR state: OPEN, MERGED, or CLOSED */
+ state: PRState;
createdAt: string;
}
diff --git a/apps/server/src/routes/worktree/routes/create-pr.ts b/apps/server/src/routes/worktree/routes/create-pr.ts
index 1bde9448..25211854 100644
--- a/apps/server/src/routes/worktree/routes/create-pr.ts
+++ b/apps/server/src/routes/worktree/routes/create-pr.ts
@@ -268,11 +268,12 @@ export function createCreatePRHandler() {
prAlreadyExisted = true;
// Store the existing PR info in metadata
+ // GitHub CLI returns uppercase states: OPEN, MERGED, CLOSED
await updateWorktreePRInfo(effectiveProjectPath, branchName, {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
- state: existingPr.state || 'open',
+ state: existingPr.state || 'OPEN',
createdAt: new Date().toISOString(),
});
logger.debug(
@@ -319,11 +320,12 @@ export function createCreatePRHandler() {
if (prNumber) {
try {
+ // Note: GitHub doesn't have a 'DRAFT' state - drafts still show as 'OPEN'
await updateWorktreePRInfo(effectiveProjectPath, branchName, {
number: prNumber,
url: prUrl,
title,
- state: draft ? 'draft' : 'open',
+ state: 'OPEN',
createdAt: new Date().toISOString(),
});
logger.debug(`Stored PR info for branch ${branchName}: PR #${prNumber}`);
@@ -352,11 +354,12 @@ export function createCreatePRHandler() {
prNumber = existingPr.number;
prAlreadyExisted = true;
+ // GitHub CLI returns uppercase states: OPEN, MERGED, CLOSED
await updateWorktreePRInfo(effectiveProjectPath, branchName, {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
- state: existingPr.state || 'open',
+ state: existingPr.state || 'OPEN',
createdAt: new Date().toISOString(),
});
logger.debug(`Fetched and stored existing PR: #${existingPr.number}`);
diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts
index 96782d64..a911fce7 100644
--- a/apps/server/src/routes/worktree/routes/list.ts
+++ b/apps/server/src/routes/worktree/routes/list.ts
@@ -14,7 +14,12 @@ import path from 'path';
import * as secureFs from '../../../lib/secure-fs.js';
import { isGitRepo } from '@automaker/git-utils';
import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js';
-import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
+import {
+ readAllWorktreeMetadata,
+ updateWorktreePRInfo,
+ type WorktreePRInfo,
+ type PRState,
+} from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
import {
checkGitHubRemote,
@@ -168,8 +173,11 @@ async function getGitHubRemoteStatus(projectPath: string): Promise();
for (const worktree of worktrees) {
const metadata = allMetadata.get(worktree.branch);
- if (metadata?.pr) {
- // Use stored metadata (more complete info)
- worktree.pr = metadata.pr;
- } else if (includeDetails) {
- // Fall back to GitHub PR detection only when includeDetails is requested
- const githubPR = githubPRs.get(worktree.branch);
- if (githubPR) {
- worktree.pr = githubPR;
+ const githubPR = githubPRs.get(worktree.branch);
+
+ if (githubPR) {
+ // Prefer fresh GitHub data (it has the current state)
+ worktree.pr = githubPR;
+
+ // Sync metadata with GitHub state when:
+ // 1. No metadata exists for this PR (PR created externally)
+ // 2. State has changed (e.g., merged/closed on GitHub)
+ const needsSync = !metadata?.pr || metadata.pr.state !== githubPR.state;
+ if (needsSync) {
+ // Fire and forget - don't block the response
+ updateWorktreePRInfo(projectPath, worktree.branch, githubPR).catch((err) => {
+ logger.warn(
+ `Failed to update PR info for ${worktree.branch}: ${getErrorMessage(err)}`
+ );
+ });
}
+ } else if (metadata?.pr) {
+ // Fall back to stored metadata (for PRs not in recent GitHub response)
+ worktree.pr = metadata.pr;
}
}
diff --git a/apps/server/tests/unit/lib/worktree-metadata.test.ts b/apps/server/tests/unit/lib/worktree-metadata.test.ts
index ab7967f3..2f84af88 100644
--- a/apps/server/tests/unit/lib/worktree-metadata.test.ts
+++ b/apps/server/tests/unit/lib/worktree-metadata.test.ts
@@ -121,7 +121,7 @@ describe('worktree-metadata.ts', () => {
number: 123,
url: 'https://github.com/owner/repo/pull/123',
title: 'Test PR',
- state: 'open',
+ state: 'OPEN',
createdAt: new Date().toISOString(),
},
};
@@ -158,7 +158,7 @@ describe('worktree-metadata.ts', () => {
number: 456,
url: 'https://github.com/owner/repo/pull/456',
title: 'Updated PR',
- state: 'closed',
+ state: 'CLOSED',
createdAt: new Date().toISOString(),
},
};
@@ -177,7 +177,7 @@ describe('worktree-metadata.ts', () => {
number: 789,
url: 'https://github.com/owner/repo/pull/789',
title: 'New PR',
- state: 'open',
+ state: 'OPEN',
createdAt: new Date().toISOString(),
};
@@ -201,7 +201,7 @@ describe('worktree-metadata.ts', () => {
number: 999,
url: 'https://github.com/owner/repo/pull/999',
title: 'Updated PR',
- state: 'merged',
+ state: 'MERGED',
createdAt: new Date().toISOString(),
};
@@ -224,7 +224,7 @@ describe('worktree-metadata.ts', () => {
number: 111,
url: 'https://github.com/owner/repo/pull/111',
title: 'PR',
- state: 'open',
+ state: 'OPEN',
createdAt: new Date().toISOString(),
};
@@ -259,7 +259,7 @@ describe('worktree-metadata.ts', () => {
number: 222,
url: 'https://github.com/owner/repo/pull/222',
title: 'Has PR',
- state: 'open',
+ state: 'OPEN',
createdAt: new Date().toISOString(),
};
@@ -297,7 +297,7 @@ describe('worktree-metadata.ts', () => {
number: 333,
url: 'https://github.com/owner/repo/pull/333',
title: 'PR 3',
- state: 'open',
+ state: 'OPEN',
createdAt: new Date().toISOString(),
},
};
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/types.ts b/apps/ui/src/components/views/board-view/worktree-panel/types.ts
index d2040048..36aa2da7 100644
--- a/apps/ui/src/components/views/board-view/worktree-panel/types.ts
+++ b/apps/ui/src/components/views/board-view/worktree-panel/types.ts
@@ -1,8 +1,12 @@
+/** GitHub PR states as returned by the GitHub API */
+export type PRState = 'OPEN' | 'MERGED' | 'CLOSED';
+
export interface WorktreePRInfo {
number: number;
url: string;
title: string;
- state: string;
+ /** PR state: OPEN, MERGED, or CLOSED */
+ state: PRState;
createdAt: string;
}
@@ -43,7 +47,8 @@ export interface PRInfo {
number: number;
title: string;
url: string;
- state: string;
+ /** PR state: OPEN, MERGED, or CLOSED */
+ state: PRState;
author: string;
body: string;
comments: Array<{
From 44e665f1bf04bfc3dbbc4cfff7a02785eef80ada Mon Sep 17 00:00:00 2001
From: Shirone
Date: Sun, 18 Jan 2026 00:22:27 +0100
Subject: [PATCH 16/38] fix: adress pr comments
---
apps/server/src/lib/worktree-metadata.ts | 16 +++-------
.../src/routes/worktree/routes/create-pr.ts | 5 +--
.../server/src/routes/worktree/routes/list.ts | 4 +--
.../views/board-view/worktree-panel/types.ts | 14 ++------
libs/types/src/index.ts | 4 +++
libs/types/src/worktree.ts | 32 +++++++++++++++++++
6 files changed, 48 insertions(+), 27 deletions(-)
create mode 100644 libs/types/src/worktree.ts
diff --git a/apps/server/src/lib/worktree-metadata.ts b/apps/server/src/lib/worktree-metadata.ts
index ab0ba67c..4742a5b0 100644
--- a/apps/server/src/lib/worktree-metadata.ts
+++ b/apps/server/src/lib/worktree-metadata.ts
@@ -5,22 +5,14 @@
import * as secureFs from './secure-fs.js';
import * as path from 'path';
+import type { PRState, WorktreePRInfo } from '@automaker/types';
+
+// Re-export types for backwards compatibility
+export type { PRState, WorktreePRInfo };
/** Maximum length for sanitized branch names in filesystem paths */
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;
-/** GitHub PR states as returned by the GitHub API */
-export type PRState = 'OPEN' | 'MERGED' | 'CLOSED';
-
-export interface WorktreePRInfo {
- number: number;
- url: string;
- title: string;
- /** PR state: OPEN, MERGED, or CLOSED */
- state: PRState;
- createdAt: string;
-}
-
export interface WorktreeMetadata {
branch: string;
createdAt: string;
diff --git a/apps/server/src/routes/worktree/routes/create-pr.ts b/apps/server/src/routes/worktree/routes/create-pr.ts
index 25211854..87777c69 100644
--- a/apps/server/src/routes/worktree/routes/create-pr.ts
+++ b/apps/server/src/routes/worktree/routes/create-pr.ts
@@ -13,6 +13,7 @@ import {
} from '../common.js';
import { updateWorktreePRInfo } from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
+import { validatePRState } from '@automaker/types';
const logger = createLogger('CreatePR');
@@ -273,7 +274,7 @@ export function createCreatePRHandler() {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
- state: existingPr.state || 'OPEN',
+ state: validatePRState(existingPr.state),
createdAt: new Date().toISOString(),
});
logger.debug(
@@ -359,7 +360,7 @@ export function createCreatePRHandler() {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
- state: existingPr.state || 'OPEN',
+ state: validatePRState(existingPr.state),
createdAt: new Date().toISOString(),
});
logger.debug(`Fetched and stored existing PR: #${existingPr.number}`);
diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts
index a911fce7..e82e5c14 100644
--- a/apps/server/src/routes/worktree/routes/list.ts
+++ b/apps/server/src/routes/worktree/routes/list.ts
@@ -18,9 +18,9 @@ import {
readAllWorktreeMetadata,
updateWorktreePRInfo,
type WorktreePRInfo,
- type PRState,
} from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
+import { validatePRState } from '@automaker/types';
import {
checkGitHubRemote,
type GitHubRemoteStatus,
@@ -221,7 +221,7 @@ async function fetchGitHubPRs(projectPath: string): Promise