Skip to content

Commit 571eaf1

Browse files
allquixoticoz-agent
andcommitted
Fix task-scoped compaction routing
Bump extension version to 3.53.15. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 677fa60 commit 571eaf1

8 files changed

Lines changed: 447 additions & 34 deletions

File tree

src/core/webview/ClineProvider.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,10 +2116,14 @@ export class ClineProvider
21162116
}
21172117
}
21182118
if (!task) {
2119+
await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
21192120
throw new Error(`Task with id ${taskId} not found in stack`)
21202121
}
2121-
await task.condenseContext()
2122-
await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
2122+
try {
2123+
await task.condenseContext()
2124+
} finally {
2125+
await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
2126+
}
21232127
}
21242128

21252129
// this function deletes a task from task history, and deletes its checkpoints and delete the task folder

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,56 @@ describe("ClineProvider", () => {
610610
await expect(provider.postMessageToWebview(message)).resolves.toBeUndefined()
611611
})
612612

613+
describe("condenseTaskContext", () => {
614+
beforeEach(async () => {
615+
await provider.resolveWebviewView(mockWebviewView)
616+
mockPostMessage.mockClear()
617+
})
618+
619+
it("posts a response after condensing the requested task", async () => {
620+
const task = new Task(defaultTaskOptions) as any
621+
task.taskId = "task-1"
622+
task.condenseContext = vi.fn().mockResolvedValue(undefined)
623+
624+
await provider.addClineToStack(task)
625+
mockPostMessage.mockClear()
626+
627+
await provider.condenseTaskContext("task-1")
628+
629+
expect(task.condenseContext).toHaveBeenCalled()
630+
expect(mockPostMessage).toHaveBeenCalledWith({
631+
type: "condenseTaskContextResponse",
632+
text: "task-1",
633+
})
634+
})
635+
636+
it("posts a response when condensing fails", async () => {
637+
const task = new Task(defaultTaskOptions) as any
638+
task.taskId = "task-1"
639+
task.condenseContext = vi.fn().mockRejectedValue(new Error("condense failed"))
640+
641+
await provider.addClineToStack(task)
642+
mockPostMessage.mockClear()
643+
644+
await expect(provider.condenseTaskContext("task-1")).rejects.toThrow("condense failed")
645+
expect(mockPostMessage).toHaveBeenCalledWith({
646+
type: "condenseTaskContextResponse",
647+
text: "task-1",
648+
})
649+
})
650+
651+
it("posts a response when the requested task is missing", async () => {
652+
await expect(provider.condenseTaskContext("missing-task")).rejects.toThrow(
653+
"Task with id missing-task not found in stack",
654+
)
655+
656+
expect(mockPostMessage).toHaveBeenCalledWith({
657+
type: "condenseTaskContextResponse",
658+
text: "missing-task",
659+
})
660+
})
661+
})
662+
613663
test("postMessageToWebview skips postMessage after dispose", async () => {
614664
await provider.resolveWebviewView(mockWebviewView)
615665

src/core/webview/webviewMessageHandler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,15 @@ export const webviewMessageHandler = async (
828828
provider.showTaskWithId(message.text!)
829829
break
830830
case "condenseTaskContextRequest":
831-
provider.condenseTaskContext(message.text!)
831+
if (message.text) {
832+
provider.condenseTaskContext(message.text).catch((error) => {
833+
provider.log(
834+
`[condenseTaskContextRequest] failed for task ${message.text}: ${
835+
error instanceof Error ? error.message : String(error)
836+
}`,
837+
)
838+
})
839+
}
832840
break
833841
case "deleteTaskWithId":
834842
provider.deleteTaskWithId(message.text!)

src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
55
"publisher": "allquixotic",
6-
"version": "3.53.14",
6+
"version": "3.53.15",
77
"icon": "assets/icons/icon.png",
88
"galleryBanner": {
99
"color": "#617A91",

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
217217
const [checkpointWarning, setCheckpointWarning] = useState<
218218
{ type: "WAIT_TIMEOUT" | "INIT_TIMEOUT"; timeout: number } | undefined
219219
>(undefined)
220-
const [isCondensing, setIsCondensing] = useState<boolean>(false)
220+
const [condensingTaskIds, setCondensingTaskIds] = useState<Set<string>>(() => new Set())
221+
const isCondensing = useMemo(
222+
() => (currentTaskId ? condensingTaskIds.has(currentTaskId) : false),
223+
[currentTaskId, condensingTaskIds],
224+
)
221225
const everVisibleMessagesTsRef = useRef<LRUCache<number, boolean>>(
222226
new LRUCache({
223227
max: 100,
@@ -226,7 +230,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
226230
)
227231
const autoApproveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
228232
const userRespondedRef = useRef<boolean>(false)
229-
const pendingPostCompactRef = useRef<{ text: string; images: string[] } | null>(null)
233+
const pendingPostCompactRef = useRef<{ taskId: string; text: string; images: string[] } | null>(null)
230234
const [currentFollowUpTs, setCurrentFollowUpTs] = useState<number | null>(null)
231235
const [aggregatedCostsMap, setAggregatedCostsMap] = useState<
232236
Map<
@@ -239,6 +243,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
239243
>
240244
>(new Map())
241245

246+
const markTaskCondensing = useCallback((taskId: string, condensing: boolean) => {
247+
setCondensingTaskIds((prev) => {
248+
const currentlyCondensing = prev.has(taskId)
249+
if (currentlyCondensing === condensing) {
250+
return prev
251+
}
252+
253+
const next = new Set(prev)
254+
if (condensing) {
255+
next.add(taskId)
256+
} else {
257+
next.delete(taskId)
258+
}
259+
return next
260+
})
261+
}, [])
262+
242263
const clineAskRef = useRef(clineAsk)
243264
useEffect(() => {
244265
clineAskRef.current = clineAsk
@@ -553,7 +574,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
553574
setExpandedRows({})
554575
everVisibleMessagesTsRef.current.clear()
555576
setCurrentFollowUpTs(null)
556-
setIsCondensing(false)
557577

558578
if (autoApproveTimeoutRef.current) {
559579
clearTimeout(autoApproveTimeoutRef.current)
@@ -660,14 +680,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
660680

661681
const handleCondenseContext = useCallback(
662682
(taskId: string) => {
663-
if (isCondensing || sendingDisabled) {
683+
if (!taskId || condensingTaskIds.has(taskId) || sendingDisabled) {
664684
return
665685
}
666-
setIsCondensing(true)
686+
markTaskCondensing(taskId, true)
667687
setSendingDisabled(true)
668688
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
669689
},
670-
[isCondensing, sendingDisabled],
690+
[condensingTaskIds, markTaskCondensing, sendingDisabled],
671691
)
672692

673693
/**
@@ -689,18 +709,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
689709
// /compact-and <msg> condenses, then sends <msg> once condensing completes.
690710
const compactMatch = /^\/compact(-and)?(?:\s+([\s\S]+))?$/.exec(text)
691711
if (compactMatch) {
692-
const taskId = currentTaskItem?.id
693-
if (!taskId || messagesRef.current.length === 0 || isCondensing || sendingDisabled) {
712+
const taskId = currentTaskId ?? currentTaskItem?.id
713+
if (
714+
!taskId ||
715+
messagesRef.current.length === 0 ||
716+
condensingTaskIds.has(taskId) ||
717+
sendingDisabled
718+
) {
694719
// Nothing to condense, or a condense is already in flight.
695720
setInputValue("")
696721
setSelectedImages([])
697722
return
698723
}
699724
const followUp = (compactMatch[2] ?? "").trim()
700725
if (compactMatch[1] && followUp) {
701-
pendingPostCompactRef.current = { text: followUp, images }
726+
pendingPostCompactRef.current = { taskId, text: followUp, images }
702727
}
703-
setIsCondensing(true)
728+
markTaskCondensing(taskId, true)
704729
setSendingDisabled(true)
705730
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
706731
setInputValue("")
@@ -794,6 +819,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
794819
sendingDisabled,
795820
isStreaming,
796821
isCondensing,
822+
condensingTaskIds,
823+
markTaskCondensing,
797824
apiConfiguration?.apiProvider,
798825
submissionDisabled,
799826
selectedDraftId,
@@ -805,16 +832,27 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
805832
// After /compact-and completes, dispatch the follow-up message stashed
806833
// when the user submitted the command.
807834
useEffect(() => {
808-
if (isCondensing || sendingDisabled) {
835+
const pending = pendingPostCompactRef.current
836+
if (!pending || condensingTaskIds.has(pending.taskId)) {
809837
return
810838
}
811-
const pending = pendingPostCompactRef.current
812-
if (!pending) {
839+
840+
if (pending.taskId === currentTaskId && sendingDisabled) {
813841
return
814842
}
815843
pendingPostCompactRef.current = null
816-
handleSendMessage(pending.text, pending.images)
817-
}, [isCondensing, sendingDisabled, handleSendMessage])
844+
if (pending.taskId === currentTaskId) {
845+
handleSendMessage(pending.text, pending.images)
846+
} else {
847+
vscode.postMessage({
848+
type: "askResponse",
849+
askResponse: "messageResponse",
850+
text: pending.text,
851+
images: pending.images,
852+
taskId: pending.taskId,
853+
})
854+
}
855+
}, [condensingTaskIds, currentTaskId, sendingDisabled, handleSendMessage])
818856

819857
const handleSetChatBoxMessage = useCallback(
820858
(text: string, images: string[]) => {
@@ -1083,24 +1121,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
10831121
}
10841122
break
10851123
case "condenseTaskContextStarted":
1086-
// Handle both manual and automatic condensation start
1087-
// We don't check the task ID because:
1088-
// 1. There can only be one active task at a time
1089-
// 2. Task switching resets isCondensing to false (see useEffect with task?.ts dependency)
1090-
// 3. For new tasks, currentTaskItem may not be populated yet due to async state updates
1124+
// Handle both manual and automatic condensation start.
10911125
if (message.text) {
1092-
setIsCondensing(true)
1126+
markTaskCondensing(message.text, true)
10931127
// Note: sendingDisabled is only set for manual condensation via handleCondenseContext
10941128
// Automatic condensation doesn't disable sending since the task is already running
10951129
}
10961130
break
10971131
case "condenseTaskContextResponse":
1098-
// Same reasoning as above - we trust this is for the current task
10991132
if (message.text) {
1100-
if (isCondensing && sendingDisabled) {
1133+
markTaskCondensing(message.text, false)
1134+
if (message.text === currentTaskId && sendingDisabled) {
11011135
setSendingDisabled(false)
11021136
}
1103-
setIsCondensing(false)
11041137
}
11051138
break
11061139
case "checkpointInitWarning":
@@ -1124,10 +1157,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
11241157
// not using its value but its reference.
11251158
},
11261159
[
1127-
isCondensing,
11281160
isHidden,
11291161
sendingDisabled,
11301162
enableButtons,
1163+
currentTaskId,
1164+
markTaskCondensing,
11311165
handleChatReset,
11321166
handleSendMessage,
11331167
handleSetChatBoxMessage,

0 commit comments

Comments
 (0)