Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0575a35
fix: prevent agent loop stall from WriteToFileTool filesystem errors …
awschmeder Jun 26, 2026
75b52e3
fix: clear stuck UI spinner and duplicate/repeated errors on write_to…
awschmeder Jun 26, 2026
0966556
fix(write-to-file): address partial filesystem error review
easonliang28 Jul 30, 2026
16c4d48
fix(write-to-file): guard diff reset cleanup
easonliang28 Jul 30, 2026
be0e154
fix(write-to-file): guard partial ask finalization
easonliang28 Jul 30, 2026
224690b
fix(write-to-file): clean partial state on task abort
easonliang28 Jul 30, 2026
dd31f44
Merge branch 'main' into fix/pr-727-review
easonLiangWorldedtech Aug 12, 2026
c17b15b
fix(write-to-file): address remaining review cleanup
easonliang28 Aug 12, 2026
c011322
Merge branch 'main' into fix/pr-727-review
easonLiangWorldedtech Aug 12, 2026
44f9aa9
Merge branch 'main' into fix/pr-727-review
easonLiangWorldedtech Aug 15, 2026
6f99772
fix(write-to-file): clean up partial state on rooignore denial
easonliang28 Aug 19, 2026
4189f88
test(task): cover finalizePartialToolAsk updateClineMessage rejection…
easonliang28 Aug 23, 2026
f3e4d40
fix(tools): finalize open partial tool ask on args parse failure
easonliang28 Aug 29, 2026
c47e3c7
fix(write-to-file): address review findings on partial-stream cleanup
easonliang28 Aug 29, 2026
a03ac8c
Merge branch 'main' into fix/pr-727-review
easonLiangWorldedtech Sep 2, 2026
d7f8038
test(write-to-file): cover partial-ask finalize rejection, revert fai…
easonliang28 Sep 2, 2026
26ad729
Merge branch 'main' into fix/pr-727-review
edelauna Sep 3, 2026
f8b6b35
fix(mutation): match direct spec files case-insensitively
easonliang28 Sep 4, 2026
5d32001
test(task): cover finalizePartialToolAsk predicate clause mismatches
easonliang28 Sep 4, 2026
26dda51
test(tools): cover write_to_file stabilization and cleanup branches
easonliang28 Sep 4, 2026
4bd7d88
Merge branch 'main' into fix/pr-727-review
easonLiangWorldedtech Sep 4, 2026
4fd1a9f
fix(core): address CodeRabbit review findings on write_to_file and ta…
easonliang28 Sep 4, 2026
3228643
fix(task): report save stages independently in saveClineMessages
easonliang28 Sep 4, 2026
9c26889
Merge upstream/main into fix/pr-727-review
easonliang28 Sep 15, 2026
3d23d9d
test(core): harden finalizePartialToolAsk and write_to_file cleanup t…
easonliang28 Sep 15, 2026
ef7a941
chore(coverage): update extension coverage population baseline after …
easonliang28 Sep 15, 2026
b9e2bda
Merge remote-tracking branch 'upstream/main' into fix/pr-727-review
easonliang28 Sep 16, 2026
a704b15
fix(write_to_file): report captured streaming error on parse failure;…
easonliang28 Sep 16, 2026
7b6835f
fix(write_to_file): run diff cleanup when handleError rejects; tighte…
easonliang28 Sep 16, 2026
9d75b10
test(write_to_file): prove revert-then-reset ordering when handleErro…
easonliang28 Sep 16, 2026
8a5bf2d
fix(write_to_file): restore diff document on parse-failure teardown
easonliang28 Sep 16, 2026
9f8e91c
Merge branch 'main' into fix/pr-727-review
easonLiangWorldedtech Sep 23, 2026
ac673f9
test(write-to-file): assert the no-argument finalize call on parse fa…
Sep 28, 2026
6db4039
Merge branch 'main' into fix/pr-727-review
easonLiangWorldedtech Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import { ApiStream, GroundingSource } from "../../api/transform/stream"
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"

// shared
import { findLastIndex } from "../../shared/array"
import { findLast, findLastIndex } from "../../shared/array"
import { combineApiRequests } from "../../shared/combineApiRequests"
import { combineCommandSequences } from "../../shared/combineCommandSequences"
import { t } from "../../i18n"
Expand Down Expand Up @@ -1829,6 +1829,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
}

