@@ -94,6 +94,11 @@ import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-pers
9494import { getNonce } from "./getNonce"
9595import { getUri } from "./getUri"
9696
97+ import {
98+ addCancelReasonToLastApiReqStarted ,
99+ appendAssistantInterruptionIfNeeded ,
100+ } from "../task-persistence/cancelBookkeeping"
101+ import { RESPONSE_INTERRUPTED_BY_USER } from "../../shared/messages"
97102/**
98103 * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
99104 * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
@@ -204,7 +209,7 @@ export class ClineProvider
204209 try {
205210 // Only rehydrate on genuine streaming failures.
206211 // User-initiated cancels are handled by cancelTask().
207- if ( ( instance as any ) . abortReason === "streaming_failed" ) {
212+ if ( instance . abortReason === "streaming_failed" ) {
208213 // Defensive safeguard: if another path already replaced this instance, skip
209214 const current = this . getCurrentTask ( )
210215 if ( current && current . instanceId !== instance . instanceId ) {
@@ -2555,9 +2560,7 @@ export class ClineProvider
25552560
25562561 console . log ( `[cancelTask] cancelling task ${ task . taskId } .${ task . instanceId } ` )
25572562
2558- const { historyItem, uiMessagesFilePath, apiConversationHistoryFilePath } = await this . getTaskWithId (
2559- task . taskId ,
2560- )
2563+ const { historyItem, uiMessagesFilePath } = await this . getTaskWithId ( task . taskId )
25612564
25622565 // Preserve parent and root task information for history item.
25632566 const rootTask = task . rootTask
@@ -2572,10 +2575,8 @@ export class ClineProvider
25722575 // Begin abort (non-blocking)
25732576 task . abortTask ( )
25742577
2575- // Immediately mark the current instance as abandoned to prevent any residual activity
2576- if ( this . getCurrentTask ( ) ) {
2577- this . getCurrentTask ( ) ! . abandoned = true
2578- }
2578+ // Immediately mark the original instance as abandoned to prevent any residual activity
2579+ task . abandoned = true
25792580
25802581 await pWaitFor (
25812582 ( ) =>
@@ -2604,60 +2605,32 @@ export class ClineProvider
26042605
26052606 // Provider-side cancel bookkeeping to mirror abortStream effects for user_cancelled
26062607 try {
2607- // Update ui_messages: add cancelReason to last api_req_started
2608- const messagesJson = await fs . readFile ( uiMessagesFilePath , "utf8" ) . catch ( ( ) => undefined )
2609- if ( messagesJson ) {
2610- const uiMsgs = JSON . parse ( messagesJson ) as ClineMessage [ ]
2611- if ( Array . isArray ( uiMsgs ) ) {
2612- const revIdx = uiMsgs
2613- . slice ( )
2614- . reverse ( )
2615- . findIndex ( ( m ) => m ?. type === "say" && ( m as any ) ?. say === "api_req_started" )
2616- if ( revIdx !== - 1 ) {
2617- const idx = uiMsgs . length - 1 - revIdx
2618- try {
2619- const existing = uiMsgs [ idx ] ?. text ? JSON . parse ( uiMsgs [ idx ] . text as string ) : { }
2620- uiMsgs [ idx ] . text = JSON . stringify ( { ...existing , cancelReason : "user_cancelled" } )
2621- await saveTaskMessages ( {
2622- messages : uiMsgs as any ,
2623- taskId : task . taskId ,
2624- globalStoragePath : this . contextProxy . globalStorageUri . fsPath ,
2625- } )
2626- } catch {
2627- // non-fatal
2628- }
2629- }
2630- }
2631- }
2608+ // Persist cancelReason to last api_req_started in UI messages
2609+ await addCancelReasonToLastApiReqStarted ( {
2610+ taskId : task . taskId ,
2611+ globalStoragePath : this . contextProxy . globalStorageUri . fsPath ,
2612+ reason : "user_cancelled" ,
2613+ } )
26322614
2633- // Update api_conversation_history: append assistant interruption if last isn't assistant
2634- try {
2635- const apiMsgs = await readApiMessages ( {
2636- taskId : task . taskId ,
2637- globalStoragePath : this . contextProxy . globalStorageUri . fsPath ,
2638- } )
2639- const last = apiMsgs . at ( - 1 )
2640- if ( ! last || last . role !== "assistant" ) {
2641- apiMsgs . push ( {
2642- role : "assistant" ,
2643- content : [ { type : "text" , text : "[Response interrupted by user]" } ] ,
2644- ts : Date . now ( ) ,
2645- } as any )
2646- await saveApiMessages ( {
2647- messages : apiMsgs as any ,
2648- taskId : task . taskId ,
2649- globalStoragePath : this . contextProxy . globalStorageUri . fsPath ,
2650- } )
2651- }
2652- } catch ( e ) {
2615+ // Append assistant interruption marker to API conversation history if needed
2616+ await appendAssistantInterruptionIfNeeded ( {
2617+ taskId : task . taskId ,
2618+ globalStoragePath : this . contextProxy . globalStorageUri . fsPath ,
2619+ text : `[${ RESPONSE_INTERRUPTED_BY_USER } ]` ,
2620+ } )
2621+ } catch ( e ) {
2622+ this . log ( `[cancelTask] Cancel bookkeeping failed: ${ e instanceof Error ? e . message : String ( e ) } ` )
2623+ }
2624+
2625+ // Final race check before rehydrate to avoid duplicate rehydration
2626+ {
2627+ const currentAfterBookkeeping = this . getCurrentTask ( )
2628+ if ( currentAfterBookkeeping && currentAfterBookkeeping . instanceId !== originalInstanceId ) {
26532629 this . log (
2654- `[cancelTask] Failed to update API history for user_cancelled: ${
2655- e instanceof Error ? e . message : String ( e )
2656- } `,
2630+ `[cancelTask] Skipping rehydrate after bookkeeping: current instance ${ currentAfterBookkeeping . instanceId } != original ${ originalInstanceId } ` ,
26572631 )
2632+ return
26582633 }
2659- } catch ( e ) {
2660- this . log ( `[cancelTask] Cancel bookkeeping failed: ${ e instanceof Error ? e . message : String ( e ) } ` )
26612634 }
26622635
26632636 // Clears task again, so we need to abortTask manually above.
0 commit comments