From 97faff3ba54645bebb6a22958ef0a56fc1c3bb82 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:17:07 -0400 Subject: [PATCH] Add CodeBlock component --- src/ClaudeDev.ts | 90 ++++++-- webview-ui/package-lock.json | 281 ++++++++++++++++++++++++ webview-ui/package.json | 2 + webview-ui/src/components/ChatRow.tsx | 64 +++--- webview-ui/src/components/CodeBlock.tsx | 143 ++++++++++++ webview-ui/src/index.css | 44 +++- 6 files changed, 571 insertions(+), 53 deletions(-) create mode 100644 webview-ui/src/components/CodeBlock.tsx diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index ca3b2cf..fac5310 100644 --- a/src/ClaudeDev.ts +++ b/src/ClaudeDev.ts @@ -69,7 +69,8 @@ const tools: Tool[] = [ properties: { command: { type: "string", - description: "The CLI command to execute. This should be a valid command-line instruction for the current operating system. For example, 'ls -l' on Unix-like systems or 'dir' on Windows. Ensure the command is properly formatted and does not contain any harmful instructions. Avoid commands that run indefinitely (like servers) that don't terminate on their own.", + description: + "The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Avoid commands that run indefinitely (like servers) that don't terminate on their own.", }, }, required: ["command"], @@ -77,7 +78,8 @@ const tools: Tool[] = [ }, { name: "list_files", - description: "List all files and directories at the top level of the specified directory. Use this to understand the contents and structure of a directory by examining file names and extensions. This information can guide decision-making on which files to process or which subdirectories to explore further. To investigate subdirectories, call this tool again with the path of the subdirectory.", + description: + "List all files and directories at the top level of the specified directory. Use this to understand the contents and structure of a directory by examining file names and extensions. This information can guide decision-making on which files to process or which subdirectories to explore further. To investigate subdirectories, call this tool again with the path of the subdirectory.", input_schema: { type: "object", properties: { @@ -99,7 +101,8 @@ const tools: Tool[] = [ properties: { path: { type: "string", - description: "The path of the file to read. Do not use absolute paths or attempt to access files outside of the current working directory.", + description: + "The path of the file to read. Do not use absolute paths or attempt to access files outside of the current working directory.", }, }, required: ["path"], @@ -114,7 +117,8 @@ const tools: Tool[] = [ properties: { path: { type: "string", - description: "The path of the file to write to. Do not use absolute paths or attempt to write to files outside of the current working directory.", + description: + "The path of the file to write to. Do not use absolute paths or attempt to write to files outside of the current working directory.", }, content: { type: "string", @@ -133,7 +137,8 @@ const tools: Tool[] = [ properties: { question: { type: "string", - description: "The question to ask the user. This should be a clear, specific question that addresses the information you need.", + description: + "The question to ask the user. This should be a clear, specific question that addresses the information you need.", }, }, required: ["question"], @@ -148,11 +153,13 @@ const tools: Tool[] = [ properties: { command: { type: "string", - description: "The CLI command to execute to show a live demo of the result to the user. For example, use 'open -a \"Google Chrome\" index.html' to display a created website. Avoid commands that run indefinitely (like servers) that don't terminate on their own. Instead, if such a command is needed, include instructions for the user to run it in the 'result' parameter.", + description: + "The CLI command to execute to show a live demo of the result to the user. For example, use 'open -a \"Google Chrome\" index.html' to display a created website. Avoid commands that run indefinitely (like servers) that don't terminate on their own. Instead, if such a command is needed, include instructions for the user to run it in the 'result' parameter.", }, result: { type: "string", - description: "The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.", + description: + "The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.", }, }, required: ["result"], @@ -227,7 +234,7 @@ Default Shell: ${defaultShell} Current Working Directory: ${process.cwd()} ## Files in Current Directory ${filesInCurrentDir}` - + const activeEditorContents = vscode.window.activeTextEditor?.document.getText() if (activeEditorContents) { userPrompt += ` @@ -305,7 +312,37 @@ ${activeEditorContents}` const diffResult = diff.createPatch(filePath, originalContent, newContent) if (diffResult) { await fs.writeFile(filePath, newContent) - this.say("tool", JSON.stringify({ tool: "editedExistingFile", path: filePath, diff: diffResult } as ClaudeSayTool)) + + // Create diff for DiffCodeView.tsx + const diffStringRaw = diff.diffLines(originalContent, newContent) + const diffStringConverted = diffStringRaw + .map((part, index) => { + const prefix = part.added ? "+ " : part.removed ? "- " : " " + return part.value + .split("\n") + .map((line, lineIndex) => { + // avoid adding an extra empty line at the very end of the diff output + if ( + line === "" && + index === diffStringRaw.length - 1 && + lineIndex === part.value.split("\n").length - 1 + ) { + return null + } + return prefix + line + "\n" + }) + .join("") + }) + .join("") + this.say( + "tool", + JSON.stringify({ + tool: "editedExistingFile", + path: filePath, + diff: diffStringConverted, + } as ClaudeSayTool) + ) + return `Changes applied to ${filePath}:\n${diffResult}` } else { this.say("tool", JSON.stringify({ tool: "editedExistingFile", path: filePath } as ClaudeSayTool)) @@ -314,12 +351,15 @@ ${activeEditorContents}` } else { await fs.mkdir(path.dirname(filePath), { recursive: true }) await fs.writeFile(filePath, newContent) - this.say("tool", JSON.stringify({ tool: "newFileCreated", path: filePath, content: newContent } as ClaudeSayTool)) + this.say( + "tool", + JSON.stringify({ tool: "newFileCreated", path: filePath, content: newContent } as ClaudeSayTool) + ) return `New file created and content written to ${filePath}` } } catch (error) { const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}` - this.say("error", JSON.stringify(serializeError(error))) + this.say("error", `Error writing file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`) return errorString } } @@ -327,11 +367,11 @@ ${activeEditorContents}` async readFile(filePath: string): Promise { try { const content = await fs.readFile(filePath, "utf-8") - this.say("tool", JSON.stringify({ tool: "readFile", path: filePath } as ClaudeSayTool)) + this.say("tool", JSON.stringify({ tool: "readFile", path: filePath, content } as ClaudeSayTool)) return content } catch (error) { const errorString = `Error reading file: ${JSON.stringify(serializeError(error))}` - this.say("error", JSON.stringify(serializeError(error))) + this.say("error", `Error reading file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`) return errorString } } @@ -343,7 +383,7 @@ ${activeEditorContents}` const isRoot = cwd === root if (isRoot) { if (shouldLog) { - this.say("tool", JSON.stringify({ tool: "listFiles", path: dirPath } as ClaudeSayTool)) + this.say("tool", JSON.stringify({ tool: "listFiles", path: dirPath, content: "/" } as ClaudeSayTool)) } return "Currently in the root directory. Cannot list all files." } @@ -356,13 +396,14 @@ ${activeEditorContents}` } // * globs all files in one dir, ** globs files in nested directories const entries = await glob("*", options) + const result = entries.slice(0, 500).join("\n") // truncate to 500 entries if (shouldLog) { - this.say("tool", JSON.stringify({ tool: "listFiles", path: dirPath } as ClaudeSayTool)) + this.say("tool", JSON.stringify({ tool: "listFiles", path: dirPath, content: result } as ClaudeSayTool)) } - return entries.slice(0, 500).join("\n") // truncate to 500 entries + return result } catch (error) { const errorString = `Error listing files and directories: ${JSON.stringify(serializeError(error))}` - this.say("error", JSON.stringify(serializeError(error))) + this.say("error", `Error listing files and directories:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`) return errorString } } @@ -384,9 +425,9 @@ ${activeEditorContents}` return `Command executed successfully. Output:\n${result}` } catch (e) { const error = e as any - let errorMessage = error.message || JSON.stringify(serializeError(error)) + let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2) const errorString = `Error executing command:\n${errorMessage}` - this.say("error", errorMessage) + this.say("error", `Error executing command:\n${errorMessage}`) // TODO: in webview show code block for command errors return errorString } } @@ -456,7 +497,14 @@ ${activeEditorContents}` let assistantResponses: Anthropic.Messages.ContentBlock[] = [] let inputTokens = response.usage.input_tokens let outputTokens = response.usage.output_tokens - await this.say("api_req_finished", JSON.stringify({ tokensIn: inputTokens, tokensOut: outputTokens, cost: this.calculateApiCost(inputTokens, outputTokens) })) + await this.say( + "api_req_finished", + JSON.stringify({ + tokensIn: inputTokens, + tokensOut: outputTokens, + cost: this.calculateApiCost(inputTokens, outputTokens), + }) + ) // A response always returns text content blocks (it's just that before we were iterating over the completion_attempt response before we could append text response, resulting in bug) for (const contentBlock of response.content) { @@ -547,7 +595,7 @@ ${activeEditorContents}` return { didCompleteTask, inputTokens, outputTokens } } catch (error) { // only called if the API request fails (executeTool errors are returned back to claude) - this.say("error", JSON.stringify(serializeError(error))) + this.say("error", `API request failed:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`) return { didCompleteTask: true, inputTokens: 0, outputTokens: 0 } } } diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 0b10c93..15856b5 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -19,6 +19,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", + "react-syntax-highlighter": "^15.5.0", "react-text-truncate": "^0.19.0", "react-textarea-autosize": "^8.5.3", "rewire": "^7.0.0", @@ -26,6 +27,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@types/react-syntax-highlighter": "^15.5.13", "@types/react-text-truncate": "^0.14.4", "@types/vscode-webview": "^1.57.5" } @@ -4421,6 +4423,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4564,6 +4575,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-text-truncate": { "version": "0.14.4", "resolved": "https://registry.npmjs.org/@types/react-text-truncate/-/react-text-truncate-0.14.4.tgz", @@ -4655,6 +4676,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "license": "MIT" + }, "node_modules/@types/vscode-webview": { "version": "1.57.5", "resolved": "https://registry.npmjs.org/@types/vscode-webview/-/vscode-webview-1.57.5.tgz", @@ -6328,6 +6355,36 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -6501,6 +6558,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -8825,6 +8892,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -9268,6 +9348,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -9710,6 +9798,33 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -9719,6 +9834,15 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -10153,6 +10277,30 @@ "node": ">= 10" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -10303,6 +10451,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -10384,6 +10542,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -13307,6 +13475,20 @@ "tslib": "^2.0.3" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -14058,6 +14240,24 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -15629,6 +15829,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -15674,6 +15883,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -16155,6 +16377,22 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", + "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/react-text-truncate": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.19.0.tgz", @@ -16266,6 +16504,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -17213,6 +17475,16 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -19659,6 +19931,15 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 00114c4..eec129b 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -14,6 +14,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", + "react-syntax-highlighter": "^15.5.0", "react-text-truncate": "^0.19.0", "react-textarea-autosize": "^8.5.3", "rewire": "^7.0.0", @@ -45,6 +46,7 @@ ] }, "devDependencies": { + "@types/react-syntax-highlighter": "^15.5.13", "@types/react-text-truncate": "^0.14.4", "@types/vscode-webview": "^1.57.5" } diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index 8452e3b..03b7b0a 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -2,6 +2,9 @@ import React, { useState } from "react" import { ClaudeMessage, ClaudeAsk, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage" import { VSCodeButton, VSCodeProgressRing, VSCodeBadge } from "@vscode/webview-ui-toolkit/react" import { COMMAND_OUTPUT_STRING } from "../utilities/combineCommandSequences" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { dark } from "react-syntax-highlighter/dist/esm/styles/prism" +import CodeBlock from "./CodeBlock" interface ChatRowProps { message: ClaudeMessage @@ -45,13 +48,6 @@ const ChatRow: React.FC = ({ message }) => { style={{ color: successColor, marginBottom: "-1.5px" }}>, Task Completed, ] - case "tool": - return [ - , - Tool, - ] case "api_req_started": return [ cost ? ( @@ -123,47 +119,51 @@ const ChatRow: React.FC = ({ message }) => { tool: "editedExistingFile", path: "/path/to/file", } + const toolIcon = (name: string) => ( + + ) + switch (tool.tool) { case "editedExistingFile": return ( <>
- {icon} - Edited File + {toolIcon("edit")} + Edited file...
-

Path: {tool.path!}

-

{tool.diff!}

+ ) case "newFileCreated": return ( <>
- {icon} - Created New File + {toolIcon("new-file")} + Created new file...
-

Path: {tool.path!}

-

{tool.content!}

+ ) case "readFile": return ( <>
- {icon} - Read File + {toolIcon("file-code")} + Read file...
-

Path: {tool.path!}

+ ) case "listFiles": return ( <>
- {icon} - Viewed Directory + {toolIcon("folder-opened")} + Viewed contents of directory...
-

Path: {tool.path!}

+ ) } @@ -244,14 +244,24 @@ const ChatRow: React.FC = ({ message }) => { {title}
-

Claude Dev wants to execute the following terminal command. Would you like to proceed?

-

{command}

+

+ Claude Dev wants to execute the following terminal command. Would you like to + proceed? +

+
+ +
+ {output && ( <> -

+

{COMMAND_OUTPUT_STRING}

-

{output}

+ )}
@@ -300,7 +310,9 @@ const ChatRow: React.FC = ({ message }) => { }}> {renderContent()} {isExpanded && message.say === "api_req_started" && ( -

{JSON.stringify(JSON.parse(message.text || "{}").request)}

+
+ +
)} ) diff --git a/webview-ui/src/components/CodeBlock.tsx b/webview-ui/src/components/CodeBlock.tsx new file mode 100644 index 0000000..e6a5a38 --- /dev/null +++ b/webview-ui/src/components/CodeBlock.tsx @@ -0,0 +1,143 @@ +import React, { useState } from "react" +import SyntaxHighlighter from "react-syntax-highlighter" +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism" +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +/* +const vscodeSyntaxStyle: React.CSSProperties = { + backgroundColor: "var(--vscode-editor-background)", + color: "var(--vscode-editor-foreground)", + fontFamily: "var(--vscode-editor-font-family)", + fontSize: "var(--vscode-editor-font-size)", + lineHeight: "var(--vscode-editor-line-height)", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + tabSize: 4, + hyphens: "none", + padding: "1em", + margin: "0.5em 0", + overflow: "auto", + borderRadius: "6px", +} + +const tokenStyles = { + comment: { color: "var(--vscode-editor-foreground)" }, + prolog: { color: "var(--vscode-editor-foreground)" }, + doctype: { color: "var(--vscode-editor-foreground)" }, + cdata: { color: "var(--vscode-editor-foreground)" }, + punctuation: { color: "var(--vscode-editor-foreground)" }, + property: { color: "var(--vscode-symbolIcon-propertyForeground)" }, + tag: { color: "var(--vscode-symbolIcon-colorForeground)" }, + boolean: { color: "var(--vscode-symbolIcon-booleanForeground)" }, + number: { color: "var(--vscode-symbolIcon-numberForeground)" }, + constant: { color: "var(--vscode-symbolIcon-constantForeground)" }, + symbol: { color: "var(--vscode-symbolIcon-colorForeground)" }, + selector: { color: "var(--vscode-symbolIcon-colorForeground)" }, + "attr-name": { color: "var(--vscode-symbolIcon-propertyForeground)" }, + string: { color: "var(--vscode-symbolIcon-stringForeground)" }, + char: { color: "var(--vscode-symbolIcon-stringForeground)" }, + builtin: { color: "var(--vscode-symbolIcon-keywordForeground)" }, + inserted: { color: "var(--vscode-gitDecoration-addedResourceForeground)" }, + operator: { color: "var(--vscode-symbolIcon-operatorForeground)" }, + entity: { color: "var(--vscode-symbolIcon-snippetForeground)", cursor: "help" }, + url: { color: "var(--vscode-textLink-foreground)" }, + variable: { color: "var(--vscode-symbolIcon-variableForeground)" }, + atrule: { color: "var(--vscode-symbolIcon-keywordForeground)" }, + "attr-value": { color: "var(--vscode-symbolIcon-stringForeground)" }, + keyword: { color: "var(--vscode-symbolIcon-keywordForeground)" }, + function: { color: "var(--vscode-symbolIcon-functionForeground)" }, + regex: { color: "var(--vscode-symbolIcon-regexForeground)" }, + important: { color: "var(--vscode-editorWarning-foreground)", fontWeight: "bold" }, + bold: { fontWeight: "bold" }, + italic: { fontStyle: "italic" }, + deleted: { color: "var(--vscode-gitDecoration-deletedResourceForeground)" }, +} +*/ + +interface CodeBlockProps { + code?: string + diff?: string + language?: string | undefined + path?: string +} + +const CodeBlock = ({ code, diff, language, path }: CodeBlockProps) => { + const [isExpanded, setIsExpanded] = useState(false) + + const backgroundColor = oneDark['pre[class*="language-"]'].background as string + + return ( +
+ {path && ( +
+ + {path} + + setIsExpanded(!isExpanded)}> + + +
+ )} + {(!path || isExpanded) && ( +
+ { + const line = diff?.split("\n")?.[lineNumber - 1] + let style: React.CSSProperties = { display: "block", width: "100%" } + if (line && line[0] === "+") { + style.backgroundColor = "var(--vscode-diffEditor-insertedTextBackground)" + } else if (line && line[0] === "-") { + style.backgroundColor = "var(--vscode-diffEditor-removedTextBackground)" + } + return { style } + } + : undefined + }> + {code ?? diff ?? ""} + +
+ )} +
+ ) +} + +export default CodeBlock diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index c6b3cc2..917fe96 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -34,7 +34,7 @@ body { } body.scrollable, -.scrollable { +.scrollable, body.code-block-scrollable, .code-block-scrollable { border-color: transparent; transition: border-color 0.7s linear; } @@ -42,16 +42,21 @@ body.scrollable, body:hover.scrollable, body:hover .scrollable, body:focus-within.scrollable, -body:focus-within .scrollable { +body:focus-within .scrollable, +body:hover.code-block-scrollable, +body:hover .code-block-scrollable, +body:focus-within.code-block-scrollable, +body:focus-within .code-block-scrollable +{ border-color: var(--vscode-scrollbarSlider-background); transition: none; } -::-webkit-scrollbar-corner { +.scrollable::-webkit-scrollbar-corner { background-color: transparent !important; } -::-webkit-scrollbar-thumb { +.scrollable::-webkit-scrollbar-thumb { background-color: transparent; border-color: inherit; border-right-style: inset; @@ -59,11 +64,11 @@ body:focus-within .scrollable { border-radius: unset !important; } -::-webkit-scrollbar-thumb:hover { +.scrollable::-webkit-scrollbar-thumb:hover { border-color: var(--vscode-scrollbarSlider-hoverBackground); } -::-webkit-scrollbar-thumb:active { +.scrollable::-webkit-scrollbar-thumb:active { border-color: var(--vscode-scrollbarSlider-activeBackground); } @@ -75,4 +80,31 @@ https://github.com/microsoft/vscode/issues/213045 html { scrollbar-color: unset; } +} + +/* +The above scrollbar styling uses some transparent background color magic to accomplish its animation. However this doesn't play nicely with SyntaxHighlighter, so we need to set a background color for the code blocks' horizontal scrollbar. This actually has the unintended consequence of always showing the scrollbar which I prefer since it makes it more obvious that there is more content to scroll to. +*/ + +.code-block-scrollable::-webkit-scrollbar-track { + background: transparent; +} + +.code-block-scrollable::-webkit-scrollbar-thumb { + background-color: var(--vscode-scrollbarSlider-background); + border-radius: 5px; + border: 2px solid transparent; + background-clip: content-box; +} + +.code-block-scrollable::-webkit-scrollbar-thumb:hover { + background-color: var(--vscode-scrollbarSlider-hoverBackground); +} + +.code-block-scrollable::-webkit-scrollbar-thumb:active { + background-color: var(--vscode-scrollbarSlider-activeBackground); +} + +.code-block-scrollable::-webkit-scrollbar-corner { + background-color: transparent; } \ No newline at end of file