/**
* Finalize a partial "tool" ask message without blocking for user input.
* Call this in error paths where a partial tool message was opened during streaming
* but execution failed before the normal approval flow could close it, so the webview
* spinner does not get stuck in a loading state.
*
* The matching partial message may no longer be the final entry if another asynchronous
* message was inserted between the partial ask and the error handler, so search backward
* instead of relying on clineMessages.at(-1).
*/
async finalizePartialToolAsk(text?: string): Promise<void> {
const partialToolAsk = findLast(
this.clineMessages,
(message) =>
message.partial === true &&
message.type === "ask" &&
message.ask === "tool" &&
(text === undefined || message.text === text),
)

if (!partialToolAsk) {
return
}

partialToolAsk.partial = false
await this.saveClineMessages()
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
await this.updateClineMessage(partialToolAsk).catch((error) => {
console.error("[Task#finalizePartialToolAsk] updateClineMessage failed:", error)
})
}

// Lifecycle
// Start / Resume / Abort / Dispose

Expand Down
119 changes: 119 additions & 0 deletions src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3350,6 +3350,125 @@ describe("Cline", () => {
saveSpy.mockRestore()
})

it("finalizePartialToolAsk persists and updates a non-last partial tool ask", async () => {
let updateSnapshot: Record<string, unknown> | undefined
const updateSpy = vi
.spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage")
.mockImplementation(async (message) => {
updateSnapshot = { ...message }
})
const saveSpy = vi.spyOn(getTaskTestAccess(Task.prototype), "saveClineMessages").mockResolvedValue(true)

const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})

const partialToolAsk = {
ts: Date.now() - 2,
type: "ask" as const,
ask: "tool" as const,
text: "partial tool message",
partial: true,
}

task.clineMessages.push(partialToolAsk)
task.clineMessages.push({
ts: Date.now() - 1,
type: "say",
say: "error",
text: "intervening async message",
})

await task.finalizePartialToolAsk("partial tool message")
await flushMicrotasks()

expect(partialToolAsk.partial).toBe(false)
expect(saveSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalledWith(partialToolAsk)
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
expect(updateSnapshot?.partial).toBe(false)

updateSpy.mockRestore()
saveSpy.mockRestore()
})

it("finalizePartialToolAsk ignores non-matching partial tool asks when text is provided", async () => {
const updateSpy = vi
.spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage")
.mockResolvedValue(undefined)
const saveSpy = vi.spyOn(getTaskTestAccess(Task.prototype), "saveClineMessages").mockResolvedValue(true)

const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})

task.clineMessages.push({
ts: Date.now() - 1,
type: "ask",
ask: "tool",
text: "other partial tool message",
partial: true,
})

await task.finalizePartialToolAsk("target partial tool message")
await flushMicrotasks()

expect(task.clineMessages[0].partial).toBe(true)
expect(saveSpy).not.toHaveBeenCalled()
expect(updateSpy).not.toHaveBeenCalled()
Comment thread
easonLiangWorldedtech marked this conversation as resolved.

updateSpy.mockRestore()
saveSpy.mockRestore()
})

it("finalizePartialToolAsk updates the latest partial tool ask when no text is provided", async () => {
const updateSpy = vi
.spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage")
.mockResolvedValue(undefined)
const saveSpy = vi.spyOn(getTaskTestAccess(Task.prototype), "saveClineMessages").mockResolvedValue(true)

const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})

const olderPartialToolAsk = {
ts: Date.now() - 2,
type: "ask" as const,
ask: "tool" as const,
text: "older partial tool message",
partial: true,
}
const latestPartialToolAsk = {
ts: Date.now() - 1,
type: "ask" as const,
ask: "tool" as const,
text: "latest partial tool message",
partial: true,
}

task.clineMessages.push(olderPartialToolAsk)
task.clineMessages.push(latestPartialToolAsk)

await task.finalizePartialToolAsk()
await flushMicrotasks()

expect(olderPartialToolAsk.partial).toBe(true)
expect(latestPartialToolAsk.partial).toBe(false)
expect(saveSpy).toHaveBeenCalled()
expect(updateSpy).toHaveBeenCalledWith(latestPartialToolAsk)

updateSpy.mockRestore()
saveSpy.mockRestore()
})

it("logs (instead of crashing) when updateClineMessage rejects from the ask() ignore-partial path", async () => {
// Pins the .catch arm on the fire-and-forget updateClineMessage call
// in ask() when a new partial ask arrives while the previous partial
Expand Down
3 changes: 3 additions & 0 deletions src/core/task/__tests__/Task.throttle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ describe("Task token usage throttling", () => {
let mockProvider: any
let mockApiConfiguration: ProviderSettings
let task: Task
let consoleLogSpy: ReturnType<typeof vi.spyOn>

beforeEach(() => {
// Reset all mocks
vi.clearAllMocks()
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
vi.useFakeTimers()

// Mock provider
Expand Down Expand Up @@ -101,6 +103,7 @@ describe("Task token usage throttling", () => {
if (task && !task.abort) {
task.dispose()
}
consoleLogSpy.mockRestore()
})

test("should emit TaskTokenUsageUpdated immediately on first change", async () => {
Expand Down
Loading
Loading