From 9beefd1ac36981c8308d25a5ed33b22936a1af8d Mon Sep 17 00:00:00 2001 From: trueheads Date: Sun, 21 Dec 2025 16:47:21 -0600 Subject: [PATCH 01/12] Rebuild of the kanban scaling logic, and adding constraints to window scaling logic for electron and web --- apps/app/server-bundle/package-lock.json | 1282 +++++++++++++++++ apps/app/server-bundle/package.json | 15 + apps/ui/src/components/layout/sidebar.tsx | 13 + .../board-view/components/kanban-column.tsx | 35 +- .../views/board-view/kanban-board.tsx | 73 +- apps/ui/src/hooks/use-responsive-kanban.ts | 160 +- apps/ui/src/main.ts | 441 ++++-- apps/ui/src/preload.ts | 30 +- .../tests/kanban-responsive-scaling.spec.ts | 112 +- libs/types/src/settings.ts | 23 + 10 files changed, 1918 insertions(+), 266 deletions(-) create mode 100644 apps/app/server-bundle/package-lock.json create mode 100644 apps/app/server-bundle/package.json diff --git a/apps/app/server-bundle/package-lock.json b/apps/app/server-bundle/package-lock.json new file mode 100644 index 00000000..1c773f35 --- /dev/null +++ b/apps/app/server-bundle/package-lock.json @@ -0,0 +1,1282 @@ +{ + "name": "@automaker/server-bundle", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@automaker/server-bundle", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.1.61", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "morgan": "^1.10.1", + "node-pty": "1.1.0-beta41", + "ws": "^8.18.0" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.69.tgz", + "integrity": "sha512-T6mb8xKGYIH0g3drS0VRxDHemj8kmWD37nuB+ENoD9sZfi/lomnugWLWBjq9Cjw10WBewE5hjv+i8swM34nkAA==", + "license": "SEE LICENSE IN README.md", + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.33.5", + "@img/sharp-darwin-x64": "^0.33.5", + "@img/sharp-linux-arm": "^0.33.5", + "@img/sharp-linux-arm64": "^0.33.5", + "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-linuxmusl-arm64": "^0.33.5", + "@img/sharp-linuxmusl-x64": "^0.33.5", + "@img/sharp-win32-x64": "^0.33.5" + }, + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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-pty": { + "version": "1.1.0-beta41", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta41.tgz", + "integrity": "sha512-OUT29KMnzh1IS0b2YcUwVz56D4iAXDsl2PtIKP3zHMljiUBq2WcaHEFfhzQfgkhWs2SExcXvfdlBPANDVU9SnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/app/server-bundle/package.json b/apps/app/server-bundle/package.json new file mode 100644 index 00000000..becf118f --- /dev/null +++ b/apps/app/server-bundle/package.json @@ -0,0 +1,15 @@ +{ + "name": "@automaker/server-bundle", + "version": "0.1.0", + "type": "module", + "main": "dist/index.js", + "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.1.61", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "morgan": "^1.10.1", + "node-pty": "1.1.0-beta41", + "ws": "^8.18.0" + } +} \ No newline at end of file diff --git a/apps/ui/src/components/layout/sidebar.tsx b/apps/ui/src/components/layout/sidebar.tsx index cca6aa22..ccf8e065 100644 --- a/apps/ui/src/components/layout/sidebar.tsx +++ b/apps/ui/src/components/layout/sidebar.tsx @@ -375,6 +375,19 @@ export function Sidebar() { return () => mediaQuery.removeEventListener('change', handleResize); }, [sidebarOpen, toggleSidebar]); + // Update Electron window minWidth when sidebar state changes + // This ensures the window can't be resized smaller than what the kanban board needs + useEffect(() => { + const electronAPI = ( + window as unknown as { + electronAPI?: { updateMinWidth?: (expanded: boolean) => Promise }; + } + ).electronAPI; + if (electronAPI?.updateMinWidth) { + electronAPI.updateMinWidth(sidebarOpen); + } + }, [sidebarOpen]); + // Filtered projects based on search query const filteredProjects = useMemo(() => { if (!projectSearchQuery.trim()) { diff --git a/apps/ui/src/components/views/board-view/components/kanban-column.tsx b/apps/ui/src/components/views/board-view/components/kanban-column.tsx index 9361e5e5..4e08cfba 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-column.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-column.tsx @@ -1,8 +1,7 @@ - -import { memo } from "react"; -import { useDroppable } from "@dnd-kit/core"; -import { cn } from "@/lib/utils"; -import type { ReactNode } from "react"; +import { memo } from 'react'; +import { useDroppable } from '@dnd-kit/core'; +import { cn } from '@/lib/utils'; +import type { ReactNode } from 'react'; interface KanbanColumnProps { id: string; @@ -39,10 +38,12 @@ export const KanbanColumn = memo(function KanbanColumn({
@@ -59,11 +60,11 @@ export const KanbanColumn = memo(function KanbanColumn({ {/* Column Header */}
-
+

{title}

{headerAction} @@ -74,11 +75,11 @@ export const KanbanColumn = memo(function KanbanColumn({ {/* Column Content */}
{children} diff --git a/apps/ui/src/components/views/board-view/kanban-board.tsx b/apps/ui/src/components/views/board-view/kanban-board.tsx index 1dddffe3..c291f646 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -1,22 +1,15 @@ - -import { - DndContext, - DragOverlay, -} from "@dnd-kit/core"; -import { - SortableContext, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { HotkeyButton } from "@/components/ui/hotkey-button"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { KanbanColumn, KanbanCard } from "./components"; -import { Feature } from "@/store/app-store"; -import { FastForward, Lightbulb, Archive } from "lucide-react"; -import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts"; -import { useResponsiveKanban } from "@/hooks/use-responsive-kanban"; -import { COLUMNS, ColumnId } from "./constants"; +import { DndContext, DragOverlay } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { HotkeyButton } from '@/components/ui/hotkey-button'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { KanbanColumn, KanbanCard } from './components'; +import { Feature } from '@/store/app-store'; +import { FastForward, Lightbulb, Archive } from 'lucide-react'; +import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; +import { useResponsiveKanban } from '@/hooks/use-responsive-kanban'; +import { COLUMNS, ColumnId } from './constants'; interface KanbanBoardProps { sensors: any; @@ -90,20 +83,18 @@ export function KanbanBoard({ onArchiveAllVerified, }: KanbanBoardProps) { // Use responsive column widths based on window size - const { columnWidth } = useResponsiveKanban(COLUMNS.length); + // containerStyle handles centering and ensures columns fit without horizontal scroll in Electron + const { columnWidth, containerStyle } = useResponsiveKanban(COLUMNS.length); return ( -
+
-
+
{COLUMNS.map((column) => { const columnFeatures = getColumnFeatures(column.id); return ( @@ -118,8 +109,7 @@ export function KanbanBoard({ showBorder={backgroundSettings.columnBorderEnabled} hideScrollbar={backgroundSettings.hideScrollbar} headerAction={ - column.id === "verified" && - columnFeatures.length > 0 ? ( + column.id === 'verified' && columnFeatures.length > 0 ? ( - ) : column.id === "backlog" ? ( + ) : column.id === 'backlog' ? (
)} @@ -410,9 +372,7 @@ export function BoardBackgroundModal({
- - {cardOpacity}% - + {cardOpacity}%
- - {columnOpacity}% - + {columnOpacity}%
-
@@ -485,9 +440,7 @@ export function BoardBackgroundModal({
- - {cardBorderOpacity}% - + {cardBorderOpacity}%
; +// Re-export for convenience +export type { FeatureImagePath, FeatureTextFilePath }; + interface DescriptionImageDropZoneProps { value: string; onChange: (value: string) => void; images: FeatureImagePath[]; onImagesChange: (images: FeatureImagePath[]) => void; + textFiles?: FeatureTextFilePath[]; + onTextFilesChange?: (textFiles: FeatureTextFilePath[]) => void; placeholder?: string; className?: string; disabled?: boolean; @@ -25,14 +45,13 @@ interface DescriptionImageDropZoneProps { error?: boolean; // Show error state with red border } -const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; -const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB - export function DescriptionImageDropZone({ value, onChange, images, onImagesChange, + textFiles = [], + onTextFilesChange, placeholder = 'Describe the feature...', className, disabled = false, @@ -81,21 +100,6 @@ export function DescriptionImageDropZone({ [currentProject?.path] ); - const fileToBase64 = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject(new Error('Failed to read file as base64')); - } - }; - reader.onerror = () => reject(new Error('Failed to read file')); - reader.readAsDataURL(file); - }); - }; - const saveImageToTemp = useCallback( async (base64Data: string, filename: string, mimeType: string): Promise => { try { @@ -129,54 +133,89 @@ export function DescriptionImageDropZone({ setIsProcessing(true); const newImages: FeatureImagePath[] = []; + const newTextFiles: FeatureTextFilePath[] = []; const newPreviews = new Map(previewImages); const errors: string[] = []; + // Calculate total current files + const currentTotalFiles = images.length + textFiles.length; + for (const file of Array.from(files)) { - // Validate file type - if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { - errors.push(`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`); - continue; - } - - // Validate file size - if (file.size > maxFileSize) { - const maxSizeMB = maxFileSize / (1024 * 1024); - errors.push(`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`); - continue; - } - - // Check if we've reached max files - if (newImages.length + images.length >= maxFiles) { - errors.push(`Maximum ${maxFiles} images allowed.`); - break; - } - - try { - const base64 = await fileToBase64(file); - const tempPath = await saveImageToTemp(base64, file.name, file.type); - - if (tempPath) { - const imageId = `img-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; - const imagePathRef: FeatureImagePath = { - id: imageId, - path: tempPath, - filename: file.name, - mimeType: file.type, - }; - newImages.push(imagePathRef); - // Store preview for display - newPreviews.set(imageId, base64); - } else { - errors.push(`${file.name}: Failed to save image.`); + // Check if it's a text file + if (isTextFile(file)) { + const validation = validateTextFile(file, DEFAULT_MAX_TEXT_FILE_SIZE); + if (!validation.isValid) { + errors.push(validation.error!); + continue; } - } catch { - errors.push(`${file.name}: Failed to process image.`); + + // Check if we've reached max files + const totalFiles = newImages.length + newTextFiles.length + currentTotalFiles; + if (totalFiles >= maxFiles) { + errors.push(`Maximum ${maxFiles} files allowed.`); + break; + } + + try { + const content = await fileToText(file); + const sanitizedName = sanitizeFilename(file.name); + const textFilePath: FeatureTextFilePath = { + id: generateFileId(), + path: '', // Text files don't need to be saved to disk + filename: sanitizedName, + mimeType: getTextFileMimeType(file.name), + content, + }; + newTextFiles.push(textFilePath); + } catch { + errors.push(`${file.name}: Failed to read text file.`); + } + } + // Check if it's an image file + else if (isImageFile(file)) { + // Validate file size + if (file.size > maxFileSize) { + const maxSizeMB = maxFileSize / (1024 * 1024); + errors.push(`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`); + continue; + } + + // Check if we've reached max files + const totalFiles = newImages.length + newTextFiles.length + currentTotalFiles; + if (totalFiles >= maxFiles) { + errors.push(`Maximum ${maxFiles} files allowed.`); + break; + } + + try { + const base64 = await fileToBase64(file); + const sanitizedName = sanitizeFilename(file.name); + const tempPath = await saveImageToTemp(base64, sanitizedName, file.type); + + if (tempPath) { + const imageId = `img-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + const imagePathRef: FeatureImagePath = { + id: imageId, + path: tempPath, + filename: sanitizedName, + mimeType: file.type, + }; + newImages.push(imagePathRef); + // Store preview for display + newPreviews.set(imageId, base64); + } else { + errors.push(`${file.name}: Failed to save image.`); + } + } catch { + errors.push(`${file.name}: Failed to process image.`); + } + } else { + errors.push(`${file.name}: Unsupported file type. Use images, .txt, or .md files.`); } } if (errors.length > 0) { - console.warn('Image upload errors:', errors); + console.warn('File upload errors:', errors); } if (newImages.length > 0) { @@ -184,15 +223,21 @@ export function DescriptionImageDropZone({ setPreviewImages(newPreviews); } + if (newTextFiles.length > 0 && onTextFilesChange) { + onTextFilesChange([...textFiles, ...newTextFiles]); + } + setIsProcessing(false); }, [ disabled, isProcessing, images, + textFiles, maxFiles, maxFileSize, onImagesChange, + onTextFilesChange, previewImages, saveImageToTemp, ] @@ -263,6 +308,15 @@ export function DescriptionImageDropZone({ [images, onImagesChange] ); + const removeTextFile = useCallback( + (fileId: string) => { + if (onTextFilesChange) { + onTextFilesChange(textFiles.filter((file) => file.id !== fileId)); + } + }, + [textFiles, onTextFilesChange] + ); + // Handle paste events to detect and process images from clipboard // Works across all OS (Windows, Linux, macOS) const handlePaste = useCallback( @@ -314,11 +368,11 @@ export function DescriptionImageDropZone({ ref={fileInputRef} type="file" multiple - accept={ACCEPTED_IMAGE_TYPES.join(',')} + accept={[...ACCEPTED_IMAGE_TYPES, ...ACCEPTED_TEXT_EXTENSIONS].join(',')} onChange={handleFileSelect} className="hidden" disabled={disabled} - data-testid="description-image-input" + data-testid="description-file-input" /> {/* Drop zone wrapper */} @@ -338,7 +392,7 @@ export function DescriptionImageDropZone({ >
- Drop images here + Drop files here
)} @@ -359,7 +413,7 @@ export function DescriptionImageDropZone({ {/* Hint text */}

- Paste, drag and drop images, or{' '} + Paste, drag and drop files, or{' '} {' '} - to attach context images + to attach context (images, .txt, .md)

{/* Processing indicator */} {isProcessing && (
- Saving images... + Processing files...
)} - {/* Image previews */} - {images.length > 0 && ( -
+ {/* File previews (images and text files) */} + {(images.length > 0 || textFiles.length > 0) && ( +

- {images.length} image{images.length > 1 ? 's' : ''} attached + {images.length + textFiles.length} file + {images.length + textFiles.length > 1 ? 's' : ''} attached

+ {/* Image previews */} {images.map((image) => (
))} + {/* Text file previews */} + {textFiles.map((file) => ( +
+ {/* Text file icon */} +
+ +
+ {/* Remove button */} + {!disabled && ( + + )} + {/* Filename and size tooltip on hover */} +
+

{file.filename}

+

{formatFileSize(file.content.length)}

+
+
+ ))}
)} diff --git a/apps/ui/src/components/ui/feature-image-upload.tsx b/apps/ui/src/components/ui/feature-image-upload.tsx index 0cb5403c..4722502e 100644 --- a/apps/ui/src/components/ui/feature-image-upload.tsx +++ b/apps/ui/src/components/ui/feature-image-upload.tsx @@ -1,6 +1,14 @@ import React, { useState, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils'; import { ImageIcon, X, Upload } from 'lucide-react'; +import { + fileToBase64, + generateImageId, + ACCEPTED_IMAGE_TYPES, + DEFAULT_MAX_FILE_SIZE, + DEFAULT_MAX_FILES, + validateImageFile, +} from '@/lib/image-utils'; export interface FeatureImage { id: string; @@ -19,13 +27,10 @@ interface FeatureImageUploadProps { disabled?: boolean; } -const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; -const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB - export function FeatureImageUpload({ images, onImagesChange, - maxFiles = 5, + maxFiles = DEFAULT_MAX_FILES, maxFileSize = DEFAULT_MAX_FILE_SIZE, className, disabled = false, @@ -34,21 +39,6 @@ export function FeatureImageUpload({ const [isProcessing, setIsProcessing] = useState(false); const fileInputRef = useRef(null); - const fileToBase64 = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject(new Error('Failed to read file as base64')); - } - }; - reader.onerror = () => reject(new Error('Failed to read file')); - reader.readAsDataURL(file); - }); - }; - const processFiles = useCallback( async (files: FileList) => { if (disabled || isProcessing) return; @@ -58,16 +48,10 @@ export function FeatureImageUpload({ const errors: string[] = []; for (const file of Array.from(files)) { - // Validate file type - if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { - errors.push(`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`); - continue; - } - - // Validate file size - if (file.size > maxFileSize) { - const maxSizeMB = maxFileSize / (1024 * 1024); - errors.push(`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`); + // Validate file + const validation = validateImageFile(file, maxFileSize); + if (!validation.isValid) { + errors.push(validation.error!); continue; } @@ -80,7 +64,7 @@ export function FeatureImageUpload({ try { const base64 = await fileToBase64(file); const imageAttachment: FeatureImage = { - id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + id: generateImageId(), data: base64, mimeType: file.type, filename: file.name, diff --git a/apps/ui/src/components/ui/image-drop-zone.tsx b/apps/ui/src/components/ui/image-drop-zone.tsx index 04e53491..2f8f5c43 100644 --- a/apps/ui/src/components/ui/image-drop-zone.tsx +++ b/apps/ui/src/components/ui/image-drop-zone.tsx @@ -2,6 +2,15 @@ import React, { useState, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils'; import { ImageIcon, X, Upload } from 'lucide-react'; import type { ImageAttachment } from '@/store/app-store'; +import { + fileToBase64, + generateImageId, + formatFileSize, + validateImageFile, + ACCEPTED_IMAGE_TYPES, + DEFAULT_MAX_FILE_SIZE, + DEFAULT_MAX_FILES, +} from '@/lib/image-utils'; interface ImageDropZoneProps { onImagesSelected: (images: ImageAttachment[]) => void; @@ -13,12 +22,9 @@ interface ImageDropZoneProps { images?: ImageAttachment[]; // Optional controlled images prop } -const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; -const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB - export function ImageDropZone({ onImagesSelected, - maxFiles = 5, + maxFiles = DEFAULT_MAX_FILES, maxFileSize = DEFAULT_MAX_FILE_SIZE, className, children, @@ -53,16 +59,10 @@ export function ImageDropZone({ const errors: string[] = []; for (const file of Array.from(files)) { - // Validate file type - if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { - errors.push(`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`); - continue; - } - - // Validate file size - if (file.size > maxFileSize) { - const maxSizeMB = maxFileSize / (1024 * 1024); - errors.push(`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`); + // Validate file + const validation = validateImageFile(file, maxFileSize); + if (!validation.isValid) { + errors.push(validation.error!); continue; } @@ -75,7 +75,7 @@ export function ImageDropZone({ try { const base64 = await fileToBase64(file); const imageAttachment: ImageAttachment = { - id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + id: generateImageId(), data: base64, mimeType: file.type, filename: file.name, @@ -89,7 +89,6 @@ export function ImageDropZone({ if (errors.length > 0) { console.warn('Image upload errors:', errors); - // You could show these errors to the user via a toast or notification } if (newImages.length > 0) { @@ -282,26 +281,3 @@ export function ImageDropZone({
); } - -function fileToBase64(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject(new Error('Failed to read file as base64')); - } - }; - reader.onerror = () => reject(new Error('Failed to read file')); - reader.readAsDataURL(file); - }); -} - -function formatFileSize(bytes: number): string { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; -} diff --git a/apps/ui/src/components/views/agent-view.tsx b/apps/ui/src/components/views/agent-view.tsx index 0d6a7f77..d5e9ea8a 100644 --- a/apps/ui/src/components/views/agent-view.tsx +++ b/apps/ui/src/components/views/agent-view.tsx @@ -16,12 +16,26 @@ import { X, ImageIcon, ChevronDown, + FileText, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useElectronAgent } from '@/hooks/use-electron-agent'; import { SessionManager } from '@/components/session-manager'; import { Markdown } from '@/components/ui/markdown'; -import type { ImageAttachment } from '@/store/app-store'; +import type { ImageAttachment, TextFileAttachment } from '@/store/app-store'; +import { + fileToBase64, + generateImageId, + generateFileId, + validateImageFile, + validateTextFile, + isTextFile, + isImageFile, + fileToText, + getTextFileMimeType, + DEFAULT_MAX_FILE_SIZE, + DEFAULT_MAX_FILES, +} from '@/lib/image-utils'; import { useKeyboardShortcuts, useKeyboardShortcutsConfig, @@ -40,6 +54,7 @@ export function AgentView() { const shortcuts = useKeyboardShortcutsConfig(); const [input, setInput] = useState(''); const [selectedImages, setSelectedImages] = useState([]); + const [selectedTextFiles, setSelectedTextFiles] = useState([]); const [showImageDropZone, setShowImageDropZone] = useState(false); const [currentTool, setCurrentTool] = useState(null); const [currentSessionId, setCurrentSessionId] = useState(null); @@ -116,17 +131,23 @@ export function AgentView() { }, [currentProject?.path]); const handleSend = useCallback(async () => { - if ((!input.trim() && selectedImages.length === 0) || isProcessing) return; + if ( + (!input.trim() && selectedImages.length === 0 && selectedTextFiles.length === 0) || + isProcessing + ) + return; const messageContent = input; const messageImages = selectedImages; + const messageTextFiles = selectedTextFiles; setInput(''); setSelectedImages([]); + setSelectedTextFiles([]); setShowImageDropZone(false); - await sendMessage(messageContent, messageImages); - }, [input, selectedImages, isProcessing, sendMessage]); + await sendMessage(messageContent, messageImages, messageTextFiles); + }, [input, selectedImages, selectedTextFiles, isProcessing, sendMessage]); const handleImagesSelected = useCallback((images: ImageAttachment[]) => { setSelectedImages(images); @@ -136,84 +157,99 @@ export function AgentView() { setShowImageDropZone(!showImageDropZone); }, [showImageDropZone]); - // Helper function to convert file to base64 - const fileToBase64 = useCallback((file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject(new Error('Failed to read file as base64')); - } - }; - reader.onerror = () => reject(new Error('Failed to read file')); - reader.readAsDataURL(file); - }); - }, []); - - // Process dropped files + // Process dropped files (images and text files) const processDroppedFiles = useCallback( async (files: FileList) => { if (isProcessing) return; - const ACCEPTED_IMAGE_TYPES = [ - 'image/jpeg', - 'image/jpg', - 'image/png', - 'image/gif', - 'image/webp', - ]; - const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB - const MAX_FILES = 5; - const newImages: ImageAttachment[] = []; + const newTextFiles: TextFileAttachment[] = []; const errors: string[] = []; for (const file of Array.from(files)) { - // Validate file type - if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { - errors.push(`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`); - continue; - } + // Check if it's a text file + if (isTextFile(file)) { + const validation = validateTextFile(file); + if (!validation.isValid) { + errors.push(validation.error!); + continue; + } - // Validate file size - if (file.size > MAX_FILE_SIZE) { - const maxSizeMB = MAX_FILE_SIZE / (1024 * 1024); - errors.push(`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`); - continue; - } + // Check if we've reached max files + const totalFiles = + newImages.length + + selectedImages.length + + newTextFiles.length + + selectedTextFiles.length; + if (totalFiles >= DEFAULT_MAX_FILES) { + errors.push(`Maximum ${DEFAULT_MAX_FILES} files allowed.`); + break; + } - // Check if we've reached max files - if (newImages.length + selectedImages.length >= MAX_FILES) { - errors.push(`Maximum ${MAX_FILES} images allowed.`); - break; + try { + const content = await fileToText(file); + const textFileAttachment: TextFileAttachment = { + id: generateFileId(), + content, + mimeType: getTextFileMimeType(file.name), + filename: file.name, + size: file.size, + }; + newTextFiles.push(textFileAttachment); + } catch { + errors.push(`${file.name}: Failed to read text file.`); + } } + // Check if it's an image file + else if (isImageFile(file)) { + const validation = validateImageFile(file, DEFAULT_MAX_FILE_SIZE); + if (!validation.isValid) { + errors.push(validation.error!); + continue; + } - try { - const base64 = await fileToBase64(file); - const imageAttachment: ImageAttachment = { - id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - data: base64, - mimeType: file.type, - filename: file.name, - size: file.size, - }; - newImages.push(imageAttachment); - } catch (error) { - errors.push(`${file.name}: Failed to process image.`); + // Check if we've reached max files + const totalFiles = + newImages.length + + selectedImages.length + + newTextFiles.length + + selectedTextFiles.length; + if (totalFiles >= DEFAULT_MAX_FILES) { + errors.push(`Maximum ${DEFAULT_MAX_FILES} files allowed.`); + break; + } + + try { + const base64 = await fileToBase64(file); + const imageAttachment: ImageAttachment = { + id: generateImageId(), + data: base64, + mimeType: file.type, + filename: file.name, + size: file.size, + }; + newImages.push(imageAttachment); + } catch { + errors.push(`${file.name}: Failed to process image.`); + } + } else { + errors.push(`${file.name}: Unsupported file type. Use images, .txt, or .md files.`); } } if (errors.length > 0) { - console.warn('Image upload errors:', errors); + console.warn('File upload errors:', errors); } if (newImages.length > 0) { setSelectedImages((prev) => [...prev, ...newImages]); } + + if (newTextFiles.length > 0) { + setSelectedTextFiles((prev) => [...prev, ...newTextFiles]); + } }, - [isProcessing, selectedImages, fileToBase64] + [isProcessing, selectedImages, selectedTextFiles] ); // Remove individual image @@ -221,6 +257,11 @@ export function AgentView() { setSelectedImages((prev) => prev.filter((img) => img.id !== imageId)); }, []); + // Remove individual text file + const removeTextFile = useCallback((fileId: string) => { + setSelectedTextFiles((prev) => prev.filter((file) => file.id !== fileId)); + }, []); + // Drag and drop handlers for the input area const handleDragEnter = useCallback( (e: React.DragEvent) => { @@ -720,16 +761,19 @@ export function AgentView() { /> )} - {/* Selected Images Preview - only show when ImageDropZone is hidden to avoid duplicate display */} - {selectedImages.length > 0 && !showImageDropZone && ( + {/* Selected Files Preview - only show when ImageDropZone is hidden to avoid duplicate display */} + {(selectedImages.length > 0 || selectedTextFiles.length > 0) && !showImageDropZone && (

- {selectedImages.length} image - {selectedImages.length > 1 ? 's' : ''} attached + {selectedImages.length + selectedTextFiles.length} file + {selectedImages.length + selectedTextFiles.length > 1 ? 's' : ''} attached

+ {/* Image attachments */} {selectedImages.map((image) => (
))} + {/* Text file attachments */} + {selectedTextFiles.map((file) => ( +
+ {/* File icon */} +
+ +
+ {/* File info */} +
+

+ {file.filename} +

+

+ {formatFileSize(file.size)} +

+
+ {/* Remove button */} + +
+ ))}
)} @@ -792,7 +866,7 @@ export function AgentView() { setInput(e.target.value)} @@ -803,14 +877,15 @@ export function AgentView() { className={cn( 'h-11 bg-background border-border rounded-xl pl-4 pr-20 text-sm transition-all', 'focus:ring-2 focus:ring-primary/20 focus:border-primary/50', - selectedImages.length > 0 && 'border-primary/30', + (selectedImages.length > 0 || selectedTextFiles.length > 0) && + 'border-primary/30', isDragOver && 'border-primary bg-primary/5' )} /> - {selectedImages.length > 0 && !isDragOver && ( + {(selectedImages.length > 0 || selectedTextFiles.length > 0) && !isDragOver && (
- {selectedImages.length} image - {selectedImages.length > 1 ? 's' : ''} + {selectedImages.length + selectedTextFiles.length} file + {selectedImages.length + selectedTextFiles.length > 1 ? 's' : ''}
)} {isDragOver && ( @@ -821,7 +896,7 @@ export function AgentView() { )}
- {/* Image Attachment Button */} + {/* File Attachment Button */} @@ -841,7 +917,11 @@ export function AgentView() { - setEnhancementMode("improve")} - > + setEnhancementMode('improve')}> Improve Clarity - setEnhancementMode("technical")} - > + setEnhancementMode('technical')}> Add Technical Details - setEnhancementMode("simplify")} - > + setEnhancementMode('simplify')}> Simplify - setEnhancementMode("acceptance")} - > + setEnhancementMode('acceptance')}> Add Acceptance Criteria @@ -422,9 +399,7 @@ export function AddFeatureDialog({ - setNewFeature({ ...newFeature, category: value }) - } + onChange={(value) => setNewFeature({ ...newFeature, category: value })} suggestions={categorySuggestions} placeholder="e.g., Core, UI, API" data-testid="feature-category-input" @@ -435,9 +410,7 @@ export function AddFeatureDialog({ useCurrentBranch={useCurrentBranch} onUseCurrentBranchChange={setUseCurrentBranch} branchName={newFeature.branchName} - onBranchNameChange={(value) => - setNewFeature({ ...newFeature, branchName: value }) - } + onBranchNameChange={(value) => setNewFeature({ ...newFeature, branchName: value })} branchSuggestions={branchSuggestions} branchCardCounts={branchCardCounts} currentBranch={currentBranch} @@ -448,25 +421,18 @@ export function AddFeatureDialog({ {/* Priority Selector */} - setNewFeature({ ...newFeature, priority }) - } + onPrioritySelect={(priority) => setNewFeature({ ...newFeature, priority })} testIdPrefix="priority" /> {/* Model Tab */} - + {/* Show Advanced Options Toggle */} {showProfilesOnly && (
-

- Simple Mode Active -

+

Simple Mode Active

Only showing AI profiles. Advanced model tweaking is hidden.

@@ -478,7 +444,7 @@ export function AddFeatureDialog({ data-testid="show-advanced-options-toggle" > - {showAdvancedOptions ? "Hide" : "Show"} Advanced + {showAdvancedOptions ? 'Hide' : 'Show'} Advanced
)} @@ -492,23 +458,19 @@ export function AddFeatureDialog({ showManageLink onManageLinkClick={() => { onOpenChange(false); - navigate({ to: "/profiles" }); + navigate({ to: '/profiles' }); }} /> {/* Separator */} - {aiProfiles.length > 0 && - (!showProfilesOnly || showAdvancedOptions) && ( -
- )} + {aiProfiles.length > 0 && (!showProfilesOnly || showAdvancedOptions) && ( +
+ )} {/* Claude Models Section */} {(!showProfilesOnly || showAdvancedOptions) && ( <> - + {newModelAllowsThinking && ( {/* Options Tab */} - + {/* Planning Mode Section */} - setNewFeature({ ...newFeature, skipTests }) - } + onSkipTestsChange={(skipTests) => setNewFeature({ ...newFeature, skipTests })} steps={newFeature.steps} onStepsChange={(steps) => setNewFeature({ ...newFeature, steps })} /> @@ -556,12 +513,10 @@ export function AddFeatureDialog({ Add Feature diff --git a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx index 683f0144..d7f9e5ac 100644 --- a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx @@ -16,6 +16,7 @@ import { CategoryAutocomplete } from '@/components/ui/category-autocomplete'; import { DescriptionImageDropZone, FeatureImagePath as DescriptionImagePath, + FeatureTextFilePath as DescriptionTextFilePath, ImagePreviewMap, } from '@/components/ui/description-image-dropzone'; import { @@ -68,6 +69,7 @@ interface EditFeatureDialogProps { model: AgentModel; thinkingLevel: ThinkingLevel; imagePaths: DescriptionImagePath[]; + textFilePaths: DescriptionTextFilePath[]; branchName: string; // Can be empty string to use current branch priority: number; planningMode: PlanningMode; @@ -168,6 +170,7 @@ export function EditFeatureDialog({ model: selectedModel, thinkingLevel: normalizedThinking, imagePaths: editingFeature.imagePaths ?? [], + textFilePaths: editingFeature.textFilePaths ?? [], branchName: finalBranchName, priority: editingFeature.priority ?? 2, planningMode, @@ -294,6 +297,13 @@ export function EditFeatureDialog({ imagePaths: images, }) } + textFiles={editingFeature.textFilePaths ?? []} + onTextFilesChange={(textFiles) => + setEditingFeature({ + ...editingFeature, + textFilePaths: textFiles, + }) + } placeholder="Describe the feature..." previewMap={editFeaturePreviewMap} onPreviewMapChange={setEditFeaturePreviewMap} diff --git a/apps/ui/src/components/views/context-view.tsx b/apps/ui/src/components/views/context-view.tsx index 434d43f6..93bfcb01 100644 --- a/apps/ui/src/components/views/context-view.tsx +++ b/apps/ui/src/components/views/context-view.tsx @@ -1,11 +1,12 @@ -import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useAppStore } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; +import { getHttpApiClient } from '@/lib/http-api-client'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { HotkeyButton } from '@/components/ui/hotkey-button'; import { Card } from '@/components/ui/card'; import { - Plus, RefreshCw, FileText, Image as ImageIcon, @@ -14,9 +15,12 @@ import { Upload, File, BookOpen, - EditIcon, Eye, Pencil, + FilePlus, + FileUp, + Loader2, + MoreVertical, } from 'lucide-react'; import { useKeyboardShortcuts, @@ -34,13 +38,26 @@ import { import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; +import { sanitizeFilename } from '@/lib/image-utils'; import { Markdown } from '../ui/markdown'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Textarea } from '@/components/ui/textarea'; interface ContextFile { name: string; type: 'text' | 'image'; content?: string; path: string; + description?: string; +} + +interface ContextMetadata { + files: Record; } export function ContextView() { @@ -52,24 +69,44 @@ export function ContextView() { const [isSaving, setIsSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [editedContent, setEditedContent] = useState(''); - const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); const [renameFileName, setRenameFileName] = useState(''); - const [newFileName, setNewFileName] = useState(''); - const [newFileType, setNewFileType] = useState<'text' | 'image'>('text'); - const [uploadedImageData, setUploadedImageData] = useState(null); - const [newFileContent, setNewFileContent] = useState(''); const [isDropHovering, setIsDropHovering] = useState(false); const [isPreviewMode, setIsPreviewMode] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [uploadingFileName, setUploadingFileName] = useState(null); + + // Create Markdown modal state + const [isCreateMarkdownOpen, setIsCreateMarkdownOpen] = useState(false); + const [newMarkdownName, setNewMarkdownName] = useState(''); + const [newMarkdownDescription, setNewMarkdownDescription] = useState(''); + const [newMarkdownContent, setNewMarkdownContent] = useState(''); + + // Track files with generating descriptions (async) + const [generatingDescriptions, setGeneratingDescriptions] = useState>(new Set()); + + // Edit description modal state + const [isEditDescriptionOpen, setIsEditDescriptionOpen] = useState(false); + const [editDescriptionValue, setEditDescriptionValue] = useState(''); + const [editDescriptionFileName, setEditDescriptionFileName] = useState(''); + + // File input ref for import + const fileInputRef = useRef(null); + + // Get images directory path + const getImagesPath = useCallback(() => { + if (!currentProject) return null; + return `${currentProject.path}/.automaker/images`; + }, [currentProject]); // Keyboard shortcuts for this view const contextShortcuts: KeyboardShortcut[] = useMemo( () => [ { key: shortcuts.addContextFile, - action: () => setIsAddDialogOpen(true), - description: 'Add new context file', + action: () => setIsCreateMarkdownOpen(true), + description: 'Create new markdown file', }, ], [shortcuts] @@ -94,6 +131,41 @@ export function ContextView() { return imageExtensions.includes(ext); }; + // Load context metadata + const loadMetadata = useCallback(async (): Promise => { + const contextPath = getContextPath(); + if (!contextPath) return { files: {} }; + + try { + const api = getElectronAPI(); + const metadataPath = `${contextPath}/context-metadata.json`; + const result = await api.readFile(metadataPath); + if (result.success && result.content) { + return JSON.parse(result.content); + } + } catch { + // Metadata file doesn't exist yet + } + return { files: {} }; + }, [getContextPath]); + + // Save context metadata + const saveMetadata = useCallback( + async (metadata: ContextMetadata) => { + const contextPath = getContextPath(); + if (!contextPath) return; + + try { + const api = getElectronAPI(); + const metadataPath = `${contextPath}/context-metadata.json`; + await api.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); + } catch (error) { + console.error('Failed to save metadata:', error); + } + }, + [getContextPath] + ); + // Load context files const loadContextFiles = useCallback(async () => { const contextPath = getContextPath(); @@ -106,15 +178,19 @@ export function ContextView() { // Ensure context directory exists await api.mkdir(contextPath); + // Load metadata for descriptions + const metadata = await loadMetadata(); + // Read directory contents const result = await api.readdir(contextPath); if (result.success && result.entries) { const files: ContextFile[] = result.entries - .filter((entry) => entry.isFile) + .filter((entry) => entry.isFile && entry.name !== 'context-metadata.json') .map((entry) => ({ name: entry.name, type: isImageFile(entry.name) ? 'image' : 'text', path: `${contextPath}/${entry.name}`, + description: metadata.files[entry.name]?.description, })); setContextFiles(files); } @@ -123,7 +199,7 @@ export function ContextView() { } finally { setIsLoading(false); } - }, [getContextPath]); + }, [getContextPath, loadMetadata]); useEffect(() => { loadContextFiles(); @@ -176,43 +252,232 @@ export function ContextView() { setHasChanges(true); }; - // Add new context file - const handleAddFile = async () => { + // Generate description for a file + const generateDescription = async ( + filePath: string, + fileName: string, + isImage: boolean + ): Promise => { + try { + const httpClient = getHttpApiClient(); + const result = isImage + ? await httpClient.context.describeImage(filePath) + : await httpClient.context.describeFile(filePath); + + if (result.success && result.description) { + return result.description; + } + + const message = + result.error || `Automaker couldn't generate a description for “${fileName}”.`; + toast.error('Failed to generate description', { description: message }); + } catch (error) { + console.error('Failed to generate description:', error); + const message = + error instanceof Error + ? error.message + : 'An unexpected error occurred while generating the description.'; + toast.error('Failed to generate description', { description: message }); + } + return undefined; + }; + + // Generate description in background and update metadata + const generateDescriptionAsync = useCallback( + async (filePath: string, fileName: string, isImage: boolean) => { + // Add to generating set + setGeneratingDescriptions((prev) => new Set(prev).add(fileName)); + + try { + const description = await generateDescription(filePath, fileName, isImage); + + if (description) { + const metadata = await loadMetadata(); + metadata.files[fileName] = { description }; + await saveMetadata(metadata); + + // Reload files to update UI with new description + await loadContextFiles(); + } + } catch (error) { + console.error('Failed to generate description:', error); + } finally { + // Remove from generating set + setGeneratingDescriptions((prev) => { + const next = new Set(prev); + next.delete(fileName); + return next; + }); + } + }, + [loadMetadata, saveMetadata, loadContextFiles] + ); + + // Upload a file and generate description asynchronously + const uploadFile = async (file: globalThis.File) => { const contextPath = getContextPath(); - if (!contextPath || !newFileName.trim()) return; + if (!contextPath) return; + + setIsUploading(true); + setUploadingFileName(file.name); try { const api = getElectronAPI(); - let filename = newFileName.trim(); + const isImage = isImageFile(file.name); - // Add default extension if not provided - if (newFileType === 'text' && !filename.includes('.')) { + let filePath: string; + let fileName: string; + let imagePathForDescription: string | undefined; + + if (isImage) { + // For images: sanitize filename, store in .automaker/images + fileName = sanitizeFilename(file.name); + + // Read file as base64 + const dataUrl = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (event) => resolve(event.target?.result as string); + reader.readAsDataURL(file); + }); + + // Extract base64 data without the data URL prefix + const base64Data = dataUrl.split(',')[1] || dataUrl; + + // Determine mime type from original file + const mimeType = file.type || 'image/png'; + + // Use saveImageToTemp to properly save as binary file in .automaker/images + const saveResult = await api.saveImageToTemp?.( + base64Data, + fileName, + mimeType, + currentProject!.path + ); + + if (!saveResult?.success || !saveResult.path) { + throw new Error(saveResult?.error || 'Failed to save image'); + } + + // The saved image path is used for description + imagePathForDescription = saveResult.path; + + // Also save to context directory for display in the UI + // (as a data URL for inline display) + filePath = `${contextPath}/${fileName}`; + await api.writeFile(filePath, dataUrl); + } else { + // For non-images: keep original behavior + fileName = file.name; + filePath = `${contextPath}/${fileName}`; + + const content = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (event) => resolve(event.target?.result as string); + reader.readAsText(file); + }); + + await api.writeFile(filePath, content); + } + + // Reload files immediately (file appears in list without description) + await loadContextFiles(); + + // Start description generation in background (don't await) + // For images, use the path in the images directory + generateDescriptionAsync(imagePathForDescription || filePath, fileName, isImage); + } catch (error) { + console.error('Failed to upload file:', error); + toast.error('Failed to upload file', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsUploading(false); + setUploadingFileName(null); + } + }; + + // Handle file drop + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropHovering(false); + + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) return; + + // Process files sequentially + for (const file of files) { + await uploadFile(file); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropHovering(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDropHovering(false); + }; + + // Handle file import via button + const handleImportClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileInputChange = async (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + for (const file of Array.from(files)) { + await uploadFile(file); + } + + // Reset input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + // Handle create markdown + const handleCreateMarkdown = async () => { + const contextPath = getContextPath(); + if (!contextPath || !newMarkdownName.trim()) return; + + try { + const api = getElectronAPI(); + let filename = newMarkdownName.trim(); + + // Add .md extension if not provided + if (!filename.includes('.')) { filename += '.md'; } const filePath = `${contextPath}/${filename}`; - if (newFileType === 'image' && uploadedImageData) { - // Write image data - await api.writeFile(filePath, uploadedImageData); - } else { - // Write text file with content (or empty if no content) - await api.writeFile(filePath, newFileContent); + // Write markdown file + await api.writeFile(filePath, newMarkdownContent); + + // Save description if provided + if (newMarkdownDescription.trim()) { + const metadata = await loadMetadata(); + metadata.files[filename] = { description: newMarkdownDescription.trim() }; + await saveMetadata(metadata); } - // Only reload files on success + // Reload files await loadContextFiles(); + + // Reset and close modal + setIsCreateMarkdownOpen(false); + setNewMarkdownName(''); + setNewMarkdownDescription(''); + setNewMarkdownContent(''); } catch (error) { - console.error('Failed to add file:', error); - // Optionally show error toast to user here - } finally { - // Close dialog and reset state - setIsAddDialogOpen(false); - setNewFileName(''); - setNewFileType('text'); - setUploadedImageData(null); - setNewFileContent(''); - setIsDropHovering(false); + console.error('Failed to create markdown:', error); } }; @@ -224,6 +489,11 @@ export function ContextView() { const api = getElectronAPI(); await api.deleteFile(selectedFile.path); + // Remove from metadata + const metadata = await loadMetadata(); + delete metadata.files[selectedFile.name]; + await saveMetadata(metadata); + setIsDeleteDialogOpen(false); setSelectedFile(null); setEditedContent(''); @@ -269,6 +539,14 @@ export function ContextView() { // Delete old file await api.deleteFile(selectedFile.path); + // Update metadata + const metadata = await loadMetadata(); + if (metadata.files[selectedFile.name]) { + metadata.files[newName] = metadata.files[selectedFile.name]; + delete metadata.files[selectedFile.name]; + await saveMetadata(metadata); + } + setIsRenameDialogOpen(false); setRenameFileName(''); @@ -281,6 +559,7 @@ export function ContextView() { type: isImageFile(newName) ? 'image' : 'text', path: newPath, content: result.content, + description: metadata.files[newName]?.description, }; setSelectedFile(renamedFile); } catch (error) { @@ -288,98 +567,60 @@ export function ContextView() { } }; - // Handle image upload - const handleImageUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; + // Save edited description + const handleSaveDescription = async () => { + if (!editDescriptionFileName) return; - const reader = new FileReader(); - reader.onload = (event) => { - const base64 = event.target?.result as string; - setUploadedImageData(base64); - if (!newFileName) { - setNewFileName(file.name); + try { + const metadata = await loadMetadata(); + metadata.files[editDescriptionFileName] = { description: editDescriptionValue.trim() }; + await saveMetadata(metadata); + + // Update selected file if it's the one being edited + if (selectedFile?.name === editDescriptionFileName) { + setSelectedFile({ ...selectedFile, description: editDescriptionValue.trim() }); } - }; - reader.readAsDataURL(file); - }; - // Handle drag and drop for file upload - const handleDrop = async (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); + // Reload files to update list + await loadContextFiles(); - const files = Array.from(e.dataTransfer.files); - if (files.length === 0) return; - - const contextPath = getContextPath(); - if (!contextPath) return; - - const api = getElectronAPI(); - - for (const file of files) { - const reader = new FileReader(); - reader.onload = async (event) => { - const content = event.target?.result as string; - const filePath = `${contextPath}/${file.name}`; - await api.writeFile(filePath, content); - await loadContextFiles(); - }; - - if (isImageFile(file.name)) { - reader.readAsDataURL(file); - } else { - reader.readAsText(file); - } + setIsEditDescriptionOpen(false); + setEditDescriptionValue(''); + setEditDescriptionFileName(''); + } catch (error) { + console.error('Failed to save description:', error); } }; - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); + // Open edit description dialog + const handleEditDescription = (file: ContextFile) => { + setEditDescriptionFileName(file.name); + setEditDescriptionValue(file.description || ''); + setIsEditDescriptionOpen(true); }; - // Handle drag and drop for .txt and .md files in the add context dialog textarea - const handleTextAreaDrop = async (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDropHovering(false); + // Delete file from list (used by dropdown) + const handleDeleteFromList = async (file: ContextFile) => { + try { + const api = getElectronAPI(); + await api.deleteFile(file.path); - const files = Array.from(e.dataTransfer.files); - if (files.length === 0) return; + // Remove from metadata + const metadata = await loadMetadata(); + delete metadata.files[file.name]; + await saveMetadata(metadata); - const file = files[0]; // Only handle the first file - const fileName = file.name.toLowerCase(); - - // Only accept .txt and .md files - if (!fileName.endsWith('.txt') && !fileName.endsWith('.md')) { - console.warn('Only .txt and .md files are supported for drag and drop'); - return; - } - - const reader = new FileReader(); - reader.onload = (event) => { - const content = event.target?.result as string; - setNewFileContent(content); - - // Auto-fill filename if empty - if (!newFileName) { - setNewFileName(file.name); + // Clear selection if this was the selected file + if (selectedFile?.path === file.path) { + setSelectedFile(null); + setEditedContent(''); + setHasChanges(false); } - }; - reader.readAsText(file); - }; - const handleTextAreaDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDropHovering(true); - }; - - const handleTextAreaDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDropHovering(false); + await loadContextFiles(); + } catch (error) { + console.error('Failed to delete file:', error); + } }; if (!currentProject) { @@ -403,6 +644,16 @@ export function ContextView() { return (
+ {/* Hidden file input for import */} + + {/* Header */}
@@ -415,26 +666,63 @@ export function ContextView() {
+ setIsAddDialogOpen(true)} + onClick={() => setIsCreateMarkdownOpen(true)} hotkey={shortcuts.addContextFile} hotkeyActive={false} - data-testid="add-context-file" + data-testid="create-markdown-button" > - - Add File + + Create Markdown
{/* Main content area with file list and editor */}
+ {/* Drop overlay */} + {isDropHovering && ( +
+
+ + Drop files to upload + + Files will be analyzed automatically + +
+
+ )} + + {/* Uploading overlay */} + {isUploading && ( +
+
+ + Uploading {uploadingFileName}... +
+
+ )} + {/* Left Panel - File List */}
@@ -449,47 +737,82 @@ export function ContextView() {

No context files yet.
- Drop files here or click Add File. + Drop files here or use the buttons above.

) : (
- {contextFiles.map((file) => ( -
- - -
- ))} + + + + + + + { + setRenameFileName(file.name); + setSelectedFile(file); + setIsRenameDialogOpen(true); + }} + data-testid={`rename-context-file-${file.name}`} + > + + Rename + + handleDeleteFromList(file)} + className="text-red-500 focus:text-red-500" + data-testid={`delete-context-file-${file.name}`} + > + + Delete + + + +
+ ); + })}
)}
@@ -501,13 +824,13 @@ export function ContextView() { <> {/* File toolbar */}
-
+
{selectedFile.type === 'image' ? ( - + ) : ( - + )} - {selectedFile.name} + {selectedFile.name}
{selectedFile.type === 'text' && isMarkdownFile(selectedFile.name) && ( @@ -519,7 +842,7 @@ export function ContextView() { > {isPreviewMode ? ( <> - + Edit ) : ( @@ -553,8 +876,42 @@ export function ContextView() {
+ {/* Description section */} +
+
+
+
+ + Description + + {generatingDescriptions.has(selectedFile.name) ? ( +
+ + Generating description with AI... +
+ ) : selectedFile.description ? ( +

{selectedFile.description}

+ ) : ( +

+ No description. Click edit to add one. +

+ )} +
+ +
+
+
+ {/* Content area */} -
+
{selectedFile.type === 'image' ? (
- {/* Add File Dialog */} - + {/* Create Markdown Dialog */} + - Add Context File - Add a new text or image file to the context. + Create Markdown Context + + Create a new markdown file to add context for AI prompts. + -
-
- - -
- +
- + setNewFileName(e.target.value)} - placeholder={newFileType === 'text' ? 'context.md' : 'image.png'} - data-testid="new-file-name" + id="markdown-filename" + value={newMarkdownName} + onChange={(e) => setNewMarkdownName(e.target.value)} + placeholder="context-file.md" + data-testid="new-markdown-name" />
- {newFileType === 'text' && ( -
- -
-