Ver Fonte

Show LLM streaming file write content (#3241)

Chris Estreich há 7 meses atrás
pai
commit
d3c469391f

+ 5 - 2
src/core/tools/applyDiffTool.ts

@@ -31,6 +31,7 @@ export async function applyDiffTool(
 	const sharedMessageProps: ClineSayTool = {
 		tool: "appliedDiff",
 		path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
+		diff: diffContent,
 	}
 
 	try {
@@ -46,8 +47,10 @@ export async function applyDiffTool(
 				return
 			}
 
-			const partialMessage = JSON.stringify(sharedMessageProps)
-			await cline.ask("tool", partialMessage, block.partial, toolProgressStatus).catch(() => {})
+			await cline
+				.ask("tool", JSON.stringify(sharedMessageProps), block.partial, toolProgressStatus)
+				.catch(() => {})
+
 			return
 		} else {
 			if (!relPath) {

+ 11 - 10
src/core/tools/insertContentTool.ts

@@ -26,13 +26,13 @@ export async function insertContentTool(
 	const sharedMessageProps: ClineSayTool = {
 		tool: "insertContent",
 		path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
+		diff: content,
 		lineNumber: line ? parseInt(line, 10) : undefined,
 	}
 
 	try {
 		if (block.partial) {
-			const partialMessage = JSON.stringify(sharedMessageProps)
-			await cline.ask("tool", partialMessage, block.partial).catch(() => {})
+			await cline.ask("tool", JSON.stringify(sharedMessageProps), block.partial).catch(() => {})
 			return
 		}
 
@@ -145,14 +145,15 @@ export async function insertContentTool(
 			return
 		}
 
-		const userFeedbackDiff = JSON.stringify({
-			tool: "insertContent",
-			path: getReadablePath(cline.cwd, relPath),
-			lineNumber: lineNumber,
-			diff: userEdits,
-		} satisfies ClineSayTool)
-
-		await cline.say("user_feedback_diff", userFeedbackDiff)
+		await cline.say(
+			"user_feedback_diff",
+			JSON.stringify({
+				tool: "insertContent",
+				path: getReadablePath(cline.cwd, relPath),
+				diff: userEdits,
+				lineNumber: lineNumber,
+			} satisfies ClineSayTool),
+		)
 
 		pushToolResult(
 			`The user made the following updates to your content:\n\n${userEdits}\n\n` +

+ 8 - 7
src/core/tools/searchAndReplaceTool.ts

@@ -226,13 +226,14 @@ export async function searchAndReplaceTool(
 			return
 		}
 
-		const userFeedbackDiff = JSON.stringify({
-			tool: "appliedDiff",
-			path: getReadablePath(cline.cwd, relPath),
-			diff: userEdits,
-		} satisfies ClineSayTool)
-
-		await cline.say("user_feedback_diff", userFeedbackDiff)
+		await cline.say(
+			"user_feedback_diff",
+			JSON.stringify({
+				tool: "appliedDiff",
+				path: getReadablePath(cline.cwd, relPath),
+				diff: userEdits,
+			} satisfies ClineSayTool),
+		)
 
 		// Format and send response with user's updates
 		const resultMessage = [

+ 1 - 0
src/core/tools/writeToFileTool.ts

@@ -72,6 +72,7 @@ export async function writeToFileTool(
 	const sharedMessageProps: ClineSayTool = {
 		tool: fileExists ? "editedExistingFile" : "newFileCreated",
 		path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
+		content: newContent,
 		isOutsideWorkspace,
 	}
 

+ 33 - 58
webview-ui/src/components/chat/ChatRow.tsx

@@ -12,10 +12,12 @@ import { useCopyToClipboard } from "@src/utils/clipboard"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
 import { vscode } from "@src/utils/vscode"
+import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
 import { Button } from "@src/components/ui"
 
-import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
-import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
+import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
+import CodeAccordian from "../common/CodeAccordian"
+import CodeBlock from "../common/CodeBlock"
 import MarkdownBlock from "../common/MarkdownBlock"
 import { ReasoningBlock } from "./ReasoningBlock"
 import Thumbnails from "../common/Thumbnails"
@@ -287,10 +289,11 @@ export const ChatRowContent = ({
 							</span>
 						</div>
 						<CodeAccordian
+							path={tool.path}
+							code={tool.content ?? tool.diff}
+							language={tool.tool === "appliedDiff" ? "diff" : undefined}
 							progressStatus={message.progressStatus}
 							isLoading={message.partial}
-							diff={tool.diff!}
-							path={tool.path!}
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
 						/>
@@ -312,10 +315,11 @@ export const ChatRowContent = ({
 							</span>
 						</div>
 						<CodeAccordian
+							path={tool.path}
+							code={tool.diff}
+							language="diff"
 							progressStatus={message.progressStatus}
 							isLoading={message.partial}
-							diff={tool.diff!}
-							path={tool.path!}
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
 						/>
@@ -333,10 +337,10 @@ export const ChatRowContent = ({
 							</span>
 						</div>
 						<CodeAccordian
+							path={tool.path}
+							code={tool.diff}
 							progressStatus={message.progressStatus}
 							isLoading={message.partial}
-							diff={tool.diff!}
-							path={tool.path!}
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
 						/>
@@ -350,9 +354,9 @@ export const ChatRowContent = ({
 							<span style={{ fontWeight: "bold" }}>{t("chat:fileOperations.wantsToCreate")}</span>
 						</div>
 						<CodeAccordian
+							path={tool.path}
+							code={tool.content}
 							isLoading={message.partial}
-							code={tool.content!}
-							path={tool.path!}
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
 						/>
@@ -371,47 +375,21 @@ export const ChatRowContent = ({
 									: t("chat:fileOperations.didRead")}
 							</span>
 						</div>
-						<div
-							style={{
-								borderRadius: 3,
-								backgroundColor: CODE_BLOCK_BG_COLOR,
-								overflow: "hidden",
-								border: "1px solid var(--vscode-editorGroup-border)",
-							}}>
-							<div
-								style={{
-									color: "var(--vscode-descriptionForeground)",
-									display: "flex",
-									alignItems: "center",
-									padding: "9px 10px",
-									cursor: "pointer",
-									userSelect: "none",
-									WebkitUserSelect: "none",
-									MozUserSelect: "none",
-									msUserSelect: "none",
-								}}
-								onClick={() => {
-									vscode.postMessage({ type: "openFile", text: tool.content })
-								}}>
+						<ToolUseBlock>
+							<ToolUseBlockHeader
+								onClick={() => vscode.postMessage({ type: "openFile", text: tool.content })}>
 								{tool.path?.startsWith(".") && <span>.</span>}
-								<span
-									style={{
-										whiteSpace: "nowrap",
-										overflow: "hidden",
-										textOverflow: "ellipsis",
-										marginRight: "8px",
-										direction: "rtl",
-										textAlign: "left",
-									}}>
+								<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
 									{removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
 									{tool.reason}
 								</span>
 								<div style={{ flexGrow: 1 }}></div>
 								<span
 									className={`codicon codicon-link-external`}
-									style={{ fontSize: 13.5, margin: "1px 0" }}></span>
-							</div>
-						</div>
+									style={{ fontSize: 13.5, margin: "1px 0" }}
+								/>
+							</ToolUseBlockHeader>
+						</ToolUseBlock>
 					</>
 				)
 			case "fetchInstructions":
@@ -422,8 +400,8 @@ export const ChatRowContent = ({
 							<span style={{ fontWeight: "bold" }}>{t("chat:instructions.wantsToFetch")}</span>
 						</div>
 						<CodeAccordian
+							code={tool.content}
 							isLoading={message.partial}
-							code={tool.content!}
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
 						/>
@@ -441,8 +419,8 @@ export const ChatRowContent = ({
 							</span>
 						</div>
 						<CodeAccordian
-							code={tool.content!}
-							path={tool.path!}
+							path={tool.path}
+							code={tool.content}
 							language="shell-session"
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
@@ -461,8 +439,8 @@ export const ChatRowContent = ({
 							</span>
 						</div>
 						<CodeAccordian
-							code={tool.content!}
-							path={tool.path!}
+							path={tool.path}
+							code={tool.content}
 							language="shell-session"
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
@@ -481,8 +459,8 @@ export const ChatRowContent = ({
 							</span>
 						</div>
 						<CodeAccordian
-							code={tool.content!}
-							path={tool.path!}
+							path={tool.path}
+							code={tool.content}
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
 						/>
@@ -510,8 +488,8 @@ export const ChatRowContent = ({
 							</span>
 						</div>
 						<CodeAccordian
-							code={tool.content!}
 							path={tool.path! + (tool.filePattern ? `/(${tool.filePattern})` : "")}
+							code={tool.content}
 							language="log"
 							isExpanded={isExpanded}
 							onToggleExpand={onToggleExpand}
@@ -881,13 +859,10 @@ export const ChatRowContent = ({
 				case "user_feedback_diff":
 					const tool = safeJsonParse<ClineSayTool>(message.text)
 					return (
-						<div
-							style={{
-								marginTop: -10,
-								width: "100%",
-							}}>
+						<div style={{ marginTop: -10, width: "100%" }}>
 							<CodeAccordian
-								diff={tool?.diff!}
+								code={tool?.diff}
+								language="diff"
 								isFeedback={true}
 								isExpanded={isExpanded}
 								onToggleExpand={onToggleExpand}

+ 4 - 2
webview-ui/src/components/chat/ContextMenu.tsx

@@ -1,13 +1,15 @@
 import React, { useEffect, useMemo, useRef, useState } from "react"
 import { getIconForFilePath, getIconUrlByName, getIconForDirectoryPath } from "vscode-material-icons"
+
+import { ModeConfig } from "@roo/shared/modes"
+
 import {
 	ContextMenuOptionType,
 	ContextMenuQueryItem,
 	getContextMenuOptions,
 	SearchResult,
 } from "@src/utils/context-mentions"
-import { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
-import { ModeConfig } from "@roo/shared/modes"
+import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
 
 interface ContextMenuProps {
 	onSelect: (type: ContextMenuOptionType, value?: string) => void

+ 35 - 92
webview-ui/src/components/common/CodeAccordian.tsx

@@ -1,109 +1,59 @@
 import { memo, useMemo } from "react"
-import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
-import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
-import { ToolProgressStatus } from "@roo/shared/ExtensionMessage"
 import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
 
+import { type ToolProgressStatus } from "@roo/shared/ExtensionMessage"
+import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
+import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
+
+import { ToolUseBlock, ToolUseBlockHeader } from "./ToolUseBlock"
+import CodeBlock from "./CodeBlock"
+
 interface CodeAccordianProps {
+	path?: string
 	code?: string
-	diff?: string
 	language?: string | undefined
-	path?: string
-	isFeedback?: boolean
-	isConsoleLogs?: boolean
+	progressStatus?: ToolProgressStatus
+	isLoading?: boolean
 	isExpanded: boolean
+	isFeedback?: boolean
 	onToggleExpand: () => void
-	isLoading?: boolean
-	progressStatus?: ToolProgressStatus
-}
-
-/*
-We need to remove certain leading characters from the path in order for our leading ellipses trick to work.
-However, we want to preserve all language characters (including CJK, Cyrillic, etc.) and only remove specific
-punctuation that might interfere with the ellipsis display.
-*/
-export const removeLeadingNonAlphanumeric = (path: string): string => {
-	// Only remove specific punctuation characters that might interfere with ellipsis display
-	// Keep all language characters (including CJK, Cyrillic, etc.) and numbers
-	return path.replace(/^[/\\:*?"<>|]+/, "")
 }
 
 const CodeAccordian = ({
-	code,
-	diff,
-	language,
 	path,
-	isFeedback,
-	isConsoleLogs,
+	code = "",
+	language,
+	progressStatus,
+	isLoading,
 	isExpanded,
+	isFeedback,
 	onToggleExpand,
-	isLoading,
-	progressStatus,
 }: CodeAccordianProps) => {
-	const inferredLanguage = useMemo(
-		() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
-		[path, language, code],
-	)
+	const inferredLanguage = useMemo(() => language ?? (path ? getLanguageFromPath(path) : undefined), [path, language])
+	const source = useMemo(() => code.trim(), [code])
+	const hasHeader = Boolean(path || isFeedback)
 
 	return (
-		<div
-			style={{
-				borderRadius: 3,
-				backgroundColor: CODE_BLOCK_BG_COLOR,
-				overflow: "hidden", // This ensures the inner scrollable area doesn't overflow the rounded corners
-				border: "1px solid var(--vscode-editorGroup-border)",
-			}}>
-			{(path || isFeedback || isConsoleLogs) && (
-				<div
-					style={{
-						color: "var(--vscode-descriptionForeground)",
-						display: "flex",
-						alignItems: "center",
-						padding: "9px 10px",
-						cursor: isLoading ? "wait" : "pointer",
-						opacity: isLoading ? 0.7 : 1,
-						// pointerEvents: isLoading ? "none" : "auto",
-						userSelect: "none",
-						WebkitUserSelect: "none",
-						MozUserSelect: "none",
-						msUserSelect: "none",
-					}}
-					className={`${isLoading ? "animate-pulse" : ""}`}
-					onClick={isLoading ? undefined : onToggleExpand}>
+		<ToolUseBlock>
+			{hasHeader && (
+				<ToolUseBlockHeader onClick={onToggleExpand}>
 					{isLoading && <VSCodeProgressRing className="size-3 mr-2" />}
-					{isFeedback || isConsoleLogs ? (
-						<div style={{ display: "flex", alignItems: "center" }}>
-							<span
-								className={`codicon codicon-${isFeedback ? "feedback" : "output"}`}
-								style={{ marginRight: "6px" }}></span>
-							<span
-								style={{
-									whiteSpace: "nowrap",
-									overflow: "hidden",
-									textOverflow: "ellipsis",
-									marginRight: "8px",
-								}}>
+					{isFeedback ? (
+						<div className="flex items-center">
+							<span className={`codicon codicon-${isFeedback ? "feedback" : "codicon-output"} mr-1.5`} />
+							<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2 rtl">
 								{isFeedback ? "User Edits" : "Console Logs"}
 							</span>
 						</div>
 					) : (
 						<>
 							{path?.startsWith(".") && <span>.</span>}
-							<span
-								style={{
-									whiteSpace: "nowrap",
-									overflow: "hidden",
-									textOverflow: "ellipsis",
-									marginRight: "8px",
-									// trick to get ellipsis at beginning of string
-									direction: "rtl",
-									textAlign: "left",
-								}}>
+							<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
 								{removeLeadingNonAlphanumeric(path ?? "") + "\u200E"}
 							</span>
 						</>
 					)}
-					<div style={{ flexGrow: 1 }}></div>
+					<div className="flex-grow-1" />
 					{progressStatus && progressStatus.text && (
 						<>
 							{progressStatus.icon && <span className={`codicon codicon-${progressStatus.icon} mr-1`} />}
@@ -113,24 +63,17 @@ const CodeAccordian = ({
 						</>
 					)}
 					<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
-				</div>
+				</ToolUseBlockHeader>
 			)}
-			{(!(path || isFeedback || isConsoleLogs) || isExpanded) && (
-				<div
-					style={{
-						overflowX: "auto",
-						overflowY: "hidden",
-						maxWidth: "100%",
-					}}>
-					<CodeBlock
-						source={(code ?? diff ?? "").trim()}
-						language={diff !== undefined ? "diff" : inferredLanguage}
-					/>
+			{(!hasHeader || isExpanded) && (
+				<div className="overflow-x-auto overflow-y-hidden max-w-full">
+					<CodeBlock source={source} language={inferredLanguage} />
 				</div>
 			)}
-		</div>
+		</ToolUseBlock>
 	)
 }
 
-// memo does shallow comparison of props, so if you need it to re-render when a nested object changes, you need to pass a custom comparison function
+// Memo does shallow comparison of props, so if you need it to re-render when a
+// nested object changes, you need to pass a custom comparison function.
 export default memo(CodeAccordian)

+ 17 - 0
webview-ui/src/components/common/ToolUseBlock.tsx

@@ -0,0 +1,17 @@
+import { cn } from "@/lib/utils"
+
+import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
+
+export const ToolUseBlock = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
+	<div
+		className={cn("overflow-hidden border border-vscode-border rounded-xs p-2 cursor-pointer", className)}
+		style={{
+			backgroundColor: CODE_BLOCK_BG_COLOR,
+		}}
+		{...props}
+	/>
+)
+
+export const ToolUseBlockHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
+	<div className={cn("flex items-center select-none text-vscode-descriptionForeground", className)} {...props} />
+)

+ 10 - 0
webview-ui/src/utils/removeLeadingNonAlphanumeric.ts

@@ -0,0 +1,10 @@
+// We need to remove certain leading characters from the path in order for our
+// leading ellipses trick to work.
+// However, we want to preserve all language characters (including CJK,
+// Cyrillic, etc.) and only remove specific punctuation that might interfere
+// with the ellipsis display.
+//
+// Only remove specific punctuation characters that might interfere with
+// ellipsis display. Keep all language characters (including CJK, Cyrillic
+//  etc.) and numbers.
+export const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[/\\:*?"<>|]+/, "")