-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: ensure cancel button properly aborts streaming responses (#7014) #7016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -1663,6 +1663,26 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |||||||||||||||||||||||||
const iterator = stream[Symbol.asyncIterator]() | ||||||||||||||||||||||||||
let item = await iterator.next() | ||||||||||||||||||||||||||
while (!item.done) { | ||||||||||||||||||||||||||
// Check abort flag BEFORE processing the chunk | ||||||||||||||||||||||||||
if (this.abort) { | ||||||||||||||||||||||||||
console.log(`aborting stream (early check), this.abandoned = ${this.abandoned}`) | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The abort check and cleanup logic is duplicated here and at lines 1730-1747. Consider extracting this into a helper method like
Suggested change
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if (!this.abandoned) { | ||||||||||||||||||||||||||
// Only need to gracefully abort if this instance | ||||||||||||||||||||||||||
// isn't abandoned (sometimes OpenRouter stream | ||||||||||||||||||||||||||
// hangs, in which case this would affect future | ||||||||||||||||||||||||||
// instances of Cline). | ||||||||||||||||||||||||||
await abortStream("user_cancelled") | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Clean up the iterator if it has a return method | ||||||||||||||||||||||||||
if (iterator.return) { | ||||||||||||||||||||||||||
await iterator.return(undefined).catch(() => {}) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
break // Aborts the stream. | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const chunk = item.value | ||||||||||||||||||||||||||
item = await iterator.next() | ||||||||||||||||||||||||||
if (!chunk) { | ||||||||||||||||||||||||||
|
@@ -1707,8 +1727,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Check abort flag AFTER processing the chunk as well | ||||||||||||||||||||||||||
if (this.abort) { | ||||||||||||||||||||||||||
console.log(`aborting stream, this.abandoned = ${this.abandoned}`) | ||||||||||||||||||||||||||
console.log(`aborting stream (after chunk), this.abandoned = ${this.abandoned}`) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if (!this.abandoned) { | ||||||||||||||||||||||||||
// Only need to gracefully abort if this instance | ||||||||||||||||||||||||||
|
@@ -1718,6 +1739,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |||||||||||||||||||||||||
await abortStream("user_cancelled") | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Clean up the iterator if it has a return method | ||||||||||||||||||||||||||
if (iterator.return) { | ||||||||||||||||||||||||||
await iterator.return(undefined).catch(() => {}) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
break // Aborts the stream. | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -1228,6 +1228,10 @@ export class ClineProvider | |||||
const rootTask = cline.rootTask | ||||||
const parentTask = cline.parentTask | ||||||
|
||||||
// Set the abort flag immediately to signal cancellation | ||||||
cline.abort = true | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good fix! Setting the abort flag immediately before calling |
||||||
|
||||||
// Then call abortTask to handle cleanup | ||||||
cline.abortTask() | ||||||
|
||||||
await pWaitFor( | ||||||
|
@@ -1243,7 +1247,7 @@ export class ClineProvider | |||||
timeout: 3_000, | ||||||
}, | ||||||
).catch(() => { | ||||||
console.error("Failed to abort task") | ||||||
console.error("Failed to abort task gracefully") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: Could be more specific about the timeout duration:
Suggested change
|
||||||
}) | ||||||
|
||||||
if (this.getCurrentCline()) { | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding tests for this critical cancel functionality to prevent regression. The fix looks good, but tests would verify: