Skip to content

Commit b4659fb

Browse files
committed
fix: resolve task persistence issue for Desktop directory on macOS
- Add arePathsEqual utility function for proper path comparison - Update useTaskSearch hook to use arePathsEqual for workspace filtering - Add comprehensive tests for Desktop directory path handling - Fix path normalization to handle mixed separators and edge cases This ensures tasks are properly filtered and persist when switching panels, especially when the workspace is the Desktop directory. Fixes #8471
1 parent 8622d93 commit b4659fb

File tree

4 files changed

+383
-1
lines changed

4 files changed

+383
-1
lines changed

webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { renderHook, act } from "@/utils/test-utils"
2+
import * as path from "path"
3+
import * as os from "os"
24

35
import type { HistoryItem } from "@roo-code/types"
46

@@ -284,4 +286,156 @@ describe("useTaskSearch", () => {
284286
// When not searching, it should fall back to newest
285287
expect(result.current.sortOption).toBe("mostRelevant")
286288
})
289+
290+
describe("Desktop directory handling", () => {
291+
it("should correctly filter tasks when workspace is Desktop on macOS", () => {
292+
const desktopPath = path.join(os.homedir(), "Desktop")
293+
const desktopPathWithSlash = path.join(os.homedir(), "Desktop/")
294+
295+
const desktopTaskHistory: HistoryItem[] = [
296+
{
297+
id: "desktop-task-1",
298+
number: 1,
299+
task: "Task created in Desktop",
300+
ts: new Date("2022-02-16T12:00:00").getTime(),
301+
tokensIn: 100,
302+
tokensOut: 50,
303+
totalCost: 0.01,
304+
workspace: desktopPath,
305+
},
306+
{
307+
id: "desktop-task-2",
308+
number: 2,
309+
task: "Another Desktop task",
310+
ts: new Date("2022-02-17T12:00:00").getTime(),
311+
tokensIn: 200,
312+
tokensOut: 100,
313+
totalCost: 0.02,
314+
workspace: desktopPathWithSlash, // With trailing slash
315+
},
316+
{
317+
id: "other-task",
318+
number: 3,
319+
task: "Task from different workspace",
320+
ts: new Date("2022-02-15T12:00:00").getTime(),
321+
tokensIn: 150,
322+
tokensOut: 75,
323+
totalCost: 0.05,
324+
workspace: "/workspace/project1",
325+
},
326+
]
327+
328+
mockUseExtensionState.mockReturnValue({
329+
taskHistory: desktopTaskHistory,
330+
cwd: desktopPath,
331+
} as any)
332+
333+
const { result } = renderHook(() => useTaskSearch())
334+
335+
// Should show both Desktop tasks despite different path formats
336+
expect(result.current.tasks).toHaveLength(2)
337+
expect(result.current.tasks[0].id).toBe("desktop-task-2")
338+
expect(result.current.tasks[1].id).toBe("desktop-task-1")
339+
})
340+
341+
it("should handle Desktop path variations on Windows", () => {
342+
// Mock Windows platform
343+
const originalPlatform = process.platform
344+
Object.defineProperty(process, "platform", {
345+
value: "win32",
346+
configurable: true,
347+
})
348+
349+
const desktopPath = "C:\\Users\\testuser\\Desktop"
350+
const desktopPathMixed = "C:/Users/testuser/Desktop"
351+
const desktopPathLowerCase = "c:\\users\\testuser\\desktop"
352+
353+
const windowsDesktopTaskHistory: HistoryItem[] = [
354+
{
355+
id: "win-desktop-task-1",
356+
number: 1,
357+
task: "Windows Desktop task 1",
358+
ts: new Date("2022-02-16T12:00:00").getTime(),
359+
tokensIn: 100,
360+
tokensOut: 50,
361+
totalCost: 0.01,
362+
workspace: desktopPath,
363+
},
364+
{
365+
id: "win-desktop-task-2",
366+
number: 2,
367+
task: "Windows Desktop task 2",
368+
ts: new Date("2022-02-17T12:00:00").getTime(),
369+
tokensIn: 200,
370+
tokensOut: 100,
371+
totalCost: 0.02,
372+
workspace: desktopPathMixed, // Mixed separators
373+
},
374+
{
375+
id: "win-desktop-task-3",
376+
number: 3,
377+
task: "Windows Desktop task 3",
378+
ts: new Date("2022-02-18T12:00:00").getTime(),
379+
tokensIn: 150,
380+
tokensOut: 75,
381+
totalCost: 0.03,
382+
workspace: desktopPathLowerCase, // Different case
383+
},
384+
]
385+
386+
mockUseExtensionState.mockReturnValue({
387+
taskHistory: windowsDesktopTaskHistory,
388+
cwd: desktopPath,
389+
} as any)
390+
391+
const { result } = renderHook(() => useTaskSearch())
392+
393+
// Should show all Desktop tasks despite path variations
394+
expect(result.current.tasks).toHaveLength(3)
395+
expect(result.current.tasks[0].id).toBe("win-desktop-task-3")
396+
expect(result.current.tasks[1].id).toBe("win-desktop-task-2")
397+
expect(result.current.tasks[2].id).toBe("win-desktop-task-1")
398+
399+
// Restore original platform
400+
Object.defineProperty(process, "platform", {
401+
value: originalPlatform,
402+
configurable: true,
403+
})
404+
})
405+
406+
it("should not lose tasks when switching between panels with Desktop workspace", () => {
407+
const desktopPath = path.join(os.homedir(), "Desktop")
408+
409+
const desktopTaskHistory: HistoryItem[] = [
410+
{
411+
id: "persistent-task-1",
412+
number: 1,
413+
task: "Task that should persist",
414+
ts: new Date("2022-02-16T12:00:00").getTime(),
415+
tokensIn: 100,
416+
tokensOut: 50,
417+
totalCost: 0.01,
418+
workspace: desktopPath,
419+
},
420+
]
421+
422+
// Initial render - tasks should be visible
423+
mockUseExtensionState.mockReturnValue({
424+
taskHistory: desktopTaskHistory,
425+
cwd: desktopPath,
426+
} as any)
427+
428+
const { result, rerender } = renderHook(() => useTaskSearch())
429+
430+
expect(result.current.tasks).toHaveLength(1)
431+
expect(result.current.tasks[0].id).toBe("persistent-task-1")
432+
433+
// Simulate switching panels (component remount)
434+
rerender()
435+
436+
// Tasks should still be visible after remount
437+
expect(result.current.tasks).toHaveLength(1)
438+
expect(result.current.tasks[0].id).toBe("persistent-task-1")
439+
})
440+
})
287441
})

