mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-11 11:19:38 +00:00
Compare commits
19 Commits
bf1b3e7376
...
v1.10.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e97c4fbc | ||
|
|
b5d1fed354 | ||
|
|
fefcc0f338 | ||
|
|
36f8f50d63 | ||
|
|
4e14222487 | ||
|
|
e2ba000e86 | ||
|
|
64e2909f0f | ||
|
|
6541760eb7 | ||
|
|
50454175c9 | ||
|
|
d6ceb222c3 | ||
|
|
9cfb7e659d | ||
|
|
018b337871 | ||
|
|
0b8b1d0677 | ||
|
|
2e1e5b463a | ||
|
|
cafe18961e | ||
|
|
8f3a97b8b0 | ||
|
|
b612035b20 | ||
|
|
c4e196692c | ||
|
|
da6f35adc9 |
45
package-lock.json
generated
45
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@siteboon/claude-code-ui",
|
||||
"version": "1.9.1",
|
||||
"version": "1.10.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@siteboon/claude-code-ui",
|
||||
"version": "1.9.1",
|
||||
"version": "1.10.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.29",
|
||||
@@ -18,6 +18,7 @@
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/merge": "^6.11.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@replit/codemirror-minimap": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@uiw/react-codemirror": "^4.23.13",
|
||||
@@ -147,7 +148,6 @@
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -590,7 +590,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
|
||||
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -2188,7 +2187,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
|
||||
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
@@ -2198,9 +2196,7 @@
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.4.tgz",
|
||||
"integrity": "sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.1",
|
||||
@@ -2218,7 +2214,6 @@
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
|
||||
"integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0",
|
||||
@@ -2232,14 +2227,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2249,7 +2242,6 @@
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz",
|
||||
"integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^10.0.2",
|
||||
@@ -2264,14 +2256,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2281,14 +2271,12 @@
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz",
|
||||
"integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz",
|
||||
"integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.1.0"
|
||||
@@ -2304,14 +2292,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2321,7 +2307,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
|
||||
"integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
@@ -2334,7 +2319,6 @@
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.1.0.tgz",
|
||||
"integrity": "sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^15.0.0"
|
||||
@@ -2350,7 +2334,6 @@
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz",
|
||||
"integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^11.0.0",
|
||||
@@ -2367,7 +2350,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
|
||||
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0"
|
||||
@@ -2380,14 +2362,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2397,14 +2377,12 @@
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
@@ -2414,7 +2392,6 @@
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz",
|
||||
"integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^7.0.2",
|
||||
@@ -2430,7 +2407,6 @@
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz",
|
||||
"integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^26.0.0"
|
||||
@@ -2916,7 +2892,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
|
||||
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -3052,8 +3027,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "2.0.0",
|
||||
@@ -3419,7 +3393,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
|
||||
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
@@ -3548,7 +3521,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
@@ -4987,7 +4959,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
|
||||
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8454,7 +8425,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8827,7 +8797,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -8840,7 +8809,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -10676,7 +10644,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -10920,7 +10887,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11203,7 +11169,6 @@
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
|
||||
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
@@ -11314,7 +11279,6 @@
|
||||
"integrity": "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -11408,7 +11372,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@siteboon/claude-code-ui",
|
||||
"version": "1.9.1",
|
||||
"version": "1.10.5",
|
||||
"description": "A web-based UI for Claude Code CLI",
|
||||
"type": "module",
|
||||
"main": "server/index.js",
|
||||
@@ -27,7 +27,7 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "npm run build && npm run server",
|
||||
"release": "release-it"
|
||||
"release": "./release.sh"
|
||||
},
|
||||
"keywords": [
|
||||
"claude coode",
|
||||
@@ -48,6 +48,7 @@
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/merge": "^6.11.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@replit/codemirror-minimap": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@uiw/react-codemirror": "^4.23.13",
|
||||
@@ -75,8 +76,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
|
||||
@@ -550,6 +550,24 @@
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>GitHub token for private repos</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>branchName</code></td>
|
||||
<td>string</td>
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>Custom branch name to use. If provided, <code>createBranch</code> is automatically enabled. Branch names are validated against Git naming rules. Works with <code>githubUrl</code> or <code>projectPath</code> (if it has a GitHub remote).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>createBranch</code></td>
|
||||
<td>boolean</td>
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>Create a new branch after successful completion (default: <code>false</code>). Automatically set to <code>true</code> if <code>branchName</code> is provided. Works with <code>githubUrl</code> or <code>projectPath</code> (if it has a GitHub remote).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>createPR</code></td>
|
||||
<td>boolean</td>
|
||||
<td><span class="badge badge-optional">Optional</span></td>
|
||||
<td>Create a pull request after successful completion (default: <code>false</code>). PR title and description auto-generated from commit messages. Works with <code>githubUrl</code> or <code>projectPath</code> (if it has a GitHub remote).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -669,7 +687,15 @@ data: {"type":"done"}</code></pre>
|
||||
"cacheCreationTokens": 0,
|
||||
"totalTokens": 200
|
||||
},
|
||||
"projectPath": "/path/to/project"
|
||||
"projectPath": "/path/to/project",
|
||||
"branch": {
|
||||
"name": "fix-authentication-bug-abc123",
|
||||
"url": "https://github.com/user/repo/tree/fix-authentication-bug-abc123"
|
||||
},
|
||||
"pullRequest": {
|
||||
"number": 42,
|
||||
"url": "https://github.com/user/repo/pull/42"
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
@@ -700,6 +726,9 @@ data: {"type":"done"}</code></pre>
|
||||
|
||||
<h3>CI/CD Integration</h3>
|
||||
<p>Integrate with GitHub Actions or other CI/CD pipelines.</p>
|
||||
|
||||
<h3>Create Branch and Pull Request</h3>
|
||||
<p>Automatically create a new branch and pull request after the agent completes its work. Branch names are auto-generated from the message, and PR title/description are auto-generated from commit messages.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -741,6 +770,49 @@ data: {"type":"done"}</code></pre>
|
||||
"githubToken": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}'</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-block">
|
||||
<h4>Create Branch and PR</h4>
|
||||
<pre><code class="language-bash">curl -X POST <span class="api-url">http://localhost:3001</span>/api/agent \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ck_..." \
|
||||
-d '{
|
||||
"githubUrl": "https://github.com/user/repo",
|
||||
"message": "Fix authentication bug",
|
||||
"createBranch": true,
|
||||
"createPR": true,
|
||||
"stream": false
|
||||
}'</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-block">
|
||||
<h4>Custom Branch Name</h4>
|
||||
<pre><code class="language-bash">curl -X POST <span class="api-url">http://localhost:3001</span>/api/agent \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ck_..." \
|
||||
-d '{
|
||||
"githubUrl": "https://github.com/user/repo",
|
||||
"message": "Add user authentication",
|
||||
"branchName": "feature/user-auth",
|
||||
"createPR": true,
|
||||
"stream": false
|
||||
}'</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-block">
|
||||
<h4>Branch & PR Response</h4>
|
||||
<pre><code class="language-json">{
|
||||
"success": true,
|
||||
"branch": {
|
||||
"name": "feature/user-auth",
|
||||
"url": "https://github.com/user/repo/tree/feature/user-auth"
|
||||
},
|
||||
"pullRequest": {
|
||||
"number": 42,
|
||||
"url": "https://github.com/user/repo/pull/42"
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
4
release.sh
Executable file
4
release.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# Load environment variables from .env
|
||||
export $(grep -v '^#' .env | grep '^GITHUB_TOKEN=' | xargs)
|
||||
exec npx release-it "$@"
|
||||
@@ -79,6 +79,16 @@ function mapCliOptionsToSDK(options = {}) {
|
||||
// Map model (default to sonnet)
|
||||
sdkOptions.model = options.model || 'sonnet';
|
||||
|
||||
// Map system prompt configuration
|
||||
sdkOptions.systemPrompt = {
|
||||
type: 'preset',
|
||||
preset: 'claude_code' // Required to use CLAUDE.md
|
||||
};
|
||||
|
||||
// Map setting sources for CLAUDE.md loading
|
||||
// This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
|
||||
sdkOptions.settingSources = ['project', 'user', 'local'];
|
||||
|
||||
// Map resume session
|
||||
if (sessionId) {
|
||||
sdkOptions.resume = sessionId;
|
||||
@@ -374,7 +384,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
||||
for await (const message of queryInstance) {
|
||||
// Capture session ID from first message
|
||||
if (message.session_id && !capturedSessionId) {
|
||||
console.log('📝 Captured session ID:', message.session_id);
|
||||
|
||||
capturedSessionId = message.session_id;
|
||||
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
||||
|
||||
|
||||
@@ -237,6 +237,71 @@ app.get('/api/config', authenticateToken, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// System update endpoint
|
||||
app.post('/api/system/update', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
// Get the project root directory (parent of server directory)
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
|
||||
console.log('Starting system update from directory:', projectRoot);
|
||||
|
||||
// Run the update command
|
||||
const updateCommand = 'git checkout main && git pull && npm install';
|
||||
|
||||
const child = spawn('sh', ['-c', updateCommand], {
|
||||
cwd: projectRoot,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
console.log('Update output:', text);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
errorOutput += text;
|
||||
console.error('Update error:', text);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
res.json({
|
||||
success: true,
|
||||
output: output || 'Update completed successfully',
|
||||
message: 'Update completed. Please restart the server to apply changes.'
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Update command failed',
|
||||
output: output,
|
||||
errorOutput: errorOutput
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error('Update process error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('System update error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const projects = await getProjects();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { apiKeysDb, githubTokensDb } from '../database/db.js';
|
||||
import { addProjectManually } from '../projects.js';
|
||||
import { queryClaudeSDK } from '../claude-sdk.js';
|
||||
import { spawnCursor } from '../cursor-cli.js';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -81,6 +82,217 @@ function normalizeGitHubUrl(url) {
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse GitHub URL to extract owner and repo
|
||||
* @param {string} url - GitHub URL (HTTPS or SSH)
|
||||
* @returns {{owner: string, repo: string}} - Parsed owner and repo
|
||||
*/
|
||||
function parseGitHubUrl(url) {
|
||||
// Handle HTTPS URLs: https://github.com/owner/repo or https://github.com/owner/repo.git
|
||||
// Handle SSH URLs: git@github.com:owner/repo or git@github.com:owner/repo.git
|
||||
const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
||||
if (!match) {
|
||||
throw new Error('Invalid GitHub URL format');
|
||||
}
|
||||
return {
|
||||
owner: match[1],
|
||||
repo: match[2].replace(/\.git$/, '')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generate a branch name from a message
|
||||
* @param {string} message - The agent message
|
||||
* @returns {string} - Generated branch name
|
||||
*/
|
||||
function autogenerateBranchName(message) {
|
||||
// Convert to lowercase, replace spaces/special chars with hyphens
|
||||
let branchName = message
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
|
||||
// Ensure non-empty fallback
|
||||
if (!branchName) {
|
||||
branchName = 'task';
|
||||
}
|
||||
|
||||
// Generate timestamp suffix (last 6 chars of base36 timestamp)
|
||||
const timestamp = Date.now().toString(36).slice(-6);
|
||||
const suffix = `-${timestamp}`;
|
||||
|
||||
// Limit length to ensure total length including suffix fits within 50 characters
|
||||
const maxBaseLength = 50 - suffix.length;
|
||||
if (branchName.length > maxBaseLength) {
|
||||
branchName = branchName.substring(0, maxBaseLength);
|
||||
}
|
||||
|
||||
// Remove any trailing hyphen after truncation and ensure no leading hyphen
|
||||
branchName = branchName.replace(/-$/, '').replace(/^-+/, '');
|
||||
|
||||
// If still empty or starts with hyphen after cleanup, use fallback
|
||||
if (!branchName || branchName.startsWith('-')) {
|
||||
branchName = 'task';
|
||||
}
|
||||
|
||||
// Combine base name with timestamp suffix
|
||||
branchName = `${branchName}${suffix}`;
|
||||
|
||||
// Final validation: ensure it matches safe pattern
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(branchName)) {
|
||||
// Fallback to deterministic safe name
|
||||
return `branch-${timestamp}`;
|
||||
}
|
||||
|
||||
return branchName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a Git branch name
|
||||
* @param {string} branchName - Branch name to validate
|
||||
* @returns {{valid: boolean, error?: string}} - Validation result
|
||||
*/
|
||||
function validateBranchName(branchName) {
|
||||
if (!branchName || branchName.trim() === '') {
|
||||
return { valid: false, error: 'Branch name cannot be empty' };
|
||||
}
|
||||
|
||||
// Git branch name rules
|
||||
const invalidPatterns = [
|
||||
{ pattern: /^\./, message: 'Branch name cannot start with a dot' },
|
||||
{ pattern: /\.$/, message: 'Branch name cannot end with a dot' },
|
||||
{ pattern: /\.\./, message: 'Branch name cannot contain consecutive dots (..)' },
|
||||
{ pattern: /\s/, message: 'Branch name cannot contain spaces' },
|
||||
{ pattern: /[~^:?*\[\\]/, message: 'Branch name cannot contain special characters: ~ ^ : ? * [ \\' },
|
||||
{ pattern: /@{/, message: 'Branch name cannot contain @{' },
|
||||
{ pattern: /\/$/, message: 'Branch name cannot end with a slash' },
|
||||
{ pattern: /^\//, message: 'Branch name cannot start with a slash' },
|
||||
{ pattern: /\/\//, message: 'Branch name cannot contain consecutive slashes' },
|
||||
{ pattern: /\.lock$/, message: 'Branch name cannot end with .lock' }
|
||||
];
|
||||
|
||||
for (const { pattern, message } of invalidPatterns) {
|
||||
if (pattern.test(branchName)) {
|
||||
return { valid: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ASCII control characters
|
||||
if (/[\x00-\x1F\x7F]/.test(branchName)) {
|
||||
return { valid: false, error: 'Branch name cannot contain control characters' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent commit messages from a repository
|
||||
* @param {string} projectPath - Path to the git repository
|
||||
* @param {number} limit - Number of commits to retrieve (default: 5)
|
||||
* @returns {Promise<string[]>} - Array of commit messages
|
||||
*/
|
||||
async function getCommitMessages(projectPath, limit = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const gitProcess = spawn('git', ['log', `-${limit}`, '--pretty=format:%s'], {
|
||||
cwd: projectPath,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
gitProcess.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
gitProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
gitProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
const messages = stdout.trim().split('\n').filter(msg => msg.length > 0);
|
||||
resolve(messages);
|
||||
} else {
|
||||
reject(new Error(`Failed to get commit messages: ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
gitProcess.on('error', (error) => {
|
||||
reject(new Error(`Failed to execute git: ${error.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new branch on GitHub using the API
|
||||
* @param {Octokit} octokit - Octokit instance
|
||||
* @param {string} owner - Repository owner
|
||||
* @param {string} repo - Repository name
|
||||
* @param {string} branchName - Name of the new branch
|
||||
* @param {string} baseBranch - Base branch to branch from (default: 'main')
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function createGitHubBranch(octokit, owner, repo, branchName, baseBranch = 'main') {
|
||||
try {
|
||||
// Get the SHA of the base branch
|
||||
const { data: ref } = await octokit.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `heads/${baseBranch}`
|
||||
});
|
||||
|
||||
const baseSha = ref.object.sha;
|
||||
|
||||
// Create the new branch
|
||||
await octokit.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/heads/${branchName}`,
|
||||
sha: baseSha
|
||||
});
|
||||
|
||||
console.log(`✅ Created branch '${branchName}' on GitHub`);
|
||||
} catch (error) {
|
||||
if (error.status === 422 && error.message.includes('Reference already exists')) {
|
||||
console.log(`ℹ️ Branch '${branchName}' already exists on GitHub`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pull request on GitHub
|
||||
* @param {Octokit} octokit - Octokit instance
|
||||
* @param {string} owner - Repository owner
|
||||
* @param {string} repo - Repository name
|
||||
* @param {string} branchName - Head branch name
|
||||
* @param {string} title - PR title
|
||||
* @param {string} body - PR body/description
|
||||
* @param {string} baseBranch - Base branch (default: 'main')
|
||||
* @returns {Promise<{number: number, url: string}>} - PR number and URL
|
||||
*/
|
||||
async function createGitHubPR(octokit, owner, repo, branchName, title, body, baseBranch = 'main') {
|
||||
const { data: pr } = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
head: branchName,
|
||||
base: baseBranch,
|
||||
body
|
||||
});
|
||||
|
||||
console.log(`✅ Created pull request #${pr.number}: ${pr.html_url}`);
|
||||
|
||||
return {
|
||||
number: pr.number,
|
||||
url: pr.html_url
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a GitHub repository to a directory
|
||||
* @param {string} githubUrl - GitHub repository URL
|
||||
@@ -361,27 +573,243 @@ class ResponseCollector {
|
||||
/**
|
||||
* POST /api/agent
|
||||
*
|
||||
* Trigger an AI agent (Claude or Cursor) to work on a project
|
||||
* Trigger an AI agent (Claude or Cursor) to work on a project.
|
||||
* Supports automatic GitHub branch and pull request creation after successful completion.
|
||||
*
|
||||
* Body:
|
||||
* - githubUrl: string (conditionally required) - GitHub repository URL to clone
|
||||
* - projectPath: string (conditionally required) - Path to existing project or where to clone
|
||||
* - message: string (required) - Message to send to the AI agent
|
||||
* - provider: string (optional) - 'claude' or 'cursor' (default: 'claude')
|
||||
* - stream: boolean (optional) - Whether to stream responses (default: true)
|
||||
* - model: string (optional) - Model to use (for Cursor)
|
||||
* - cleanup: boolean (optional) - Whether to cleanup project after completion (default: true)
|
||||
* - githubToken: string (optional) - GitHub token for private repos (overrides stored token)
|
||||
* ================================================================================================
|
||||
* REQUEST BODY PARAMETERS
|
||||
* ================================================================================================
|
||||
*
|
||||
* Note: Either githubUrl OR projectPath must be provided. If both are provided, githubUrl will be cloned to projectPath.
|
||||
* @param {string} githubUrl - (Conditionally Required) GitHub repository URL to clone.
|
||||
* Supported formats:
|
||||
* - HTTPS: https://github.com/owner/repo
|
||||
* - HTTPS with .git: https://github.com/owner/repo.git
|
||||
* - SSH: git@github.com:owner/repo
|
||||
* - SSH with .git: git@github.com:owner/repo.git
|
||||
*
|
||||
* @param {string} projectPath - (Conditionally Required) Path to existing project OR destination for cloning.
|
||||
* Behavior depends on usage:
|
||||
* - If used alone: Must point to existing project directory
|
||||
* - If used with githubUrl: Target location for cloning
|
||||
* - If omitted with githubUrl: Auto-generates temporary path in ~/.claude/external-projects/
|
||||
*
|
||||
* @param {string} message - (Required) Task description for the AI agent. Used as:
|
||||
* - Instructions for the agent
|
||||
* - Source for auto-generated branch names (if createBranch=true and no branchName)
|
||||
* - Fallback for PR title if no commits are made
|
||||
*
|
||||
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor'
|
||||
* Default: 'claude'
|
||||
*
|
||||
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
|
||||
* Default: true
|
||||
* - true: Returns text/event-stream with incremental updates
|
||||
* - false: Returns complete JSON response after completion
|
||||
*
|
||||
* @param {string} model - (Optional) Model identifier for Cursor provider.
|
||||
* Only applicable when provider='cursor'.
|
||||
* Examples: 'gpt-4', 'claude-3-opus', etc.
|
||||
*
|
||||
* @param {boolean} cleanup - (Optional) Auto-cleanup project directory after completion.
|
||||
* Default: true
|
||||
* Behavior:
|
||||
* - Only applies when cloning via githubUrl (not for existing projectPath)
|
||||
* - Deletes cloned repository after 5 seconds
|
||||
* - Also deletes associated Claude session directory
|
||||
* - Remote branch and PR remain on GitHub if created
|
||||
*
|
||||
* @param {string} githubToken - (Optional) GitHub Personal Access Token for authentication.
|
||||
* Overrides stored token from user settings.
|
||||
* Required for:
|
||||
* - Private repositories
|
||||
* - Branch/PR creation features
|
||||
* Token must have 'repo' scope for full functionality.
|
||||
*
|
||||
* @param {string} branchName - (Optional) Custom name for the Git branch.
|
||||
* If provided, createBranch is automatically set to true.
|
||||
* Validation rules (errors returned if violated):
|
||||
* - Cannot be empty or whitespace only
|
||||
* - Cannot start or end with dot (.)
|
||||
* - Cannot contain consecutive dots (..)
|
||||
* - Cannot contain spaces
|
||||
* - Cannot contain special characters: ~ ^ : ? * [ \
|
||||
* - Cannot contain @{
|
||||
* - Cannot start or end with forward slash (/)
|
||||
* - Cannot contain consecutive slashes (//)
|
||||
* - Cannot end with .lock
|
||||
* - Cannot contain ASCII control characters
|
||||
* Examples: 'feature/user-auth', 'bugfix/login-error', 'refactor/db-optimization'
|
||||
*
|
||||
* @param {boolean} createBranch - (Optional) Create a new Git branch after successful agent completion.
|
||||
* Default: false (or true if branchName is provided)
|
||||
* Behavior:
|
||||
* - Creates branch locally and pushes to remote
|
||||
* - If branch exists locally: Checks out existing branch (no error)
|
||||
* - If branch exists on remote: Uses existing branch (no error)
|
||||
* - Branch name: Custom (if branchName provided) or auto-generated from message
|
||||
* - Requires either githubUrl OR projectPath with GitHub remote
|
||||
*
|
||||
* @param {boolean} createPR - (Optional) Create a GitHub Pull Request after successful completion.
|
||||
* Default: false
|
||||
* Behavior:
|
||||
* - PR title: First commit message (or fallback to message parameter)
|
||||
* - PR description: Auto-generated from all commit messages
|
||||
* - Base branch: Always 'main' (currently hardcoded)
|
||||
* - If PR already exists: GitHub returns error with details
|
||||
* - Requires either githubUrl OR projectPath with GitHub remote
|
||||
*
|
||||
* ================================================================================================
|
||||
* PATH HANDLING BEHAVIOR
|
||||
* ================================================================================================
|
||||
*
|
||||
* Scenario 1: Only githubUrl provided
|
||||
* Input: { githubUrl: "https://github.com/owner/repo" }
|
||||
* Action: Clones to auto-generated temporary path: ~/.claude/external-projects/<hash>/
|
||||
* Cleanup: Yes (if cleanup=true)
|
||||
*
|
||||
* Scenario 2: Only projectPath provided
|
||||
* Input: { projectPath: "/home/user/my-project" }
|
||||
* Action: Uses existing project at specified path
|
||||
* Validation: Path must exist and be accessible
|
||||
* Cleanup: No (never cleanup existing projects)
|
||||
*
|
||||
* Scenario 3: Both githubUrl and projectPath provided
|
||||
* Input: { githubUrl: "https://github.com/owner/repo", projectPath: "/custom/path" }
|
||||
* Action: Clones githubUrl to projectPath location
|
||||
* Validation:
|
||||
* - If projectPath exists with git repo:
|
||||
* - Compares remote URL with githubUrl
|
||||
* - If URLs match: Reuses existing repo
|
||||
* - If URLs differ: Returns error
|
||||
* Cleanup: Yes (if cleanup=true)
|
||||
*
|
||||
* ================================================================================================
|
||||
* GITHUB BRANCH/PR CREATION REQUIREMENTS
|
||||
* ================================================================================================
|
||||
*
|
||||
* For createBranch or createPR to work, one of the following must be true:
|
||||
*
|
||||
* Option A: githubUrl provided
|
||||
* - Repository URL directly specified
|
||||
* - Works with both cloning and existing paths
|
||||
*
|
||||
* Option B: projectPath with GitHub remote
|
||||
* - Project must be a Git repository
|
||||
* - Must have 'origin' remote configured
|
||||
* - Remote URL must point to github.com
|
||||
* - System auto-detects GitHub URL via: git remote get-url origin
|
||||
*
|
||||
* Additional Requirements:
|
||||
* - Valid GitHub token (from settings or githubToken parameter)
|
||||
* - Token must have 'repo' scope for private repos
|
||||
* - Project must have commits (for PR creation)
|
||||
*
|
||||
* ================================================================================================
|
||||
* VALIDATION & ERROR HANDLING
|
||||
* ================================================================================================
|
||||
*
|
||||
* Input Validations (400 Bad Request):
|
||||
* - Either githubUrl OR projectPath must be provided (not neither)
|
||||
* - message must be non-empty string
|
||||
* - provider must be 'claude' or 'cursor'
|
||||
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
|
||||
* - branchName must pass Git naming rules (if provided)
|
||||
*
|
||||
* Runtime Validations (500 Internal Server Error or specific error in response):
|
||||
* - projectPath must exist (if used alone)
|
||||
* - GitHub URL format must be valid
|
||||
* - Git remote URL must include github.com (for projectPath + branch/PR)
|
||||
* - GitHub token must be available (for private repos and branch/PR)
|
||||
* - Directory conflicts handled (existing path with different repo)
|
||||
*
|
||||
* Branch Name Validation Errors (returned in response, not HTTP error):
|
||||
* Invalid names return: { branch: { error: "Invalid branch name: <reason>" } }
|
||||
* Examples:
|
||||
* - "my branch" → "Branch name cannot contain spaces"
|
||||
* - ".feature" → "Branch name cannot start with a dot"
|
||||
* - "feature.lock" → "Branch name cannot end with .lock"
|
||||
*
|
||||
* ================================================================================================
|
||||
* RESPONSE FORMATS
|
||||
* ================================================================================================
|
||||
*
|
||||
* Streaming Response (stream=true):
|
||||
* Content-Type: text/event-stream
|
||||
* Events:
|
||||
* - { type: "status", message: "...", projectPath: "..." }
|
||||
* - { type: "claude-response", data: {...} }
|
||||
* - { type: "github-branch", branch: { name: "...", url: "..." } }
|
||||
* - { type: "github-pr", pullRequest: { number: 42, url: "..." } }
|
||||
* - { type: "github-error", error: "..." }
|
||||
* - { type: "done" }
|
||||
*
|
||||
* Non-Streaming Response (stream=false):
|
||||
* Content-Type: application/json
|
||||
* {
|
||||
* success: true,
|
||||
* sessionId: "session-123",
|
||||
* messages: [...], // Assistant messages only (filtered)
|
||||
* tokens: {
|
||||
* inputTokens: 150,
|
||||
* outputTokens: 50,
|
||||
* cacheReadTokens: 0,
|
||||
* cacheCreationTokens: 0,
|
||||
* totalTokens: 200
|
||||
* },
|
||||
* projectPath: "/path/to/project",
|
||||
* branch: { // Only if createBranch=true
|
||||
* name: "feature/xyz",
|
||||
* url: "https://github.com/owner/repo/tree/feature/xyz"
|
||||
* } | { error: "..." },
|
||||
* pullRequest: { // Only if createPR=true
|
||||
* number: 42,
|
||||
* url: "https://github.com/owner/repo/pull/42"
|
||||
* } | { error: "..." }
|
||||
* }
|
||||
*
|
||||
* Error Response:
|
||||
* HTTP Status: 400, 401, 500
|
||||
* Content-Type: application/json
|
||||
* { success: false, error: "Error description" }
|
||||
*
|
||||
* ================================================================================================
|
||||
* EXAMPLES
|
||||
* ================================================================================================
|
||||
*
|
||||
* Example 1: Clone and process with auto-cleanup
|
||||
* POST /api/agent
|
||||
* { "githubUrl": "https://github.com/user/repo", "message": "Fix bug" }
|
||||
*
|
||||
* Example 2: Use existing project with custom branch and PR
|
||||
* POST /api/agent
|
||||
* {
|
||||
* "projectPath": "/home/user/project",
|
||||
* "message": "Add feature",
|
||||
* "branchName": "feature/new-feature",
|
||||
* "createPR": true
|
||||
* }
|
||||
*
|
||||
* Example 3: Clone to specific path with auto-generated branch
|
||||
* POST /api/agent
|
||||
* {
|
||||
* "githubUrl": "https://github.com/user/repo",
|
||||
* "projectPath": "/tmp/work",
|
||||
* "message": "Refactor code",
|
||||
* "createBranch": true,
|
||||
* "cleanup": false
|
||||
* }
|
||||
*/
|
||||
router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken } = req.body;
|
||||
const { githubUrl, projectPath, message, provider = 'claude', model, githubToken, branchName } = req.body;
|
||||
|
||||
// Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
|
||||
const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
|
||||
const cleanup = req.body.cleanup === undefined ? true : (req.body.cleanup === true || req.body.cleanup === 'true');
|
||||
|
||||
// If branchName is provided, automatically enable createBranch
|
||||
const createBranch = branchName ? true : (req.body.createBranch === true || req.body.createBranch === 'true');
|
||||
const createPR = req.body.createPR === true || req.body.createPR === 'true';
|
||||
|
||||
// Validate inputs
|
||||
if (!githubUrl && !projectPath) {
|
||||
return res.status(400).json({ error: 'Either githubUrl or projectPath is required' });
|
||||
@@ -395,6 +823,12 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
return res.status(400).json({ error: 'provider must be "claude" or "cursor"' });
|
||||
}
|
||||
|
||||
// Validate GitHub branch/PR creation requirements
|
||||
// Allow branch/PR creation with projectPath as long as it has a GitHub remote
|
||||
if ((createBranch || createPR) && !githubUrl && !projectPath) {
|
||||
return res.status(400).json({ error: 'createBranch and createPR require either githubUrl or projectPath with a GitHub remote' });
|
||||
}
|
||||
|
||||
let finalProjectPath = null;
|
||||
let writer = null;
|
||||
|
||||
@@ -492,6 +926,187 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
}, writer);
|
||||
}
|
||||
|
||||
// Handle GitHub branch and PR creation after successful agent completion
|
||||
let branchInfo = null;
|
||||
let prInfo = null;
|
||||
|
||||
if (createBranch || createPR) {
|
||||
try {
|
||||
console.log('🔄 Starting GitHub branch/PR creation workflow...');
|
||||
|
||||
// Get GitHub token
|
||||
const tokenToUse = githubToken || githubTokensDb.getActiveGithubToken(req.user.id);
|
||||
|
||||
if (!tokenToUse) {
|
||||
throw new Error('GitHub token required for branch/PR creation. Please configure a GitHub token in settings.');
|
||||
}
|
||||
|
||||
// Initialize Octokit
|
||||
const octokit = new Octokit({ auth: tokenToUse });
|
||||
|
||||
// Get GitHub URL - either from parameter or from git remote
|
||||
let repoUrl = githubUrl;
|
||||
if (!repoUrl) {
|
||||
console.log('🔍 Getting GitHub URL from git remote...');
|
||||
try {
|
||||
repoUrl = await getGitRemoteUrl(finalProjectPath);
|
||||
if (!repoUrl.includes('github.com')) {
|
||||
throw new Error('Project does not have a GitHub remote configured');
|
||||
}
|
||||
console.log(`✅ Found GitHub remote: ${repoUrl}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get GitHub remote URL: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse GitHub URL to get owner and repo
|
||||
const { owner, repo } = parseGitHubUrl(repoUrl);
|
||||
console.log(`📦 Repository: ${owner}/${repo}`);
|
||||
|
||||
// Use provided branch name or auto-generate from message
|
||||
const finalBranchName = branchName || autogenerateBranchName(message);
|
||||
if (branchName) {
|
||||
console.log(`🌿 Using provided branch name: ${finalBranchName}`);
|
||||
|
||||
// Validate custom branch name
|
||||
const validation = validateBranchName(finalBranchName);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Invalid branch name: ${validation.error}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`🌿 Auto-generated branch name: ${finalBranchName}`);
|
||||
}
|
||||
|
||||
if (createBranch) {
|
||||
// Create and checkout the new branch locally
|
||||
console.log('🔄 Creating local branch...');
|
||||
const checkoutProcess = spawn('git', ['checkout', '-b', finalBranchName], {
|
||||
cwd: finalProjectPath,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let stderr = '';
|
||||
checkoutProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
checkoutProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`✅ Created and checked out local branch '${finalBranchName}'`);
|
||||
resolve();
|
||||
} else {
|
||||
// Branch might already exist locally, try to checkout
|
||||
if (stderr.includes('already exists')) {
|
||||
console.log(`ℹ️ Branch '${finalBranchName}' already exists locally, checking out...`);
|
||||
const checkoutExisting = spawn('git', ['checkout', finalBranchName], {
|
||||
cwd: finalProjectPath,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
checkoutExisting.on('close', (checkoutCode) => {
|
||||
if (checkoutCode === 0) {
|
||||
console.log(`✅ Checked out existing branch '${finalBranchName}'`);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to checkout existing branch: ${stderr}`));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Failed to create branch: ${stderr}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Push the branch to remote
|
||||
console.log('🔄 Pushing branch to remote...');
|
||||
const pushProcess = spawn('git', ['push', '-u', 'origin', finalBranchName], {
|
||||
cwd: finalProjectPath,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
pushProcess.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
pushProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
pushProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`✅ Pushed branch '${finalBranchName}' to remote`);
|
||||
resolve();
|
||||
} else {
|
||||
// Check if branch exists on remote but has different commits
|
||||
if (stderr.includes('already exists') || stderr.includes('up-to-date')) {
|
||||
console.log(`ℹ️ Branch '${finalBranchName}' already exists on remote, using existing branch`);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to push branch: ${stderr}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
branchInfo = {
|
||||
name: finalBranchName,
|
||||
url: `https://github.com/${owner}/${repo}/tree/${finalBranchName}`
|
||||
};
|
||||
}
|
||||
|
||||
if (createPR) {
|
||||
// Get commit messages to generate PR description
|
||||
console.log('🔄 Generating PR title and description...');
|
||||
const commitMessages = await getCommitMessages(finalProjectPath, 5);
|
||||
|
||||
// Use the first commit message as the PR title, or fallback to the agent message
|
||||
const prTitle = commitMessages.length > 0 ? commitMessages[0] : message;
|
||||
|
||||
// Generate PR body from commit messages
|
||||
let prBody = '## Changes\n\n';
|
||||
if (commitMessages.length > 0) {
|
||||
prBody += commitMessages.map(msg => `- ${msg}`).join('\n');
|
||||
} else {
|
||||
prBody += `Agent task: ${message}`;
|
||||
}
|
||||
prBody += '\n\n---\n*This pull request was automatically created by Claude Code UI Agent.*';
|
||||
|
||||
console.log(`📝 PR Title: ${prTitle}`);
|
||||
|
||||
// Create the pull request
|
||||
console.log('🔄 Creating pull request...');
|
||||
prInfo = await createGitHubPR(octokit, owner, repo, finalBranchName, prTitle, prBody, 'main');
|
||||
}
|
||||
|
||||
// Send branch/PR info in response
|
||||
if (stream) {
|
||||
if (branchInfo) {
|
||||
writer.send({
|
||||
type: 'github-branch',
|
||||
branch: branchInfo
|
||||
});
|
||||
}
|
||||
if (prInfo) {
|
||||
writer.send({
|
||||
type: 'github-pr',
|
||||
pullRequest: prInfo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ GitHub branch/PR creation error:', error);
|
||||
|
||||
// Send error but don't fail the entire request
|
||||
if (stream) {
|
||||
writer.send({
|
||||
type: 'github-error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
// Store error info for non-streaming response
|
||||
if (!stream) {
|
||||
branchInfo = { error: error.message };
|
||||
prInfo = { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle response based on streaming mode
|
||||
if (stream) {
|
||||
// Streaming mode: end the SSE stream
|
||||
@@ -501,13 +1116,23 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
const assistantMessages = writer.getAssistantMessages();
|
||||
const tokenSummary = writer.getTotalTokens();
|
||||
|
||||
res.json({
|
||||
const response = {
|
||||
success: true,
|
||||
sessionId: writer.getSessionId(),
|
||||
messages: assistantMessages,
|
||||
tokens: tokenSummary,
|
||||
projectPath: finalProjectPath
|
||||
});
|
||||
};
|
||||
|
||||
// Add branch/PR info if created
|
||||
if (branchInfo) {
|
||||
response.branch = branchInfo;
|
||||
}
|
||||
if (prInfo) {
|
||||
response.pullRequest = prInfo;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
}
|
||||
|
||||
// Clean up if requested
|
||||
|
||||
@@ -502,16 +502,16 @@ router.post('/generate-commit-message', async (req, res) => {
|
||||
*/
|
||||
async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
|
||||
// Create the prompt
|
||||
const prompt = `You are a git commit message generator. Based on the following file changes and diffs, generate a commit message in conventional commit format.
|
||||
const prompt = `Generate a conventional commit message for these changes.
|
||||
|
||||
REQUIREMENTS:
|
||||
- Use conventional commit format: type(scope): subject
|
||||
- Include a body that explains what changed and why
|
||||
- Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
- Keep subject line under 50 characters
|
||||
- Wrap body at 72 characters
|
||||
- Be specific and descriptive
|
||||
- Return ONLY the commit message, nothing else - no markdown, no explanations, no code blocks
|
||||
- Format: type(scope): subject
|
||||
- Include body explaining what changed and why
|
||||
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
- Subject under 50 chars, body wrapped at 72 chars
|
||||
- Focus on user-facing changes, not implementation details
|
||||
- Consider what's being added AND removed
|
||||
- Return ONLY the commit message (no markdown, explanations, or code blocks)
|
||||
|
||||
FILES CHANGED:
|
||||
${files.map(f => `- ${f}`).join('\n')}
|
||||
@@ -519,7 +519,7 @@ ${files.map(f => `- ${f}`).join('\n')}
|
||||
DIFFS:
|
||||
${diffContext.substring(0, 4000)}
|
||||
|
||||
Generate the commit message now:`;
|
||||
Generate the commit message:`;
|
||||
|
||||
try {
|
||||
// Create a simple writer that collects the response
|
||||
|
||||
264
src/App.jsx
264
src/App.jsx
@@ -20,6 +20,7 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Settings as SettingsIcon, Sparkles } from 'lucide-react';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import MainContent from './components/MainContent';
|
||||
import MobileNav from './components/MobileNav';
|
||||
@@ -42,7 +43,7 @@ function AppContent() {
|
||||
const navigate = useNavigate();
|
||||
const { sessionId } = useParams();
|
||||
|
||||
const { updateAvailable, latestVersion, currentVersion } = useVersionCheck('siteboon', 'claudecodeui');
|
||||
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
|
||||
const [showVersionModal, setShowVersionModal] = useState(false);
|
||||
|
||||
const [projects, setProjects] = useState([]);
|
||||
@@ -54,12 +55,14 @@ function AppContent() {
|
||||
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [settingsInitialTab, setSettingsInitialTab] = useState('tools');
|
||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
|
||||
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
|
||||
const [showThinking, setShowThinking] = useLocalStorage('showThinking', true);
|
||||
const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true);
|
||||
const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', false);
|
||||
const [sidebarVisible, setSidebarVisible] = useLocalStorage('sidebarVisible', true);
|
||||
// Session Protection System: Track sessions with active conversations to prevent
|
||||
// automatic project updates from interrupting ongoing chats. When a user sends
|
||||
// a message, the session is marked as "active" and project updates are paused
|
||||
@@ -306,6 +309,12 @@ function AppContent() {
|
||||
// Expose fetchProjects globally for component access
|
||||
window.refreshProjects = fetchProjects;
|
||||
|
||||
// Expose openSettings function globally for component access
|
||||
window.openSettings = useCallback((tab = 'tools') => {
|
||||
setSettingsInitialTab(tab);
|
||||
setShowSettings(true);
|
||||
}, []);
|
||||
|
||||
// Handle URL-based session loading
|
||||
useEffect(() => {
|
||||
if (sessionId && projects.length > 0) {
|
||||
@@ -536,8 +545,60 @@ function AppContent() {
|
||||
|
||||
// Version Upgrade Modal Component
|
||||
const VersionUpgradeModal = () => {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [updateOutput, setUpdateOutput] = useState('');
|
||||
const [updateError, setUpdateError] = useState('');
|
||||
|
||||
if (!showVersionModal) return null;
|
||||
|
||||
// Clean up changelog by removing GitHub-specific metadata
|
||||
const cleanChangelog = (body) => {
|
||||
if (!body) return '';
|
||||
|
||||
return body
|
||||
// Remove full commit hashes (40 character hex strings)
|
||||
.replace(/\b[0-9a-f]{40}\b/gi, '')
|
||||
// Remove short commit hashes (7-10 character hex strings at start of line or after dash/space)
|
||||
.replace(/(?:^|\s|-)([0-9a-f]{7,10})\b/gi, '')
|
||||
// Remove "Full Changelog" links
|
||||
.replace(/\*\*Full Changelog\*\*:.*$/gim, '')
|
||||
// Remove compare links (e.g., https://github.com/.../compare/v1.0.0...v1.0.1)
|
||||
.replace(/https?:\/\/github\.com\/[^\/]+\/[^\/]+\/compare\/[^\s)]+/gi, '')
|
||||
// Clean up multiple consecutive empty lines
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||||
// Trim whitespace
|
||||
.trim();
|
||||
};
|
||||
|
||||
const handleUpdateNow = async () => {
|
||||
setIsUpdating(true);
|
||||
setUpdateOutput('Starting update...\n');
|
||||
setUpdateError('');
|
||||
|
||||
try {
|
||||
// Call the backend API to run the update command
|
||||
const response = await authenticatedFetch('/api/system/update', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setUpdateOutput(prev => prev + data.output + '\n');
|
||||
setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n');
|
||||
setUpdateOutput(prev => prev + 'Please restart the server to apply changes.\n');
|
||||
} else {
|
||||
setUpdateError(data.error || 'Update failed');
|
||||
setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n');
|
||||
}
|
||||
} catch (error) {
|
||||
setUpdateError(error.message);
|
||||
setUpdateOutput(prev => prev + '\n❌ Update failed: ' + error.message + '\n');
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
@@ -546,9 +607,9 @@ function AppContent() {
|
||||
onClick={() => setShowVersionModal(false)}
|
||||
aria-label="Close version upgrade modal"
|
||||
/>
|
||||
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-md mx-4 p-6 space-y-4">
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-2xl mx-4 p-6 space-y-4 max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -559,7 +620,9 @@ function AppContent() {
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Update Available</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">A new version is ready</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{releaseInfo?.title || 'A new version is ready'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -584,18 +647,57 @@ function AppContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upgrade Instructions */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">How to upgrade:</h3>
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
|
||||
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
|
||||
git checkout main && git pull && npm install
|
||||
</code>
|
||||
{/* Changelog */}
|
||||
{releaseInfo?.body && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">What's New:</h3>
|
||||
{releaseInfo?.htmlUrl && (
|
||||
<a
|
||||
href={releaseInfo.htmlUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline flex items-center gap-1"
|
||||
>
|
||||
View full release
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 border border-gray-200 dark:border-gray-600 max-h-64 overflow-y-auto">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap prose prose-sm dark:prose-invert max-w-none">
|
||||
{cleanChangelog(releaseInfo.body)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Run this command in your Claude Code UI directory to update to the latest version.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Update Output */}
|
||||
{updateOutput && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Update Progress:</h3>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 border border-gray-700 max-h-48 overflow-y-auto">
|
||||
<pre className="text-xs text-green-400 font-mono whitespace-pre-wrap">{updateOutput}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upgrade Instructions */}
|
||||
{!isUpdating && !updateOutput && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Manual upgrade:</h3>
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
|
||||
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
|
||||
git checkout main && git pull && npm install
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Or click "Update Now" to run the update automatically.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
@@ -603,18 +705,34 @@ function AppContent() {
|
||||
onClick={() => setShowVersionModal(false)}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Copy command to clipboard
|
||||
navigator.clipboard.writeText('git checkout main && git pull && npm install');
|
||||
setShowVersionModal(false);
|
||||
}}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
||||
>
|
||||
Copy Command
|
||||
{updateOutput ? 'Close' : 'Later'}
|
||||
</button>
|
||||
{!updateOutput && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText('git checkout main && git pull && npm install');
|
||||
}}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
||||
>
|
||||
Copy Command
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdateNow}
|
||||
disabled={isUpdating}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed rounded-md transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
{isUpdating ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Updating...
|
||||
</>
|
||||
) : (
|
||||
'Update Now'
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -625,27 +743,78 @@ function AppContent() {
|
||||
<div className="fixed inset-0 flex bg-background">
|
||||
{/* Fixed Desktop Sidebar */}
|
||||
{!isMobile && (
|
||||
<div className="w-80 flex-shrink-0 border-r border-border bg-card">
|
||||
<div
|
||||
className={`flex-shrink-0 border-r border-border bg-card transition-all duration-300 ${
|
||||
sidebarVisible ? 'w-80' : 'w-14'
|
||||
}`}
|
||||
>
|
||||
<div className="h-full overflow-hidden">
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
onProjectSelect={handleProjectSelect}
|
||||
onSessionSelect={handleSessionSelect}
|
||||
onNewSession={handleNewSession}
|
||||
onSessionDelete={handleSessionDelete}
|
||||
onProjectDelete={handleProjectDelete}
|
||||
isLoading={isLoadingProjects}
|
||||
onRefresh={handleSidebarRefresh}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
updateAvailable={updateAvailable}
|
||||
latestVersion={latestVersion}
|
||||
currentVersion={currentVersion}
|
||||
onShowVersionModal={() => setShowVersionModal(true)}
|
||||
isPWA={isPWA}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
{sidebarVisible ? (
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
onProjectSelect={handleProjectSelect}
|
||||
onSessionSelect={handleSessionSelect}
|
||||
onNewSession={handleNewSession}
|
||||
onSessionDelete={handleSessionDelete}
|
||||
onProjectDelete={handleProjectDelete}
|
||||
isLoading={isLoadingProjects}
|
||||
onRefresh={handleSidebarRefresh}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
updateAvailable={updateAvailable}
|
||||
latestVersion={latestVersion}
|
||||
currentVersion={currentVersion}
|
||||
releaseInfo={releaseInfo}
|
||||
onShowVersionModal={() => setShowVersionModal(true)}
|
||||
isPWA={isPWA}
|
||||
isMobile={isMobile}
|
||||
onToggleSidebar={() => setSidebarVisible(false)}
|
||||
/>
|
||||
) : (
|
||||
/* Collapsed Sidebar */
|
||||
<div className="h-full flex flex-col items-center py-4 gap-4">
|
||||
{/* Expand Button */}
|
||||
<button
|
||||
onClick={() => setSidebarVisible(true)}
|
||||
className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group"
|
||||
aria-label="Show sidebar"
|
||||
title="Show sidebar"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Settings Icon */}
|
||||
<button
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
||||
aria-label="Settings"
|
||||
title="Settings"
|
||||
>
|
||||
<SettingsIcon className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
</button>
|
||||
|
||||
{/* Update Indicator */}
|
||||
{updateAvailable && (
|
||||
<button
|
||||
onClick={() => setShowVersionModal(true)}
|
||||
className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
||||
aria-label="Update available"
|
||||
title="Update available"
|
||||
>
|
||||
<Sparkles className="w-5 h-5 text-blue-500" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -691,9 +860,11 @@ function AppContent() {
|
||||
updateAvailable={updateAvailable}
|
||||
latestVersion={latestVersion}
|
||||
currentVersion={currentVersion}
|
||||
releaseInfo={releaseInfo}
|
||||
onShowVersionModal={() => setShowVersionModal(true)}
|
||||
isPWA={isPWA}
|
||||
isMobile={isMobile}
|
||||
onToggleSidebar={() => setSidebarVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -763,6 +934,7 @@ function AppContent() {
|
||||
isOpen={showSettings}
|
||||
onClose={() => setShowSettings(false)}
|
||||
projects={projects}
|
||||
initialTab={settingsInitialTab}
|
||||
/>
|
||||
|
||||
{/* Version Upgrade Modal */}
|
||||
|
||||
@@ -416,21 +416,76 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
<div className="w-full">
|
||||
|
||||
{message.isToolUse && !['Read', 'TodoWrite', 'TodoRead'].includes(message.toolName) ? (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-2 sm:p-3 mb-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-5 h-5 bg-blue-600 rounded flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
(() => {
|
||||
// Minimize Grep and Glob tools since they happen frequently
|
||||
const isSearchTool = ['Grep', 'Glob'].includes(message.toolName);
|
||||
|
||||
if (isSearchTool) {
|
||||
return (
|
||||
<>
|
||||
<div className="group relative bg-gray-50/50 dark:bg-gray-800/30 border-l-2 border-blue-400 dark:border-blue-500 pl-3 py-2 my-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400 flex-1 min-w-0">
|
||||
<svg className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span className="font-medium flex-shrink-0">{message.toolName}</span>
|
||||
<span className="text-gray-400 dark:text-gray-500 flex-shrink-0">•</span>
|
||||
{message.toolInput && (() => {
|
||||
try {
|
||||
const input = JSON.parse(message.toolInput);
|
||||
return (
|
||||
<span className="font-mono truncate flex-1 min-w-0">
|
||||
{input.pattern && <span>pattern: <span className="text-blue-600 dark:text-blue-400">{input.pattern}</span></span>}
|
||||
{input.path && <span className="ml-2">in: {input.path}</span>}
|
||||
</span>
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
{message.toolResult && (
|
||||
<a
|
||||
href={`#tool-result-${message.toolId}`}
|
||||
className="flex-shrink-0 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>results</span>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Full display for other tools
|
||||
return (
|
||||
<div className="group relative bg-gradient-to-br from-blue-50/50 to-indigo-50/50 dark:from-blue-950/20 dark:to-indigo-950/20 border border-blue-100/30 dark:border-blue-800/30 rounded-lg p-3 mb-2">
|
||||
{/* Decorative gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/3 to-indigo-500/3 dark:from-blue-400/3 dark:to-indigo-400/3 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<div className="relative flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 dark:from-blue-400 dark:to-indigo-500 rounded-lg flex items-center justify-center shadow-lg shadow-blue-500/20 dark:shadow-blue-400/20">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
{/* Subtle pulse animation */}
|
||||
<div className="absolute inset-0 rounded-lg bg-blue-500 dark:bg-blue-400 animate-pulse opacity-20"></div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-gray-900 dark:text-white text-sm">
|
||||
{message.toolName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
{message.toolId}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium text-blue-900 dark:text-blue-100">
|
||||
Using {message.toolName}
|
||||
</span>
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400 font-mono">
|
||||
{message.toolId}
|
||||
</span>
|
||||
</div>
|
||||
{onShowSettings && (
|
||||
<button
|
||||
@@ -438,10 +493,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
e.stopPropagation();
|
||||
onShowSettings();
|
||||
}}
|
||||
className="p-1 rounded hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
|
||||
className="p-2 rounded-lg hover:bg-white/60 dark:hover:bg-gray-800/60 transition-all duration-200 group/btn backdrop-blur-sm"
|
||||
title="Tool Settings"
|
||||
>
|
||||
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover/btn:text-blue-600 dark:group-hover/btn:text-blue-400 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
@@ -453,12 +508,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
const input = JSON.parse(message.toolInput);
|
||||
if (input.file_path && input.old_string && input.new_string) {
|
||||
return (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<details className="relative mt-3 group/details" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2.5 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-4 h-4 transition-transform duration-200 group-open/details:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
📝 View edit diff for
|
||||
<span className="flex items-center gap-2">
|
||||
<span>View edit diff for</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -491,14 +548,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
onFileOpen(input.file_path);
|
||||
}
|
||||
}}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-mono"
|
||||
className="px-2.5 py-1 rounded-md bg-white/60 dark:bg-gray-800/60 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 font-mono text-xs font-medium transition-all duration-200 shadow-sm"
|
||||
>
|
||||
{input.file_path.split('/').pop()}
|
||||
</button>
|
||||
</summary>
|
||||
<div className="mt-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-3 pl-6">
|
||||
<div className="bg-white dark:bg-gray-900/50 border border-gray-200/60 dark:border-gray-700/60 rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-gradient-to-r from-gray-50 to-gray-100/50 dark:from-gray-800/80 dark:to-gray-800/40 border-b border-gray-200/60 dark:border-gray-700/60 backdrop-blur-sm">
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!onFileOpen) return;
|
||||
@@ -528,11 +585,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
onFileOpen(input.file_path);
|
||||
}
|
||||
}}
|
||||
className="text-xs font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 truncate underline cursor-pointer"
|
||||
className="text-xs font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 truncate cursor-pointer font-medium transition-colors"
|
||||
>
|
||||
{input.file_path}
|
||||
</button>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium px-2 py-0.5 bg-gray-100 dark:bg-gray-700/50 rounded">
|
||||
Diff
|
||||
</span>
|
||||
</div>
|
||||
@@ -558,11 +615,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
</div>
|
||||
</div>
|
||||
{showRawParameters && (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
|
||||
<details className="relative mt-3 pl-6 group/raw" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-400 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-3 h-3 transition-transform duration-200 group-open/raw:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
View raw parameters
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
|
||||
<pre className="mt-2 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/60 dark:border-gray-700/60 p-3 rounded-lg whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono">
|
||||
{message.toolInput}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -575,11 +635,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
// Fall back to raw display if parsing fails
|
||||
}
|
||||
return (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200">
|
||||
<details className="relative mt-3 group/params" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2.5 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-4 h-4 transition-transform duration-200 group-open/params:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
View input parameters
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
|
||||
<pre className="mt-3 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/60 dark:border-gray-700/60 p-3 rounded-lg whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono">
|
||||
{message.toolInput}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -602,12 +665,15 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
|
||||
if (input.file_path && input.content !== undefined) {
|
||||
return (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<details className="relative mt-3 group/details" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2.5 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-4 h-4 transition-transform duration-200 group-open/details:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
📄 Creating new file:
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-lg leading-none">📄</span>
|
||||
<span>Creating new file:</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -635,14 +701,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-mono"
|
||||
className="px-2.5 py-1 rounded-md bg-white/60 dark:bg-gray-800/60 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 font-mono text-xs font-medium transition-all duration-200 shadow-sm"
|
||||
>
|
||||
{input.file_path.split('/').pop()}
|
||||
</button>
|
||||
</summary>
|
||||
<div className="mt-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-3 pl-6">
|
||||
<div className="bg-white dark:bg-gray-900/50 border border-gray-200/60 dark:border-gray-700/60 rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-gradient-to-r from-gray-50 to-gray-100/50 dark:from-gray-800/80 dark:to-gray-800/40 border-b border-gray-200/60 dark:border-gray-700/60 backdrop-blur-sm">
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!onFileOpen) return;
|
||||
@@ -668,11 +734,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="text-xs font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 truncate underline cursor-pointer"
|
||||
className="text-xs font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 truncate cursor-pointer font-medium transition-colors"
|
||||
>
|
||||
{input.file_path}
|
||||
</button>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium px-2 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded">
|
||||
New File
|
||||
</span>
|
||||
</div>
|
||||
@@ -698,11 +764,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
</div>
|
||||
</div>
|
||||
{showRawParameters && (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
|
||||
<details className="relative mt-3 pl-6 group/raw" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-400 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-3 h-3 transition-transform duration-200 group-open/raw:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
View raw parameters
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
|
||||
<pre className="mt-2 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/60 dark:border-gray-700/60 p-3 rounded-lg whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono">
|
||||
{message.toolInput}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -722,21 +791,27 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
const input = JSON.parse(message.toolInput);
|
||||
if (input.todos && Array.isArray(input.todos)) {
|
||||
return (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<details className="relative mt-3 group/todo" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2.5 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-4 h-4 transition-transform duration-200 group-open/todo:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
Updating Todo List
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-lg leading-none">✓</span>
|
||||
<span>Updating Todo List</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div className="mt-3">
|
||||
<TodoList todos={input.todos} />
|
||||
{showRawParameters && (
|
||||
<details className="mt-3" open={autoExpandTools}>
|
||||
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
|
||||
<details className="relative mt-3 group/raw" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-400 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-3 h-3 transition-transform duration-200 group-open/raw:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
View raw parameters
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded overflow-x-auto text-blue-900 dark:text-blue-100">
|
||||
<pre className="mt-2 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/60 dark:border-gray-700/60 p-3 rounded-lg overflow-x-auto text-gray-700 dark:text-gray-300 font-mono">
|
||||
{message.toolInput}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -755,42 +830,17 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
try {
|
||||
const input = JSON.parse(message.toolInput);
|
||||
return (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
Running command
|
||||
</summary>
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="bg-gray-900 dark:bg-gray-950 text-gray-100 rounded-lg p-3 font-mono text-sm">
|
||||
<div className="flex items-center gap-2 mb-2 text-gray-400">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-xs">Terminal</span>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-all text-green-400">
|
||||
$ {input.command}
|
||||
</div>
|
||||
</div>
|
||||
{input.description && (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 italic">
|
||||
{input.description}
|
||||
</div>
|
||||
)}
|
||||
{showRawParameters && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
|
||||
View raw parameters
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
|
||||
{message.toolInput}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
<div className="my-2">
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-md px-3 py-2 font-mono text-sm">
|
||||
<span className="text-green-400">$</span>
|
||||
<span className="text-gray-100 ml-2">{input.command}</span>
|
||||
</div>
|
||||
</details>
|
||||
{input.description && (
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400 italic ml-1">
|
||||
{input.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
// Fall back to regular display
|
||||
@@ -849,14 +899,14 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
|
||||
// Regular tool input display for other tools
|
||||
return (
|
||||
<details className="mt-2" open={autoExpandTools}>
|
||||
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<details className="relative mt-3 group/params" open={autoExpandTools}>
|
||||
<summary className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2.5 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<svg className="w-4 h-4 transition-transform duration-200 group-open/params:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
View input parameters
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
|
||||
<pre className="mt-3 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/60 dark:border-gray-700/60 p-3 rounded-lg whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono">
|
||||
{message.toolInput}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -864,35 +914,57 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
})()}
|
||||
|
||||
{/* Tool Result Section */}
|
||||
{message.toolResult && (
|
||||
<div className="mt-3 border-t border-blue-200 dark:border-blue-700 pt-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className={`w-4 h-4 rounded flex items-center justify-center ${
|
||||
message.toolResult.isError
|
||||
? 'bg-red-500'
|
||||
: 'bg-green-500'
|
||||
{message.toolResult && (() => {
|
||||
// Hide tool results for Edit/Write/Bash unless there's an error
|
||||
const shouldHideResult = !message.toolResult.isError &&
|
||||
(message.toolName === 'Edit' || message.toolName === 'Write' || message.toolName === 'ApplyPatch' || message.toolName === 'Bash');
|
||||
|
||||
if (shouldHideResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`tool-result-${message.toolId}`}
|
||||
className={`relative mt-4 p-4 rounded-lg border backdrop-blur-sm scroll-mt-4 ${
|
||||
message.toolResult.isError
|
||||
? 'bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-950/20 dark:to-rose-950/20 border-red-200/60 dark:border-red-800/60'
|
||||
: 'bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200/60 dark:border-green-800/60'
|
||||
}`}>
|
||||
{/* Decorative gradient overlay */}
|
||||
<div className={`absolute inset-0 rounded-lg opacity-50 ${
|
||||
message.toolResult.isError
|
||||
? 'bg-gradient-to-br from-red-500/5 to-rose-500/5 dark:from-red-400/5 dark:to-rose-400/5'
|
||||
: 'bg-gradient-to-br from-green-500/5 to-emerald-500/5 dark:from-green-400/5 dark:to-emerald-400/5'
|
||||
}`}></div>
|
||||
|
||||
<div className="relative flex items-center gap-2.5 mb-3">
|
||||
<div className={`w-6 h-6 rounded-lg flex items-center justify-center shadow-md ${
|
||||
message.toolResult.isError
|
||||
? 'bg-gradient-to-br from-red-500 to-rose-600 dark:from-red-400 dark:to-rose-500 shadow-red-500/20'
|
||||
: 'bg-gradient-to-br from-green-500 to-emerald-600 dark:from-green-400 dark:to-emerald-500 shadow-green-500/20'
|
||||
}`}>
|
||||
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{message.toolResult.isError ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${
|
||||
message.toolResult.isError
|
||||
? 'text-red-700 dark:text-red-300'
|
||||
: 'text-green-700 dark:text-green-300'
|
||||
<span className={`text-sm font-semibold ${
|
||||
message.toolResult.isError
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
: 'text-green-800 dark:text-green-200'
|
||||
}`}>
|
||||
{message.toolResult.isError ? 'Tool Error' : 'Tool Result'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={`text-sm ${
|
||||
message.toolResult.isError
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
: 'text-green-800 dark:text-green-200'
|
||||
|
||||
<div className={`relative text-sm ${
|
||||
message.toolResult.isError
|
||||
? 'text-red-900 dark:text-red-100'
|
||||
: 'text-green-900 dark:text-green-100'
|
||||
}`}>
|
||||
{(() => {
|
||||
const content = String(message.toolResult.content || '');
|
||||
@@ -957,6 +1029,57 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for Grep/Glob results with structured data
|
||||
if ((message.toolName === 'Grep' || message.toolName === 'Glob') && message.toolResult?.toolUseResult) {
|
||||
const toolData = message.toolResult.toolUseResult;
|
||||
|
||||
// Handle files_with_matches mode or any tool result with filenames array
|
||||
if (toolData.filenames && Array.isArray(toolData.filenames) && toolData.filenames.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="font-medium">
|
||||
Found {toolData.numFiles || toolData.filenames.length} {(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 max-h-96 overflow-y-auto">
|
||||
{toolData.filenames.map((filePath, index) => {
|
||||
const fileName = filePath.split('/').pop();
|
||||
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
if (onFileOpen) {
|
||||
onFileOpen(filePath);
|
||||
}
|
||||
}}
|
||||
className="group flex items-center gap-2 px-2 py-1.5 rounded hover:bg-green-100/50 dark:hover:bg-green-800/20 cursor-pointer transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-sm font-medium text-green-800 dark:text-green-200 truncate group-hover:text-green-900 dark:group-hover:text-green-100">
|
||||
{fileName}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-green-600/70 dark:text-green-400/70 truncate">
|
||||
{dirPath}
|
||||
</div>
|
||||
</div>
|
||||
<svg className="w-4 h-4 text-green-600 dark:text-green-400 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for interactive prompts
|
||||
if (content.includes('Do you want to proceed?') && message.toolName === 'Bash') {
|
||||
const lines = content.split('\n');
|
||||
@@ -1193,8 +1316,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : message.isInteractivePrompt ? (
|
||||
// Special handling for interactive prompts
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
@@ -1286,21 +1412,31 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
if (input.file_path) {
|
||||
const filename = input.file_path.split('/').pop();
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
📖 Read{' '}
|
||||
<button
|
||||
onClick={() => onFileOpen && onFileOpen(input.file_path)}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-mono"
|
||||
>
|
||||
{filename}
|
||||
</button>
|
||||
<div className="bg-gray-50/50 dark:bg-gray-800/30 border-l-2 border-gray-400 dark:border-gray-500 pl-3 py-2 my-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<span className="font-medium">Read</span>
|
||||
<button
|
||||
onClick={() => onFileOpen && onFileOpen(input.file_path)}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-mono transition-colors"
|
||||
>
|
||||
{filename}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
📖 Read file
|
||||
<div className="bg-gray-50/50 dark:bg-gray-800/30 border-l-2 border-gray-400 dark:border-gray-500 pl-3 py-2 my-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<span className="font-medium">Read file</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1312,9 +1448,12 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
const input = JSON.parse(message.toolInput);
|
||||
if (input.todos && Array.isArray(input.todos)) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2">
|
||||
<div className="text-sm text-blue-700 dark:text-blue-300 mb-2">
|
||||
📝 Update todo list
|
||||
<div className="bg-gray-50/50 dark:bg-gray-800/30 border-l-2 border-gray-400 dark:border-gray-500 pl-3 py-2 my-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||
<svg className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
<span className="font-medium">Update todo list</span>
|
||||
</div>
|
||||
<TodoList todos={input.todos} />
|
||||
</div>
|
||||
@@ -1322,16 +1461,26 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
}
|
||||
} catch (e) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
📝 Update todo list
|
||||
<div className="bg-gray-50/50 dark:bg-gray-800/30 border-l-2 border-gray-400 dark:border-gray-500 pl-3 py-2 my-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
<span className="font-medium">Update todo list</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()
|
||||
) : message.isToolUse && message.toolName === 'TodoRead' ? (
|
||||
// Simple TodoRead tool indicator
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
📋 Read todo list
|
||||
<div className="bg-gray-50/50 dark:bg-gray-800/30 border-l-2 border-gray-400 dark:border-gray-500 pl-3 py-2 my-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<span className="font-medium">Read todo list</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
@@ -2328,7 +2477,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
toolResults.set(part.tool_use_id, {
|
||||
content: part.content,
|
||||
isError: part.is_error,
|
||||
timestamp: new Date(msg.timestamp || Date.now())
|
||||
timestamp: new Date(msg.timestamp || Date.now()),
|
||||
// Extract structured tool result data (e.g., for Grep, Glob)
|
||||
toolUseResult: msg.toolUseResult || null
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2404,7 +2555,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
} else if (part.type === 'tool_use') {
|
||||
// Get the corresponding tool result
|
||||
const toolResult = toolResults.get(part.id);
|
||||
|
||||
|
||||
converted.push({
|
||||
type: 'assistant',
|
||||
content: '',
|
||||
@@ -2412,7 +2563,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
isToolUse: true,
|
||||
toolName: part.name,
|
||||
toolInput: JSON.stringify(part.input),
|
||||
toolResult: toolResult ? (typeof toolResult.content === 'string' ? toolResult.content : JSON.stringify(toolResult.content)) : null,
|
||||
toolResult: toolResult ? {
|
||||
content: typeof toolResult.content === 'string' ? toolResult.content : JSON.stringify(toolResult.content),
|
||||
isError: toolResult.isError,
|
||||
toolUseResult: toolResult.toolUseResult
|
||||
} : null,
|
||||
toolError: toolResult?.isError || false,
|
||||
toolResultTimestamp: toolResult?.timestamp || new Date()
|
||||
});
|
||||
@@ -2448,7 +2603,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||
setIsUserScrolledUp(false);
|
||||
// Don't reset isUserScrolledUp here - let the scroll handler manage it
|
||||
// This prevents fighting with user's scroll position during streaming
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -3367,7 +3523,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
const prevTop = scrollPositionRef.current.top;
|
||||
const newHeight = container.scrollHeight;
|
||||
const heightDiff = newHeight - prevHeight;
|
||||
|
||||
|
||||
// If content was added above the current view, adjust scroll position
|
||||
if (heightDiff > 0 && prevTop > 0) {
|
||||
container.scrollTop = prevTop + heightDiff;
|
||||
@@ -3381,9 +3537,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
if (scrollContainerRef.current && chatMessages.length > 0 && !isLoadingSessionRef.current) {
|
||||
// Only scroll if we're not in the middle of loading a session
|
||||
// This prevents the "double scroll" effect during session switching
|
||||
// Also reset scroll state
|
||||
// Reset scroll state when switching sessions
|
||||
setIsUserScrolledUp(false);
|
||||
setTimeout(() => scrollToBottom(), 200); // Delay to ensure full rendering
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
// After scrolling, the scroll event handler will naturally set isUserScrolledUp based on position
|
||||
}, 200); // Delay to ensure full rendering
|
||||
}
|
||||
}, [selectedSession?.id, selectedProject?.name]); // Only trigger when session/project changes
|
||||
|
||||
@@ -4549,19 +4708,20 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* Hint text */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center mt-2 hidden sm:block">
|
||||
{sendByCtrlEnter
|
||||
? "Ctrl+Enter to send (IME safe) • Shift+Enter for new line • Tab to change modes • Type / for slash commands"
|
||||
: "Press Enter to send • Shift+Enter for new line • Tab to change modes • Type / for slash commands"}
|
||||
</div>
|
||||
<div className={`text-xs text-gray-500 dark:text-gray-400 text-center mt-2 sm:hidden transition-opacity duration-200 ${
|
||||
isInputFocused ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
{sendByCtrlEnter
|
||||
? "Ctrl+Enter to send (IME safe) • Tab for modes • @ for files • / for commands"
|
||||
: "Enter to send • Tab for modes • @ for files • / for commands"}
|
||||
|
||||
{/* Hint text inside input box at bottom */}
|
||||
<div className="absolute bottom-1 left-12 right-14 sm:right-40 text-xs text-gray-400 dark:text-gray-500 pointer-events-none hidden sm:block">
|
||||
{sendByCtrlEnter
|
||||
? "Ctrl+Enter to send • Shift+Enter for new line • Tab to change modes • / for slash commands"
|
||||
: "Enter to send • Shift+Enter for new line • Tab to change modes • / for slash commands"}
|
||||
</div>
|
||||
<div className={`absolute bottom-1 left-12 right-14 text-xs text-gray-400 dark:text-gray-500 pointer-events-none sm:hidden transition-opacity duration-200 ${
|
||||
isInputFocused ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
{sendByCtrlEnter
|
||||
? "Ctrl+Enter to send • Tab for modes • / for commands"
|
||||
: "Enter to send • Tab for modes • / for commands"}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -10,23 +10,37 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView, showPanel, ViewPlugin } from '@codemirror/view';
|
||||
import { unifiedMergeView, getChunks } from '@codemirror/merge';
|
||||
import { showMinimap } from '@replit/codemirror-minimap';
|
||||
import { X, Save, Download, Maximize2, Minimize2, Eye, EyeOff } from 'lucide-react';
|
||||
import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function CodeEditor({ file, onClose, projectPath }) {
|
||||
function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) {
|
||||
const [content, setContent] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(true);
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
const savedTheme = localStorage.getItem('codeEditorTheme');
|
||||
return savedTheme ? savedTheme === 'dark' : true;
|
||||
});
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [showDiff, setShowDiff] = useState(!!file.diffInfo);
|
||||
const [wordWrap, setWordWrap] = useState(false);
|
||||
const [wordWrap, setWordWrap] = useState(() => {
|
||||
return localStorage.getItem('codeEditorWordWrap') === 'true';
|
||||
});
|
||||
const [minimapEnabled, setMinimapEnabled] = useState(() => {
|
||||
return localStorage.getItem('codeEditorShowMinimap') !== 'false';
|
||||
});
|
||||
const [showLineNumbers, setShowLineNumbers] = useState(() => {
|
||||
return localStorage.getItem('codeEditorLineNumbers') !== 'false';
|
||||
});
|
||||
const [fontSize, setFontSize] = useState(() => {
|
||||
return localStorage.getItem('codeEditorFontSize') || '14';
|
||||
});
|
||||
const editorRef = useRef(null);
|
||||
|
||||
// Create minimap extension with chunk-based gutters
|
||||
const minimapExtension = useMemo(() => {
|
||||
if (!file.diffInfo || !showDiff) return [];
|
||||
if (!file.diffInfo || !showDiff || !minimapEnabled) return [];
|
||||
|
||||
const gutters = {};
|
||||
|
||||
@@ -58,7 +72,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
};
|
||||
})
|
||||
];
|
||||
}, [file.diffInfo, showDiff, isDarkMode]);
|
||||
}, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]);
|
||||
|
||||
// Create extension to scroll to first chunk on mount
|
||||
const scrollToFirstChunkExtension = useMemo(() => {
|
||||
@@ -89,24 +103,28 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
];
|
||||
}, [file.diffInfo, showDiff]);
|
||||
|
||||
// Create diff navigation panel extension
|
||||
const diffNavigationPanel = useMemo(() => {
|
||||
if (!file.diffInfo || !showDiff) return [];
|
||||
|
||||
// Create editor toolbar panel - always visible
|
||||
const editorToolbarPanel = useMemo(() => {
|
||||
const createPanel = (view) => {
|
||||
const dom = document.createElement('div');
|
||||
dom.className = 'cm-diff-navigation-panel';
|
||||
dom.className = 'cm-editor-toolbar-panel';
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const updatePanel = () => {
|
||||
// Use getChunks API to get ALL chunks regardless of viewport
|
||||
const chunksData = getChunks(view.state);
|
||||
// Check if we have diff info and it's enabled
|
||||
const hasDiff = file.diffInfo && showDiff;
|
||||
const chunksData = hasDiff ? getChunks(view.state) : null;
|
||||
const chunks = chunksData?.chunks || [];
|
||||
const chunkCount = chunks.length;
|
||||
|
||||
dom.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
// Build the toolbar HTML
|
||||
let toolbarHTML = '<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">';
|
||||
|
||||
// Left side - diff navigation (if applicable)
|
||||
toolbarHTML += '<div style="display: flex; align-items: center; gap: 8px;">';
|
||||
if (hasDiff) {
|
||||
toolbarHTML += `
|
||||
<span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes</span>
|
||||
<button class="cm-diff-nav-btn cm-diff-nav-prev" title="Previous change" ${chunkCount === 0 ? 'disabled' : ''}>
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -118,41 +136,110 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
toolbarHTML += '</div>';
|
||||
|
||||
// Right side - action buttons
|
||||
toolbarHTML += '<div style="display: flex; align-items: center; gap: 4px;">';
|
||||
|
||||
// Show/hide diff button (only if there's diff info)
|
||||
if (file.diffInfo) {
|
||||
toolbarHTML += `
|
||||
<button class="cm-toolbar-btn cm-toggle-diff-btn" title="${showDiff ? 'Hide diff highlighting' : 'Show diff highlighting'}">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
${showDiff ?
|
||||
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />' :
|
||||
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />'
|
||||
}
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// Settings button
|
||||
toolbarHTML += `
|
||||
<button class="cm-toolbar-btn cm-settings-btn" title="Editor Settings">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const prevBtn = dom.querySelector('.cm-diff-nav-prev');
|
||||
const nextBtn = dom.querySelector('.cm-diff-nav-next');
|
||||
// Expand button (only in sidebar mode)
|
||||
if (isSidebar && onToggleExpand) {
|
||||
toolbarHTML += `
|
||||
<button class="cm-toolbar-btn cm-expand-btn" title="${isExpanded ? 'Collapse editor' : 'Expand editor to full width'}">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
${isExpanded ?
|
||||
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />' :
|
||||
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />'
|
||||
}
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
prevBtn?.addEventListener('click', () => {
|
||||
if (chunks.length === 0) return;
|
||||
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
|
||||
toolbarHTML += '</div>';
|
||||
toolbarHTML += '</div>';
|
||||
|
||||
// Navigate to the chunk - use fromB which is the position in the current document
|
||||
const chunk = chunks[currentIndex];
|
||||
if (chunk) {
|
||||
// Scroll to the start of the chunk in the B side (current document)
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
||||
});
|
||||
dom.innerHTML = toolbarHTML;
|
||||
|
||||
// Attach event listeners for diff navigation
|
||||
if (hasDiff) {
|
||||
const prevBtn = dom.querySelector('.cm-diff-nav-prev');
|
||||
const nextBtn = dom.querySelector('.cm-diff-nav-next');
|
||||
|
||||
prevBtn?.addEventListener('click', () => {
|
||||
if (chunks.length === 0) return;
|
||||
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
|
||||
|
||||
const chunk = chunks[currentIndex];
|
||||
if (chunk) {
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
||||
});
|
||||
}
|
||||
updatePanel();
|
||||
});
|
||||
|
||||
nextBtn?.addEventListener('click', () => {
|
||||
if (chunks.length === 0) return;
|
||||
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0;
|
||||
|
||||
const chunk = chunks[currentIndex];
|
||||
if (chunk) {
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
||||
});
|
||||
}
|
||||
updatePanel();
|
||||
});
|
||||
}
|
||||
|
||||
// Attach event listener for toggle diff button
|
||||
if (file.diffInfo) {
|
||||
const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn');
|
||||
toggleDiffBtn?.addEventListener('click', () => {
|
||||
setShowDiff(!showDiff);
|
||||
});
|
||||
}
|
||||
|
||||
// Attach event listener for settings button
|
||||
const settingsBtn = dom.querySelector('.cm-settings-btn');
|
||||
settingsBtn?.addEventListener('click', () => {
|
||||
if (window.openSettings) {
|
||||
window.openSettings('appearance');
|
||||
}
|
||||
updatePanel();
|
||||
});
|
||||
|
||||
nextBtn?.addEventListener('click', () => {
|
||||
if (chunks.length === 0) return;
|
||||
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0;
|
||||
|
||||
// Navigate to the chunk - use fromB which is the position in the current document
|
||||
const chunk = chunks[currentIndex];
|
||||
if (chunk) {
|
||||
// Scroll to the start of the chunk in the B side (current document)
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
||||
});
|
||||
}
|
||||
updatePanel();
|
||||
});
|
||||
// Attach event listener for expand button
|
||||
if (isSidebar && onToggleExpand) {
|
||||
const expandBtn = dom.querySelector('.cm-expand-btn');
|
||||
expandBtn?.addEventListener('click', () => {
|
||||
onToggleExpand();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updatePanel();
|
||||
@@ -165,7 +252,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
};
|
||||
|
||||
return [showPanel.of(createPanel)];
|
||||
}, [file.diffInfo, showDiff]);
|
||||
}, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]);
|
||||
|
||||
// Get language extension based on file extension
|
||||
const getLanguageExtension = (filename) => {
|
||||
@@ -290,6 +377,57 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
setIsFullscreen(!isFullscreen);
|
||||
};
|
||||
|
||||
// Save theme preference to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorTheme', isDarkMode ? 'dark' : 'light');
|
||||
}, [isDarkMode]);
|
||||
|
||||
// Save word wrap preference to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorWordWrap', wordWrap.toString());
|
||||
}, [wordWrap]);
|
||||
|
||||
// Listen for settings changes from the Settings modal
|
||||
useEffect(() => {
|
||||
const handleStorageChange = () => {
|
||||
const newTheme = localStorage.getItem('codeEditorTheme');
|
||||
if (newTheme) {
|
||||
setIsDarkMode(newTheme === 'dark');
|
||||
}
|
||||
|
||||
const newWordWrap = localStorage.getItem('codeEditorWordWrap');
|
||||
if (newWordWrap !== null) {
|
||||
setWordWrap(newWordWrap === 'true');
|
||||
}
|
||||
|
||||
const newShowMinimap = localStorage.getItem('codeEditorShowMinimap');
|
||||
if (newShowMinimap !== null) {
|
||||
setMinimapEnabled(newShowMinimap !== 'false');
|
||||
}
|
||||
|
||||
const newShowLineNumbers = localStorage.getItem('codeEditorLineNumbers');
|
||||
if (newShowLineNumbers !== null) {
|
||||
setShowLineNumbers(newShowLineNumbers !== 'false');
|
||||
}
|
||||
|
||||
const newFontSize = localStorage.getItem('codeEditorFontSize');
|
||||
if (newFontSize) {
|
||||
setFontSize(newFontSize);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for storage events (changes from other tabs/windows)
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
// Custom event for same-window updates
|
||||
window.addEventListener('codeEditorSettingsChanged', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
window.removeEventListener('codeEditorSettingsChanged', handleStorageChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
@@ -321,14 +459,23 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="fixed inset-0 z-50 md:bg-black/50 md:flex md:items-center md:justify-center">
|
||||
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center">
|
||||
{isSidebar ? (
|
||||
<div className="w-full h-full flex items-center justify-center bg-white dark:bg-gray-900">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="fixed inset-0 z-40 md:bg-black/50 md:flex md:items-center md:justify-center">
|
||||
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -372,8 +519,8 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'};
|
||||
}
|
||||
|
||||
/* Diff navigation panel styling */
|
||||
.cm-diff-navigation-panel {
|
||||
/* Editor toolbar panel styling */
|
||||
.cm-editor-toolbar-panel {
|
||||
padding: 8px 12px;
|
||||
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
|
||||
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
|
||||
@@ -381,7 +528,8 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cm-diff-nav-btn {
|
||||
.cm-diff-nav-btn,
|
||||
.cm-toolbar-btn {
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -391,9 +539,11 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.cm-diff-nav-btn:hover {
|
||||
.cm-diff-nav-btn:hover,
|
||||
.cm-toolbar-btn:hover {
|
||||
background-color: ${isDarkMode ? '#374151' : '#f3f4f6'};
|
||||
}
|
||||
|
||||
@@ -403,67 +553,36 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className={`fixed inset-0 z-50 ${
|
||||
<div className={isSidebar ?
|
||||
'w-full h-full flex flex-col' :
|
||||
`fixed inset-0 z-40 ${
|
||||
// Mobile: native fullscreen, Desktop: modal with backdrop
|
||||
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
|
||||
} ${isFullscreen ? 'md:p-0' : ''}`}>
|
||||
<div className={`bg-white shadow-2xl flex flex-col ${
|
||||
// Mobile: always fullscreen, Desktop: modal sizing
|
||||
'w-full h-full md:rounded-lg md:shadow-2xl' +
|
||||
(isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]')
|
||||
}`}>
|
||||
<div className={isSidebar ?
|
||||
'bg-white dark:bg-gray-900 flex flex-col w-full h-full' :
|
||||
`bg-white shadow-2xl flex flex-col ${
|
||||
// Mobile: always fullscreen, Desktop: modal sizing
|
||||
'w-full h-full md:rounded-lg md:shadow-2xl' +
|
||||
(isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]')
|
||||
}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 flex-shrink-0 min-w-0">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 min-w-0">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white text-sm font-mono">
|
||||
{file.name.split('.').pop()?.toUpperCase() || 'FILE'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">{file.name}</h3>
|
||||
<h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3>
|
||||
{file.diffInfo && (
|
||||
<span className="text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded whitespace-nowrap">
|
||||
📝 Has changes
|
||||
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-1 rounded whitespace-nowrap">
|
||||
Showing changes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 truncate">{file.path}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">{file.path}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
|
||||
{file.diffInfo && (
|
||||
<button
|
||||
onClick={() => setShowDiff(!showDiff)}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title={showDiff ? "Hide diff highlighting" : "Show diff highlighting"}
|
||||
>
|
||||
{showDiff ? <EyeOff className="w-5 h-5 md:w-4 md:h-4" /> : <Eye className="w-5 h-5 md:w-4 md:h-4" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setWordWrap(!wordWrap)}
|
||||
className={`p-2 md:p-2 rounded-md hover:bg-gray-100 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center ${
|
||||
wordWrap
|
||||
? 'text-blue-600 bg-blue-50'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
|
||||
>
|
||||
<span className="text-sm md:text-xs font-mono font-bold">↵</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<span className="text-lg md:text-base">{isDarkMode ? '☀️' : '🌙'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
@@ -496,13 +615,15 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center"
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
{!isSidebar && (
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center"
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -522,6 +643,9 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
onChange={setContent}
|
||||
extensions={[
|
||||
...getLanguageExtension(file.name),
|
||||
// Always show the toolbar
|
||||
...editorToolbarPanel,
|
||||
// Only show diff-related extensions when diff is enabled
|
||||
...(file.diffInfo && showDiff && file.diffInfo.old_string !== undefined
|
||||
? [
|
||||
unifiedMergeView({
|
||||
@@ -533,8 +657,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
// NOTE: NO collapseUnchanged - this shows the full file!
|
||||
}),
|
||||
...minimapExtension,
|
||||
...scrollToFirstChunkExtension,
|
||||
...diffNavigationPanel
|
||||
...scrollToFirstChunkExtension
|
||||
]
|
||||
: []),
|
||||
...(wordWrap ? [EditorView.lineWrapping] : [])
|
||||
@@ -542,11 +665,11 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
theme={isDarkMode ? oneDark : undefined}
|
||||
height="100%"
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontSize: `${fontSize}px`,
|
||||
height: '100%',
|
||||
}}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
lineNumbers: showLineNumbers,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
@@ -565,7 +688,6 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Lines: {content.split('\n').length}</span>
|
||||
<span>Characters: {content.length}</span>
|
||||
<span>Language: {file.name.split('.').pop()?.toUpperCase() || 'Text'}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* No session protection logic is implemented here - it's purely a props bridge.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import ChatInterface from './ChatInterface';
|
||||
import FileTree from './FileTree';
|
||||
import CodeEditor from './CodeEditor';
|
||||
@@ -28,13 +28,13 @@ import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
ws,
|
||||
sendMessage,
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
ws,
|
||||
sendMessage,
|
||||
messages,
|
||||
isMobile,
|
||||
isPWA,
|
||||
@@ -61,6 +61,10 @@ function MainContent({
|
||||
const [editingFile, setEditingFile] = useState(null);
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
||||
const [editorWidth, setEditorWidth] = useState(600);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [editorExpanded, setEditorExpanded] = useState(false);
|
||||
const resizeRef = useRef(null);
|
||||
|
||||
// PRD Editor state
|
||||
const [showPRDEditor, setShowPRDEditor] = useState(false);
|
||||
@@ -127,6 +131,11 @@ function MainContent({
|
||||
|
||||
const handleCloseEditor = () => {
|
||||
setEditingFile(null);
|
||||
setEditorExpanded(false);
|
||||
};
|
||||
|
||||
const handleToggleEditorExpand = () => {
|
||||
setEditorExpanded(!editorExpanded);
|
||||
};
|
||||
|
||||
const handleTaskClick = (task) => {
|
||||
@@ -153,6 +162,52 @@ function MainContent({
|
||||
console.log('Update task status:', taskId, newStatus);
|
||||
refreshTasks?.();
|
||||
};
|
||||
|
||||
// Handle resize functionality
|
||||
const handleMouseDown = (e) => {
|
||||
if (isMobile) return; // Disable resize on mobile
|
||||
setIsResizing(true);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const container = resizeRef.current?.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const newWidth = containerRect.right - e.clientX;
|
||||
|
||||
// Min width: 300px, Max width: 80% of container
|
||||
const minWidth = 300;
|
||||
const maxWidth = containerRect.width * 0.8;
|
||||
|
||||
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||
setEditorWidth(newWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
if (isResizing) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -234,10 +289,10 @@ function MainContent({
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header with tabs */}
|
||||
<div
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between relative">
|
||||
<div className="flex items-center space-x-2 sm:space-x-3">
|
||||
{isMobile && (
|
||||
<button
|
||||
@@ -409,11 +464,13 @@ function MainContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
||||
<ErrorBoundary showDetails={true}>
|
||||
<ChatInterface
|
||||
{/* Content Area with Right Sidebar */}
|
||||
<div className="flex-1 flex min-h-0 overflow-hidden">
|
||||
{/* Main Content */}
|
||||
<div className={`flex-1 flex flex-col min-h-0 overflow-hidden ${editingFile ? 'mr-0' : ''} ${editorExpanded ? 'hidden' : ''}`}>
|
||||
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
||||
<ErrorBoundary showDetails={true}>
|
||||
<ChatInterface
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
ws={ws}
|
||||
@@ -513,14 +570,49 @@ function MainContent({
|
||||
onClearLogs={() => setServerLogs([])}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Editor Right Sidebar - Desktop only, Mobile uses modal */}
|
||||
{editingFile && !isMobile && (
|
||||
<>
|
||||
{/* Resize Handle - Hidden when expanded */}
|
||||
{!editorExpanded && (
|
||||
<div
|
||||
ref={resizeRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
className="flex-shrink-0 w-1 bg-gray-200 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 cursor-col-resize transition-colors relative group"
|
||||
title="Drag to resize"
|
||||
>
|
||||
{/* Visual indicator on hover */}
|
||||
<div className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-1 bg-blue-500 dark:bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor Sidebar */}
|
||||
<div
|
||||
className={`flex-shrink-0 border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden ${editorExpanded ? 'flex-1' : ''}`}
|
||||
style={editorExpanded ? {} : { width: `${editorWidth}px` }}
|
||||
>
|
||||
<CodeEditor
|
||||
file={editingFile}
|
||||
onClose={handleCloseEditor}
|
||||
projectPath={selectedProject?.path}
|
||||
isSidebar={true}
|
||||
isExpanded={editorExpanded}
|
||||
onToggleExpand={handleToggleEditorExpand}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Code Editor Modal */}
|
||||
{editingFile && (
|
||||
{/* Code Editor Modal for Mobile */}
|
||||
{editingFile && isMobile && (
|
||||
<CodeEditor
|
||||
file={editingFile}
|
||||
onClose={handleCloseEditor}
|
||||
projectPath={selectedProject?.path}
|
||||
isSidebar={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={autoExpandTools}
|
||||
onChange={(e) => onAutoExpandChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -125,7 +125,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={showRawParameters}
|
||||
onChange={(e) => onShowRawParametersChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -138,7 +138,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={showThinking}
|
||||
onChange={(e) => onShowThinkingChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -155,7 +155,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={autoScrollToBottom}
|
||||
onChange={(e) => onAutoScrollChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -173,7 +173,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={sendByCtrlEnter}
|
||||
onChange={(e) => onSendByCtrlEnterChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3">
|
||||
|
||||
@@ -10,7 +10,7 @@ import ClaudeLogo from './ClaudeLogo';
|
||||
import CursorLogo from './CursorLogo';
|
||||
import CredentialsSettings from './CredentialsSettings';
|
||||
|
||||
function Settings({ isOpen, onClose, projects = [] }) {
|
||||
function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
const {
|
||||
tasksEnabled,
|
||||
@@ -52,9 +52,26 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
const [mcpTestResults, setMcpTestResults] = useState({});
|
||||
const [mcpServerTools, setMcpServerTools] = useState({});
|
||||
const [mcpToolsLoading, setMcpToolsLoading] = useState({});
|
||||
const [activeTab, setActiveTab] = useState('tools');
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
const [jsonValidationError, setJsonValidationError] = useState('');
|
||||
const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor'
|
||||
|
||||
// Code Editor settings
|
||||
const [codeEditorTheme, setCodeEditorTheme] = useState(() =>
|
||||
localStorage.getItem('codeEditorTheme') || 'dark'
|
||||
);
|
||||
const [codeEditorWordWrap, setCodeEditorWordWrap] = useState(() =>
|
||||
localStorage.getItem('codeEditorWordWrap') === 'true'
|
||||
);
|
||||
const [codeEditorShowMinimap, setCodeEditorShowMinimap] = useState(() =>
|
||||
localStorage.getItem('codeEditorShowMinimap') !== 'false' // Default true
|
||||
);
|
||||
const [codeEditorLineNumbers, setCodeEditorLineNumbers] = useState(() =>
|
||||
localStorage.getItem('codeEditorLineNumbers') !== 'false' // Default true
|
||||
);
|
||||
const [codeEditorFontSize, setCodeEditorFontSize] = useState(() =>
|
||||
localStorage.getItem('codeEditorFontSize') || '14'
|
||||
);
|
||||
|
||||
// Cursor-specific states
|
||||
const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
|
||||
@@ -327,8 +344,36 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadSettings();
|
||||
// Set the active tab when the modal opens
|
||||
setActiveTab(initialTab);
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, initialTab]);
|
||||
|
||||
// Persist code editor settings to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorTheme', codeEditorTheme);
|
||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||
}, [codeEditorTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorWordWrap', codeEditorWordWrap.toString());
|
||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||
}, [codeEditorWordWrap]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorShowMinimap', codeEditorShowMinimap.toString());
|
||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||
}, [codeEditorShowMinimap]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorLineNumbers', codeEditorLineNumbers.toString());
|
||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||
}, [codeEditorLineNumbers]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('codeEditorFontSize', codeEditorFontSize);
|
||||
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||
}, [codeEditorFontSize]);
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
@@ -625,7 +670,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-background/95">
|
||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[9999] md:p-4 bg-background/95">
|
||||
<div className="bg-background border border-border md:rounded-lg shadow-xl w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-border flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -758,6 +803,158 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Editor Settings */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">Code Editor</h3>
|
||||
|
||||
{/* Editor Theme */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Editor Theme
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Default theme for the code editor
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCodeEditorTheme(codeEditorTheme === 'dark' ? 'light' : 'dark')}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={codeEditorTheme === 'dark'}
|
||||
aria-label="Toggle editor theme"
|
||||
>
|
||||
<span className="sr-only">Toggle editor theme</span>
|
||||
<span
|
||||
className={`${
|
||||
codeEditorTheme === 'dark' ? 'translate-x-7' : 'translate-x-1'
|
||||
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
|
||||
>
|
||||
{codeEditorTheme === 'dark' ? (
|
||||
<Moon className="w-3.5 h-3.5 text-gray-700" />
|
||||
) : (
|
||||
<Sun className="w-3.5 h-3.5 text-yellow-500" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Word Wrap */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Word Wrap
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Enable word wrapping by default in the editor
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCodeEditorWordWrap(!codeEditorWordWrap)}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={codeEditorWordWrap}
|
||||
aria-label="Toggle word wrap"
|
||||
>
|
||||
<span className="sr-only">Toggle word wrap</span>
|
||||
<span
|
||||
className={`${
|
||||
codeEditorWordWrap ? 'translate-x-7' : 'translate-x-1'
|
||||
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show Minimap */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Show Minimap
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Display a minimap for easier navigation in diff view
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCodeEditorShowMinimap(!codeEditorShowMinimap)}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={codeEditorShowMinimap}
|
||||
aria-label="Toggle minimap"
|
||||
>
|
||||
<span className="sr-only">Toggle minimap</span>
|
||||
<span
|
||||
className={`${
|
||||
codeEditorShowMinimap ? 'translate-x-7' : 'translate-x-1'
|
||||
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show Line Numbers */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Show Line Numbers
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Display line numbers in the editor
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCodeEditorLineNumbers(!codeEditorLineNumbers)}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={codeEditorLineNumbers}
|
||||
aria-label="Toggle line numbers"
|
||||
>
|
||||
<span className="sr-only">Toggle line numbers</span>
|
||||
<span
|
||||
className={`${
|
||||
codeEditorLineNumbers ? 'translate-x-7' : 'translate-x-1'
|
||||
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Font Size */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Font Size
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Editor font size in pixels
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
value={codeEditorFontSize}
|
||||
onChange={(e) => setCodeEditorFontSize(e.target.value)}
|
||||
className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-24"
|
||||
>
|
||||
<option value="10">10px</option>
|
||||
<option value="11">11px</option>
|
||||
<option value="12">12px</option>
|
||||
<option value="13">13px</option>
|
||||
<option value="14">14px</option>
|
||||
<option value="15">15px</option>
|
||||
<option value="16">16px</option>
|
||||
<option value="18">18px</option>
|
||||
<option value="20">20px</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -818,7 +1015,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
type="checkbox"
|
||||
checked={skipPermissions}
|
||||
onChange={(e) => setSkipPermissions(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||
@@ -1578,7 +1775,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
type="checkbox"
|
||||
checked={cursorSkipPermissions}
|
||||
onChange={(e) => setCursorSkipPermissions(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||
|
||||
@@ -39,12 +39,12 @@ const formatTimeAgo = (dateString, currentTime) => {
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
function Sidebar({
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
onProjectSelect,
|
||||
onSessionSelect,
|
||||
function Sidebar({
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
onProjectSelect,
|
||||
onSessionSelect,
|
||||
onNewSession,
|
||||
onSessionDelete,
|
||||
onProjectDelete,
|
||||
@@ -54,9 +54,11 @@ function Sidebar({
|
||||
updateAvailable,
|
||||
latestVersion,
|
||||
currentVersion,
|
||||
releaseInfo,
|
||||
onShowVersionModal,
|
||||
isPWA,
|
||||
isMobile
|
||||
isMobile,
|
||||
onToggleSidebar
|
||||
}) {
|
||||
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
||||
const [editingProject, setEditingProject] = useState(null);
|
||||
@@ -586,34 +588,24 @@ function Sidebar({
|
||||
<p className="text-sm text-muted-foreground">AI coding assistant interface</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{onToggleSidebar && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 w-9 px-0 hover:bg-accent transition-colors duration-200 group"
|
||||
onClick={async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}}
|
||||
disabled={isRefreshing}
|
||||
title="Refresh projects and sessions (Ctrl+R)"
|
||||
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200"
|
||||
onClick={onToggleSidebar}
|
||||
title="Hide sidebar"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-9 w-9 px-0 bg-primary hover:bg-primary/90 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||
onClick={() => setShowNewProject(true)}
|
||||
title="Create new project (Ctrl+N)"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Header */}
|
||||
@@ -913,9 +905,9 @@ function Sidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Filter */}
|
||||
{/* Search Filter and Actions */}
|
||||
{projects.length > 0 && !isLoading && (
|
||||
<div className="px-3 md:px-4 py-2 border-b border-border">
|
||||
<div className="px-3 md:px-4 py-2 border-b border-border space-y-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -934,6 +926,39 @@ function Sidebar({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - Desktop only */}
|
||||
{!isMobile && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200"
|
||||
onClick={() => setShowNewProject(true)}
|
||||
title="Create new project (Ctrl+N)"
|
||||
>
|
||||
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
|
||||
New Project
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
|
||||
onClick={async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}}
|
||||
disabled={isRefreshing}
|
||||
title="Refresh projects and sessions (Ctrl+R)"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1611,8 +1636,10 @@ function Sidebar({
|
||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">Update Available</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">Version {latestVersion} is ready</div>
|
||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||
{releaseInfo?.title || `Version ${latestVersion}`}
|
||||
</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">Update available</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1630,8 +1657,10 @@ function Sidebar({
|
||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">Update Available</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">Version {latestVersion} is ready</div>
|
||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||
{releaseInfo?.title || `Version ${latestVersion}`}
|
||||
</div>
|
||||
<div className="text-xs text-blue-600 dark:text-blue-400">Update available</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,28 +5,39 @@ import { version } from '../../package.json';
|
||||
export const useVersionCheck = (owner, repo) => {
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||
const [latestVersion, setLatestVersion] = useState(null);
|
||||
const [releaseInfo, setReleaseInfo] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkVersion = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
// Handle the case where there might not be any releases
|
||||
if (data.tag_name) {
|
||||
const latest = data.tag_name.replace(/^v/, '');
|
||||
setLatestVersion(latest);
|
||||
setUpdateAvailable(version !== latest);
|
||||
|
||||
// Store release information
|
||||
setReleaseInfo({
|
||||
title: data.name || data.tag_name,
|
||||
body: data.body || '',
|
||||
htmlUrl: data.html_url || `https://github.com/${owner}/${repo}/releases/latest`,
|
||||
publishedAt: data.published_at
|
||||
});
|
||||
} else {
|
||||
// No releases found, don't show update notification
|
||||
setUpdateAvailable(false);
|
||||
setLatestVersion(null);
|
||||
setReleaseInfo(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Version check failed:', error);
|
||||
// On error, don't show update notification
|
||||
setUpdateAvailable(false);
|
||||
setLatestVersion(null);
|
||||
setReleaseInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,5 +46,5 @@ export const useVersionCheck = (owner, repo) => {
|
||||
return () => clearInterval(interval);
|
||||
}, [owner, repo]);
|
||||
|
||||
return { updateAvailable, latestVersion, currentVersion: version };
|
||||
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo };
|
||||
};
|
||||
@@ -825,4 +825,16 @@
|
||||
background-color: rgb(31 41 55) !important;
|
||||
color: rgb(243 244 246) !important;
|
||||
}
|
||||
|
||||
/* Tool details chevron animation */
|
||||
details[open] .details-chevron,
|
||||
details[open] summary svg[class*="group-open"] {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Smooth chevron transition */
|
||||
.details-chevron,
|
||||
summary svg[class*="transition-transform"] {
|
||||
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user