mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Show LLM streaming file write content (#3241)
This commit is contained in:
parent
7ea934c93d
commit
867c6c1c6b
9 changed files with 130 additions and 177 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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` +
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
interface CodeAccordianProps {
|
||||
code?: string
|
||||
diff?: string
|
||||
language?: string | undefined
|
||||
path?: string
|
||||
isFeedback?: boolean
|
||||
isConsoleLogs?: boolean
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
isLoading?: boolean
|
||||
progressStatus?: ToolProgressStatus
|
||||
}
|
||||
import { type ToolProgressStatus } from "@roo/shared/ExtensionMessage"
|
||||
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
|
||||
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
|
||||
|
||||
/*
|
||||
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(/^[/\\:*?"<>|]+/, "")
|
||||
import { ToolUseBlock, ToolUseBlockHeader } from "./ToolUseBlock"
|
||||
import CodeBlock from "./CodeBlock"
|
||||
|
||||
interface CodeAccordianProps {
|
||||
path?: string
|
||||
code?: string
|
||||
language?: string | undefined
|
||||
progressStatus?: ToolProgressStatus
|
||||
isLoading?: boolean
|
||||
isExpanded: boolean
|
||||
isFeedback?: boolean
|
||||
onToggleExpand: () => void
|
||||
}
|
||||
|
||||
const CodeAccordian = ({
|
||||
code,
|
||||
diff,
|
||||
language,
|
||||
path,
|
||||
isFeedback,
|
||||
isConsoleLogs,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
isLoading,
|
||||
code = "",
|
||||
language,
|
||||
progressStatus,
|
||||
isLoading,
|
||||
isExpanded,
|
||||
isFeedback,
|
||||
onToggleExpand,
|
||||
}: 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>
|
||||
</ToolUseBlockHeader>
|
||||
)}
|
||||
{(!hasHeader || isExpanded) && (
|
||||
<div className="overflow-x-auto overflow-y-hidden max-w-full">
|
||||
<CodeBlock source={source} language={inferredLanguage} />
|
||||
</div>
|
||||
)}
|
||||
{(!(path || isFeedback || isConsoleLogs) || isExpanded) && (
|
||||
<div
|
||||
style={{
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
maxWidth: "100%",
|
||||
}}>
|
||||
<CodeBlock
|
||||
source={(code ?? diff ?? "").trim()}
|
||||
language={diff !== undefined ? "diff" : 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
webview-ui/src/components/common/ToolUseBlock.tsx
Normal file
17
webview-ui/src/components/common/ToolUseBlock.tsx
Normal file
|
|
@ -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
webview-ui/src/utils/removeLeadingNonAlphanumeric.ts
Normal file
10
webview-ui/src/utils/removeLeadingNonAlphanumeric.ts
Normal file
|
|
@ -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(/^[/\\:*?"<>|]+/, "")
|
||||
Loading…
Add table
Reference in a new issue