diff --git a/package-lock.json b/package-lock.json index 1522dcaa6..2afea02e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -135,6 +135,46 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.23.tgz", + "integrity": "sha512-DktXOjzS2hOuuj2Zpo7fEooONfMa5bm09pt1/Vt4vn30YugELoezn/ZQ/TG5uSQ7+Zl/ElxAvi4vGDorj1Tirg==", + "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-win32-x64": "^0.33.5" + }, + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "node_modules/@anthropic-ai/claude-code": { + "version": "2.0.24", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.0.24.tgz", + "integrity": "sha512-6f/AXoTi3SmFYZl42l6L8brdPSkL+MDQWesRBJwgZy3enNI0LaVn1j/6RxQ7toPKnIyChCN0r6hZi61N8znzzQ==", + "license": "SEE LICENSE IN README.md", + "bin": { + "claude": "cli.js" + }, + "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-win32-x64": "^0.33.5" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -4076,6 +4116,215 @@ "url": "https://github.com/sponsors/nzakas" } }, + "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-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-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/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -21087,6 +21336,7 @@ "dependencies": { "@agentclientprotocol/sdk": "^0.4.9", "@ai-sdk/openai": "^2.0.52", + "@boudra/claude-code-acp": "^0.8.0", "@deepgram/sdk": "^3.4.0", "@modelcontextprotocol/sdk": "^1.20.1", "@openrouter/ai-sdk-provider": "^1.2.0", @@ -21112,6 +21362,297 @@ "typescript": "^5.2.2", "vitest": "^3.2.4" } + }, + "packages/server/node_modules/@boudra/claude-code-acp": { + "version": "0.8.0", + "resolved": "https://npm.pkg.github.com/download/@boudra/claude-code-acp/0.8.0/b67fdb37b189c68b9b155e421b2e03a061069239", + "integrity": "sha512-JdCzu8yp/I/D50FKJgT05KJkHK8gMF6CT3tDu9CdHcMbLcizTg/BZbwSXzlPPx9Po6uzttTykLkyexzAQR23EQ==", + "license": "Apache-2.0", + "dependencies": { + "@agentclientprotocol/sdk": "0.4.9", + "@anthropic-ai/claude-agent-sdk": "0.1.23", + "@anthropic-ai/claude-code": "2.0.24", + "@modelcontextprotocol/sdk": "1.20.1", + "diff": "8.0.2", + "express": "5.1.0", + "uuid": "13.0.0" + }, + "bin": { + "claude-code-acp": "dist/index.js" + } + }, + "packages/server/node_modules/@boudra/claude-code-acp/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.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" + } + }, + "packages/server/node_modules/@boudra/claude-code-acp/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "packages/server/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" + } + }, + "packages/server/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/server/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/server/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" + } + }, + "packages/server/node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "packages/server/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "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": ">= 0.8" + } + }, + "packages/server/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" + } + }, + "packages/server/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "packages/server/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" + } + }, + "packages/server/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" + } + }, + "packages/server/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" + } + }, + "packages/server/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/server/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" + } + }, + "packages/server/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" + } + }, + "packages/server/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" + } + }, + "packages/server/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" + } + }, + "packages/server/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" + } } } } diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx new file mode 100644 index 000000000..9a7ecd4e0 --- /dev/null +++ b/packages/app/src/components/agent-status-bar.tsx @@ -0,0 +1,145 @@ +import { View, Text, Pressable } from "react-native"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { ChevronDown } from "lucide-react-native"; +import { useSession } from "@/contexts/session-context"; +import { useState } from "react"; +import { ModeSelectorModal } from "./mode-selector-modal"; + +interface AgentStatusBarProps { + agentId: string; +} + +export function AgentStatusBar({ agentId }: AgentStatusBarProps) { + const { theme } = useUnistyles(); + const { agents, setAgentMode } = useSession(); + const [showModeSelector, setShowModeSelector] = useState(false); + + const agent = agents.get(agentId); + + if (!agent) { + return null; + } + + function handleModeChange(modeId: string) { + setAgentMode(agentId, modeId); + } + + function getStatusLabel(status: string): string { + switch (status) { + case "ready": + return "Ready"; + case "processing": + return "Working"; + case "completed": + return "Completed"; + case "failed": + return "Failed"; + case "killed": + return "Stopped"; + default: + return status; + } + } + + function getStatusColor(status: string): string { + switch (status) { + case "ready": + return theme.colors.palette.green[500]; + case "processing": + return theme.colors.palette.blue[500]; + case "completed": + return theme.colors.palette.gray[500]; + case "failed": + return theme.colors.palette.red[500]; + case "killed": + return theme.colors.palette.orange[500]; + default: + return theme.colors.mutedForeground; + } + } + + return ( + + {/* Agent Mode Badge */} + {agent.availableModes && agent.availableModes.length > 0 && ( + setShowModeSelector(true)} + style={({ pressed }) => [ + styles.modeBadge, + pressed && styles.modeBadgePressed, + ]} + > + + {agent.availableModes?.find((m) => m.id === agent.currentModeId)?.name || + agent.currentModeId || + "default"} + + + + )} + + {/* Agent Status Indicator */} + + + {getStatusLabel(agent.status)} + + + {/* Mode selector modal */} + setShowModeSelector(false)} + /> + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[3], + paddingHorizontal: theme.spacing[4], + paddingTop: theme.spacing[2], + paddingBottom: theme.spacing[1], + }, + modeBadge: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + backgroundColor: theme.colors.muted, + borderRadius: theme.borderRadius.full, + }, + modeBadgePressed: { + backgroundColor: theme.colors.accent, + }, + modeBadgeText: { + color: theme.colors.mutedForeground, + fontSize: theme.fontSize.xs, + fontWeight: theme.fontWeight.semibold, + textTransform: "capitalize", + }, + statusIndicator: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, + statusDot: { + width: 8, + height: 8, + borderRadius: 4, + }, + statusText: { + color: theme.colors.mutedForeground, + fontSize: theme.fontSize.xs, + fontWeight: theme.fontWeight.semibold, + }, +})); diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 3e0a0a044..bcc77ba12 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -45,7 +45,14 @@ export function AgentStreamView({ function handleOpenToolCallDetails(toolCall: SelectedToolCall) { setSelectedToolCall(toolCall); - bottomSheetRef.current?.present(); + // Delay present to next frame to ensure component is mounted + setTimeout(() => { + bottomSheetRef.current?.present(); + }, 0); + } + + function handleBottomSheetDismiss() { + setSelectedToolCall(null); } return ( @@ -171,6 +178,7 @@ export function AgentStreamView({ ); diff --git a/packages/app/src/components/global-footer.tsx b/packages/app/src/components/global-footer.tsx index 0a774a337..f4f2b8632 100644 --- a/packages/app/src/components/global-footer.tsx +++ b/packages/app/src/components/global-footer.tsx @@ -72,6 +72,9 @@ const styles = StyleSheet.create((theme) => ({ borderTopWidth: theme.borderWidth[1], borderTopColor: theme.colors.border, }, + realtimeContainer: { + backgroundColor: theme.colors.background, + }, centeredButtonContainer: { padding: theme.spacing[6], alignItems: "center", diff --git a/packages/app/src/components/orchestrator-messages-view.tsx b/packages/app/src/components/orchestrator-messages-view.tsx index 8cf6da8e6..e8c9a8ba9 100644 --- a/packages/app/src/components/orchestrator-messages-view.tsx +++ b/packages/app/src/components/orchestrator-messages-view.tsx @@ -23,6 +23,7 @@ export const OrchestratorMessagesView = forwardRef(null); const [selectedToolCall, setSelectedToolCall] = useState(null); + const [isBottomSheetVisible, setIsBottomSheetVisible] = useState(false); function handleOpenToolCallDetails(toolCall: { toolName: string; @@ -45,7 +46,16 @@ export const OrchestratorMessagesView = forwardRef { + bottomSheetRef.current?.present(); + }, 0); + } + + function handleBottomSheetDismiss() { + setIsBottomSheetVisible(false); + setSelectedToolCall(null); } return ( @@ -139,10 +149,13 @@ export const OrchestratorMessagesView = forwardRef - + {isBottomSheetVisible && ( + + )} ); } diff --git a/packages/app/src/components/realtime-controls.tsx b/packages/app/src/components/realtime-controls.tsx index 8c45adf14..82fe099a4 100644 --- a/packages/app/src/components/realtime-controls.tsx +++ b/packages/app/src/components/realtime-controls.tsx @@ -31,13 +31,8 @@ export function RealtimeControls() { isMuted={isMuted} isDetecting={isDetecting} isSpeaking={isSpeaking} + orientation="horizontal" /> - {/* Debug timer */} - {(isDetecting || isSpeaking) && ( - - {(segmentDuration / 1000).toFixed(1)}s - - )} {/* Mute button */} @@ -71,20 +66,23 @@ export function RealtimeControls() { const styles = StyleSheet.create((theme) => ({ container: { - minHeight: 200, - padding: theme.spacing[4], + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[4], + height: 88, }, volumeContainer: { flex: 1, justifyContent: "center", - alignItems: "center", + alignItems: "flex-start", }, buttons: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: theme.spacing[3], - paddingTop: theme.spacing[4], }, muteButton: { width: 48, @@ -108,10 +106,4 @@ const styles = StyleSheet.create((theme) => ({ justifyContent: "center", backgroundColor: theme.colors.palette.red[600], }, - debugTimer: { - marginTop: theme.spacing[2], - color: theme.colors.mutedForeground, - fontSize: theme.fontSize.sm, - fontFamily: "monospace", - }, })); diff --git a/packages/app/src/components/tool-call-bottom-sheet.tsx b/packages/app/src/components/tool-call-bottom-sheet.tsx index fc2cea89d..5aaa2f410 100644 --- a/packages/app/src/components/tool-call-bottom-sheet.tsx +++ b/packages/app/src/components/tool-call-bottom-sheet.tsx @@ -15,11 +15,13 @@ import type { SelectedToolCall } from "@/types/shared"; interface ToolCallBottomSheetProps { bottomSheetRef: React.RefObject; selectedToolCall: SelectedToolCall | null; + onDismiss: () => void; } export function ToolCallBottomSheet({ bottomSheetRef, selectedToolCall, + onDismiss, }: ToolCallBottomSheetProps) { const insets = useSafeAreaInsets(); const snapPoints = useMemo(() => ["80%"], []); @@ -88,6 +90,7 @@ export function ToolCallBottomSheet({ handleIndicatorStyle={styles.handleIndicator} backgroundStyle={styles.background} topInset={insets.top} + onDismiss={onDismiss} > {/* Header */} diff --git a/packages/app/src/components/volume-meter.tsx b/packages/app/src/components/volume-meter.tsx index b2481f580..2023d3c29 100644 --- a/packages/app/src/components/volume-meter.tsx +++ b/packages/app/src/components/volume-meter.tsx @@ -16,15 +16,16 @@ interface VolumeMeterProps { isMuted?: boolean; isDetecting?: boolean; isSpeaking?: boolean; + orientation?: "vertical" | "horizontal"; } -export function VolumeMeter({ volume, isMuted = false, isDetecting = false, isSpeaking = false }: VolumeMeterProps) { +export function VolumeMeter({ volume, isMuted = false, isDetecting = false, isSpeaking = false, orientation = "vertical" }: VolumeMeterProps) { const { theme } = useUnistyles(); // Base dimensions const LINE_SPACING = 8; - const MAX_HEIGHT = 50; - const MIN_HEIGHT = 20; + const MAX_HEIGHT = orientation === "horizontal" ? 30 : 50; + const MIN_HEIGHT = orientation === "horizontal" ? 12 : 20; // Shared values for each line's height const line1Height = useSharedValue(MIN_HEIGHT); @@ -162,8 +163,10 @@ export function VolumeMeter({ volume, isMuted = false, isDetecting = false, isSp const lineColor = "#FFFFFF"; const lineWidth = 8; + const containerHeight = orientation === "horizontal" ? 60 : 100; + return ( - + ({ flexDirection: "row", alignItems: "center", justifyContent: "center", - height: 100, }, line: { borderRadius: theme.borderRadius.full, diff --git a/packages/server/agents.json b/packages/server/agents.json new file mode 100644 index 000000000..bd7a6e23d --- /dev/null +++ b/packages/server/agents.json @@ -0,0 +1,9 @@ +[ + { + "id": "bcc0019c-9293-4235-adb7-921f8e7d67eb", + "title": "Fix Real-time Footer Spacing", + "sessionId": "019a1575-706b-73fc-8955-d2c7b524e707", + "createdAt": "2025-10-24T09:03:28.577Z", + "cwd": "/Users/moboudra/dev/voice-dev" + } +] \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json index 141387ba2..acb308faf 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,6 +18,7 @@ "dependencies": { "@agentclientprotocol/sdk": "^0.4.9", "@ai-sdk/openai": "^2.0.52", + "@boudra/claude-code-acp": "^0.8.0", "@deepgram/sdk": "^3.4.0", "@modelcontextprotocol/sdk": "^1.20.1", "@openrouter/ai-sdk-provider": "^1.2.0", diff --git a/packages/server/src/server/acp/agent-manager.ts b/packages/server/src/server/acp/agent-manager.ts index ef0d5eeb7..27b1ff1f2 100644 --- a/packages/server/src/server/acp/agent-manager.ts +++ b/packages/server/src/server/acp/agent-manager.ts @@ -26,6 +26,7 @@ import type { EnrichedSessionNotification, EnrichedSessionUpdate, } from "./types.js"; +import { AgentPersistence, type PersistedAgent } from "./agent-persistence.js"; interface PendingPermission { requestId: string; @@ -107,6 +108,153 @@ class ACPClient implements Client { */ export class AgentManager { private agents = new Map(); + private persistence = new AgentPersistence(); + + /** + * Initialize the agent manager and load persisted agents + */ + async initialize(): Promise { + console.log("[AgentManager] Initializing and loading persisted agents..."); + const persistedAgents = await this.persistence.load(); + + for (const persistedAgent of persistedAgents) { + try { + console.log(`[AgentManager] Resuming agent ${persistedAgent.id} with session ${persistedAgent.sessionId}`); + await this.resumeAgent(persistedAgent); + } catch (error) { + console.error(`[AgentManager] Failed to resume agent ${persistedAgent.id}:`, error); + // Remove failed agent from persistence + await this.persistence.remove(persistedAgent.id); + } + } + + console.log(`[AgentManager] Loaded ${this.agents.size} agents`); + } + + /** + * Resume an existing agent from persisted data + */ + async resumeAgent(persisted: PersistedAgent): Promise { + const agentId = persisted.id; + const cwd = expandTilde(persisted.cwd); + + // Validate that the working directory exists + try { + await access(cwd, constants.R_OK | constants.X_OK); + } catch (error) { + throw new Error( + `Working directory does not exist or is not accessible: ${cwd}` + ); + } + + // Spawn the ACP process without any special flags + const agentProcess = spawn("npx", ["@boudra/claude-code-acp"], { + stdio: ["pipe", "pipe", "pipe"], + cwd, + }); + + // Create streams for ACP communication + const input = Writable.toWeb(agentProcess.stdin); + const output = Readable.toWeb(agentProcess.stdout); + const stream = ndJsonStream(input, output); + + // Create the ACP client and connection + const client = new ACPClient( + agentId, + (id, update) => { + this.handleSessionNotification(id, update); + }, + (id, params) => { + return this.handlePermissionRequest(id, params); + } + ); + const connection = new ClientSideConnection(() => client, stream); + + // Create the managed agent record + const agent: ManagedAgent = { + id: agentId, + status: "initializing", + createdAt: new Date(persisted.createdAt), + process: agentProcess, + connection, + subscribers: new Set(), + updates: [], + cwd, + title: persisted.title, + titleGenerationTriggered: true, // Already has a title + pendingPermissions: new Map(), + currentAssistantMessageId: null, + currentThoughtId: null, + sessionId: persisted.sessionId, + }; + + this.agents.set(agentId, agent); + + // Handle process errors + agentProcess.on("error", (error) => { + this.handleAgentError(agentId, `Process error: ${error.message}`); + }); + + agentProcess.on("exit", (code, signal) => { + const agent = this.agents.get(agentId); + if (!agent) return; + + if (agent.status !== "completed" && agent.status !== "killed") { + this.handleAgentError( + agentId, + `Process exited unexpectedly: code=${code}, signal=${signal}` + ); + } + }); + + // Capture stderr for debugging + agentProcess.stderr.on("data", (data) => { + console.error(`[Agent ${agentId}] stderr:`, data.toString()); + }); + + try { + // Initialize the connection + await agent.connection.initialize({ + protocolVersion: PROTOCOL_VERSION, + clientCapabilities: { + fs: { + readTextFile: true, + writeTextFile: true, + }, + }, + }); + + // Use the loadSession API to resume the existing session + console.log(`[Agent ${agentId}] Loading session ${persisted.sessionId} via loadSession API`); + const sessionResponse = await agent.connection.loadSession({ + sessionId: persisted.sessionId, + cwd, + mcpServers: [], + }); + + console.log(`[Agent ${agentId}] Session loaded:`, JSON.stringify(sessionResponse, null, 2)); + + // Store session modes from response + if (sessionResponse.modes) { + agent.currentModeId = sessionResponse.modes.currentModeId; + agent.availableModes = sessionResponse.modes.availableModes; + console.log( + `[Agent ${agentId}] Session loaded with mode: ${agent.currentModeId}`, + `Available modes:`, agent.availableModes?.map(m => m.id).join(', ') + ); + } + + agent.status = "ready"; + + return agentId; + } catch (error) { + this.handleAgentError( + agentId, + `Resume failed: ${error instanceof Error ? error.message : String(error)}` + ); + throw error; + } + } /** * Create a new agent @@ -126,7 +274,7 @@ export class AgentManager { } // Spawn the ACP process - const agentProcess = spawn("npx", ["@zed-industries/claude-code-acp"], { + const agentProcess = spawn("npx", ["@boudra/claude-code-acp"], { stdio: ["pipe", "pipe", "pipe"], cwd, }); @@ -231,6 +379,17 @@ export class AgentManager { agent.status = "ready"; + // Persist the new agent + if (agent.sessionId) { + await this.persistence.upsert({ + id: agentId, + title: agent.title || `Agent ${agentId.slice(0, 8)}`, + sessionId: agent.sessionId, + createdAt: agent.createdAt.toISOString(), + cwd: agent.cwd, + }); + } + // If an initial prompt was provided, send it if (options.initialPrompt) { console.log(`[Agent ${agentId}] Sending initial prompt`); @@ -433,6 +592,9 @@ export class AgentManager { // This ensures subscribers can still query agent info this.notifySubscribers(agentId); + // Remove from persistence + await this.persistence.remove(agentId); + // Kill the process agent.process.kill("SIGTERM"); @@ -717,7 +879,7 @@ export class AgentManager { /** * Set the title for an agent */ - setAgentTitle(agentId: string, title: string): void { + async setAgentTitle(agentId: string, title: string): Promise { const agent = this.agents.get(agentId); if (!agent) { throw new Error(`Agent ${agentId} not found`); @@ -725,6 +887,11 @@ export class AgentManager { agent.title = title; console.log(`[Agent ${agentId}] Title set to: "${title}"`); + + // Persist the title update + if (agent.sessionId) { + await this.persistence.updateTitle(agentId, title); + } } /** diff --git a/packages/server/src/server/acp/agent-persistence.ts b/packages/server/src/server/acp/agent-persistence.ts new file mode 100644 index 000000000..c5001ecfc --- /dev/null +++ b/packages/server/src/server/acp/agent-persistence.ts @@ -0,0 +1,100 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export interface PersistedAgent { + id: string; + title: string; + sessionId: string; + createdAt: string; + cwd: string; +} + +export class AgentPersistence { + private persistencePath: string; + + constructor() { + // Store agents.json in the project root + this.persistencePath = path.join(__dirname, "../../../agents.json"); + } + + /** + * Load all persisted agents + */ + async load(): Promise { + try { + const data = await fs.readFile(this.persistencePath, "utf-8"); + return JSON.parse(data); + } catch (error) { + // File doesn't exist or is invalid, return empty array + console.log("[AgentPersistence] No existing agents found or file is invalid"); + return []; + } + } + + /** + * Save agents to disk + */ + async save(agents: PersistedAgent[]): Promise { + try { + await fs.writeFile( + this.persistencePath, + JSON.stringify(agents, null, 2), + "utf-8" + ); + console.log(`[AgentPersistence] Saved ${agents.length} agents to disk`); + } catch (error) { + console.error("[AgentPersistence] Failed to save agents:", error); + throw error; + } + } + + /** + * Add or update an agent + */ + async upsert(agent: PersistedAgent): Promise { + const agents = await this.load(); + const existingIndex = agents.findIndex((a) => a.id === agent.id); + + if (existingIndex >= 0) { + agents[existingIndex] = agent; + console.log(`[AgentPersistence] Updated agent ${agent.id}`); + } else { + agents.push(agent); + console.log(`[AgentPersistence] Added new agent ${agent.id}`); + } + + await this.save(agents); + } + + /** + * Remove an agent + */ + async remove(agentId: string): Promise { + const agents = await this.load(); + const filtered = agents.filter((a) => a.id !== agentId); + + if (filtered.length < agents.length) { + await this.save(filtered); + console.log(`[AgentPersistence] Removed agent ${agentId}`); + } + } + + /** + * Update agent title + */ + async updateTitle(agentId: string, title: string): Promise { + const agents = await this.load(); + const agent = agents.find((a) => a.id === agentId); + + if (agent) { + agent.title = title; + await this.save(agents); + console.log(`[AgentPersistence] Updated title for agent ${agentId}: "${title}"`); + } + } +} \ No newline at end of file diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index 91864a3ef..9543fcf39 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -60,7 +60,7 @@ function createServer() { return app; } -function main() { +async function main() { const port = parseInt(process.env.PORT || "6767", 10); const app = createServer(); @@ -68,7 +68,8 @@ function main() { // Initialize global agent manager const agentManager = new AgentManager(); - console.log("✓ Global agent manager initialized"); + await agentManager.initialize(); // Load persisted agents + console.log("✓ Global agent manager initialized with persisted agents"); // Initialize WebSocket server const wsServer = new VoiceAssistantWebSocketServer(httpServer, agentManager);