-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: Add pause/resume button for AI streaming responses (#8331) #8333
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
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 |
|---|---|---|
|
|
@@ -2650,12 +2650,40 @@ export class ClineProvider | |
| } | ||
| } | ||
|
|
||
| public resumeTask(taskId: string): void { | ||
| // Use the existing showTaskWithId method which handles both current and | ||
| // historical tasks. | ||
| this.showTaskWithId(taskId).catch((error) => { | ||
| this.log(`Failed to resume task ${taskId}: ${error.message}`) | ||
| }) | ||
| public async pauseTask(): Promise<void> { | ||
| const task = this.getCurrentTask() | ||
| if (!task) { | ||
| return | ||
| } | ||
|
|
||
| this.log(`[pauseTask] pausing task ${task.taskId}`) | ||
|
|
||
|
Contributor
Author
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. [P2] API compatibility: |
||
| // Set the task as paused | ||
| task.isPaused = true | ||
|
|
||
| // Emit pause event for tracking | ||
| task.emit(RooCodeEventName.TaskPaused, task.taskId) | ||
|
|
||
| // Update the UI to reflect the paused state | ||
| await this.postStateToWebview() | ||
| } | ||
|
|
||
| public async resumeTask(): Promise<void> { | ||
| const task = this.getCurrentTask() | ||
| if (!task) { | ||
| return | ||
| } | ||
|
|
||
| this.log(`[resumeTask] resuming task ${task.taskId}`) | ||
|
|
||
| // Set the task as not paused | ||
| task.isPaused = false | ||
|
Contributor
Author
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. [P2] Event typing: Emitting |
||
|
|
||
| // Emit resume event for tracking | ||
| task.emit(RooCodeEventName.TaskUnpaused, task.taskId) | ||
|
|
||
| // Update the UI to reflect the resumed state | ||
| await this.postStateToWebview() | ||
| } | ||
|
|
||
| // Modes | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro | |
| ref, | ||
| ) => { | ||
| const isMountedRef = useRef(true) | ||
| const [isPaused, setIsPaused] = useState(false) | ||
|
|
||
| const [audioBaseUri] = useState(() => { | ||
| const w = window as any | ||
|
|
@@ -459,6 +460,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro | |
| setEnableButtons(false) | ||
| setPrimaryButtonText(undefined) | ||
| setSecondaryButtonText(undefined) | ||
| setIsPaused(false) | ||
| } | ||
| }, [messages.length]) | ||
|
|
||
|
|
@@ -578,6 +580,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro | |
| setSelectedImages([]) | ||
| setClineAsk(undefined) | ||
| setEnableButtons(false) | ||
| setIsPaused(false) | ||
| // Do not reset mode here as it should persist. | ||
| // setPrimaryButtonText(undefined) | ||
| // setSecondaryButtonText(undefined) | ||
|
|
@@ -728,8 +731,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro | |
| const trimmedInput = text?.trim() | ||
|
|
||
| if (isStreaming) { | ||
| vscode.postMessage({ type: "cancelTask" }) | ||
| setDidClickCancel(true) | ||
| // If paused, resume; otherwise pause | ||
| if (isPaused) { | ||
|
Contributor
Author
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. [P2] |
||
| vscode.postMessage({ type: "resumeTask" }) | ||
| setIsPaused(false) | ||
| } else { | ||
| vscode.postMessage({ type: "pauseTask" }) | ||
| setIsPaused(true) | ||
| } | ||
| return | ||
| } | ||
|
|
||
|
|
@@ -767,7 +776,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro | |
| setClineAsk(undefined) | ||
| setEnableButtons(false) | ||
| }, | ||
| [clineAsk, startNewTask, isStreaming], | ||
| [clineAsk, startNewTask, isStreaming, isPaused], | ||
| ) | ||
|
|
||
| const { info: model } = useSelectedModel(apiConfiguration) | ||
|
|
@@ -1945,26 +1954,69 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro | |
| </StandardTooltip> | ||
| )} | ||
| {(secondaryButtonText || isStreaming) && ( | ||
| <StandardTooltip | ||
| content={ | ||
| isStreaming | ||
| ? t("chat:cancel.tooltip") | ||
| : secondaryButtonText === t("chat:startNewTask.title") | ||
| ? t("chat:startNewTask.tooltip") | ||
| : secondaryButtonText === t("chat:reject.title") | ||
| ? t("chat:reject.tooltip") | ||
| : secondaryButtonText === t("chat:terminate.title") | ||
| ? t("chat:terminate.tooltip") | ||
| : undefined | ||
| }> | ||
| <VSCodeButton | ||
| appearance="secondary" | ||
| disabled={!enableButtons && !(isStreaming && !didClickCancel)} | ||
| className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"} | ||
| onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}> | ||
| {isStreaming ? t("chat:cancel.title") : secondaryButtonText} | ||
| </VSCodeButton> | ||
| </StandardTooltip> | ||
| <> | ||
| {isStreaming && ( | ||
| <StandardTooltip | ||
| content={ | ||
| isPaused | ||
| ? t("chat:resume.tooltip", { | ||
| defaultValue: "Resume the AI's response", | ||
|
Contributor
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. Inline fallback strings are used in translation calls (e.g. t('chat:resume.title', { defaultValue: 'Resume' }) and t('chat:pause.title', { defaultValue: 'Pause' })). According to our guidelines, remove these defaultValue options so that the system falls back on the English JSON file automatically. This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX. |
||
| }) | ||
| : t("chat:pause.tooltip", { | ||
| defaultValue: "Pause the AI's response", | ||
| }) | ||
| }> | ||
| <VSCodeButton | ||
| appearance="secondary" | ||
| disabled={didClickCancel} | ||
| className="flex-1 mr-[6px]" | ||
| onClick={() => | ||
| handleSecondaryButtonClick(inputValue, selectedImages) | ||
| }> | ||
| {isPaused | ||
| ? t("chat:resume.title", { defaultValue: "Resume" }) | ||
| : t("chat:pause.title", { defaultValue: "Pause" })} | ||
| </VSCodeButton> | ||
| </StandardTooltip> | ||
| )} | ||
| {isStreaming && ( | ||
| <StandardTooltip content={t("chat:cancel.tooltip")}> | ||
| <VSCodeButton | ||
| appearance="secondary" | ||
| disabled={didClickCancel} | ||
| className="flex-1 ml-[6px]" | ||
| onClick={() => { | ||
| vscode.postMessage({ type: "cancelTask" }) | ||
| setDidClickCancel(true) | ||
| setIsPaused(false) | ||
| }}> | ||
| {t("chat:cancel.title")} | ||
| </VSCodeButton> | ||
| </StandardTooltip> | ||
| )} | ||
| {!isStreaming && secondaryButtonText && ( | ||
| <StandardTooltip | ||
| content={ | ||
| secondaryButtonText === t("chat:startNewTask.title") | ||
| ? t("chat:startNewTask.tooltip") | ||
| : secondaryButtonText === t("chat:reject.title") | ||
| ? t("chat:reject.tooltip") | ||
| : secondaryButtonText === t("chat:terminate.title") | ||
| ? t("chat:terminate.tooltip") | ||
| : undefined | ||
| }> | ||
| <VSCodeButton | ||
| appearance="secondary" | ||
| disabled={!enableButtons} | ||
| className="flex-1 ml-[6px]" | ||
| onClick={() => | ||
| handleSecondaryButtonClick(inputValue, selectedImages) | ||
| }> | ||
| {secondaryButtonText} | ||
| </VSCodeButton> | ||
| </StandardTooltip> | ||
| )} | ||
| </> | ||
| )} | ||
| </> | ||
| )} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,6 +92,14 @@ | |
| "title": "Cancel", | ||
| "tooltip": "Cancel the current operation" | ||
| }, | ||
| "pause": { | ||
| "title": "Pause", | ||
|
Contributor
Author
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. [P3] New i18n keys are added for |
||
| "tooltip": "Pause the AI's response" | ||
| }, | ||
| "resume": { | ||
| "title": "Resume", | ||
| "tooltip": "Resume the AI's response" | ||
| }, | ||
| "editMessage": { | ||
| "placeholder": "Edit your message..." | ||
| }, | ||
|
|
||
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.
[P1] If
this.abortis set while paused,breakexits only the inner pause loop and then the method continues processing the current chunk. This risks emitting partial content after an abort. Consider exiting the outer streaming loop (or returning) when abort is detected inside the pause wait.