mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Add ability to attach images to messages
This commit is contained in:
535
package-lock.json
generated
535
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-dev",
|
"name": "claude-dev",
|
||||||
"version": "1.0.91",
|
"version": "1.0.95",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-dev",
|
"name": "claude-dev",
|
||||||
"version": "1.0.91",
|
"version": "1.0.95",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"os-name": "^6.0.0",
|
"os-name": "^6.0.0",
|
||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
|
"sharp": "^0.33.4",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"web-tree-sitter": "^0.22.6"
|
"web-tree-sitter": "^0.22.6"
|
||||||
@@ -2146,6 +2147,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime/node_modules/tslib": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
@@ -2683,6 +2699,437 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.26",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.26",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"macos": ">=11",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"macos": ">=10.13",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.28",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.26",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.28",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.26",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"musl": ">=1.2.2",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"musl": ">=1.2.2",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.28",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.26",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.31",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"glibc": ">=2.26",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"musl": ">=1.2.2",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"musl": ">=1.2.2",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||||
|
"npm": ">=9.6.5",
|
||||||
|
"pnpm": ">=7.1.0",
|
||||||
|
"yarn": ">=3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -5343,11 +5790,22 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
|
"color-string": "^1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
@@ -5360,9 +5818,17 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/color-string": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.0.0",
|
||||||
|
"simple-swizzle": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -5570,6 +6036,14 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
|
||||||
@@ -8854,7 +9328,6 @@
|
|||||||
"version": "7.6.2",
|
"version": "7.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||||
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -8941,6 +9414,45 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.33.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz",
|
||||||
|
"integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"detect-libc": "^2.0.3",
|
||||||
|
"semver": "^7.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"libvips": ">=8.15.2",
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.33.4",
|
||||||
|
"@img/sharp-darwin-x64": "0.33.4",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.0.2",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.0.2",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.0.2",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.0.2",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.0.2",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.0.2",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.0.2",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.0.2",
|
||||||
|
"@img/sharp-linux-arm": "0.33.4",
|
||||||
|
"@img/sharp-linux-arm64": "0.33.4",
|
||||||
|
"@img/sharp-linux-s390x": "0.33.4",
|
||||||
|
"@img/sharp-linux-x64": "0.33.4",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.33.4",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.33.4",
|
||||||
|
"@img/sharp-wasm32": "0.33.4",
|
||||||
|
"@img/sharp-win32-ia32": "0.33.4",
|
||||||
|
"@img/sharp-win32-x64": "0.33.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -9003,6 +9515,19 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-swizzle": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||||
|
|||||||
@@ -135,8 +135,9 @@
|
|||||||
"os-name": "^6.0.0",
|
"os-name": "^6.0.0",
|
||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
|
"sharp": "^0.33.4",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"web-tree-sitter": "^0.22.6"
|
"web-tree-sitter": "^0.22.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
176
src/ClaudeDev.ts
176
src/ClaudeDev.ts
@@ -56,6 +56,7 @@ RULES
|
|||||||
- NEVER end completion_attempt with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
- NEVER end completion_attempt with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||||
- NEVER start your responses with affirmations like "Certaintly", "Okay", "Sure", "Great", etc. You should NOT be conversational in your responses, but rather direct and to the point.
|
- NEVER start your responses with affirmations like "Certaintly", "Okay", "Sure", "Great", etc. You should NOT be conversational in your responses, but rather direct and to the point.
|
||||||
- Feel free to use markdown as much as you'd like in your responses. When using code blocks, always include a language specifier.
|
- Feel free to use markdown as much as you'd like in your responses. When using code blocks, always include a language specifier.
|
||||||
|
- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task.
|
||||||
|
|
||||||
====
|
====
|
||||||
|
|
||||||
@@ -229,6 +230,8 @@ const tools: Tool[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||||
|
|
||||||
export class ClaudeDev {
|
export class ClaudeDev {
|
||||||
private api: ApiHandler
|
private api: ApiHandler
|
||||||
private maxRequestsPerTask: number
|
private maxRequestsPerTask: number
|
||||||
@@ -237,6 +240,7 @@ export class ClaudeDev {
|
|||||||
claudeMessages: ClaudeMessage[] = []
|
claudeMessages: ClaudeMessage[] = []
|
||||||
private askResponse?: ClaudeAskResponse
|
private askResponse?: ClaudeAskResponse
|
||||||
private askResponseText?: string
|
private askResponseText?: string
|
||||||
|
private askResponseImages?: string[]
|
||||||
private lastMessageTs?: number
|
private lastMessageTs?: number
|
||||||
private providerRef: WeakRef<ClaudeDevProvider>
|
private providerRef: WeakRef<ClaudeDevProvider>
|
||||||
abort: boolean = false
|
abort: boolean = false
|
||||||
@@ -245,13 +249,14 @@ export class ClaudeDev {
|
|||||||
provider: ClaudeDevProvider,
|
provider: ClaudeDevProvider,
|
||||||
task: string,
|
task: string,
|
||||||
apiConfiguration: ApiConfiguration,
|
apiConfiguration: ApiConfiguration,
|
||||||
maxRequestsPerTask?: number
|
maxRequestsPerTask?: number,
|
||||||
|
images?: string[]
|
||||||
) {
|
) {
|
||||||
this.providerRef = new WeakRef(provider)
|
this.providerRef = new WeakRef(provider)
|
||||||
this.api = buildApiHandler(apiConfiguration)
|
this.api = buildApiHandler(apiConfiguration)
|
||||||
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
|
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
|
||||||
|
|
||||||
this.startTask(task)
|
this.startTask(task, images)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateApi(apiConfiguration: ApiConfiguration) {
|
updateApi(apiConfiguration: ApiConfiguration) {
|
||||||
@@ -262,18 +267,23 @@ export class ClaudeDev {
|
|||||||
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
|
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleWebviewAskResponse(askResponse: ClaudeAskResponse, text?: string) {
|
async handleWebviewAskResponse(askResponse: ClaudeAskResponse, text?: string, images?: string[]) {
|
||||||
this.askResponse = askResponse
|
this.askResponse = askResponse
|
||||||
this.askResponseText = text
|
this.askResponseText = text
|
||||||
|
this.askResponseImages = images
|
||||||
}
|
}
|
||||||
|
|
||||||
async ask(type: ClaudeAsk, question: string): Promise<{ response: ClaudeAskResponse; text?: string }> {
|
async ask(
|
||||||
|
type: ClaudeAsk,
|
||||||
|
question: string
|
||||||
|
): Promise<{ response: ClaudeAskResponse; text?: string; images?: string[] }> {
|
||||||
// If this ClaudeDev instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of ClaudeDev now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set claudeDev = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
|
// If this ClaudeDev instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of ClaudeDev now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set claudeDev = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
throw new Error("ClaudeDev instance aborted")
|
throw new Error("ClaudeDev instance aborted")
|
||||||
}
|
}
|
||||||
this.askResponse = undefined
|
this.askResponse = undefined
|
||||||
this.askResponseText = undefined
|
this.askResponseText = undefined
|
||||||
|
this.askResponseImages = undefined
|
||||||
const askTs = Date.now()
|
const askTs = Date.now()
|
||||||
this.lastMessageTs = askTs
|
this.lastMessageTs = askTs
|
||||||
this.claudeMessages.push({ ts: askTs, type: "ask", ask: type, text: question })
|
this.claudeMessages.push({ ts: askTs, type: "ask", ask: type, text: question })
|
||||||
@@ -282,23 +292,44 @@ export class ClaudeDev {
|
|||||||
if (this.lastMessageTs !== askTs) {
|
if (this.lastMessageTs !== askTs) {
|
||||||
throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
|
throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
|
||||||
}
|
}
|
||||||
const result = { response: this.askResponse!, text: this.askResponseText }
|
const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages }
|
||||||
this.askResponse = undefined
|
this.askResponse = undefined
|
||||||
this.askResponseText = undefined
|
this.askResponseText = undefined
|
||||||
|
this.askResponseImages = undefined
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async say(type: ClaudeSay, text?: string): Promise<undefined> {
|
async say(type: ClaudeSay, text?: string, images?: string[]): Promise<undefined> {
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
throw new Error("ClaudeDev instance aborted")
|
throw new Error("ClaudeDev instance aborted")
|
||||||
}
|
}
|
||||||
const sayTs = Date.now()
|
const sayTs = Date.now()
|
||||||
this.lastMessageTs = sayTs
|
this.lastMessageTs = sayTs
|
||||||
this.claudeMessages.push({ ts: sayTs, type: "say", say: type, text: text })
|
this.claudeMessages.push({ ts: sayTs, type: "say", say: type, text: text, images })
|
||||||
await this.providerRef.deref()?.postStateToWebview()
|
await this.providerRef.deref()?.postStateToWebview()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startTask(task: string): Promise<void> {
|
private formatImagesIntoBlocks(images?: string[]): Anthropic.ImageBlockParam[] {
|
||||||
|
return images
|
||||||
|
? images.map((base64) => ({
|
||||||
|
type: "image",
|
||||||
|
source: { type: "base64", media_type: "image/webp", data: base64 },
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatIntoToolResponse(text?: string, images?: string[]): ToolResponse {
|
||||||
|
if (images && images.length > 0) {
|
||||||
|
const textBlock: Anthropic.TextBlockParam = { type: "text", text: text ?? "" }
|
||||||
|
const imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
|
||||||
|
// "Just as with document-query placement, Claude works best when images come before text. Images placed after text or interpolated with text will still perform well, but if your use case allows it, we recommend an image-then-text structure."
|
||||||
|
return [...imageBlocks, textBlock]
|
||||||
|
} else {
|
||||||
|
return text ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startTask(task: string, images?: string[]): Promise<void> {
|
||||||
// conversationHistory (for API) and claudeMessages (for webview) need to be in sync
|
// conversationHistory (for API) and claudeMessages (for webview) need to be in sync
|
||||||
// if the extension process were killed, then on restart the claudeMessages might not be empty, so we need to set it to [] when we create a new ClaudeDev client (otherwise webview would show stale messages from previous session)
|
// if the extension process were killed, then on restart the claudeMessages might not be empty, so we need to set it to [] when we create a new ClaudeDev client (otherwise webview would show stale messages from previous session)
|
||||||
this.claudeMessages = []
|
this.claudeMessages = []
|
||||||
@@ -306,19 +337,22 @@ export class ClaudeDev {
|
|||||||
await this.providerRef.deref()?.postStateToWebview()
|
await this.providerRef.deref()?.postStateToWebview()
|
||||||
|
|
||||||
// This first message kicks off a task, it is not included in every subsequent message.
|
// This first message kicks off a task, it is not included in every subsequent message.
|
||||||
let userPrompt = `Task: \"${task}\"`
|
|
||||||
|
let textBlock: Anthropic.TextBlockParam = { type: "text", text: `Task: \"${task}\"` }
|
||||||
|
let imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
|
||||||
|
|
||||||
// TODO: create tools that let Claude interact with VSCode (e.g. open a file, list open files, etc.)
|
// TODO: create tools that let Claude interact with VSCode (e.g. open a file, list open files, etc.)
|
||||||
//const openFiles = vscode.window.visibleTextEditors?.map((editor) => editor.document.uri.fsPath).join("\n")
|
//const openFiles = vscode.window.visibleTextEditors?.map((editor) => editor.document.uri.fsPath).join("\n")
|
||||||
|
|
||||||
await this.say("text", task)
|
await this.say("text", task, images)
|
||||||
|
|
||||||
let totalInputTokens = 0
|
let totalInputTokens = 0
|
||||||
let totalOutputTokens = 0
|
let totalOutputTokens = 0
|
||||||
|
|
||||||
while (this.requestCount < this.maxRequestsPerTask) {
|
while (this.requestCount < this.maxRequestsPerTask) {
|
||||||
const { didEndLoop, inputTokens, outputTokens } = await this.recursivelyMakeClaudeRequests([
|
const { didEndLoop, inputTokens, outputTokens } = await this.recursivelyMakeClaudeRequests([
|
||||||
{ type: "text", text: userPrompt },
|
...imageBlocks,
|
||||||
|
textBlock,
|
||||||
])
|
])
|
||||||
totalInputTokens += inputTokens
|
totalInputTokens += inputTokens
|
||||||
totalOutputTokens += outputTokens
|
totalOutputTokens += outputTokens
|
||||||
@@ -328,6 +362,7 @@ export class ClaudeDev {
|
|||||||
|
|
||||||
//const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
|
//const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
|
||||||
if (didEndLoop) {
|
if (didEndLoop) {
|
||||||
|
// for now this never happens
|
||||||
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
|
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
@@ -335,13 +370,16 @@ export class ClaudeDev {
|
|||||||
// "tool",
|
// "tool",
|
||||||
// "Claude responded with only text blocks but has not called attempt_completion yet. Forcing him to continue with task..."
|
// "Claude responded with only text blocks but has not called attempt_completion yet. Forcing him to continue with task..."
|
||||||
// )
|
// )
|
||||||
userPrompt =
|
textBlock = {
|
||||||
"Ask yourself if you have completed the user's task. If you have, use the attempt_completion tool, otherwise proceed to the next step. (This is an automated message, so do not respond to it conversationally. Just proceed with the task.)"
|
type: "text",
|
||||||
|
text: "Ask yourself if you have completed the user's task. If you have, use the attempt_completion tool, otherwise proceed to the next step. (This is an automated message, so do not respond to it conversationally. Just proceed with the task.)",
|
||||||
|
}
|
||||||
|
imageBlocks = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeTool(toolName: ToolName, toolInput: any, isLastWriteToFile: boolean = false): Promise<string> {
|
async executeTool(toolName: ToolName, toolInput: any, isLastWriteToFile: boolean = false): Promise<ToolResponse> {
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case "write_to_file":
|
case "write_to_file":
|
||||||
return this.writeToFile(toolInput.path, toolInput.content, isLastWriteToFile)
|
return this.writeToFile(toolInput.path, toolInput.content, isLastWriteToFile)
|
||||||
@@ -374,7 +412,7 @@ export class ClaudeDev {
|
|||||||
return totalCost
|
return totalCost
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeToFile(relPath: string, newContent: string, isLast: boolean): Promise<string> {
|
async writeToFile(relPath: string, newContent: string, isLast: boolean): Promise<ToolResponse> {
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.resolve(cwd, relPath)
|
const absolutePath = path.resolve(cwd, relPath)
|
||||||
const fileExists = await fs
|
const fileExists = await fs
|
||||||
@@ -414,7 +452,7 @@ export class ClaudeDev {
|
|||||||
`${fileName}: Original ↔ Suggested Changes`
|
`${fileName}: Original ↔ Suggested Changes`
|
||||||
)
|
)
|
||||||
|
|
||||||
const { response, text } = await this.ask(
|
const { response, text, images } = await this.ask(
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tool: "editedExistingFile",
|
tool: "editedExistingFile",
|
||||||
@@ -426,9 +464,12 @@ export class ClaudeDev {
|
|||||||
if (isLast) {
|
if (isLast) {
|
||||||
await this.closeDiffViews()
|
await this.closeDiffViews()
|
||||||
}
|
}
|
||||||
if (response === "textResponse" && text) {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text)
|
await this.say("user_feedback", text, images)
|
||||||
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user denied this operation and provided the following feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
@@ -451,7 +492,7 @@ export class ClaudeDev {
|
|||||||
}),
|
}),
|
||||||
`${fileName}: New File`
|
`${fileName}: New File`
|
||||||
)
|
)
|
||||||
const { response, text } = await this.ask(
|
const { response, text, images } = await this.ask(
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tool: "newFileCreated",
|
tool: "newFileCreated",
|
||||||
@@ -463,9 +504,12 @@ export class ClaudeDev {
|
|||||||
if (isLast) {
|
if (isLast) {
|
||||||
await this.closeDiffViews()
|
await this.closeDiffViews()
|
||||||
}
|
}
|
||||||
if (response === "textResponse" && text) {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text)
|
await this.say("user_feedback", text, images)
|
||||||
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user denied this operation and provided the following feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
@@ -497,18 +541,21 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readFile(relPath: string): Promise<string> {
|
async readFile(relPath: string): Promise<ToolResponse> {
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.resolve(cwd, relPath)
|
const absolutePath = path.resolve(cwd, relPath)
|
||||||
const content = await fs.readFile(absolutePath, "utf-8")
|
const content = await fs.readFile(absolutePath, "utf-8")
|
||||||
const { response, text } = await this.ask(
|
const { response, text, images } = await this.ask(
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({ tool: "readFile", path: this.getReadablePath(relPath), content } as ClaudeSayTool)
|
JSON.stringify({ tool: "readFile", path: this.getReadablePath(relPath), content } as ClaudeSayTool)
|
||||||
)
|
)
|
||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "textResponse" && text) {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text)
|
await this.say("user_feedback", text, images)
|
||||||
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user denied this operation and provided the following feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
@@ -520,12 +567,12 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFilesTopLevel(relDirPath: string): Promise<string> {
|
async listFilesTopLevel(relDirPath: string): Promise<ToolResponse> {
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.resolve(cwd, relDirPath)
|
const absolutePath = path.resolve(cwd, relDirPath)
|
||||||
const files = await listFiles(absolutePath, false)
|
const files = await listFiles(absolutePath, false)
|
||||||
const result = this.formatFilesList(absolutePath, files)
|
const result = this.formatFilesList(absolutePath, files)
|
||||||
const { response, text } = await this.ask(
|
const { response, text, images } = await this.ask(
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tool: "listFilesTopLevel",
|
tool: "listFilesTopLevel",
|
||||||
@@ -534,9 +581,12 @@ export class ClaudeDev {
|
|||||||
} as ClaudeSayTool)
|
} as ClaudeSayTool)
|
||||||
)
|
)
|
||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "textResponse" && text) {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text)
|
await this.say("user_feedback", text, images)
|
||||||
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user denied this operation and provided the following feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
@@ -553,12 +603,12 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFilesRecursive(relDirPath: string): Promise<string> {
|
async listFilesRecursive(relDirPath: string): Promise<ToolResponse> {
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.resolve(cwd, relDirPath)
|
const absolutePath = path.resolve(cwd, relDirPath)
|
||||||
const files = await listFiles(absolutePath, true)
|
const files = await listFiles(absolutePath, true)
|
||||||
const result = this.formatFilesList(absolutePath, files)
|
const result = this.formatFilesList(absolutePath, files)
|
||||||
const { response, text } = await this.ask(
|
const { response, text, images } = await this.ask(
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tool: "listFilesRecursive",
|
tool: "listFilesRecursive",
|
||||||
@@ -567,9 +617,12 @@ export class ClaudeDev {
|
|||||||
} as ClaudeSayTool)
|
} as ClaudeSayTool)
|
||||||
)
|
)
|
||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "textResponse" && text) {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text)
|
await this.say("user_feedback", text, images)
|
||||||
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user denied this operation and provided the following feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
@@ -633,11 +686,11 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async viewSourceCodeDefinitionsTopLevel(relDirPath: string): Promise<string> {
|
async viewSourceCodeDefinitionsTopLevel(relDirPath: string): Promise<ToolResponse> {
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.resolve(cwd, relDirPath)
|
const absolutePath = path.resolve(cwd, relDirPath)
|
||||||
const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
|
const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
|
||||||
const { response, text } = await this.ask(
|
const { response, text, images } = await this.ask(
|
||||||
"tool",
|
"tool",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tool: "viewSourceCodeDefinitionsTopLevel",
|
tool: "viewSourceCodeDefinitionsTopLevel",
|
||||||
@@ -646,9 +699,12 @@ export class ClaudeDev {
|
|||||||
} as ClaudeSayTool)
|
} as ClaudeSayTool)
|
||||||
)
|
)
|
||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "textResponse" && text) {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text)
|
await this.say("user_feedback", text, images)
|
||||||
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user denied this operation and provided the following feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
@@ -665,12 +721,15 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeCommand(command: string, returnEmptyStringOnSuccess: boolean = false): Promise<string> {
|
async executeCommand(command: string, returnEmptyStringOnSuccess: boolean = false): Promise<ToolResponse> {
|
||||||
const { response, text } = await this.ask("command", command)
|
const { response, text, images } = await this.ask("command", command)
|
||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "textResponse" && text) {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text)
|
await this.say("user_feedback", text, images)
|
||||||
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user denied this operation and provided the following feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
@@ -756,13 +815,13 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async askFollowupQuestion(question: string): Promise<string> {
|
async askFollowupQuestion(question: string): Promise<ToolResponse> {
|
||||||
const { text } = await this.ask("followup", question)
|
const { text, images } = await this.ask("followup", question)
|
||||||
await this.say("user_feedback", text ?? "")
|
await this.say("user_feedback", text ?? "", images)
|
||||||
return `User's response:\n\"${text}\"`
|
return this.formatIntoToolResponse(`User's response:\n\"${text}\"`, images)
|
||||||
}
|
}
|
||||||
|
|
||||||
async attemptCompletion(result: string, command?: string): Promise<string> {
|
async attemptCompletion(result: string, command?: string): Promise<ToolResponse> {
|
||||||
let resultToSend = result
|
let resultToSend = result
|
||||||
if (command) {
|
if (command) {
|
||||||
await this.say("completion_result", resultToSend)
|
await this.say("completion_result", resultToSend)
|
||||||
@@ -774,12 +833,15 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
resultToSend = ""
|
resultToSend = ""
|
||||||
}
|
}
|
||||||
const { response, text } = await this.ask("completion_result", resultToSend) // this prompts webview to show 'new task' button, and enable text input (which would be the 'text' here)
|
const { response, text, images } = await this.ask("completion_result", resultToSend) // this prompts webview to show 'new task' button, and enable text input (which would be the 'text' here)
|
||||||
if (response === "yesButtonTapped") {
|
if (response === "yesButtonTapped") {
|
||||||
return ""
|
return "" // signals to recursive loop to stop (for now this never happens since yesButtonTapped will trigger a new task)
|
||||||
}
|
}
|
||||||
await this.say("user_feedback", text ?? "")
|
await this.say("user_feedback", text ?? "", images)
|
||||||
return `The user is not pleased with the results. Use the feedback they provided to successfully complete the task, and then attempt completion again.\nUser's feedback:\n\"${text}\"`
|
return this.formatIntoToolResponse(
|
||||||
|
`The user is not pleased with the results. Use the feedback they provided to successfully complete the task, and then attempt completion again.\nUser's feedback:\n\"${text}\"`,
|
||||||
|
images
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async attemptApiRequest(): Promise<Anthropic.Messages.Message> {
|
async attemptApiRequest(): Promise<Anthropic.Messages.Message> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import { ApiHandler } from "."
|
import { ApiHandler, withoutImageData } from "."
|
||||||
import { ApiHandlerOptions } from "../shared/api"
|
import { ApiHandlerOptions } from "../shared/api"
|
||||||
|
|
||||||
export class AnthropicHandler implements ApiHandler {
|
export class AnthropicHandler implements ApiHandler {
|
||||||
@@ -44,7 +44,7 @@ export class AnthropicHandler implements ApiHandler {
|
|||||||
model: "claude-3-5-sonnet-20240620",
|
model: "claude-3-5-sonnet-20240620",
|
||||||
max_tokens: 8192,
|
max_tokens: 8192,
|
||||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||||
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
|
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||||
tools: "(see tools in src/ClaudeDev.ts)",
|
tools: "(see tools in src/ClaudeDev.ts)",
|
||||||
tool_choice: { type: "auto" },
|
tool_choice: { type: "auto" },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import { ApiHandlerOptions } from "../shared/api"
|
import { ApiHandlerOptions } from "../shared/api"
|
||||||
import { ApiHandler } from "."
|
import { ApiHandler, withoutImageData } from "."
|
||||||
|
|
||||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||||
export class AwsBedrockHandler implements ApiHandler {
|
export class AwsBedrockHandler implements ApiHandler {
|
||||||
@@ -49,7 +49,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
|||||||
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||||
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
|
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||||
tools: "(see tools in src/ClaudeDev.ts)",
|
tools: "(see tools in src/ClaudeDev.ts)",
|
||||||
tool_choice: { type: "auto" },
|
tool_choice: { type: "auto" },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,3 +34,31 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
|||||||
return new AnthropicHandler(options)
|
return new AnthropicHandler(options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function withoutImageData(
|
||||||
|
userContent: Array<
|
||||||
|
| Anthropic.TextBlockParam
|
||||||
|
| Anthropic.ImageBlockParam
|
||||||
|
| Anthropic.ToolUseBlockParam
|
||||||
|
| Anthropic.ToolResultBlockParam
|
||||||
|
>
|
||||||
|
): Array<
|
||||||
|
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
|
||||||
|
> {
|
||||||
|
return userContent.map((part) => {
|
||||||
|
if (part.type === "image") {
|
||||||
|
return { ...part, source: { ...part.source, data: "..." } }
|
||||||
|
} else if (part.type === "tool_result" && typeof part.content !== "string") {
|
||||||
|
return {
|
||||||
|
...part,
|
||||||
|
content: part.content?.map((contentPart) => {
|
||||||
|
if (contentPart.type === "image") {
|
||||||
|
return { ...contentPart, source: { ...contentPart.source, data: "..." } }
|
||||||
|
}
|
||||||
|
return contentPart
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return part
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import OpenAI from "openai"
|
import OpenAI from "openai"
|
||||||
import { ApiHandler } from "."
|
import { ApiHandler, withoutImageData } from "."
|
||||||
import { ApiHandlerOptions } from "../shared/api"
|
import { ApiHandlerOptions } from "../shared/api"
|
||||||
|
|
||||||
export class OpenRouterHandler implements ApiHandler {
|
export class OpenRouterHandler implements ApiHandler {
|
||||||
@@ -118,6 +118,7 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
openAiMessages.push({ role: anthropicMessage.role, content: anthropicMessage.content })
|
openAiMessages.push({ role: anthropicMessage.role, content: anthropicMessage.content })
|
||||||
} else {
|
} else {
|
||||||
// image_url.url is base64 encoded image data
|
// image_url.url is base64 encoded image data
|
||||||
|
// ensure it contains the content-type of the image: data:image/png;base64,
|
||||||
/*
|
/*
|
||||||
{ role: "user", content: "" | { type: "text", text: string } | { type: "image_url", image_url: { url: string } } },
|
{ role: "user", content: "" | { type: "text", text: string } | { type: "image_url", image_url: { url: string } } },
|
||||||
// content required unless tool_calls is present
|
// content required unless tool_calls is present
|
||||||
@@ -146,7 +147,10 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
role: "user",
|
role: "user",
|
||||||
content: nonToolMessages.map((part) => {
|
content: nonToolMessages.map((part) => {
|
||||||
if (part.type === "image") {
|
if (part.type === "image") {
|
||||||
return { type: "image_url", image_url: { url: part.source.data } }
|
return {
|
||||||
|
type: "image_url",
|
||||||
|
image_url: { url: "data:image/webp;base64," + part.source.data },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { type: "text", text: part.text }
|
return { type: "text", text: part.text }
|
||||||
}),
|
}),
|
||||||
@@ -157,6 +161,7 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
toolMessages.forEach((toolMessage) => {
|
toolMessages.forEach((toolMessage) => {
|
||||||
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the OpenAI SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
|
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the OpenAI SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
|
||||||
let content: string
|
let content: string
|
||||||
|
let images: string[] = []
|
||||||
if (typeof toolMessage.content === "string") {
|
if (typeof toolMessage.content === "string") {
|
||||||
content = toolMessage.content
|
content = toolMessage.content
|
||||||
} else {
|
} else {
|
||||||
@@ -164,7 +169,8 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
toolMessage.content
|
toolMessage.content
|
||||||
?.map((part) => {
|
?.map((part) => {
|
||||||
if (part.type === "image") {
|
if (part.type === "image") {
|
||||||
return `{ type: "image_url", image_url: { url: ${part.source.data} } }`
|
images.push(part.source.data)
|
||||||
|
return "(see following user message for image)"
|
||||||
}
|
}
|
||||||
return part.text
|
return part.text
|
||||||
})
|
})
|
||||||
@@ -175,6 +181,16 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
tool_call_id: toolMessage.tool_use_id,
|
tool_call_id: toolMessage.tool_use_id,
|
||||||
content: content,
|
content: content,
|
||||||
})
|
})
|
||||||
|
// If tool results contain images, send as a separate user message
|
||||||
|
if (images.length > 0) {
|
||||||
|
openAiMessages.push({
|
||||||
|
role: "user",
|
||||||
|
content: images.map((image) => ({
|
||||||
|
type: "image_url",
|
||||||
|
image_url: { url: "data:image/webp;base64," + image },
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else if (anthropicMessage.role === "assistant") {
|
} else if (anthropicMessage.role === "assistant") {
|
||||||
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
||||||
@@ -198,7 +214,7 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
content = nonToolMessages
|
content = nonToolMessages
|
||||||
.map((part) => {
|
.map((part) => {
|
||||||
if (part.type === "image") {
|
if (part.type === "image") {
|
||||||
return `{ type: "image_url", image_url: { url: ${part.source.data} } }`
|
return "" // impossible as the assistant cannot send images
|
||||||
}
|
}
|
||||||
return part.text
|
return part.text
|
||||||
})
|
})
|
||||||
@@ -239,7 +255,7 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
return {
|
return {
|
||||||
model: "anthropic/claude-3.5-sonnet:beta",
|
model: "anthropic/claude-3.5-sonnet:beta",
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
|
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||||
tools: "(see tools in src/ClaudeDev.ts)",
|
tools: "(see tools in src/ClaudeDev.ts)",
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Uri, Webview } from "vscode"
|
|
||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
|
import { Uri, Webview } from "vscode"
|
||||||
import { ClaudeDev } from "../ClaudeDev"
|
import { ClaudeDev } from "../ClaudeDev"
|
||||||
import { ApiProvider } from "../shared/api"
|
import { ApiProvider } from "../shared/api"
|
||||||
import { ExtensionMessage } from "../shared/ExtensionMessage"
|
import { ExtensionMessage } from "../shared/ExtensionMessage"
|
||||||
import { WebviewMessage } from "../shared/WebviewMessage"
|
import { WebviewMessage } from "../shared/WebviewMessage"
|
||||||
|
import { processPastedImages, selectAndProcessImages } from "../utils/process-images"
|
||||||
|
import { downloadTask } from "../utils/export-markdown"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||||
@@ -134,7 +136,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
this.outputChannel.appendLine("Webview view resolved")
|
this.outputChannel.appendLine("Webview view resolved")
|
||||||
}
|
}
|
||||||
|
|
||||||
async initClaudeDevWithTask(task: string) {
|
async initClaudeDevWithTask(task: string, images?: string[]) {
|
||||||
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||||
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion, maxRequestsPerTask } =
|
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion, maxRequestsPerTask } =
|
||||||
await this.getState()
|
await this.getState()
|
||||||
@@ -142,7 +144,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
this,
|
this,
|
||||||
task,
|
task,
|
||||||
{ apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
|
{ apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
|
||||||
maxRequestsPerTask
|
maxRequestsPerTask,
|
||||||
|
images
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +206,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
||||||
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||||
|
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
|
||||||
|
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
|
||||||
|
|
||||||
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
||||||
*/
|
*/
|
||||||
@@ -217,7 +221,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||||
<meta name="theme-color" content="#000000">
|
<meta name="theme-color" content="#000000">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}';">
|
||||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||||
<link href="${codiconsUri}" rel="stylesheet" />
|
<link href="${codiconsUri}" rel="stylesheet" />
|
||||||
<title>Claude Dev</title>
|
<title>Claude Dev</title>
|
||||||
@@ -253,7 +257,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
// Could also do this in extension .ts
|
// Could also do this in extension .ts
|
||||||
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
|
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
|
||||||
// initializing new instance of ClaudeDev will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
// initializing new instance of ClaudeDev will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
||||||
await this.initClaudeDevWithTask(message.text!)
|
await this.initClaudeDevWithTask(message.text!, message.images)
|
||||||
break
|
break
|
||||||
case "apiConfiguration":
|
case "apiConfiguration":
|
||||||
if (message.apiConfiguration) {
|
if (message.apiConfiguration) {
|
||||||
@@ -282,7 +286,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
case "askResponse":
|
case "askResponse":
|
||||||
this.claudeDev?.handleWebviewAskResponse(message.askResponse!, message.text)
|
this.claudeDev?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||||
break
|
break
|
||||||
case "clearTask":
|
case "clearTask":
|
||||||
// newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
|
// newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
|
||||||
@@ -294,7 +298,19 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
case "downloadTask":
|
case "downloadTask":
|
||||||
this.downloadTask()
|
downloadTask(this.claudeDev?.apiConversationHistory ?? [])
|
||||||
|
break
|
||||||
|
case "selectImages":
|
||||||
|
const images = await selectAndProcessImages()
|
||||||
|
await this.postMessageToWebview({ type: "selectedImages", images })
|
||||||
|
break
|
||||||
|
case "processPastedImages":
|
||||||
|
const pastedImages = message.images ?? []
|
||||||
|
if (pastedImages.length > 0) {
|
||||||
|
const processedImages = await processPastedImages(pastedImages)
|
||||||
|
await this.postMessageToWebview({ type: "selectedImages", images: processedImages })
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
// Add more switch case statements here as more webview message commands
|
// Add more switch case statements here as more webview message commands
|
||||||
// are created within the webview context (i.e. inside media/main.js)
|
// are created within the webview context (i.e. inside media/main.js)
|
||||||
@@ -305,82 +321,6 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadTask() {
|
|
||||||
// File name
|
|
||||||
const date = new Date()
|
|
||||||
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
|
|
||||||
const day = date.getDate()
|
|
||||||
const year = date.getFullYear()
|
|
||||||
let hours = date.getHours()
|
|
||||||
const minutes = date.getMinutes().toString().padStart(2, "0")
|
|
||||||
const ampm = hours >= 12 ? "pm" : "am"
|
|
||||||
hours = hours % 12
|
|
||||||
hours = hours ? hours : 12 // the hour '0' should be '12'
|
|
||||||
const fileName = `claude_dev_task_${month}-${day}-${year}_${hours}-${minutes}-${ampm}.md`
|
|
||||||
|
|
||||||
// Generate markdown
|
|
||||||
const conversationHistory = this.claudeDev?.apiConversationHistory || []
|
|
||||||
const markdownContent = conversationHistory
|
|
||||||
.map((message) => {
|
|
||||||
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
|
|
||||||
const content = Array.isArray(message.content)
|
|
||||||
? message.content.map(this.formatContentBlockToMarkdown).join("\n")
|
|
||||||
: message.content
|
|
||||||
|
|
||||||
return `${role}\n\n${content}\n\n`
|
|
||||||
})
|
|
||||||
.join("---\n\n")
|
|
||||||
|
|
||||||
// Prompt user for save location
|
|
||||||
const saveUri = await vscode.window.showSaveDialog({
|
|
||||||
filters: { Markdown: ["md"] },
|
|
||||||
defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (saveUri) {
|
|
||||||
// Write content to the selected location
|
|
||||||
await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent))
|
|
||||||
vscode.window.showTextDocument(saveUri, { preview: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatContentBlockToMarkdown(
|
|
||||||
block:
|
|
||||||
| Anthropic.TextBlockParam
|
|
||||||
| Anthropic.ImageBlockParam
|
|
||||||
| Anthropic.ToolUseBlockParam
|
|
||||||
| Anthropic.ToolResultBlockParam
|
|
||||||
): string {
|
|
||||||
switch (block.type) {
|
|
||||||
case "text":
|
|
||||||
return block.text
|
|
||||||
case "image":
|
|
||||||
return `[Image: ${block.source.media_type}]`
|
|
||||||
case "tool_use":
|
|
||||||
let input: string
|
|
||||||
if (typeof block.input === "object" && block.input !== null) {
|
|
||||||
input = Object.entries(block.input)
|
|
||||||
.map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`)
|
|
||||||
.join("\n")
|
|
||||||
} else {
|
|
||||||
input = String(block.input)
|
|
||||||
}
|
|
||||||
return `[Tool Use: ${block.name}]\n${input}`
|
|
||||||
case "tool_result":
|
|
||||||
if (typeof block.content === "string") {
|
|
||||||
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content}`
|
|
||||||
} else if (Array.isArray(block.content)) {
|
|
||||||
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content
|
|
||||||
.map(this.formatContentBlockToMarkdown)
|
|
||||||
.join("\n")}`
|
|
||||||
} else {
|
|
||||||
return `[Tool Result${block.is_error ? " (Error)" : ""}]`
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return "[Unexpected content type]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async postStateToWebview() {
|
async postStateToWebview() {
|
||||||
const {
|
const {
|
||||||
apiProvider,
|
apiProvider,
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { ApiConfiguration } from "./api"
|
|||||||
|
|
||||||
// webview will hold state
|
// webview will hold state
|
||||||
export interface ExtensionMessage {
|
export interface ExtensionMessage {
|
||||||
type: "action" | "state"
|
type: "action" | "state" | "selectedImages"
|
||||||
text?: string
|
text?: string
|
||||||
action?: "plusButtonTapped" | "settingsButtonTapped" | "didBecomeVisible"
|
action?: "plusButtonTapped" | "settingsButtonTapped" | "didBecomeVisible"
|
||||||
state?: ExtensionState
|
state?: ExtensionState
|
||||||
|
images?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionState {
|
export interface ExtensionState {
|
||||||
@@ -24,6 +25,7 @@ export interface ClaudeMessage {
|
|||||||
ask?: ClaudeAsk
|
ask?: ClaudeAsk
|
||||||
say?: ClaudeSay
|
say?: ClaudeSay
|
||||||
text?: string
|
text?: string
|
||||||
|
images?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClaudeAsk =
|
export type ClaudeAsk =
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ export interface WebviewMessage {
|
|||||||
| "clearTask"
|
| "clearTask"
|
||||||
| "didShowAnnouncement"
|
| "didShowAnnouncement"
|
||||||
| "downloadTask"
|
| "downloadTask"
|
||||||
|
| "selectImages"
|
||||||
|
| "processPastedImages"
|
||||||
text?: string
|
text?: string
|
||||||
askResponse?: ClaudeAskResponse
|
askResponse?: ClaudeAskResponse
|
||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
|
images?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClaudeAskResponse = "yesButtonTapped" | "noButtonTapped" | "textResponse"
|
export type ClaudeAskResponse = "yesButtonTapped" | "noButtonTapped" | "messageResponse"
|
||||||
|
|||||||
79
src/utils/export-markdown.ts
Normal file
79
src/utils/export-markdown.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
|
import os from "os"
|
||||||
|
import * as path from "path"
|
||||||
|
import * as vscode from "vscode"
|
||||||
|
|
||||||
|
export async function downloadTask(conversationHistory: Anthropic.MessageParam[]) {
|
||||||
|
// File name
|
||||||
|
const date = new Date()
|
||||||
|
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
|
||||||
|
const day = date.getDate()
|
||||||
|
const year = date.getFullYear()
|
||||||
|
let hours = date.getHours()
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, "0")
|
||||||
|
const ampm = hours >= 12 ? "pm" : "am"
|
||||||
|
hours = hours % 12
|
||||||
|
hours = hours ? hours : 12 // the hour '0' should be '12'
|
||||||
|
const fileName = `claude_dev_task_${month}-${day}-${year}_${hours}-${minutes}-${ampm}.md`
|
||||||
|
|
||||||
|
// Generate markdown
|
||||||
|
const markdownContent = conversationHistory
|
||||||
|
.map((message) => {
|
||||||
|
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
|
||||||
|
const content = Array.isArray(message.content)
|
||||||
|
? message.content.map(formatContentBlockToMarkdown).join("\n")
|
||||||
|
: message.content
|
||||||
|
|
||||||
|
return `${role}\n\n${content}\n\n`
|
||||||
|
})
|
||||||
|
.join("---\n\n")
|
||||||
|
|
||||||
|
// Prompt user for save location
|
||||||
|
const saveUri = await vscode.window.showSaveDialog({
|
||||||
|
filters: { Markdown: ["md"] },
|
||||||
|
defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (saveUri) {
|
||||||
|
// Write content to the selected location
|
||||||
|
await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent))
|
||||||
|
vscode.window.showTextDocument(saveUri, { preview: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatContentBlockToMarkdown(
|
||||||
|
block:
|
||||||
|
| Anthropic.TextBlockParam
|
||||||
|
| Anthropic.ImageBlockParam
|
||||||
|
| Anthropic.ToolUseBlockParam
|
||||||
|
| Anthropic.ToolResultBlockParam
|
||||||
|
): string {
|
||||||
|
switch (block.type) {
|
||||||
|
case "text":
|
||||||
|
return block.text
|
||||||
|
case "image":
|
||||||
|
return `[Image]`
|
||||||
|
case "tool_use":
|
||||||
|
let input: string
|
||||||
|
if (typeof block.input === "object" && block.input !== null) {
|
||||||
|
input = Object.entries(block.input)
|
||||||
|
.map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`)
|
||||||
|
.join("\n")
|
||||||
|
} else {
|
||||||
|
input = String(block.input)
|
||||||
|
}
|
||||||
|
return `[Tool Use: ${block.name}]\n${input}`
|
||||||
|
case "tool_result":
|
||||||
|
if (typeof block.content === "string") {
|
||||||
|
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content}`
|
||||||
|
} else if (Array.isArray(block.content)) {
|
||||||
|
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content
|
||||||
|
.map(formatContentBlockToMarkdown)
|
||||||
|
.join("\n")}`
|
||||||
|
} else {
|
||||||
|
return `[Tool Result${block.is_error ? " (Error)" : ""}]`
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "[Unexpected content type]"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/utils/process-images.ts
Normal file
64
src/utils/process-images.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as vscode from "vscode"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import sharp from "sharp"
|
||||||
|
|
||||||
|
export async function selectAndProcessImages(): Promise<string[]> {
|
||||||
|
const options: vscode.OpenDialogOptions = {
|
||||||
|
canSelectMany: true,
|
||||||
|
openLabel: "Select",
|
||||||
|
filters: {
|
||||||
|
Images: ["png", "jpg", "jpeg", "gif", "webp", "tiff", "avif", "svg"], // sharp can convert these to webp which both anthropic and openrouter support
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUris = await vscode.window.showOpenDialog(options)
|
||||||
|
|
||||||
|
if (!fileUris || fileUris.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
fileUris.map(async (uri) => {
|
||||||
|
const imagePath = uri.fsPath
|
||||||
|
const originalBuffer = await fs.readFile(imagePath)
|
||||||
|
return convertToWebpBase64(originalBuffer)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processPastedImages(base64Strings: string[]): Promise<string[]> {
|
||||||
|
return await Promise.all(
|
||||||
|
base64Strings.map(async (base64) => {
|
||||||
|
const buffer = Buffer.from(base64, "base64")
|
||||||
|
return convertToWebpBase64(buffer)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertToWebpBase64(buffer: Buffer): Promise<string> {
|
||||||
|
const processedBuffer = await sharp(buffer)
|
||||||
|
/*
|
||||||
|
Anthropic docs recommendations:
|
||||||
|
- To improve time-to-first-token resize images to no more than 1.15 megapixels (and within 1568 pixels in both dimensions)
|
||||||
|
- WebP is a newer image format that's more efficient than PNG and JPEG, so ideal for keeping token usage low. (ive seen the following compression decrease size by 10x)
|
||||||
|
*/
|
||||||
|
.resize(1568, 1568, {
|
||||||
|
fit: "inside", // maintain aspect ratio
|
||||||
|
withoutEnlargement: true, // don't enlarge smaller images
|
||||||
|
})
|
||||||
|
.webp({
|
||||||
|
// NOTE: consider increasing effort from 4 to 6 (max), this may increase processing time by up to ~500ms
|
||||||
|
quality: 80,
|
||||||
|
})
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
const base64 = processedBuffer.toString("base64")
|
||||||
|
|
||||||
|
// console.log({
|
||||||
|
// originalSize: buffer.length,
|
||||||
|
// processedSize: processedBuffer.length,
|
||||||
|
// base64,
|
||||||
|
// })
|
||||||
|
|
||||||
|
return base64
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTh
|
|||||||
import CodeBlock from "./CodeBlock/CodeBlock"
|
import CodeBlock from "./CodeBlock/CodeBlock"
|
||||||
import Markdown from "react-markdown"
|
import Markdown from "react-markdown"
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||||
|
import Thumbnails from "./Thumbnails"
|
||||||
|
|
||||||
interface ChatRowProps {
|
interface ChatRowProps {
|
||||||
message: ClaudeMessage
|
message: ClaudeMessage
|
||||||
@@ -294,7 +295,10 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|||||||
whiteSpace: "pre-line",
|
whiteSpace: "pre-line",
|
||||||
wordWrap: "break-word",
|
wordWrap: "break-word",
|
||||||
}}>
|
}}>
|
||||||
<span>{message.text}</span>
|
<span style={{ display: "block" }}>{message.text}</span>
|
||||||
|
{message.images && message.images.length > 0 && (
|
||||||
|
<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case "error":
|
case "error":
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ChatRow from "./ChatRow"
|
|||||||
import TaskHeader from "./TaskHeader"
|
import TaskHeader from "./TaskHeader"
|
||||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||||
import Announcement from "./Announcement"
|
import Announcement from "./Announcement"
|
||||||
|
import Thumbnails from "./Thumbnails"
|
||||||
|
|
||||||
interface ChatViewProps {
|
interface ChatViewProps {
|
||||||
messages: ClaudeMessage[]
|
messages: ClaudeMessage[]
|
||||||
@@ -21,7 +22,9 @@ interface ChatViewProps {
|
|||||||
showAnnouncement: boolean
|
showAnnouncement: boolean
|
||||||
hideAnnouncement: () => void
|
hideAnnouncement: () => void
|
||||||
}
|
}
|
||||||
// maybe instead of storing state in App, just make chatview always show so dont conditionally load/unload? need to make sure messages are persisted (i remember seeing something about how webviews can be frozen in docs)
|
|
||||||
|
const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||||
|
|
||||||
const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideAnnouncement }: ChatViewProps) => {
|
const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideAnnouncement }: ChatViewProps) => {
|
||||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined
|
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined
|
||||||
const task = messages.length > 0 ? messages[0] : undefined // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see ClaudeDev.abort)
|
const task = messages.length > 0 ? messages[0] : undefined // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see ClaudeDev.abort)
|
||||||
@@ -32,6 +35,9 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
const [inputValue, setInputValue] = useState("")
|
const [inputValue, setInputValue] = useState("")
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [textAreaDisabled, setTextAreaDisabled] = useState(false)
|
const [textAreaDisabled, setTextAreaDisabled] = useState(false)
|
||||||
|
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||||
|
const [selectedImages, setSelectedImages] = useState<string[]>([])
|
||||||
|
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
|
||||||
|
|
||||||
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
|
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
|
||||||
const [claudeAsk, setClaudeAsk] = useState<ClaudeAsk | undefined>(undefined)
|
const [claudeAsk, setClaudeAsk] = useState<ClaudeAsk | undefined>(undefined)
|
||||||
@@ -39,11 +45,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
const [enableButtons, setEnableButtons] = useState<boolean>(false)
|
const [enableButtons, setEnableButtons] = useState<boolean>(false)
|
||||||
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
|
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
|
||||||
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
|
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
const [syntaxHighlighterStyle, setSyntaxHighlighterStyle] = useState(vsDarkPlus)
|
const [syntaxHighlighterStyle, setSyntaxHighlighterStyle] = useState(vsDarkPlus)
|
||||||
|
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
||||||
|
|
||||||
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
|
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
|
||||||
|
|
||||||
const toggleRowExpansion = (ts: number) => {
|
const toggleRowExpansion = (ts: number) => {
|
||||||
@@ -136,6 +139,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
// if the last ask is a command_output, and we receive an api_req_started, then that means the command has finished and we don't need input from the user anymore (in every other case, the user has to interact with input field or buttons to continue, which does the following automatically)
|
// if the last ask is a command_output, and we receive an api_req_started, then that means the command has finished and we don't need input from the user anymore (in every other case, the user has to interact with input field or buttons to continue, which does the following automatically)
|
||||||
setInputValue("")
|
setInputValue("")
|
||||||
setTextAreaDisabled(true)
|
setTextAreaDisabled(true)
|
||||||
|
setSelectedImages([])
|
||||||
setClaudeAsk(undefined)
|
setClaudeAsk(undefined)
|
||||||
setEnableButtons(false)
|
setEnableButtons(false)
|
||||||
}
|
}
|
||||||
@@ -175,7 +179,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
const text = inputValue.trim()
|
const text = inputValue.trim()
|
||||||
if (text) {
|
if (text) {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
vscode.postMessage({ type: "newTask", text })
|
vscode.postMessage({ type: "newTask", text, images: selectedImages })
|
||||||
} else if (claudeAsk) {
|
} else if (claudeAsk) {
|
||||||
switch (claudeAsk) {
|
switch (claudeAsk) {
|
||||||
case "followup":
|
case "followup":
|
||||||
@@ -183,13 +187,19 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
case "command": // user can provide feedback to a tool or command use
|
case "command": // user can provide feedback to a tool or command use
|
||||||
case "command_output": // user can send input to command stdin
|
case "command_output": // user can send input to command stdin
|
||||||
case "completion_result": // if this happens then the user has feedback for the completion result
|
case "completion_result": // if this happens then the user has feedback for the completion result
|
||||||
vscode.postMessage({ type: "askResponse", askResponse: "textResponse", text })
|
vscode.postMessage({
|
||||||
|
type: "askResponse",
|
||||||
|
askResponse: "messageResponse",
|
||||||
|
text,
|
||||||
|
images: selectedImages,
|
||||||
|
})
|
||||||
break
|
break
|
||||||
// there is no other case that a textfield should be enabled
|
// there is no other case that a textfield should be enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setInputValue("")
|
setInputValue("")
|
||||||
setTextAreaDisabled(true)
|
setTextAreaDisabled(true)
|
||||||
|
setSelectedImages([])
|
||||||
setClaudeAsk(undefined)
|
setClaudeAsk(undefined)
|
||||||
setEnableButtons(false)
|
setEnableButtons(false)
|
||||||
// setPrimaryButtonText(undefined)
|
// setPrimaryButtonText(undefined)
|
||||||
@@ -255,6 +265,65 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
vscode.postMessage({ type: "clearTask" })
|
vscode.postMessage({ type: "clearTask" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectImages = () => {
|
||||||
|
vscode.postMessage({ type: "selectImages" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||||
|
const items = e.clipboardData.items
|
||||||
|
const acceptedTypes = ["png", "jpg", "jpeg", "gif", "webp", "tiff", "avif", "svg"]
|
||||||
|
const imageItems = Array.from(items).filter((item) => {
|
||||||
|
const [type, subtype] = item.type.split("/")
|
||||||
|
return type === "image" && acceptedTypes.includes(subtype)
|
||||||
|
})
|
||||||
|
if (imageItems.length > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
const imagePromises = imageItems.map((item) => {
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
const blob = item.getAsFile()
|
||||||
|
if (!blob) {
|
||||||
|
resolve(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
if (reader.error) {
|
||||||
|
console.error("Error reading file:", reader.error)
|
||||||
|
resolve(null)
|
||||||
|
} else {
|
||||||
|
const result = reader.result
|
||||||
|
resolve(typeof result === "string" ? result : null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const imageDataArray = await Promise.all(imagePromises)
|
||||||
|
const base64Strings = imageDataArray
|
||||||
|
.filter((dataUrl): dataUrl is string => dataUrl !== null)
|
||||||
|
.map((dataUrl) => dataUrl.split(",")[1]) // strip the mime type prefix, sharp doesn't need it
|
||||||
|
if (base64Strings.length > 0) {
|
||||||
|
// Send base64 encoded image data to the extension
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "processPastedImages",
|
||||||
|
images: base64Strings,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.warn("No valid images were processed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedImages.length === 0) {
|
||||||
|
setThumbnailsHeight(0)
|
||||||
|
}
|
||||||
|
}, [selectedImages])
|
||||||
|
|
||||||
|
const handleThumbnailsHeightChange = useCallback((height: number) => {
|
||||||
|
setThumbnailsHeight(height)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleMessage = useCallback(
|
const handleMessage = useCallback(
|
||||||
(e: MessageEvent) => {
|
(e: MessageEvent) => {
|
||||||
const message: ExtensionMessage = e.data
|
const message: ExtensionMessage = e.data
|
||||||
@@ -268,6 +337,14 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "selectedImages":
|
||||||
|
const newImages = message.images ?? []
|
||||||
|
if (newImages.length > 0) {
|
||||||
|
setSelectedImages((prevImages) =>
|
||||||
|
[...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
// textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference.
|
// textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference.
|
||||||
},
|
},
|
||||||
@@ -324,11 +401,12 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [visibleMessages])
|
}, [visibleMessages])
|
||||||
|
|
||||||
const placeholderText = useMemo(() => {
|
const [placeholderText, isInputPipingToStdin] = useMemo(() => {
|
||||||
if (messages.at(-1)?.ask === "command_output") {
|
if (messages.at(-1)?.ask === "command_output") {
|
||||||
return "Type input to command stdin..."
|
return ["Type input to command stdin...", true]
|
||||||
}
|
}
|
||||||
return task ? "Type a message..." : "Type your task here..."
|
const text = task ? "Type a message..." : "Type your task here..."
|
||||||
|
return [text, false]
|
||||||
}, [task, messages])
|
}, [task, messages])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -345,7 +423,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
}}>
|
}}>
|
||||||
{task ? (
|
{task ? (
|
||||||
<TaskHeader
|
<TaskHeader
|
||||||
taskText={task.text || ""}
|
task={task}
|
||||||
tokensIn={apiMetrics.totalTokensIn}
|
tokensIn={apiMetrics.totalTokensIn}
|
||||||
tokensOut={apiMetrics.totalTokensOut}
|
tokensOut={apiMetrics.totalTokensOut}
|
||||||
totalCost={apiMetrics.totalCost}
|
totalCost={apiMetrics.totalCost}
|
||||||
@@ -427,13 +505,33 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: "10px 15px", opacity: textAreaDisabled ? 0.5 : 1, position: "relative" }}>
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "10px 15px",
|
||||||
|
opacity: textAreaDisabled ? 0.5 : 1,
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
}}>
|
||||||
|
{!isTextAreaFocused && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: "10px 15px",
|
||||||
|
border: "1px solid var(--vscode-input-border)",
|
||||||
|
borderRadius: 2,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DynamicTextArea
|
<DynamicTextArea
|
||||||
ref={textAreaRef}
|
ref={textAreaRef}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
disabled={textAreaDisabled}
|
disabled={textAreaDisabled}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={() => setIsTextAreaFocused(true)}
|
||||||
|
onBlur={() => setIsTextAreaFocused(false)}
|
||||||
|
onPaste={handlePaste}
|
||||||
onHeightChange={() =>
|
onHeightChange={() =>
|
||||||
//virtuosoRef.current?.scrollToIndex({ index: "LAST", align: "end", behavior: "auto" })
|
//virtuosoRef.current?.scrollToIndex({ index: "LAST", align: "end", behavior: "auto" })
|
||||||
virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "auto" })
|
virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "auto" })
|
||||||
@@ -446,32 +544,66 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
|
|||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
backgroundColor: "var(--vscode-input-background)",
|
backgroundColor: "var(--vscode-input-background)",
|
||||||
color: "var(--vscode-input-foreground)",
|
color: "var(--vscode-input-foreground)",
|
||||||
border: "1px solid var(--vscode-input-border)",
|
//border: "1px solid var(--vscode-input-border)",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
fontFamily: "var(--vscode-font-family)",
|
fontFamily: "var(--vscode-font-family)",
|
||||||
fontSize: "var(--vscode-editor-font-size)",
|
fontSize: "var(--vscode-editor-font-size)",
|
||||||
lineHeight: "var(--vscode-editor-line-height)",
|
lineHeight: "var(--vscode-editor-line-height)",
|
||||||
resize: "none",
|
resize: "none",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
padding: "8px 36px 8px 8px",
|
// Since we have maxRows, when text is long enough it starts to overflow the bottom padding, appearing behind the thumbnails. To fix this, we use a transparent border to push the text up instead. (https://stackoverflow.com/questions/42631947/maintaining-a-padding-inside-of-text-area/52538410#52538410)
|
||||||
|
borderTop: "9px solid transparent",
|
||||||
|
borderBottom: `${thumbnailsHeight + 9}px solid transparent`,
|
||||||
|
borderRight: "54px solid transparent",
|
||||||
|
borderLeft: "9px solid transparent",
|
||||||
|
// Instead of using boxShadow, we use a div with a border to better replicate the behavior when the textarea is focused
|
||||||
|
// boxShadow: "0px 0px 0px 1px var(--vscode-input-border)",
|
||||||
|
padding: 0,
|
||||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||||
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{selectedImages.length > 0 && (
|
||||||
|
<Thumbnails
|
||||||
|
images={selectedImages}
|
||||||
|
setImages={setSelectedImages}
|
||||||
|
onHeightChange={handleThumbnailsHeightChange}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
paddingTop: 4,
|
||||||
|
bottom: 14,
|
||||||
|
left: 22,
|
||||||
|
right: 67, // (54 + 9) + 4 extra padding
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: 20,
|
right: 20,
|
||||||
|
bottom: 14, // Align with the bottom padding of the container
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "flex-end",
|
||||||
top: 0,
|
height: "calc(100% - 20px)", // Full height minus top and bottom padding
|
||||||
bottom: 1.5,
|
|
||||||
}}>
|
}}>
|
||||||
|
<VSCodeButton
|
||||||
|
disabled={
|
||||||
|
textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE || isInputPipingToStdin
|
||||||
|
}
|
||||||
|
appearance="icon"
|
||||||
|
aria-label="Attach Images"
|
||||||
|
onClick={selectImages}
|
||||||
|
style={{ marginRight: "4px" }}>
|
||||||
|
<span
|
||||||
|
className="codicon codicon-device-camera"
|
||||||
|
style={{ fontSize: 18, marginLeft: -2, marginBottom: 1 }}></span>
|
||||||
|
</VSCodeButton>
|
||||||
<VSCodeButton
|
<VSCodeButton
|
||||||
disabled={textAreaDisabled}
|
disabled={textAreaDisabled}
|
||||||
appearance="icon"
|
appearance="icon"
|
||||||
aria-label="Send Message"
|
aria-label="Send Message"
|
||||||
onClick={handleSendMessage}>
|
onClick={handleSendMessage}>
|
||||||
<span className="codicon codicon-send"></span>
|
<span className="codicon codicon-send" style={{ marginBottom: -1 }}></span>
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import React, { useEffect, useRef, useState } from "react"
|
|||||||
import TextTruncate from "react-text-truncate"
|
import TextTruncate from "react-text-truncate"
|
||||||
import { useWindowSize } from "react-use"
|
import { useWindowSize } from "react-use"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
|
import { ClaudeMessage } from "@shared/ExtensionMessage"
|
||||||
|
import Thumbnails from "./Thumbnails"
|
||||||
|
|
||||||
interface TaskHeaderProps {
|
interface TaskHeaderProps {
|
||||||
taskText: string
|
task: ClaudeMessage
|
||||||
tokensIn: number
|
tokensIn: number
|
||||||
tokensOut: number
|
tokensOut: number
|
||||||
totalCost: number
|
totalCost: number
|
||||||
@@ -13,7 +15,7 @@ interface TaskHeaderProps {
|
|||||||
isHidden: boolean
|
isHidden: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut, totalCost, onClose, isHidden }) => {
|
const TaskHeader: React.FC<TaskHeaderProps> = ({ task, tokensIn, tokensOut, totalCost, onClose, isHidden }) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false)
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
const [textTruncateKey, setTextTruncateKey] = useState(0)
|
const [textTruncateKey, setTextTruncateKey] = useState(0)
|
||||||
const textContainerRef = useRef<HTMLDivElement>(null)
|
const textContainerRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -116,7 +118,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
|
|||||||
line={isExpanded ? 0 : 3}
|
line={isExpanded ? 0 : 3}
|
||||||
element="span"
|
element="span"
|
||||||
truncateText="…"
|
truncateText="…"
|
||||||
text={taskText}
|
text={task.text}
|
||||||
textTruncateChild={
|
textTruncateChild={
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -141,6 +143,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||||
<span style={{ fontWeight: "bold" }}>Tokens:</span>
|
<span style={{ fontWeight: "bold" }}>Tokens:</span>
|
||||||
|
|||||||
91
webview-ui/src/components/Thumbnails.tsx
Normal file
91
webview-ui/src/components/Thumbnails.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { useState, useRef, useLayoutEffect } from "react"
|
||||||
|
import { useWindowSize } from "react-use"
|
||||||
|
|
||||||
|
interface ThumbnailsProps {
|
||||||
|
images: string[]
|
||||||
|
style?: React.CSSProperties
|
||||||
|
setImages?: React.Dispatch<React.SetStateAction<string[]>>
|
||||||
|
onHeightChange?: (height: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Thumbnails: React.FC<ThumbnailsProps> = ({ images, style, setImages, onHeightChange }) => {
|
||||||
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
let height = containerRef.current.clientHeight
|
||||||
|
// some browsers return 0 for clientHeight
|
||||||
|
if (!height) {
|
||||||
|
height = containerRef.current.getBoundingClientRect().height
|
||||||
|
}
|
||||||
|
onHeightChange?.(height)
|
||||||
|
}
|
||||||
|
setHoveredIndex(null)
|
||||||
|
}, [images, width, onHeightChange])
|
||||||
|
|
||||||
|
const handleDelete = (index: number) => {
|
||||||
|
setImages?.((prevImages) => prevImages.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDeletable = setImages !== undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 5,
|
||||||
|
rowGap: 3,
|
||||||
|
...style,
|
||||||
|
}}>
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
onMouseEnter={() => setHoveredIndex(index)}
|
||||||
|
onMouseLeave={() => setHoveredIndex(null)}>
|
||||||
|
<img
|
||||||
|
src={`data:image/webp;base64,${image}`}
|
||||||
|
alt={`Thumbnail ${index + 1}`}
|
||||||
|
style={{
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isDeletable && hoveredIndex === index && (
|
||||||
|
<div
|
||||||
|
onClick={() => handleDelete(index)}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
width: 13,
|
||||||
|
height: 13,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "var(--vscode-badge-background)",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}>
|
||||||
|
<span
|
||||||
|
className="codicon codicon-close"
|
||||||
|
style={{
|
||||||
|
color: "var(--vscode-foreground)",
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Thumbnails
|
||||||
Reference in New Issue
Block a user