webview-ui/src/components/history/useTaskSearch.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Fzf } from "fzf"
33

44
import { highlightFzfMatch } from "@/utils/highlight"
55
import { useExtensionState } from "@/context/ExtensionStateContext"
6+
import { arePathsEqual } from "@/utils/path"
67

78
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
89

@@ -26,7 +27,8 @@ export const useTaskSearch = () => {
2627
const presentableTasks = useMemo(() => {
2728
let tasks = taskHistory.filter((item) => item.ts && item.task)
2829
if (!showAllWorkspaces) {
29-
tasks = tasks.filter((item) => item.workspace === cwd)
30+
// Use arePathsEqual for proper path comparison that handles Desktop directory correctly
31+
tasks = tasks.filter((item) => arePathsEqual(item.workspace, cwd))
3032
}
3133
return tasks
3234
}, [taskHistory, showAllWorkspaces, cwd])
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest"
2+
import * as path from "path"
3+
import * as os from "os"
4+
import { arePathsEqual } from "../path"
5+
6+
describe("arePathsEqual", () => {
7+
const originalPlatform = process.platform
8+
9+
beforeEach(() => {
10+
vi.clearAllMocks()
11+
})
12+
13+
afterEach(() => {
14+
Object.defineProperty(process, "platform", {
15+
value: originalPlatform,
16+
})
17+
})
18+
19+
describe("cross-platform path comparison", () => {
20+
it("should return true for identical paths", () => {
21+
expect(arePathsEqual("/home/user/project", "/home/user/project")).toBe(true)
22+
expect(arePathsEqual("C:\\Users\\project", "C:\\Users\\project")).toBe(true)
23+
})
24+
25+
it("should return true for paths with different separators", () => {
26+
expect(arePathsEqual("/home/user/project", "/home/user/project/")).toBe(true)
27+
expect(arePathsEqual("C:\\Users\\project", "C:\\Users\\project\\")).toBe(true)
28+
})
29+
30+
it("should normalize paths with . and .. segments", () => {
31+
expect(arePathsEqual("/home/user/../user/project", "/home/user/project")).toBe(true)
32+
expect(arePathsEqual("/home/./user/project", "/home/user/project")).toBe(true)
33+
})
34+
35+
it("should handle undefined and null paths", () => {
36+
expect(arePathsEqual(undefined, undefined)).toBe(true)
37+
expect(arePathsEqual(null as any, null as any)).toBe(true)
38+
expect(arePathsEqual(undefined, "/home/user")).toBe(false)
39+
expect(arePathsEqual("/home/user", undefined)).toBe(false)
40+
})
41+
42+
it("should handle empty strings", () => {
43+
expect(arePathsEqual("", "")).toBe(true)
44+
expect(arePathsEqual("", "/home/user")).toBe(false)
45+
expect(arePathsEqual("/home/user", "")).toBe(false)
46+
})
47+
})
48+
49+
describe("Windows-specific behavior", () => {
50+
beforeEach(() => {
51+
Object.defineProperty(process, "platform", {
52+
value: "win32",
53+
configurable: true,
54+
})
55+
})
56+
57+
it("should perform case-insensitive comparison on Windows", () => {
58+
expect(arePathsEqual("C:\\Users\\Project", "c:\\users\\project")).toBe(true)
59+
expect(arePathsEqual("C:\\USERS\\PROJECT", "c:\\Users\\Project")).toBe(true)
60+
})
61+
62+
it("should handle mixed separators on Windows", () => {
63+
expect(arePathsEqual("C:\\Users\\Project", "C:/Users/Project")).toBe(true)
64+
expect(arePathsEqual("C:/Users/Project", "C:\\Users\\Project")).toBe(true)
65+
})
66+
})
67+
68+
describe("POSIX-specific behavior", () => {
69+
beforeEach(() => {
70+
Object.defineProperty(process, "platform", {
71+
value: "darwin",
72+
configurable: true,
73+
})
74+
})
75+
76+
it("should perform case-sensitive comparison on POSIX systems", () => {
77+
expect(arePathsEqual("/Users/Project", "/users/project")).toBe(false)
78+
expect(arePathsEqual("/Users/Project", "/Users/Project")).toBe(true)
79+
})
80+
})
81+
82+
describe("Desktop directory handling", () => {
83+
it("should correctly compare Desktop paths on macOS", () => {
84+
Object.defineProperty(process, "platform", {
85+
value: "darwin",
86+
configurable: true,
87+
})
88+
89+
const desktopPath = "/Users/testuser/Desktop"
90+
const desktopPathWithSlash = "/Users/testuser/Desktop/"
91+
const desktopPathNormalized = path.normalize("/Users/testuser/Desktop")
92+
93+
expect(arePathsEqual(desktopPath, desktopPath)).toBe(true)
94+
expect(arePathsEqual(desktopPath, desktopPathWithSlash)).toBe(true)
95+
expect(arePathsEqual(desktopPath, desktopPathNormalized)).toBe(true)
96+
})
97+
98+
it("should correctly compare Desktop paths on Windows", () => {
99+
Object.defineProperty(process, "platform", {
100+
value: "win32",
101+
configurable: true,
102+
})
103+
104+
const desktopPath = "C:\\Users\\testuser\\Desktop"
105+
const desktopPathWithSlash = "C:\\Users\\testuser\\Desktop\\"
106+
const desktopPathMixedCase = "c:\\users\\testuser\\desktop"
107+
const desktopPathForwardSlash = "C:/Users/testuser/Desktop"
108+
109+
expect(arePathsEqual(desktopPath, desktopPath)).toBe(true)
110+
expect(arePathsEqual(desktopPath, desktopPathWithSlash)).toBe(true)
111+
expect(arePathsEqual(desktopPath, desktopPathMixedCase)).toBe(true)
112+
expect(arePathsEqual(desktopPath, desktopPathForwardSlash)).toBe(true)
113+
})
114+
115+
it("should handle relative Desktop paths", () => {
116+
const homeDir = os.homedir()
117+
const desktopRelative = path.join("~", "Desktop").replace("~", homeDir)
118+
const desktopAbsolute = path.join(homeDir, "Desktop")
119+
120+
expect(arePathsEqual(desktopRelative, desktopAbsolute)).toBe(true)
121+
})
122+
})
123+
124+
describe("edge cases", () => {
125+
it("should handle paths with multiple slashes", () => {
126+
expect(arePathsEqual("/home//user///project", "/home/user/project")).toBe(true)
127+
expect(arePathsEqual("C:\\\\Users\\\\\\project", "C:\\Users\\project")).toBe(true)
128+
})
129+
130+
it("should handle root paths", () => {
131+
expect(arePathsEqual("/", "/")).toBe(true)
132+
expect(arePathsEqual("C:\\", "C:\\")).toBe(true)
133+
134+
// Root paths should keep their trailing slash
135+
Object.defineProperty(process, "platform", {
136+
value: "win32",
137+
configurable: true,
138+
})
139+
expect(arePathsEqual("C:\\", "c:/")).toBe(true)
140+
})
141+
142+
it("should return false for different paths", () => {
143+
expect(arePathsEqual("/home/user/project1", "/home/user/project2")).toBe(false)
144+
expect(arePathsEqual("/home/user", "/home/user/project")).toBe(false)
145+
})
146+
})
147+
})

0 commit comments

Comments
 (0)