|
@@ -50,7 +50,6 @@ import { Package } from "../../shared/package"
|
|
|
import { findLast } from "../../shared/array"
|
|
import { findLast } from "../../shared/array"
|
|
|
import { supportPrompt } from "../../shared/support-prompt"
|
|
import { supportPrompt } from "../../shared/support-prompt"
|
|
|
import { GlobalFileNames } from "../../shared/globalFileNames"
|
|
import { GlobalFileNames } from "../../shared/globalFileNames"
|
|
|
-import { safeJsonParse } from "../../shared/safeJsonParse"
|
|
|
|
|
import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
|
|
import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
|
|
|
import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
|
|
import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
|
|
|
import { experimentDefault } from "../../shared/experiments"
|
|
import { experimentDefault } from "../../shared/experiments"
|
|
@@ -145,10 +144,6 @@ export class ClineProvider
|
|
|
private pendingOperations: Map<string, PendingEditOperation> = new Map()
|
|
private pendingOperations: Map<string, PendingEditOperation> = new Map()
|
|
|
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
|
|
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
|
|
|
|
|
|
|
|
- // Transactional state posting
|
|
|
|
|
- private uiUpdatePaused: boolean = false
|
|
|
|
|
- private pendingState: ExtensionState | null = null
|
|
|
|
|
-
|
|
|
|
|
public isViewLaunched = false
|
|
public isViewLaunched = false
|
|
|
public settingsImportedAt?: number
|
|
public settingsImportedAt?: number
|
|
|
public readonly latestAnnouncementId = "nov-2025-v3.30.0-pr-fixer" // v3.30.0 PR Fixer announcement
|
|
public readonly latestAnnouncementId = "nov-2025-v3.30.0-pr-fixer" // v3.30.0 PR Fixer announcement
|
|
@@ -1629,26 +1624,8 @@ export class ClineProvider
|
|
|
await this.postStateToWebview()
|
|
await this.postStateToWebview()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public beginStateTransaction(): void {
|
|
|
|
|
- this.uiUpdatePaused = true
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public async endStateTransaction(): Promise<void> {
|
|
|
|
|
- this.uiUpdatePaused = false
|
|
|
|
|
- if (this.pendingState) {
|
|
|
|
|
- await this.view?.webview.postMessage({ type: "state", state: this.pendingState })
|
|
|
|
|
- this.pendingState = null
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
async postStateToWebview() {
|
|
async postStateToWebview() {
|
|
|
const state = await this.getStateToPostToWebview()
|
|
const state = await this.getStateToPostToWebview()
|
|
|
-
|
|
|
|
|
- if (this.uiUpdatePaused) {
|
|
|
|
|
- this.pendingState = state
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
this.postMessageToWebview({ type: "state", state })
|
|
this.postMessageToWebview({ type: "state", state })
|
|
|
|
|
|
|
|
// Check MDM compliance and send user to account tab if not compliant
|
|
// Check MDM compliance and send user to account tab if not compliant
|
|
@@ -2640,13 +2617,24 @@ export class ClineProvider
|
|
|
|
|
|
|
|
console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`)
|
|
console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`)
|
|
|
|
|
|
|
|
- // Mark this as a user-initiated cancellation
|
|
|
|
|
|
|
+ const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId)
|
|
|
|
|
+
|
|
|
|
|
+ // Preserve parent and root task information for history item.
|
|
|
|
|
+ const rootTask = task.rootTask
|
|
|
|
|
+ const parentTask = task.parentTask
|
|
|
|
|
+
|
|
|
|
|
+ // Mark this as a user-initiated cancellation so provider-only rehydration can occur
|
|
|
task.abortReason = "user_cancelled"
|
|
task.abortReason = "user_cancelled"
|
|
|
|
|
|
|
|
- // Soft abort (isAbandoned = false) to keep the instance alive
|
|
|
|
|
- await task.abortTask(false)
|
|
|
|
|
|
|
+ // Capture the current instance to detect if rehydrate already occurred elsewhere
|
|
|
|
|
+ const originalInstanceId = task.instanceId
|
|
|
|
|
+
|
|
|
|
|
+ // Begin abort (non-blocking)
|
|
|
|
|
+ task.abortTask()
|
|
|
|
|
+
|
|
|
|
|
+ // Immediately mark the original instance as abandoned to prevent any residual activity
|
|
|
|
|
+ task.abandoned = true
|
|
|
|
|
|
|
|
- // Wait for abort to complete
|
|
|
|
|
await pWaitFor(
|
|
await pWaitFor(
|
|
|
() =>
|
|
() =>
|
|
|
this.getCurrentTask()! === undefined ||
|
|
this.getCurrentTask()! === undefined ||
|
|
@@ -2663,52 +2651,28 @@ export class ClineProvider
|
|
|
console.error("Failed to abort task")
|
|
console.error("Failed to abort task")
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- // Deterministic spinner stop: If the last api_req_started has no cost and no cancelReason,
|
|
|
|
|
- // inject cancelReason to stop the spinner
|
|
|
|
|
- try {
|
|
|
|
|
- let lastApiReqStartedIndex = -1
|
|
|
|
|
- for (let i = task.clineMessages.length - 1; i >= 0; i--) {
|
|
|
|
|
- if (task.clineMessages[i].type === "say" && task.clineMessages[i].say === "api_req_started") {
|
|
|
|
|
- lastApiReqStartedIndex = i
|
|
|
|
|
- break
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Defensive safeguard: if current instance already changed, skip rehydrate
|
|
|
|
|
+ const current = this.getCurrentTask()
|
|
|
|
|
+ if (current && current.instanceId !== originalInstanceId) {
|
|
|
|
|
+ this.log(
|
|
|
|
|
+ `[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`,
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (lastApiReqStartedIndex !== -1) {
|
|
|
|
|
- const lastApiReqStarted = task.clineMessages[lastApiReqStartedIndex]
|
|
|
|
|
- const apiReqInfo = safeJsonParse<{ cost?: number; cancelReason?: string }>(
|
|
|
|
|
- lastApiReqStarted.text || "{}",
|
|
|
|
|
- {},
|
|
|
|
|
|
|
+ // Final race check before rehydrate to avoid duplicate rehydration
|
|
|
|
|
+ {
|
|
|
|
|
+ const currentAfterCheck = this.getCurrentTask()
|
|
|
|
|
+ if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) {
|
|
|
|
|
+ this.log(
|
|
|
|
|
+ `[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`,
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- if (apiReqInfo && apiReqInfo.cost === undefined && apiReqInfo.cancelReason === undefined) {
|
|
|
|
|
- apiReqInfo.cancelReason = "user_cancelled"
|
|
|
|
|
- lastApiReqStarted.text = JSON.stringify(apiReqInfo)
|
|
|
|
|
- await task.overwriteClineMessages([...task.clineMessages])
|
|
|
|
|
- console.log(`[cancelTask] Injected cancelReason for deterministic spinner stop`)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error(`[cancelTask] Failed to inject cancelReason:`, error)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Update UI immediately to reflect current state
|
|
|
|
|
- await this.postStateToWebview()
|
|
|
|
|
-
|
|
|
|
|
- // Schedule non-blocking resumption to present "Resume Task" ask
|
|
|
|
|
- // Use setImmediate to avoid blocking the webview handler
|
|
|
|
|
- setImmediate(() => {
|
|
|
|
|
- if (task && !task.abandoned) {
|
|
|
|
|
- // Present a resume ask without rehydrating - just show the Resume/Terminate UI
|
|
|
|
|
- task.presentResumableAsk().catch((error) => {
|
|
|
|
|
- console.error(
|
|
|
|
|
- `[cancelTask] Failed to present resume ask: ${
|
|
|
|
|
- error instanceof Error ? error.message : String(error)
|
|
|
|
|
- }`,
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // Clears task again, so we need to abortTask manually above.
|
|
|
|
|
+ await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Clear the current task without treating it as a subtask.
|
|
// Clear the current task without treating it as a subtask.
|