Skip to content

Commit bfd6009

Browse files
authored
Merge pull request #1682 from GitlyHallows/bug/old-task-deletion
Fix old task deletion bug
2 parents 7459ac5 + 2a2e467 commit bfd6009

File tree

3 files changed

+143
-22
lines changed

3 files changed

+143
-22
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2208,33 +2208,51 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
22082208
}> {
22092209
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
22102210
const historyItem = history.find((item) => item.id === id)
2211-
if (historyItem) {
2212-
const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", id)
2213-
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
2214-
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
2215-
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
2216-
if (fileExists) {
2217-
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
2218-
return {
2219-
historyItem,
2220-
taskDirPath,
2221-
apiConversationHistoryFilePath,
2222-
uiMessagesFilePath,
2223-
apiConversationHistory,
2224-
}
2225-
}
2211+
if (!historyItem) {
2212+
throw new Error("Task not found in history")
2213+
}
2214+
2215+
const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", id)
2216+
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
2217+
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
2218+
2219+
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
2220+
if (!fileExists) {
2221+
// Instead of silently deleting, throw a specific error
2222+
throw new Error("TASK_FILES_MISSING")
2223+
}
2224+
2225+
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
2226+
return {
2227+
historyItem,
2228+
taskDirPath,
2229+
apiConversationHistoryFilePath,
2230+
uiMessagesFilePath,
2231+
apiConversationHistory,
22262232
}
2227-
// if we tried to get a task that doesn't exist, remove it from state
2228-
// FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason
2229-
await this.deleteTaskFromState(id)
2230-
throw new Error("Task not found")
22312233
}
22322234

22332235
async showTaskWithId(id: string) {
22342236
if (id !== this.getCurrentCline()?.taskId) {
2235-
// Non-current task.
2236-
const { historyItem } = await this.getTaskWithId(id)
2237-
await this.initClineWithHistoryItem(historyItem) // Clears existing task.
2237+
try {
2238+
const { historyItem } = await this.getTaskWithId(id)
2239+
await this.initClineWithHistoryItem(historyItem)
2240+
} catch (error) {
2241+
if (error.message === "TASK_FILES_MISSING") {
2242+
const response = await vscode.window.showWarningMessage(
2243+
"This task's files are missing. Would you like to remove it from the task list?",
2244+
"Remove",
2245+
"Keep",
2246+
)
2247+
2248+
if (response === "Remove") {
2249+
await this.deleteTaskFromState(id)
2250+
await this.postStateToWebview()
2251+
}
2252+
return
2253+
}
2254+
throw error
2255+
}
22382256
}
22392257

22402258
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
@@ -2702,4 +2720,30 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
27022720

27032721
return properties
27042722
}
2723+
2724+
async validateTaskHistory() {
2725+
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
2726+
const validTasks: HistoryItem[] = []
2727+
2728+
for (const item of history) {
2729+
const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", item.id)
2730+
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
2731+
2732+
if (await fileExistsAtPath(apiConversationHistoryFilePath)) {
2733+
validTasks.push(item)
2734+
}
2735+
}
2736+
2737+
if (validTasks.length !== history.length) {
2738+
await this.updateGlobalState("taskHistory", validTasks)
2739+
await this.postStateToWebview()
2740+
2741+
const removedCount = history.length - validTasks.length
2742+
if (removedCount > 0) {
2743+
await vscode.window.showInformationMessage(
2744+
`Cleaned up ${removedCount} task(s) with missing files from history.`,
2745+
)
2746+
}
2747+
}
2748+
}
27052749
}

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,78 @@ jest.mock("../../contextProxy", () => {
5454
}
5555
})
5656

57+
describe("validateTaskHistory", () => {
58+
let provider: ClineProvider
59+
let mockContext: vscode.ExtensionContext
60+
let mockOutputChannel: vscode.OutputChannel
61+
let mockUpdate: jest.Mock
62+
63+
beforeEach(() => {
64+
// Reset mocks
65+
jest.clearAllMocks()
66+
67+
mockUpdate = jest.fn()
68+
69+
// Setup basic mocks
70+
mockContext = {
71+
globalState: {
72+
get: jest.fn(),
73+
update: mockUpdate,
74+
keys: jest.fn().mockReturnValue([]),
75+
},
76+
secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
77+
extensionUri: {} as vscode.Uri,
78+
globalStorageUri: { fsPath: "/test/path" },
79+
extension: { packageJSON: { version: "1.0.0" } },
80+
} as unknown as vscode.ExtensionContext
81+
82+
mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
83+
provider = new ClineProvider(mockContext, mockOutputChannel)
84+
})
85+
86+
test("should remove tasks with missing files", async () => {
87+
// Mock the global state with some test data
88+
const mockHistory = [
89+
{ id: "task1", ts: Date.now() },
90+
{ id: "task2", ts: Date.now() },
91+
]
92+
93+
// Setup mocks
94+
jest.spyOn(mockContext.globalState, "get").mockReturnValue(mockHistory)
95+
96+
// Mock fileExistsAtPath to only return true for task1
97+
const mockFs = require("../../../utils/fs")
98+
mockFs.fileExistsAtPath = jest.fn().mockImplementation((path) => Promise.resolve(path.includes("task1")))
99+
100+
// Call validateTaskHistory
101+
await provider.validateTaskHistory()
102+
103+
// Verify the results
104+
const expectedHistory = [expect.objectContaining({ id: "task1" })]
105+
106+
expect(mockUpdate).toHaveBeenCalledWith("taskHistory", expect.arrayContaining(expectedHistory))
107+
expect(mockUpdate.mock.calls[0][1].length).toBe(1)
108+
})
109+
110+
test("should handle empty history", async () => {
111+
// Mock empty history
112+
jest.spyOn(mockContext.globalState, "get").mockReturnValue([])
113+
114+
await provider.validateTaskHistory()
115+
116+
expect(mockUpdate).toHaveBeenCalledWith("taskHistory", [])
117+
})
118+
119+
test("should handle null history", async () => {
120+
// Mock null history
121+
jest.spyOn(mockContext.globalState, "get").mockReturnValue(null)
122+
123+
await provider.validateTaskHistory()
124+
125+
expect(mockUpdate).toHaveBeenCalledWith("taskHistory", [])
126+
})
127+
})
128+
57129
// Mock dependencies
58130
jest.mock("vscode")
59131
jest.mock("delay")

src/extension.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export function activate(context: vscode.ExtensionContext) {
5959
const provider = new ClineProvider(context, outputChannel)
6060
telemetryService.setProvider(provider)
6161

62+
// Validate task history on extension activation
63+
provider.validateTaskHistory().catch((error) => {
64+
outputChannel.appendLine(`Failed to validate task history: ${error}`)
65+
})
66+
6267
context.subscriptions.push(
6368
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, provider, {
6469
webviewOptions: { retainContextWhenHidden: true },

0 commit comments

Comments
 (0)