Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 3ab1d08

Browse files
daniel-lxsroomote
andauthored
Fix EXT-553: Remove percentage-based progress tracking for worktree file copying (#10905)
* Fix EXT-553: Remove percentage-based progress tracking for worktree file copying - Removed totalBytes from CopyProgress interface - Removed Math.min() clamping that caused stuck-at-100% issue - Changed UI from progress bar to spinner with activity indicator - Shows 'item — X MB copied' instead of percentage - Updated all 18 locale files - Uses native cp with polling (no new dependencies) * fix: translate copyingProgress text in all 17 non-English locale files --------- Co-authored-by: Roo Code <roomote@roocode.com>
1 parent 9d65772 commit 3ab1d08

22 files changed

Lines changed: 48 additions & 77 deletions

packages/core/src/worktree/__tests__/worktree-include.spec.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -265,30 +265,28 @@ describe("WorktreeIncludeService", () => {
265265
expect(result).toContain("node_modules")
266266
})
267267

268-
it("should call progress callback with size-based progress", async () => {
268+
it("should call progress callback with bytesCopied progress", async () => {
269269
// Set up files to copy
270270
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\n.env.local")
271271
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env.local")
272272
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
273273
await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test")
274274
await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value")
275275

276-
const progressCalls: Array<{ bytesCopied: number; totalBytes: number; itemName: string }> = []
277-
const onProgress = vi.fn((progress: { bytesCopied: number; totalBytes: number; itemName: string }) => {
276+
const progressCalls: Array<{ bytesCopied: number; itemName: string }> = []
277+
const onProgress = vi.fn((progress: { bytesCopied: number; itemName: string }) => {
278278
progressCalls.push({ ...progress })
279279
})
280280

281281
await service.copyWorktreeIncludeFiles(sourceDir, targetDir, onProgress)
282282

283-
// Should be called multiple times (initial + after each copy + during polling)
283+
// Should be called multiple times (initial + after each copy)
284284
expect(onProgress).toHaveBeenCalled()
285285

286-
// All calls should have totalBytes > 0 (since we have files)
287-
expect(progressCalls.every((p) => p.totalBytes > 0)).toBe(true)
288-
289-
// Final call should have bytesCopied === totalBytes (complete)
286+
// bytesCopied should increase over time
287+
expect(progressCalls.length).toBeGreaterThan(0)
290288
const finalCall = progressCalls[progressCalls.length - 1]
291-
expect(finalCall?.bytesCopied).toBe(finalCall?.totalBytes)
289+
expect(finalCall?.bytesCopied).toBeGreaterThan(0)
292290

293291
// Each call should have an item name
294292
expect(progressCalls.every((p) => typeof p.itemName === "string")).toBe(true)

packages/core/src/worktree/worktree-include.ts

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ import ignore, { type Ignore } from "ignore"
1515
import type { WorktreeIncludeStatus } from "./types.js"
1616

1717
/**
18-
* Progress info for size-based copy tracking.
18+
* Progress info for copy tracking.
19+
* Shows activity without trying to predict total size (which is inaccurate).
1920
*/
2021
export interface CopyProgress {
2122
/** Current bytes copied */
2223
bytesCopied: number
23-
/** Total bytes to copy */
24-
totalBytes: number
2524
/** Name of current item being copied */
2625
itemName: string
2726
}
@@ -164,61 +163,50 @@ export class WorktreeIncludeService {
164163
return []
165164
}
166165

167-
// Calculate total size of all items to copy (for accurate progress)
168-
const itemSizes = await Promise.all(
169-
itemsToCopy.map(async (item) => {
170-
const sourcePath = path.join(sourceDir, item)
171-
const size = await this.getPathSize(sourcePath)
172-
return { item, size }
173-
}),
174-
)
175-
176-
const totalBytes = itemSizes.reduce((sum, { size }) => sum + size, 0)
177166
let bytesCopied = 0
178167

179168
// Report initial progress
180-
if (onProgress && totalBytes > 0) {
181-
onProgress({ bytesCopied: 0, totalBytes, itemName: itemsToCopy[0]! })
169+
if (onProgress && itemsToCopy.length > 0) {
170+
onProgress({ bytesCopied: 0, itemName: itemsToCopy[0]! })
182171
}
183172

184-
// Copy the items with size-based progress tracking
173+
// Copy the items with progress tracking (no total size calculation)
185174
const copiedItems: string[] = []
186-
for (const { item, size } of itemSizes) {
175+
for (const item of itemsToCopy) {
187176
const sourcePath = path.join(sourceDir, item)
188177
const targetPath = path.join(targetDir, item)
189178

190179
try {
191180
const stats = await fs.stat(sourcePath)
192181

193182
if (stats.isDirectory()) {
194-
// Use native cp for directories with progress polling
195-
await this.copyDirectoryWithProgress(
183+
// Copy directory with progress tracking
184+
bytesCopied = await this.copyDirectoryWithProgress(
196185
sourcePath,
197186
targetPath,
198187
item,
199188
bytesCopied,
200-
totalBytes,
201189
onProgress,
202190
)
203191
} else {
204192
// Report progress before copying
205-
onProgress?.({ bytesCopied, totalBytes, itemName: item })
193+
onProgress?.({ bytesCopied, itemName: item })
206194

207195
// Ensure parent directory exists
208196
await fs.mkdir(path.dirname(targetPath), { recursive: true })
209197
await fs.copyFile(sourcePath, targetPath)
198+
199+
// Update bytes copied
200+
bytesCopied += this.getSizeOnDisk(stats)
210201
}
211202

212-
bytesCopied += size
213203
copiedItems.push(item)
214204

215205
// Report progress after copying
216-
onProgress?.({ bytesCopied, totalBytes, itemName: item })
206+
onProgress?.({ bytesCopied, itemName: item })
217207
} catch (error) {
218208
// Log but don't fail on individual copy errors
219209
console.error(`Failed to copy ${item}:`, error)
220-
// Still count the size as "processed" to avoid progress getting stuck
221-
bytesCopied += size
222210
}
223211
}
224212

@@ -302,22 +290,21 @@ export class WorktreeIncludeService {
302290
}
303291

304292
/**
305-
* Copy directory with progress polling.
293+
* Copy directory with progress polling using native cp command.
306294
* Starts native copy and polls target directory size to report progress.
295+
* Returns the updated bytesCopied count.
307296
*/
308297
private async copyDirectoryWithProgress(
309298
source: string,
310299
target: string,
311300
itemName: string,
312301
bytesCopiedBefore: number,
313-
totalBytes: number,
314302
onProgress?: CopyProgressCallback,
315-
): Promise<void> {
303+
): Promise<number> {
316304
// Ensure parent directory exists
317305
await fs.mkdir(path.dirname(target), { recursive: true })
318306

319307
const isWindows = process.platform === "win32"
320-
const expectedSize = await this.getPathSize(source)
321308

322309
// Start the copy process
323310
const copyPromise = new Promise<void>((resolve, reject) => {
@@ -361,8 +348,7 @@ export class WorktreeIncludeService {
361348
const totalCopied = bytesCopiedBefore + currentSize
362349

363350
onProgress?.({
364-
bytesCopied: Math.min(totalCopied, bytesCopiedBefore + expectedSize),
365-
totalBytes,
351+
bytesCopied: totalCopied,
366352
itemName,
367353
})
368354

@@ -380,6 +366,10 @@ export class WorktreeIncludeService {
380366
// Wait for final poll iteration to complete
381367
await pollPromise.catch(() => {})
382368
}
369+
370+
// Get the final size of the copied directory
371+
const finalSize = await this.getPathSize(target)
372+
return bytesCopiedBefore + finalSize
383373
}
384374

385375
/**

src/core/webview/webviewMessageHandler.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3387,7 +3387,6 @@ export const webviewMessageHandler = async (
33873387
provider.postMessageToWebview({
33883388
type: "worktreeCopyProgress",
33893389
copyProgressBytesCopied: progress.bytesCopied,
3390-
copyProgressTotalBytes: progress.totalBytes,
33913390
copyProgressItemName: progress.itemName,
33923391
})
33933392
},

webview-ui/src/components/worktrees/CreateWorktreeModal.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export const CreateWorktreeModal = ({
4747
const [error, setError] = useState<string | null>(null)
4848
const [copyProgress, setCopyProgress] = useState<{
4949
bytesCopied: number
50-
totalBytes: number
5150
itemName: string
5251
} | null>(null)
5352

@@ -85,7 +84,6 @@ export const CreateWorktreeModal = ({
8584
case "worktreeCopyProgress": {
8685
setCopyProgress({
8786
bytesCopied: message.copyProgressBytesCopied ?? 0,
88-
totalBytes: message.copyProgressTotalBytes ?? 0,
8987
itemName: message.copyProgressItemName ?? "",
9088
})
9189
break
@@ -226,30 +224,16 @@ export const CreateWorktreeModal = ({
226224
{/* Progress section - appears during file copying */}
227225
{copyProgress && (
228226
<div className="flex flex-col gap-2 px-3 py-3 rounded-lg bg-vscode-editor-background border border-vscode-panel-border">
229-
<div className="flex items-center justify-between text-sm">
227+
<div className="flex items-center gap-2 text-sm">
228+
<span className="codicon codicon-loading codicon-modifier-spin text-vscode-button-background" />
230229
<span className="text-vscode-foreground font-medium">
231230
{t("worktrees:copyingFiles")}
232231
</span>
233-
<span className="text-vscode-descriptionForeground">
234-
{copyProgress.totalBytes > 0
235-
? Math.round((copyProgress.bytesCopied / copyProgress.totalBytes) * 100)
236-
: 0}
237-
%
238-
</span>
239-
</div>
240-
<div className="w-full h-2 bg-vscode-input-background rounded-full overflow-hidden">
241-
<div
242-
className="h-full bg-vscode-button-background rounded-full transition-all duration-200"
243-
style={{
244-
width: `${copyProgress.totalBytes > 0 ? (copyProgress.bytesCopied / copyProgress.totalBytes) * 100 : 0}%`,
245-
}}
246-
/>
247232
</div>
248233
<div className="text-xs text-vscode-descriptionForeground truncate">
249234
{t("worktrees:copyingProgress", {
250235
item: copyProgress.itemName,
251236
copied: prettyBytes(copyProgress.bytesCopied),
252-
total: prettyBytes(copyProgress.totalBytes),
253237
})}
254238
</div>
255239
</div>

webview-ui/src/i18n/locales/ca/worktrees.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/worktrees.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/en/worktrees.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"create": "Create",
4444
"creating": "Creating...",
4545
"copyingFiles": "Copying files...",
46-
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
46+
"copyingProgress": "{{item}} — {{copied}} copied",
4747
"cancel": "Cancel",
4848

4949
"deleteWorktree": "Delete Worktree",

webview-ui/src/i18n/locales/es/worktrees.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/fr/worktrees.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/hi/worktrees.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)