diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index 90fc385..3c1bbef 100644 --- a/src/ClaudeDev.ts +++ b/src/ClaudeDev.ts @@ -1138,7 +1138,7 @@ export class ClaudeDev { const message = JSON.stringify({ tool: "readFile", path: this.getReadablePath(relPath), - content, + content: absolutePath, } as ClaudeSayTool) if (this.alwaysAllowReadOnly) { await this.say("tool", message) diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index 8d1d513..7414806 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -10,7 +10,7 @@ import fs from "fs/promises" import { HistoryItem } from "../shared/HistoryItem" import axios from "axios" import { getTheme } from "../utils/getTheme" -import { openImage } from "../utils/open-image" +import { openFile, openImage } from "../utils/open-file" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -402,6 +402,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { case "openImage": openImage(message.text!) break + case "openFile": + openFile(message.text!) + break // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index c3a35f0..c7bc8e4 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -18,6 +18,7 @@ export interface WebviewMessage { | "resetState" | "requestOllamaModels" | "openImage" + | "openFile" text?: string askResponse?: ClaudeAskResponse apiConfiguration?: ApiConfiguration diff --git a/src/utils/open-file.ts b/src/utils/open-file.ts new file mode 100644 index 0000000..ce3f590 --- /dev/null +++ b/src/utils/open-file.ts @@ -0,0 +1,50 @@ +import * as path from "path" +import * as os from "os" +import * as vscode from "vscode" + +export async function openImage(dataUri: string) { + const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) + if (!matches) { + vscode.window.showErrorMessage("Invalid data URI format") + return + } + const [, format, base64Data] = matches + const imageBuffer = Buffer.from(base64Data, "base64") + const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`) + try { + await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer) + await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath)) + } catch (error) { + vscode.window.showErrorMessage(`Error opening image: ${error}`) + } +} + +export async function openFile(absolutePath: string) { + try { + const uri = vscode.Uri.file(absolutePath) + + // Check if the document is already open in a tab group that's not in the active editor's column. If it is, then close it (if not dirty) so that we don't duplicate tabs + try { + for (const group of vscode.window.tabGroups.all) { + const existingTab = group.tabs.find( + (tab) => tab.input instanceof vscode.TabInputText && tab.input.uri.fsPath === uri.fsPath + ) + if (existingTab) { + const activeColumn = vscode.window.activeTextEditor?.viewColumn + const tabColumn = vscode.window.tabGroups.all.find((group) => + group.tabs.includes(existingTab) + )?.viewColumn + if (activeColumn && activeColumn !== tabColumn && !existingTab.isDirty) { + await vscode.window.tabGroups.close(existingTab) + } + break + } + } + } catch {} // not essential, sometimes tab operations fail + + const document = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(document, { preview: false }) + } catch (error) { + vscode.window.showErrorMessage(`Could not open file!`) + } +} diff --git a/src/utils/open-image.ts b/src/utils/open-image.ts deleted file mode 100644 index 42e09ee..0000000 --- a/src/utils/open-image.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as path from "path" -import * as os from "os" -import * as vscode from "vscode" - -export async function openImage(dataUri: string) { - const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) - if (!matches) { - vscode.window.showErrorMessage("Invalid data URI format") - return - } - const [, format, base64Data] = matches - const imageBuffer = Buffer.from(base64Data, "base64") - const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`) - try { - await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer) - await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath)) - } catch (error) { - vscode.window.showErrorMessage(`Error opening image: ${error}`) - } -} diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index 256e95c..8ca4560 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -4,9 +4,10 @@ import React, { memo, useMemo } from "react" import ReactMarkdown from "react-markdown" import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage" import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences" -import CodeAccordian from "./CodeAccordian" +import CodeAccordian, { removeLeadingNonAlphanumeric } from "./CodeAccordian" import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock" import Thumbnails from "./Thumbnails" +import { vscode } from "../utils/vscode" interface ChatRowProps { message: ClaudeMessage @@ -190,12 +191,54 @@ const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessa {message.type === "ask" ? "Claude wants to read this file:" : "Claude read this file:"} - + /> */} +
+
{ + vscode.postMessage({ type: "openFile", text: tool.content }) + }}> +
+ + {removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"} + +
+ +
+
) case "listFilesTopLevel": diff --git a/webview-ui/src/components/CodeAccordian.tsx b/webview-ui/src/components/CodeAccordian.tsx index 6fd3dc7..a6530bf 100644 --- a/webview-ui/src/components/CodeAccordian.tsx +++ b/webview-ui/src/components/CodeAccordian.tsx @@ -18,7 +18,7 @@ We need to remove leading non-alphanumeric characters from the path in order for [^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric. The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character. */ -const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "") +export const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "") const CodeAccordian = ({ code, diff, language, path, isFeedback, isExpanded, onToggleExpand }: CodeAccordianProps) => { const inferredLanguage = useMemo(