fix: show preparing message before file path stabilizes

This change implements a two-phase UX for file editing operations:
1. Show "Roo is preparing to edit a file..." immediately when a file edit starts
2. Update to "Roo wants to edit this file" with the actual filename once path stabilizes

The issue was that file paths arrive character-by-character during streaming, and
the UI waited for path stabilization before showing anything, creating the appearance
of a frozen UI.

Changes:
- Added preparingToEdit translation string to all 18 locale files
- Modified ChatRow.tsx to show preparing message when tool.path is undefined
- Updated CodeAccordian.tsx to show loading placeholder for undefined paths
- Added hasSentInitialMessage flag to BaseTool.ts
- Updated ApplyDiffTool, EditFileTool, WriteToFileTool, SearchAndReplaceTool,
  and SearchReplaceTool to send initial "preparing" message immediately
- Updated writeToFileTool test to reflect new behavior

Addresses issue #10855
This commit is contained in:
Roo Code 2026-01-21 04:09:39 +00:00
parent 8de9337e63
commit 5979bda843
27 changed files with 130 additions and 34 deletions

View file

@ -271,7 +271,18 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
const relPath: string | undefined = block.params.path
const diffContent: string | undefined = block.params.diff
// Wait for path to stabilize before showing UI (prevents truncated paths)
// Send initial "preparing" message immediately (before path stabilizes)
if (!this.hasSentInitialMessage) {
this.hasSentInitialMessage = true
const initialMessageProps: ClineSayTool = {
tool: "appliedDiff",
path: undefined as any, // Undefined path triggers "preparing to edit" message in frontend
diff: diffContent,
}
await task.ask("tool", JSON.stringify(initialMessageProps), block.partial).catch(() => {})
}
// Wait for path to stabilize before showing the full UI with filename
if (!this.hasPathStabilized(relPath)) {
return
}

View file

