diff --git a/auto-claude-ui/electron.vite.config.ts b/auto-claude-ui/electron.vite.config.ts index 6fc4aa22..7d3b23aa 100644 --- a/auto-claude-ui/electron.vite.config.ts +++ b/auto-claude-ui/electron.vite.config.ts @@ -5,13 +5,21 @@ import { resolve } from 'path'; export default defineConfig({ main: { plugins: [externalizeDepsPlugin({ - exclude: [] + // Bundle these packages into the main process (they won't be in node_modules in packaged app) + exclude: [ + 'uuid', + 'chokidar', + 'ioredis', + 'electron-updater', + '@electron-toolkit/utils' + ] })], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/main/index.ts') }, + // Only node-pty needs to be external (native module rebuilt by electron-builder) external: ['node-pty'] } } diff --git a/auto-claude-ui/package-lock.json b/auto-claude-ui/package-lock.json index 92920912..32602cef 100644 --- a/auto-claude-ui/package-lock.json +++ b/auto-claude-ui/package-lock.json @@ -42,7 +42,7 @@ "ioredis": "^5.8.2", "lucide-react": "^0.560.0", "motion": "^12.23.26", - "node-pty": "^1.0.0", + "node-pty": "^1.1.0-beta42", "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^10.1.0", @@ -150,6 +150,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -535,6 +536,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -558,6 +560,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -597,6 +600,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -991,7 +995,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1013,7 +1016,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3928,8 +3930,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4126,6 +4127,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4136,6 +4138,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4227,6 +4230,7 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -4626,7 +4630,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/7zip-bin": { "version": "5.2.0", @@ -4648,6 +4653,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4708,6 +4714,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4880,7 +4887,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -5265,6 +5271,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5944,8 +5951,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -6289,6 +6295,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -6346,8 +6353,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dotenv": { "version": "16.6.1", @@ -6423,6 +6429,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -6551,7 +6558,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -6572,7 +6578,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -6588,7 +6593,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -6599,7 +6603,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -6969,6 +6972,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8988,6 +8992,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -9931,7 +9936,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11198,12 +11202,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", - "license": "MIT" - }, "node_modules/nano-spawn": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", @@ -11285,15 +11283,21 @@ } }, "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "version": "1.1.0-beta42", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta42.tgz", + "integrity": "sha512-59KoV6xxhJciRVpo4lQ9wnP38SPaBlXgwszYS8nlHAHrt02d14peg+kHtJ4AOtyLWiCf8WPCeJNbxBkiA7Oy7Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "nan": "^2.17.0" + "node-addon-api": "^7.1.0" } }, + "node_modules/node-pty/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -11771,6 +11775,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11868,6 +11873,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11904,7 +11910,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -11922,7 +11927,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -11943,7 +11947,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11959,7 +11962,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -11972,8 +11974,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/proc-log": { "version": "2.0.1", @@ -12077,6 +12078,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12086,6 +12088,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13402,7 +13405,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -13459,7 +13463,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -13486,7 +13489,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -13508,7 +13510,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13522,7 +13523,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -13537,7 +13537,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -13854,6 +13853,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14194,6 +14194,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15227,6 +15228,7 @@ "integrity": "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/auto-claude-ui/package.json b/auto-claude-ui/package.json index 9da2ff1c..3d5ebb96 100644 --- a/auto-claude-ui/package.json +++ b/auto-claude-ui/package.json @@ -59,7 +59,7 @@ "ioredis": "^5.8.2", "lucide-react": "^0.560.0", "motion": "^12.23.26", - "node-pty": "^1.0.0", + "node-pty": "^1.1.0-beta42", "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^10.1.0", @@ -127,7 +127,18 @@ "buildResources": "resources" }, "files": [ - "out/**/*" + "out/**/*", + "package.json" + ], + "extraResources": [ + { + "from": "node_modules/node-pty", + "to": "node_modules/node-pty" + }, + { + "from": "resources/icon.ico", + "to": "icon.ico" + } ], "mac": { "category": "public.app-category.developer-tools", @@ -138,7 +149,7 @@ ] }, "win": { - "icon": "resources/icon-256.png", + "icon": "resources/icon.ico", "target": [ "nsis", "zip" diff --git a/auto-claude-ui/resources/icon.ico b/auto-claude-ui/resources/icon.ico new file mode 100644 index 00000000..9d5a5ff3 Binary files /dev/null and b/auto-claude-ui/resources/icon.ico differ diff --git a/auto-claude-ui/src/main/agent/agent-process.ts b/auto-claude-ui/src/main/agent/agent-process.ts index d0afe73e..d55b57e8 100644 --- a/auto-claude-ui/src/main/agent/agent-process.ts +++ b/auto-claude-ui/src/main/agent/agent-process.ts @@ -176,7 +176,8 @@ export class AgentProcessManager { ...extraEnv, ...profileEnv, // Include active Claude profile config PYTHONUNBUFFERED: '1', // Ensure real-time output - PYTHONIOENCODING: 'utf-8' // Ensure UTF-8 encoding on Windows + PYTHONIOENCODING: 'utf-8', // Ensure UTF-8 encoding on Windows + PYTHONUTF8: '1' // Force Python UTF-8 mode on Windows (Python 3.7+) } }); @@ -241,17 +242,17 @@ export class AgentProcessManager { } }; - // Handle stdout + // Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support childProcess.stdout?.on('data', (data: Buffer) => { - const log = data.toString(); + const log = data.toString('utf8'); console.log('[spawnProcess] stdout:', log.substring(0, 200)); this.emitter.emit('log', taskId, log); processLog(log); }); - // Handle stderr + // Handle stderr - explicitly decode as UTF-8 for cross-platform Unicode support childProcess.stderr?.on('data', (data: Buffer) => { - const log = data.toString(); + const log = data.toString('utf8'); console.log('[spawnProcess] stderr:', log.substring(0, 200)); // Some Python output goes to stderr (like progress bars) // so we treat it as log, not error diff --git a/auto-claude-ui/src/main/agent/agent-queue.ts b/auto-claude-ui/src/main/agent/agent-queue.ts index 167151a5..7dd6cdd4 100644 --- a/auto-claude-ui/src/main/agent/agent-queue.ts +++ b/auto-claude-ui/src/main/agent/agent-queue.ts @@ -156,7 +156,9 @@ export class AgentQueueManager { ...process.env, ...combinedEnv, ...profileEnv, - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' } }); @@ -193,9 +195,9 @@ export class AgentQueueManager { const completedTypes = new Set(); const totalTypes = 7; // Default all types - // Handle stdout + // Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support childProcess.stdout?.on('data', (data: Buffer) => { - const log = data.toString(); + const log = data.toString('utf8'); // Collect output for rate limit detection (keep last 10KB) allOutput = (allOutput + log).slice(-10000); @@ -242,9 +244,9 @@ export class AgentQueueManager { }); }); - // Handle stderr - also emit as logs + // Handle stderr - also emit as logs, explicitly decode as UTF-8 childProcess.stderr?.on('data', (data: Buffer) => { - const log = data.toString(); + const log = data.toString('utf8'); // Collect stderr for rate limit detection too allOutput = (allOutput + log).slice(-10000); console.error('[Ideation STDERR]', log); @@ -354,7 +356,9 @@ export class AgentQueueManager { ...process.env, ...combinedEnv, ...profileEnv, - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' } }); @@ -384,9 +388,9 @@ export class AgentQueueManager { } }; - // Handle stdout + // Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support childProcess.stdout?.on('data', (data: Buffer) => { - const log = data.toString(); + const log = data.toString('utf8'); // Collect output for rate limit detection (keep last 10KB) allRoadmapOutput = (allRoadmapOutput + log).slice(-10000); @@ -406,9 +410,9 @@ export class AgentQueueManager { }); }); - // Handle stderr + // Handle stderr - explicitly decode as UTF-8 childProcess.stderr?.on('data', (data: Buffer) => { - const log = data.toString(); + const log = data.toString('utf8'); // Collect stderr for rate limit detection too allRoadmapOutput = (allRoadmapOutput + log).slice(-10000); console.error('[Roadmap STDERR]', log); diff --git a/auto-claude-ui/src/main/changelog/generator.ts b/auto-claude-ui/src/main/changelog/generator.ts index 72c88e5e..3e4769a9 100644 --- a/auto-claude-ui/src/main/changelog/generator.ts +++ b/auto-claude-ui/src/main/changelog/generator.ts @@ -277,7 +277,9 @@ export class ChangelogGenerator extends EventEmitter { USER: process.env.USER || process.env.USERNAME || 'user', // Add common binary locations to PATH for claude CLI PATH: [process.env.PATH || '', ...pathAdditions].filter(Boolean).join(path.delimiter), - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' }; this.debug('Spawn environment', { diff --git a/auto-claude-ui/src/main/changelog/version-suggester.ts b/auto-claude-ui/src/main/changelog/version-suggester.ts index c2ac06c8..ebb945f6 100644 --- a/auto-claude-ui/src/main/changelog/version-suggester.ts +++ b/auto-claude-ui/src/main/changelog/version-suggester.ts @@ -237,7 +237,9 @@ except Exception as e: ...(isWindows ? { USERPROFILE: homeDir } : { HOME: homeDir }), USER: process.env.USER || process.env.USERNAME || 'user', PATH: [process.env.PATH || '', ...pathAdditions].filter(Boolean).join(path.delimiter), - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' }; return spawnEnv; diff --git a/auto-claude-ui/src/main/index.ts b/auto-claude-ui/src/main/index.ts index dcf8530b..a6d973e7 100644 --- a/auto-claude-ui/src/main/index.ts +++ b/auto-claude-ui/src/main/index.ts @@ -22,7 +22,7 @@ function getIconPath(): string { // Use PNG in dev mode (works better), ICNS in production iconName = is.dev ? 'icon-256.png' : 'icon.icns'; } else if (process.platform === 'win32') { - iconName = 'icon-256.png'; + iconName = 'icon.ico'; } else { iconName = 'icon.png'; } diff --git a/auto-claude-ui/src/main/insights/config.ts b/auto-claude-ui/src/main/insights/config.ts index 11884379..848449f1 100644 --- a/auto-claude-ui/src/main/insights/config.ts +++ b/auto-claude-ui/src/main/insights/config.ts @@ -105,7 +105,8 @@ export class InsightsConfig { ...autoBuildEnv, ...profileEnv, PYTHONUNBUFFERED: '1', - PYTHONIOENCODING: 'utf-8' + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' }; } } diff --git a/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts index 868a5b25..230fcadd 100644 --- a/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts @@ -9,13 +9,16 @@ import type { ProjectSettings, IPCResult, InitializationResult, - AutoBuildVersionInfo + AutoBuildVersionInfo, + GitStatus } from '../../shared/types'; import { projectStore } from '../project-store'; import { initializeProject, isInitialized, - hasLocalSource + hasLocalSource, + checkGitStatus, + initializeGit } from '../project-initializer'; import { PythonEnvManager, type PythonEnvStatus } from '../python-env-manager'; import { AgentManager } from '../agent'; @@ -505,4 +508,42 @@ export function registerProjectHandlers( } } ); + + // Check git status for a project (is it a repo? has commits?) + ipcMain.handle( + IPC_CHANNELS.GIT_CHECK_STATUS, + async (_, projectPath: string): Promise> => { + try { + if (!existsSync(projectPath)) { + return { success: false, error: 'Directory does not exist' }; + } + const gitStatus = checkGitStatus(projectPath); + return { success: true, data: gitStatus }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); + + // Initialize git in a project (run git init and create initial commit) + ipcMain.handle( + IPC_CHANNELS.GIT_INITIALIZE, + async (_, projectPath: string): Promise> => { + try { + if (!existsSync(projectPath)) { + return { success: false, error: 'Directory does not exist' }; + } + const result = initializeGit(projectPath); + return { success: result.success, data: result, error: result.error }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); } diff --git a/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts index cb98c302..aaade6a0 100644 --- a/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts @@ -6,6 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { AgentManager } from '../../agent'; import { fileWatcher } from '../../file-watcher'; import { findTaskAndProject } from './shared'; +import { checkGitStatus } from '../../project-initializer'; /** * Register task execution handlers (start, stop, review, status management, recovery) @@ -40,6 +41,27 @@ export function registerTaskExecutionHandlers( return; } + // Check git status - Auto Claude requires git for worktree-based builds + const gitStatus = checkGitStatus(project.path); + if (!gitStatus.isGitRepo) { + console.log('[TASK_START] Project is not a git repository:', project.path); + mainWindow.webContents.send( + IPC_CHANNELS.TASK_ERROR, + taskId, + 'Git repository required. Please run "git init" in your project directory. Auto Claude uses git worktrees for isolated builds.' + ); + return; + } + if (!gitStatus.hasCommits) { + console.log('[TASK_START] Git repository has no commits:', project.path); + mainWindow.webContents.send( + IPC_CHANNELS.TASK_ERROR, + taskId, + 'Git repository has no commits. Please make an initial commit first (git add . && git commit -m "Initial commit").' + ); + return; + } + console.log('[TASK_START] Found task:', task.specId, 'status:', task.status, 'subtasks:', task.subtasks.length); // Start file watcher for this task @@ -287,6 +309,21 @@ export function registerTaskExecutionHandlers( // Auto-start task when status changes to 'in_progress' and no process is running if (status === 'in_progress' && !agentManager.isRunning(taskId)) { const mainWindow = getMainWindow(); + + // Check git status before auto-starting + const gitStatusCheck = checkGitStatus(project.path); + if (!gitStatusCheck.isGitRepo || !gitStatusCheck.hasCommits) { + console.log('[TASK_UPDATE_STATUS] Git check failed, cannot auto-start task'); + if (mainWindow) { + mainWindow.webContents.send( + IPC_CHANNELS.TASK_ERROR, + taskId, + gitStatusCheck.error || 'Git repository with commits required to run tasks.' + ); + } + return { success: false, error: gitStatusCheck.error || 'Git repository required' }; + } + console.log('[TASK_UPDATE_STATUS] Auto-starting task:', taskId); // Start file watcher for this task @@ -498,6 +535,23 @@ export function registerTaskExecutionHandlers( // Auto-restart the task if requested let autoRestarted = false; if (autoRestart && project) { + // Check git status before auto-restarting + const gitStatusForRestart = checkGitStatus(project.path); + if (!gitStatusForRestart.isGitRepo || !gitStatusForRestart.hasCommits) { + console.log('[Recovery] Git check failed, cannot auto-restart task'); + // Recovery succeeded but we can't restart without git + return { + success: true, + data: { + taskId, + recovered: true, + newStatus, + message: `Task recovered but cannot restart: ${gitStatusForRestart.error || 'Git repository with commits required.'}`, + autoRestarted: false + } + }; + } + try { // Set status to in_progress for the restart newStatus = 'in_progress'; diff --git a/auto-claude-ui/src/main/ipc-handlers/task/worktree-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/task/worktree-handlers.ts index 92371e5d..bc424886 100644 --- a/auto-claude-ui/src/main/ipc-handlers/task/worktree-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/task/worktree-handlers.ts @@ -315,7 +315,9 @@ export function registerWorktreeHandlers( env: { ...process.env, ...profileEnv, // Include active Claude profile OAuth token - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' }, stdio: ['ignore', 'pipe', 'pipe'] // Don't connect stdin to avoid blocking }); @@ -568,7 +570,7 @@ export function registerWorktreeHandlers( return new Promise((resolve) => { const previewProcess = spawn(pythonPath, args, { cwd: sourcePath, - env: { ...process.env, ...previewProfileEnv, PYTHONUNBUFFERED: '1', DEBUG: 'true' } + env: { ...process.env, ...previewProfileEnv, PYTHONUNBUFFERED: '1', PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', DEBUG: 'true' } }); let stdout = ''; diff --git a/auto-claude-ui/src/main/project-initializer.ts b/auto-claude-ui/src/main/project-initializer.ts index 9bc687b2..2ba339ac 100644 --- a/auto-claude-ui/src/main/project-initializer.ts +++ b/auto-claude-ui/src/main/project-initializer.ts @@ -1,5 +1,6 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'fs'; import path from 'path'; +import { execSync } from 'child_process'; /** * Debug logging - only logs when AUTO_CLAUDE_DEBUG env var is set @@ -16,6 +17,137 @@ function debug(message: string, data?: Record): void { } } +/** + * Git status information for a project + */ +export interface GitStatus { + isGitRepo: boolean; + hasCommits: boolean; + currentBranch: string | null; + error?: string; +} + +/** + * Check if a directory is a git repository and has at least one commit + */ +export function checkGitStatus(projectPath: string): GitStatus { + try { + // Check if it's a git repository + execSync('git rev-parse --git-dir', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + } catch { + return { + isGitRepo: false, + hasCommits: false, + currentBranch: null, + error: 'Not a git repository. Please run "git init" to initialize git.' + }; + } + + // Check if there are any commits + let hasCommits = false; + try { + execSync('git rev-parse HEAD', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + hasCommits = true; + } catch { + // No commits yet + hasCommits = false; + } + + // Get current branch + let currentBranch: string | null = null; + try { + currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }).trim(); + } catch { + // Branch detection failed + } + + if (!hasCommits) { + return { + isGitRepo: true, + hasCommits: false, + currentBranch, + error: 'Git repository has no commits. Please make an initial commit first.' + }; + } + + return { + isGitRepo: true, + hasCommits: true, + currentBranch + }; +} + +/** + * Initialize git in a project directory and create an initial commit. + * This is a user-friendly way to set up git for non-technical users. + */ +export function initializeGit(projectPath: string): InitializationResult { + debug('initializeGit called', { projectPath }); + + // Check current git status + const status = checkGitStatus(projectPath); + + try { + // Step 1: Initialize git if needed + if (!status.isGitRepo) { + debug('Initializing git repository'); + execSync('git init', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + } + + // Step 2: Check if there are files to commit + const statusOutput = execSync('git status --porcelain', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }).trim(); + + // Step 3: If there are untracked/modified files, add and commit them + if (statusOutput || !status.hasCommits) { + debug('Adding files and creating initial commit'); + + // Add all files + execSync('git add -A', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + + // Create initial commit + execSync('git commit -m "Initial commit" --allow-empty', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + } + + debug('Git initialization complete'); + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error during git initialization'; + debug('Git initialization failed', { error: errorMessage }); + return { + success: false, + error: errorMessage + }; + } +} + /** * Entries to add to .gitignore when initializing a project */ @@ -130,6 +262,10 @@ export function isInitialized(projectPath: string): boolean { * * Creates .auto-claude/ with data directories (specs, ideation, insights, roadmap). * The framework code runs from the source repo - only data is stored here. + * + * Requires: + * - Project directory must exist + * - Project must be a git repository with at least one commit */ export function initializeProject(projectPath: string): InitializationResult { debug('initializeProject called', { projectPath }); @@ -143,6 +279,16 @@ export function initializeProject(projectPath: string): InitializationResult { }; } + // Check git status - Auto Claude requires git for worktree-based builds + const gitStatus = checkGitStatus(projectPath); + if (!gitStatus.isGitRepo || !gitStatus.hasCommits) { + debug('Git check failed', { gitStatus }); + return { + success: false, + error: gitStatus.error || 'Git repository required. Auto Claude uses git worktrees for isolated builds.' + }; + } + // Check if already initialized const dotAutoBuildPath = path.join(projectPath, '.auto-claude'); diff --git a/auto-claude-ui/src/main/terminal-name-generator.ts b/auto-claude-ui/src/main/terminal-name-generator.ts index d5d8010e..d7f9153b 100644 --- a/auto-claude-ui/src/main/terminal-name-generator.ts +++ b/auto-claude-ui/src/main/terminal-name-generator.ts @@ -137,7 +137,8 @@ export class TerminalNameGenerator extends EventEmitter { ...autoBuildEnv, ...profileEnv, // Include active Claude profile config PYTHONUNBUFFERED: '1', - PYTHONIOENCODING: 'utf-8' + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' } }); diff --git a/auto-claude-ui/src/main/title-generator.ts b/auto-claude-ui/src/main/title-generator.ts index e53fb6ee..9085604a 100644 --- a/auto-claude-ui/src/main/title-generator.ts +++ b/auto-claude-ui/src/main/title-generator.ts @@ -136,7 +136,8 @@ export class TitleGenerator extends EventEmitter { ...autoBuildEnv, ...profileEnv, // Include active Claude profile config PYTHONUNBUFFERED: '1', - PYTHONIOENCODING: 'utf-8' + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' } }); diff --git a/auto-claude-ui/src/preload/api/project-api.ts b/auto-claude-ui/src/preload/api/project-api.ts index 6b55c47c..e8a8955d 100644 --- a/auto-claude-ui/src/preload/api/project-api.ts +++ b/auto-claude-ui/src/preload/api/project-api.ts @@ -15,7 +15,8 @@ import type { ClaudeAuthResult, InfrastructureStatus, GraphitiValidationResult, - GraphitiConnectionTestResult + GraphitiConnectionTestResult, + GitStatus } from '../../shared/types'; export interface ProjectAPI { @@ -72,6 +73,8 @@ export interface ProjectAPI { getGitBranches: (projectPath: string) => Promise>; getCurrentGitBranch: (projectPath: string) => Promise>; detectMainBranch: (projectPath: string) => Promise>; + checkGitStatus: (projectPath: string) => Promise>; + initializeGit: (projectPath: string) => Promise>; } export const createProjectAPI = (): ProjectAPI => ({ @@ -180,5 +183,11 @@ export const createProjectAPI = (): ProjectAPI => ({ ipcRenderer.invoke(IPC_CHANNELS.GIT_GET_CURRENT_BRANCH, projectPath), detectMainBranch: (projectPath: string): Promise> => - ipcRenderer.invoke(IPC_CHANNELS.GIT_DETECT_MAIN_BRANCH, projectPath) + ipcRenderer.invoke(IPC_CHANNELS.GIT_DETECT_MAIN_BRANCH, projectPath), + + checkGitStatus: (projectPath: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.GIT_CHECK_STATUS, projectPath), + + initializeGit: (projectPath: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.GIT_INITIALIZE, projectPath) }); diff --git a/auto-claude-ui/src/renderer/components/GitSetupModal.tsx b/auto-claude-ui/src/renderer/components/GitSetupModal.tsx new file mode 100644 index 00000000..16ac15a6 --- /dev/null +++ b/auto-claude-ui/src/renderer/components/GitSetupModal.tsx @@ -0,0 +1,206 @@ +import { useState } from 'react'; +import { GitBranch, Terminal, CheckCircle2, AlertCircle, Loader2, FolderGit2 } from 'lucide-react'; +import { Button } from './ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from './ui/dialog'; +import type { Project, GitStatus } from '../../shared/types'; + +interface GitSetupModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + project: Project | null; + gitStatus: GitStatus | null; + onGitInitialized: () => void; + onSkip?: () => void; +} + +export function GitSetupModal({ + open, + onOpenChange, + project, + gitStatus, + onGitInitialized, + onSkip +}: GitSetupModalProps) { + const [isInitializing, setIsInitializing] = useState(false); + const [error, setError] = useState(null); + const [step, setStep] = useState<'info' | 'initializing' | 'success'>('info'); + + const needsGitInit = gitStatus && !gitStatus.isGitRepo; + const needsCommit = gitStatus && gitStatus.isGitRepo && !gitStatus.hasCommits; + + const handleInitializeGit = async () => { + if (!project) return; + + setIsInitializing(true); + setError(null); + setStep('initializing'); + + try { + // Call the backend to initialize git + const result = await window.electronAPI.initializeGit(project.path); + + if (result.success) { + setStep('success'); + // Wait a moment to show success, then trigger callback + setTimeout(() => { + onGitInitialized(); + onOpenChange(false); + setStep('info'); + }, 1500); + } else { + setError(result.error || 'Failed to initialize git'); + setStep('info'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to initialize git'); + setStep('info'); + } finally { + setIsInitializing(false); + } + }; + + const handleSkip = () => { + onSkip?.(); + onOpenChange(false); + }; + + const renderInfoStep = () => ( + <> + + + + Git Repository Required + + + Auto Claude uses git to safely build features in isolated workspaces + + + +
+ {/* Status indicator */} +
+
+ +
+

+ {needsGitInit + ? 'This folder is not a git repository' + : 'Git repository has no commits'} +

+

+ {needsGitInit + ? 'Git needs to be initialized before Auto Claude can manage your code.' + : 'At least one commit is required for Auto Claude to create worktrees.'} +

+
+
+
+ + {/* What will happen */} +
+

We'll set up git for you:

+
    + {needsGitInit && ( +
  • + + Initialize a new git repository +
  • + )} +
  • + + Create an initial commit with your current files +
  • +
+
+ + {/* Manual instructions for advanced users */} +
+ + Prefer to do it manually? + +
+

Open a terminal in your project folder and run:

+ {needsGitInit &&

git init

} +

git add .

+

git commit -m "Initial commit"

+
+
+ + {error && ( +
+ {error} +
+ )} +
+ + + + + + + ); + + const renderInitializingStep = () => ( + <> + + + + Setting up Git + + + +
+
+ +

+ Initializing git repository and creating initial commit... +

+
+
+ + ); + + const renderSuccessStep = () => ( + <> + + + + Git Initialized + + + +
+
+
+ +
+

+ Your project is now ready to use with Auto Claude! +

+
+
+ + ); + + return ( + + + {step === 'info' && renderInfoStep()} + {step === 'initializing' && renderInitializingStep()} + {step === 'success' && renderSuccessStep()} + + + ); +} diff --git a/auto-claude-ui/src/renderer/components/Sidebar.tsx b/auto-claude-ui/src/renderer/components/Sidebar.tsx index bbbd008b..f0eb41e8 100644 --- a/auto-claude-ui/src/renderer/components/Sidebar.tsx +++ b/auto-claude-ui/src/renderer/components/Sidebar.tsx @@ -55,8 +55,9 @@ import { } from '../stores/project-store'; import { useSettingsStore, saveSettings } from '../stores/settings-store'; import { AddProjectModal } from './AddProjectModal'; +import { GitSetupModal } from './GitSetupModal'; import { RateLimitIndicator } from './RateLimitIndicator'; -import type { Project, AutoBuildVersionInfo } from '../../shared/types'; +import type { Project, AutoBuildVersionInfo, GitStatus } from '../../shared/types'; export type SidebarView = 'kanban' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'github-issues' | 'changelog' | 'insights' | 'worktrees' | 'agent-tools' | 'agent-profiles'; @@ -104,6 +105,8 @@ export function Sidebar({ const [showAddProjectModal, setShowAddProjectModal] = useState(false); const [showInitDialog, setShowInitDialog] = useState(false); const [showUpdateDialog, setShowUpdateDialog] = useState(false); + const [showGitSetupModal, setShowGitSetupModal] = useState(false); + const [gitStatus, setGitStatus] = useState(null); const [pendingProject, setPendingProject] = useState(null); const [versionInfo, setVersionInfo] = useState(null); const [isInitializing, setIsInitializing] = useState(false); @@ -159,6 +162,29 @@ export function Sidebar({ checkUpdates(); }, [selectedProjectId, settings.autoUpdateAutoBuild]); + // Check git status when project changes + useEffect(() => { + const checkGit = async () => { + if (selectedProject) { + try { + const result = await window.electronAPI.checkGitStatus(selectedProject.path); + if (result.success && result.data) { + setGitStatus(result.data); + // Show git setup modal if project is not a git repo or has no commits + if (!result.data.isGitRepo || !result.data.hasCommits) { + setShowGitSetupModal(true); + } + } + } catch (error) { + console.error('Failed to check git status:', error); + } + } else { + setGitStatus(null); + } + }; + checkGit(); + }, [selectedProject]); + const handleAddProject = () => { setShowAddProjectModal(true); }; @@ -210,6 +236,20 @@ export function Sidebar({ setVersionInfo(null); }; + const handleGitInitialized = async () => { + // Refresh git status after initialization + if (selectedProject) { + try { + const result = await window.electronAPI.checkGitStatus(selectedProject.path); + if (result.success && result.data) { + setGitStatus(result.data); + } + } catch (error) { + console.error('Failed to refresh git status:', error); + } + } + }; + const handleRemoveProject = async (projectId: string, e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); @@ -518,6 +558,15 @@ export function Sidebar({ onOpenChange={setShowAddProjectModal} onProjectAdded={handleProjectAdded} /> + + {/* Git Setup Modal */} + ); } diff --git a/auto-claude-ui/src/renderer/lib/mocks/project-mock.ts b/auto-claude-ui/src/renderer/lib/mocks/project-mock.ts index 2c8ba526..13926a44 100644 --- a/auto-claude-ui/src/renderer/lib/mocks/project-mock.ts +++ b/auto-claude-ui/src/renderer/lib/mocks/project-mock.ts @@ -84,5 +84,19 @@ export const projectMock = { detectMainBranch: async () => ({ success: true, data: 'main' + }), + + checkGitStatus: async () => ({ + success: true, + data: { + isGitRepo: true, + hasCommits: true, + currentBranch: 'main' + } + }), + + initializeGit: async () => ({ + success: true, + data: { success: true } }) }; diff --git a/auto-claude-ui/src/shared/constants/ipc.ts b/auto-claude-ui/src/shared/constants/ipc.ts index 3999b4d4..e3f2563a 100644 --- a/auto-claude-ui/src/shared/constants/ipc.ts +++ b/auto-claude-ui/src/shared/constants/ipc.ts @@ -258,6 +258,8 @@ export const IPC_CHANNELS = { GIT_GET_BRANCHES: 'git:getBranches', GIT_GET_CURRENT_BRANCH: 'git:getCurrentBranch', GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch', + GIT_CHECK_STATUS: 'git:checkStatus', + GIT_INITIALIZE: 'git:initialize', // App auto-update operations APP_UPDATE_CHECK: 'app-update:check', diff --git a/auto-claude-ui/src/shared/types/index.ts b/auto-claude-ui/src/shared/types/index.ts index d779164b..5da875a6 100644 --- a/auto-claude-ui/src/shared/types/index.ts +++ b/auto-claude-ui/src/shared/types/index.ts @@ -15,8 +15,6 @@ export * from './changelog'; export * from './insights'; export * from './roadmap'; export * from './integrations'; -export * from './glossary'; -export * from './status'; export * from './app-update'; // IPC types (must be last to use types from other modules) diff --git a/auto-claude-ui/src/shared/types/ipc.ts b/auto-claude-ui/src/shared/types/ipc.ts index 2c82e633..44b147bc 100644 --- a/auto-claude-ui/src/shared/types/ipc.ts +++ b/auto-claude-ui/src/shared/types/ipc.ts @@ -18,7 +18,8 @@ import type { ProjectEnvConfig, InfrastructureStatus, GraphitiValidationResult, - GraphitiConnectionTestResult + GraphitiConnectionTestResult, + GitStatus } from './project'; import type { Task, @@ -500,6 +501,8 @@ export interface ElectronAPI { getGitBranches: (projectPath: string) => Promise>; getCurrentGitBranch: (projectPath: string) => Promise>; detectMainBranch: (projectPath: string) => Promise>; + checkGitStatus: (projectPath: string) => Promise>; + initializeGit: (projectPath: string) => Promise>; } declare global { diff --git a/auto-claude-ui/src/shared/types/project.ts b/auto-claude-ui/src/shared/types/project.ts index 840cbcbf..c4675fca 100644 --- a/auto-claude-ui/src/shared/types/project.ts +++ b/auto-claude-ui/src/shared/types/project.ts @@ -305,6 +305,13 @@ export interface InitializationResult { error?: string; } +export interface GitStatus { + isGitRepo: boolean; + hasCommits: boolean; + currentBranch: string | null; + error?: string; +} + export interface CreateProjectFolderResult { path: string; name: string; diff --git a/auto-claude/core/worktree.py b/auto-claude/core/worktree.py index ef7e0f80..8e3dece8 100644 --- a/auto-claude/core/worktree.py +++ b/auto-claude/core/worktree.py @@ -79,11 +79,15 @@ class WorktreeManager: cwd=self.project_dir, capture_output=True, text=True, + encoding="utf-8", + errors="replace", ) if result.returncode == 0: return env_branch else: - print(f"Warning: DEFAULT_BRANCH '{env_branch}' not found, auto-detecting...") + print( + f"Warning: DEFAULT_BRANCH '{env_branch}' not found, auto-detecting..." + ) # 2. Auto-detect main/master for branch in ["main", "master"]: @@ -92,15 +96,17 @@ class WorktreeManager: cwd=self.project_dir, capture_output=True, text=True, + encoding="utf-8", + errors="replace", ) if result.returncode == 0: return branch # 3. Fall back to current branch with warning current = self._get_current_branch() - print(f"Warning: Could not find 'main' or 'master' branch.") + print("Warning: Could not find 'main' or 'master' branch.") print(f"Warning: Using current branch '{current}' as base for worktree.") - print(f"Tip: Set DEFAULT_BRANCH=your-branch in .env to avoid this.") + print("Tip: Set DEFAULT_BRANCH=your-branch in .env to avoid this.") return current def _get_current_branch(self) -> str: @@ -110,6 +116,8 @@ class WorktreeManager: cwd=self.project_dir, capture_output=True, text=True, + encoding="utf-8", + errors="replace", ) if result.returncode != 0: raise WorktreeError(f"Failed to get current branch: {result.stderr}") @@ -124,6 +132,8 @@ class WorktreeManager: cwd=cwd or self.project_dir, capture_output=True, text=True, + encoding="utf-8", + errors="replace", ) def _unstage_gitignored_files(self) -> None: @@ -153,6 +163,8 @@ class WorktreeManager: input="\n".join(staged_files), capture_output=True, text=True, + encoding="utf-8", + errors="replace", ) if result.stdout.strip(): diff --git a/auto-claude/implementation_plan/plan.py b/auto-claude/implementation_plan/plan.py index 277cb103..46ef62eb 100644 --- a/auto-claude/implementation_plan/plan.py +++ b/auto-claude/implementation_plan/plan.py @@ -105,8 +105,8 @@ class ImplementationPlan: self.update_status_from_subtasks() path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "w") as f: - json.dump(self.to_dict(), f, indent=2) + with open(path, "w", encoding="utf-8") as f: + json.dump(self.to_dict(), f, indent=2, ensure_ascii=False) def update_status_from_subtasks(self): """Update overall status and planStatus based on subtask completion state. @@ -169,7 +169,7 @@ class ImplementationPlan: @classmethod def load(cls, path: Path) -> "ImplementationPlan": """Load plan from JSON file.""" - with open(path) as f: + with open(path, encoding="utf-8") as f: return cls.from_dict(json.load(f)) def get_available_phases(self) -> list[Phase]: diff --git a/auto-claude/run.py b/auto-claude/run.py index 3def46ca..f8936d74 100644 --- a/auto-claude/run.py +++ b/auto-claude/run.py @@ -28,6 +28,38 @@ Prerequisites: - Claude Code CLI installed """ +import io +import sys + +# Configure safe encoding on Windows BEFORE any imports that might print +# This handles both TTY and piped output (e.g., from Electron) +if sys.platform == "win32": + for _stream_name in ("stdout", "stderr"): + _stream = getattr(sys, _stream_name) + # Method 1: Try reconfigure (works for TTY) + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") + continue + except (AttributeError, io.UnsupportedOperation, OSError): + pass + # Method 2: Wrap with TextIOWrapper for piped output + try: + if hasattr(_stream, "buffer"): + _new_stream = io.TextIOWrapper( + _stream.buffer, + encoding="utf-8", + errors="replace", + line_buffering=True, + ) + setattr(sys, _stream_name, _new_stream) + except (AttributeError, io.UnsupportedOperation, OSError): + pass + # Clean up temporary variables + del _stream_name, _stream + if "_new_stream" in dir(): + del _new_stream + from cli import main if __name__ == "__main__": diff --git a/auto-claude/runners/spec_runner.py b/auto-claude/runners/spec_runner.py index 928b7354..c4426465 100644 --- a/auto-claude/runners/spec_runner.py +++ b/auto-claude/runners/spec_runner.py @@ -34,10 +34,40 @@ Usage: """ import asyncio +import io import os import sys from pathlib import Path +# Configure safe encoding on Windows BEFORE any imports that might print +# This handles both TTY and piped output (e.g., from Electron) +if sys.platform == "win32": + for _stream_name in ("stdout", "stderr"): + _stream = getattr(sys, _stream_name) + # Method 1: Try reconfigure (works for TTY) + if hasattr(_stream, "reconfigure"): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") + continue + except (AttributeError, io.UnsupportedOperation, OSError): + pass + # Method 2: Wrap with TextIOWrapper for piped output + try: + if hasattr(_stream, "buffer"): + _new_stream = io.TextIOWrapper( + _stream.buffer, + encoding="utf-8", + errors="replace", + line_buffering=True, + ) + setattr(sys, _stream_name, _new_stream) + except (AttributeError, io.UnsupportedOperation, OSError): + pass + # Clean up temporary variables + del _stream_name, _stream + if "_new_stream" in dir(): + del _new_stream + # Add auto-claude to path (parent of runners/) sys.path.insert(0, str(Path(__file__).parent.parent)) @@ -230,7 +260,9 @@ Examples: print( f" {muted('Or re-run spec_runner with --auto-approve to skip review:')}" ) - example_cmd = 'python auto-claude/spec_runner.py --task "..." --auto-approve' + example_cmd = ( + 'python auto-claude/spec_runner.py --task "..." --auto-approve' + ) print(f" {highlight(example_cmd)}") sys.exit(1) diff --git a/auto-claude/spec/pipeline/agent_runner.py b/auto-claude/spec/pipeline/agent_runner.py index 49edcc65..e06c2285 100644 --- a/auto-claude/spec/pipeline/agent_runner.py +++ b/auto-claude/spec/pipeline/agent_runner.py @@ -7,6 +7,11 @@ Handles the execution of AI agents for the spec creation pipeline. from pathlib import Path +# Configure safe encoding before any output (fixes Windows encoding errors) +from ui.capabilities import configure_safe_encoding + +configure_safe_encoding() + from client import create_client from task_logger import ( LogEntryType, diff --git a/auto-claude/spec/pipeline/orchestrator.py b/auto-claude/spec/pipeline/orchestrator.py index b488268b..4db90d9a 100644 --- a/auto-claude/spec/pipeline/orchestrator.py +++ b/auto-claude/spec/pipeline/orchestrator.py @@ -457,9 +457,11 @@ class SpecOrchestrator: print_key_value("Reasoning", assessment.reasoning) if assessment.needs_research: - print(f" {muted('→ Research phase enabled')}") + print(f" {muted(icon(Icons.ARROW_RIGHT) + ' Research phase enabled')}") if assessment.needs_self_critique: - print(f" {muted('→ Self-critique phase enabled')}") + print( + f" {muted(icon(Icons.ARROW_RIGHT) + ' Self-critique phase enabled')}" + ) def _print_phases_to_run(self) -> None: """Print the list of phases that will be executed.""" diff --git a/auto-claude/spec/validate_pkg/models.py b/auto-claude/spec/validate_pkg/models.py index 6ece9156..984f4c07 100644 --- a/auto-claude/spec/validate_pkg/models.py +++ b/auto-claude/spec/validate_pkg/models.py @@ -30,16 +30,16 @@ class ValidationResult: if self.errors: lines.append("\nErrors:") for err in self.errors: - lines.append(f" ✗ {err}") + lines.append(f" [X] {err}") if self.warnings: lines.append("\nWarnings:") for warn in self.warnings: - lines.append(f" ⚠ {warn}") + lines.append(f" [!] {warn}") if self.fixes and not self.valid: lines.append("\nSuggested Fixes:") for fix in self.fixes: - lines.append(f" → {fix}") + lines.append(f" -> {fix}") return "\n".join(lines) diff --git a/auto-claude/task_logger/storage.py b/auto-claude/task_logger/storage.py index 572c14de..ba2605e9 100644 --- a/auto-claude/task_logger/storage.py +++ b/auto-claude/task_logger/storage.py @@ -30,7 +30,7 @@ class LogStorage: """Load existing logs or create new structure.""" if self.log_file.exists(): try: - with open(self.log_file) as f: + with open(self.log_file, encoding="utf-8") as f: return json.load(f) except (OSError, json.JSONDecodeError): pass @@ -69,8 +69,8 @@ class LogStorage: self._data["updated_at"] = self._timestamp() try: self.spec_dir.mkdir(parents=True, exist_ok=True) - with open(self.log_file, "w") as f: - json.dump(self._data, f, indent=2) + with open(self.log_file, "w", encoding="utf-8") as f: + json.dump(self._data, f, indent=2, ensure_ascii=False) except OSError as e: print(f"Warning: Failed to save task logs: {e}", file=sys.stderr) @@ -159,7 +159,7 @@ def load_task_logs(spec_dir: Path) -> dict | None: return None try: - with open(log_file) as f: + with open(log_file, encoding="utf-8") as f: return json.load(f) except (OSError, json.JSONDecodeError): return None diff --git a/auto-claude/ui/__init__.py b/auto-claude/ui/__init__.py index 13b57982..959db946 100644 --- a/auto-claude/ui/__init__.py +++ b/auto-claude/ui/__init__.py @@ -21,6 +21,7 @@ from .capabilities import ( FANCY_UI, INTERACTIVE, UNICODE, + configure_safe_encoding, supports_color, supports_interactive, supports_unicode, @@ -57,6 +58,7 @@ _INTERACTIVE = INTERACTIVE __all__ = [ # Capabilities + "configure_safe_encoding", "supports_unicode", "supports_color", "supports_interactive", diff --git a/auto-claude/ui/capabilities.py b/auto-claude/ui/capabilities.py index ed4c7c34..ac8de510 100644 --- a/auto-claude/ui/capabilities.py +++ b/auto-claude/ui/capabilities.py @@ -8,10 +8,56 @@ Detects terminal capabilities for: - Interactive input support """ +import io import os import sys +def configure_safe_encoding() -> None: + """ + Configure stdout/stderr to handle Unicode safely on Windows. + + On Windows, the default console encoding (cp1252) can't display many + Unicode characters. This function forces UTF-8 encoding with 'replace' + error handling, so unrenderable characters are replaced with '?' instead + of raising exceptions. + + This handles both: + 1. Regular console output (reconfigure method) + 2. Piped output from subprocess (TextIOWrapper replacement) + """ + if sys.platform != "win32": + return + + # Method 1: Try reconfigure (works for TTY) + for stream_name in ("stdout", "stderr"): + stream = getattr(sys, stream_name) + if hasattr(stream, "reconfigure"): + try: + stream.reconfigure(encoding="utf-8", errors="replace") + continue + except (AttributeError, io.UnsupportedOperation, OSError): + pass + + # Method 2: Wrap with TextIOWrapper for piped output + # This is needed when stdout/stderr are pipes (e.g., from Electron) + try: + if hasattr(stream, "buffer"): + new_stream = io.TextIOWrapper( + stream.buffer, + encoding="utf-8", + errors="replace", + line_buffering=True, + ) + setattr(sys, stream_name, new_stream) + except (AttributeError, io.UnsupportedOperation, OSError): + pass + + +# Configure safe encoding on module import +configure_safe_encoding() + + def _is_fancy_ui_enabled() -> bool: """Check if fancy UI is enabled via environment variable.""" value = os.environ.get("ENABLE_FANCY_UI", "true").lower()