@ -38,6 +38,12 @@ export abstract class BaseTool<TName extends ToolName> {
*/
protected lastSeenPartialPath: string | undefined = undefined
/**
* Track whether the initial "preparing" message has been sent for this tool invocation.
* Used by file editing tools to show immediate feedback before the path stabilizes.
*/
protected hasSentInitialMessage: boolean = false
/**
* Execute the tool with typed parameters.
*
@ -96,6 +102,7 @@ export abstract class BaseTool<TName extends ToolName> {
*/
resetPartialState(): void {
this.lastSeenPartialPath = undefined
this.hasSentInitialMessage = false
}
/**

View file

@ -488,7 +488,18 @@ export class EditFileTool extends BaseTool<"edit_file"> {
const filePath: string | undefined = block.params.file_path
const oldString: string | undefined = block.params.old_string
// Wait for path to stabilize before showing UI (prevents truncated paths)
// Send initial "preparing" message immediately (before path stabilizes)
if (!this.hasSentInitialMessage) {
this.hasSentInitialMessage = true
const initialMessageProps: ClineSayTool = {
tool: "appliedDiff",
path: undefined as any, // Undefined path triggers "preparing to edit" message in frontend
diff: undefined,
}
await task.ask("tool", JSON.stringify(initialMessageProps), block.partial).catch(() => {})
}
// Wait for path to stabilize before showing the full UI with filename
if (!this.hasPathStabilized(filePath)) {
return
}

View file

@ -257,7 +257,18 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
override async handlePartial(task: Task, block: ToolUse<"search_and_replace">): Promise<void> {
const relPath: string | undefined = block.params.path
// Wait for path to stabilize before showing UI (prevents truncated paths)
// Send initial "preparing" message immediately (before path stabilizes)
if (!this.hasSentInitialMessage) {
this.hasSentInitialMessage = true
const initialMessageProps: ClineSayTool = {
tool: "appliedDiff",
path: undefined as any, // Undefined path triggers "preparing to edit" message in frontend
diff: "",
}
await task.ask("tool", JSON.stringify(initialMessageProps), block.partial).catch(() => {})
}
// Wait for path to stabilize before showing the full UI with filename
if (!this.hasPathStabilized(relPath)) {
return
}

View file

@ -243,7 +243,18 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
const filePath: string | undefined = block.params.file_path
const oldString: string | undefined = block.params.old_string
// Wait for path to stabilize before showing UI (prevents truncated paths)
// Send initial "preparing" message immediately (before path stabilizes)
if (!this.hasSentInitialMessage) {
this.hasSentInitialMessage = true
const initialMessageProps: ClineSayTool = {
tool: "appliedDiff",
path: undefined as any, // Undefined path triggers "preparing to edit" message in frontend
diff: "",
}
await task.ask("tool", JSON.stringify(initialMessageProps), block.partial).catch(() => {})
}
// Wait for path to stabilize before showing the full UI with filename
if (!this.hasPathStabilized(filePath)) {
return
}

View file

@ -197,7 +197,18 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
const relPath: string | undefined = block.params.path
let newContent: string | undefined = block.params.content
// Wait for path to stabilize before showing UI (prevents truncated paths)
// Send initial "preparing" message immediately (before path stabilizes)
if (!this.hasSentInitialMessage) {
this.hasSentInitialMessage = true
const initialMessageProps: ClineSayTool = {
tool: "newFileCreated",
path: undefined as any, // Undefined path triggers "preparing to edit" message in frontend
content: "",
}
await task.ask("tool", JSON.stringify(initialMessageProps), block.partial).catch(() => {})
}
// Wait for path to stabilize before showing the full UI with filename
if (!this.hasPathStabilized(relPath) || newContent === undefined) {
return
}

View file

@ -400,12 +400,16 @@ describe("writeToFileTool", () => {
})
it("streams content updates during partial execution after path stabilizes", async () => {
// First call - path not yet stabilized, early return (no file operations)
// First call - path not yet stabilized, sends initial "preparing" message but no file operations
await executeWriteFileTool({}, { isPartial: true })
expect(mockCline.ask).not.toHaveBeenCalled()
// Initial "preparing" message is sent with undefined path to show UI immediately
expect(mockCline.ask).toHaveBeenCalledWith("tool", expect.stringContaining('"tool":"newFileCreated"'), true)
expect(mockCline.diffViewProvider.open).not.toHaveBeenCalled()
// Second call with same path - path is now stabilized, file operations proceed
// Clear mocks to track subsequent calls
vi.clearAllMocks()
// Second call with same path - path is now stabilized, full file operations proceed
await executeWriteFileTool({}, { isPartial: true })
expect(mockCline.ask).toHaveBeenCalled()
expect(mockCline.diffViewProvider.open).toHaveBeenCalledWith(testFilePath)
@ -455,11 +459,11 @@ describe("writeToFileTool", () => {
it("handles partial streaming errors after path stabilizes", async () => {
mockCline.diffViewProvider.open.mockRejectedValue(new Error("Open failed"))
// First call - path not yet stabilized, no error yet
// First call - path not yet stabilized, sends initial message but no error (ask catches errors)
await executeWriteFileTool({}, { isPartial: true })
expect(mockHandleError).not.toHaveBeenCalled()
// Second call with same path - path is now stabilized, error occurs
// Second call with same path - path is now stabilized, error occurs during file operations
await executeWriteFileTool({}, { isPartial: true })
expect(mockHandleError).toHaveBeenCalledWith("handling partial write_to_file", expect.any(Error))
})

View file

@ -445,11 +445,13 @@ export const ChatRowContent = ({
toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")
)}
<span style={{ fontWeight: "bold" }}>
{tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: t("chat:fileOperations.wantsToEdit")}
{!tool.path
? t("chat:fileOperations.preparingToEdit")
: tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: t("chat:fileOperations.wantsToEdit")}
</span>
</div>
<div className="pl-6">
@ -479,15 +481,17 @@ export const ChatRowContent = ({
toolIcon("insert")
)}
<span style={{ fontWeight: "bold" }}>
{tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: tool.lineNumber === 0
? t("chat:fileOperations.wantsToInsertAtEnd")
: t("chat:fileOperations.wantsToInsertWithLineNumber", {
lineNumber: tool.lineNumber,
})}
{!tool.path
? t("chat:fileOperations.preparingToEdit")
: tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: tool.lineNumber === 0
? t("chat:fileOperations.wantsToInsertAtEnd")
: t("chat:fileOperations.wantsToInsertWithLineNumber", {
lineNumber: tool.lineNumber,
})}
</span>
</div>
<div className="pl-6">
@ -517,11 +521,13 @@ export const ChatRowContent = ({
toolIcon("replace")
)}
<span style={{ fontWeight: "bold" }}>
{tool.isProtected && message.type === "ask"
? t("chat:fileOperations.wantsToEditProtected")
: message.type === "ask"
? t("chat:fileOperations.wantsToSearchReplace")
: t("chat:fileOperations.didSearchReplace")}
{!tool.path && message.type === "ask"
? t("chat:fileOperations.preparingToEdit")
: tool.isProtected && message.type === "ask"
? t("chat:fileOperations.wantsToEditProtected")
: message.type === "ask"
? t("chat:fileOperations.wantsToSearchReplace")
: t("chat:fileOperations.didSearchReplace")}
</span>
</div>
<div className="pl-6">
@ -580,9 +586,11 @@ export const ChatRowContent = ({
toolIcon("new-file")
)}
<span style={{ fontWeight: "bold" }}>
{tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: t("chat:fileOperations.wantsToCreate")}
{!tool.path
? t("chat:fileOperations.preparingToEdit")
: tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: t("chat:fileOperations.wantsToCreate")}
</span>
</div>
<div className="pl-6">

View file

@ -68,15 +68,19 @@ const CodeAccordian = ({
{isFeedback ? "User Edits" : "Console Logs"}
</span>
</div>
) : (
) : path ? (
<>
{path?.startsWith(".") && <span>.</span>}
{path.startsWith(".") && <span>.</span>}
<PathTooltip content={formatPathTooltip(path)}>
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{formatPathTooltip(path)}
</span>
</PathTooltip>
</>
) : (
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 text-vscode-descriptionForeground italic">
Loading file...
</span>
)}
<div className="flex-grow-1" />
{/* Prefer diff stats over generic progress indicator if available */}

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo vol obtenir instruccions detallades per ajudar amb la tasca actual."
},
"fileOperations": {
"preparingToEdit": "Roo s'està preparant per editar un fitxer...",
"wantsToRead": "Roo vol llegir aquest fitxer",
"wantsToReadOutsideWorkspace": "Roo vol llegir aquest fitxer fora de l'espai de treball",
"didRead": "Roo ha llegit aquest fitxer",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo möchte detaillierte Anweisungen abrufen, um bei der aktuellen Aufgabe zu helfen"
},
"fileOperations": {
"preparingToEdit": "Roo bereitet die Bearbeitung einer Datei vor...",
"wantsToRead": "Roo möchte diese Datei lesen",
"wantsToReadAndXMore": "Roo möchte diese Datei und {{count}} weitere lesen",
"wantsToReadOutsideWorkspace": "Roo möchte diese Datei außerhalb des Arbeitsbereichs lesen",

View file

@ -203,6 +203,7 @@
"wantsToFetch": "Roo wants to fetch detailed instructions to assist with the current task"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo wants to read this file",
"wantsToReadMultiple": "Roo wants to read multiple files",
"wantsToReadAndXMore": "Roo wants to read this file and {{count}} more",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo quiere obtener instrucciones detalladas para ayudar con la tarea actual"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo quiere leer este archivo",
"wantsToReadOutsideWorkspace": "Roo quiere leer este archivo fuera del espacio de trabajo",
"didRead": "Roo leyó este archivo",

View file

@ -179,6 +179,7 @@
"current": "Actuel"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo veut lire ce fichier",
"wantsToReadOutsideWorkspace": "Roo veut lire ce fichier en dehors de l'espace de travail",
"didRead": "Roo a lu ce fichier",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo को वर्तमान कार्य में सहायता के लिए विस्तृत निर्देश प्राप्त करना है"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo इस फ़ाइल को पढ़ना चाहता है",
"wantsToReadOutsideWorkspace": "Roo कार्यक्षेत्र के बाहर इस फ़ाइल को पढ़ना चाहता है",
"didRead": "Roo ने इस फ़ाइल को पढ़ा",

View file

@ -206,6 +206,7 @@
"wantsToFetch": "Roo ingin mengambil instruksi detail untuk membantu tugas saat ini"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo ingin membaca file ini",
"wantsToReadMultiple": "Roo ingin membaca beberapa file",
"wantsToReadAndXMore": "Roo ingin membaca file ini dan {{count}} lainnya",

View file

@ -182,6 +182,7 @@
"current": "Corrente"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo vuole leggere questo file",
"wantsToReadOutsideWorkspace": "Roo vuole leggere questo file al di fuori dell'area di lavoro",
"didRead": "Roo ha letto questo file",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Rooは現在のタスクを支援するための詳細な指示を取得したい"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Rooはこのファイルを読みたい",
"wantsToReadOutsideWorkspace": "Rooはワークスペース外のこのファイルを読みたい",
"didRead": "Rooはこのファイルを読みました",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo는 현재 작업을 지원하기 위해 자세한 지침을 가져오려고 합니다"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo가 이 파일을 읽고 싶어합니다",
"wantsToReadOutsideWorkspace": "Roo가 워크스페이스 외부의 이 파일을 읽고 싶어합니다",
"didRead": "Roo가 이 파일을 읽었습니다",

View file

@ -177,6 +177,7 @@
"wantsToFetch": "Roo wil gedetailleerde instructies ophalen om te helpen met de huidige taak"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo wil dit bestand lezen",
"wantsToReadOutsideWorkspace": "Roo wil dit bestand buiten de werkruimte lezen",
"didRead": "Roo heeft dit bestand gelezen",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo chce pobrać szczegółowe instrukcje, aby pomóc w bieżącym zadaniu"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo chce przeczytać ten plik",
"wantsToReadOutsideWorkspace": "Roo chce przeczytać ten plik poza obszarem roboczym",
"didRead": "Roo przeczytał ten plik",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo quer buscar instruções detalhadas para ajudar com a tarefa atual"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo quer ler este arquivo",
"wantsToReadOutsideWorkspace": "Roo quer ler este arquivo fora do espaço de trabalho",
"didRead": "Roo leu este arquivo",

View file

@ -177,6 +177,7 @@
"wantsToFetch": "Roo хочет получить подробные инструкции для помощи с текущей задачей"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo хочет прочитать этот файл",
"wantsToReadOutsideWorkspace": "Roo хочет прочитать этот файл вне рабочей области",
"didRead": "Roo прочитал этот файл",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo mevcut göreve yardımcı olmak için ayrıntılı talimatlar almak istiyor"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo bu dosyayı okumak istiyor",
"wantsToReadOutsideWorkspace": "Roo çalışma alanı dışındaki bu dosyayı okumak istiyor",
"didRead": "Roo bu dosyayı okudu",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo muốn lấy hướng dẫn chi tiết để hỗ trợ nhiệm vụ hiện tại"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo muốn đọc tệp này",
"wantsToReadOutsideWorkspace": "Roo muốn đọc tệp này bên ngoài không gian làm việc",
"didRead": "Roo đã đọc tệp này",

View file

@ -182,6 +182,7 @@
"wantsToFetch": "Roo 想要获取详细指示以协助当前任务"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "需要读取文件",
"wantsToReadOutsideWorkspace": "请求访问外部文件",
"didRead": "已读取文件",

View file

@ -203,6 +203,7 @@
"wantsToFetch": "Roo 想要取得詳細指示以協助目前工作"
},
"fileOperations": {
"preparingToEdit": "Roo is preparing to edit a file...",
"wantsToRead": "Roo 想要讀取此檔案",
"wantsToReadMultiple": "Roo 想要讀取多個檔案",
"wantsToReadAndXMore": "Roo 想要讀取此檔案以及另外 {{count}} 個檔案",