Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/types/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export interface TaskLike {

readonly rootTask?: TaskLike

// Context condensing state
isCondensing?: boolean
cancelCondenseContext?(): void

on<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this

Expand Down
32 changes: 25 additions & 7 deletions src/core/condense/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export type SummarizeResponse = {
* @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically
* @param {string} customCondensingPrompt - Optional custom prompt to use for condensing
* @param {ApiHandler} condensingApiHandler - Optional specific API handler to use for condensing
* @param {AbortSignal} abortSignal - Optional abort signal to cancel the operation
* @returns {SummarizeResponse} - The result of the summarization operation (see above)
*/
export async function summarizeConversation(
Expand All @@ -91,6 +92,7 @@ export async function summarizeConversation(
isAutomaticTrigger?: boolean,
customCondensingPrompt?: string,
condensingApiHandler?: ApiHandler,
abortSignal?: AbortSignal,
): Promise<SummarizeResponse> {
TelemetryService.instance.captureContextCondensed(
taskId,
Expand Down Expand Up @@ -160,14 +162,30 @@ export async function summarizeConversation(
let cost = 0
let outputTokens = 0

for await (const chunk of stream) {
if (chunk.type === "text") {
summary += chunk.text
} else if (chunk.type === "usage") {
// Record final usage chunk only
cost = chunk.totalCost ?? 0
outputTokens = chunk.outputTokens ?? 0
try {
for await (const chunk of stream) {
// Check if operation was cancelled
if (abortSignal?.aborted) {
const error = t("common:errors.condense_cancelled")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Issue: The translation key is not defined in the i18n files. This will cause a runtime error when the cancellation occurs.

You need to add this key to under the section:

Also add translations for other supported languages.

return { ...response, error }
}

if (chunk.type === "text") {
summary += chunk.text
} else if (chunk.type === "usage") {
// Record final usage chunk only
cost = chunk.totalCost ?? 0
outputTokens = chunk.outputTokens ?? 0
}
}
} catch (error) {
// If the stream was aborted, return a cancelled error
if (abortSignal?.aborted) {
const cancelError = t("common:errors.condense_cancelled")
return { ...response, error: cancelError }
}
// Otherwise, re-throw the error
throw error
}

summary = summary.trim()
Expand Down
2 changes: 2 additions & 0 deletions src/core/sliding-window/__tests__/sliding-window.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ describe("Sliding Window", () => {
true,
undefined, // customCondensingPrompt
undefined, // condensingApiHandler
undefined, // abortSignal
)

// Verify the result contains the summary information
Expand Down Expand Up @@ -765,6 +766,7 @@ describe("Sliding Window", () => {
true,
undefined, // customCondensingPrompt
undefined, // condensingApiHandler
undefined, // abortSignal
)

// Verify the result contains the summary information
Expand Down
1 change: 1 addition & 0 deletions src/core/sliding-window/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export async function truncateConversationIfNeeded({
true, // automatic trigger
customCondensingPrompt,
condensingApiHandler,
undefined, // No abort signal for automatic condensing
)
if (result.error) {
error = result.error
Expand Down
148 changes: 93 additions & 55 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
readonly taskNumber: number
readonly workspacePath: string

// Context condensing state
isCondensing: boolean = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding proper TypeScript types instead of using type assertions. You could extend the Task interface to include these properties:

private condensingAbortController?: AbortController

/**
* The mode associated with this task. Persisted across sessions
* to maintain user context when reopening tasks from history.
Expand Down Expand Up @@ -859,72 +863,107 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}

public async condenseContext(): Promise<void> {
const systemPrompt = await this.getSystemPrompt()
// Prevent concurrent condensing operations
if (this.isCondensing) {
console.warn("Context condensing is already in progress")
return
}

// Get condensing configuration
// Using type assertion to handle the case where Phase 1 hasn't been implemented yet
const state = await this.providerRef.deref()?.getState()
const customCondensingPrompt = state ? (state as any).customCondensingPrompt : undefined
const condensingApiConfigId = state ? (state as any).condensingApiConfigId : undefined
const listApiConfigMeta = state ? (state as any).listApiConfigMeta : undefined
// Wait a bit if there was a recent abort to ensure cleanup is complete
if (this.condensingAbortController && this.condensingAbortController.signal.aborted) {
console.warn("Waiting for previous condensing operation to fully clean up")
// Give the previous operation time to clean up
await new Promise((resolve) => setTimeout(resolve, 100))
}

// Determine API handler to use
let condensingApiHandler: ApiHandler | undefined
if (condensingApiConfigId && listApiConfigMeta && Array.isArray(listApiConfigMeta)) {
// Using type assertion for the id property to avoid implicit any
const matchingConfig = listApiConfigMeta.find((config: any) => config.id === condensingApiConfigId)
if (matchingConfig) {
const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({
id: condensingApiConfigId,
})
// Ensure profile and apiProvider exist before trying to build handler
if (profile && profile.apiProvider) {
condensingApiHandler = buildApiHandler(profile)
// Mark as condensing
this.isCondensing = true
this.condensingAbortController = new AbortController()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good use of AbortController pattern for cancellation! The implementation is clean and follows best practices for handling async cancellation.


try {
const systemPrompt = await this.getSystemPrompt()

// Get condensing configuration
const state = await this.providerRef.deref()?.getState()
const customCondensingPrompt = state?.customCondensingPrompt
const condensingApiConfigId = state?.condensingApiConfigId
const listApiConfigMeta = state?.listApiConfigMeta

// Determine API handler to use
let condensingApiHandler: ApiHandler | undefined
if (condensingApiConfigId && listApiConfigMeta && Array.isArray(listApiConfigMeta)) {
const matchingConfig = listApiConfigMeta.find((config) => config.id === condensingApiConfigId)
if (matchingConfig) {
const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({
id: condensingApiConfigId,
})
// Ensure profile and apiProvider exist before trying to build handler
if (profile && profile.apiProvider) {
condensingApiHandler = buildApiHandler(profile)
}
}
}
}

const { contextTokens: prevContextTokens } = this.getTokenUsage()
const {
messages,
summary,
cost,
newContextTokens = 0,
error,
} = await summarizeConversation(
this.apiConversationHistory,
this.api, // Main API handler (fallback)
systemPrompt, // Default summarization prompt (fallback)
this.taskId,
prevContextTokens,
false, // manual trigger
customCondensingPrompt, // User's custom prompt
condensingApiHandler, // Specific handler for condensing
)
if (error) {
this.say(
"condense_context_error",
const { contextTokens: prevContextTokens } = this.getTokenUsage()

// Pass the abort signal to summarizeConversation
const {
messages,
summary,
cost,
newContextTokens = 0,
error,
} = await summarizeConversation(
this.apiConversationHistory,
this.api, // Main API handler (fallback)
systemPrompt, // Default summarization prompt (fallback)
this.taskId,
prevContextTokens,
false, // manual trigger
customCondensingPrompt, // User's custom prompt
condensingApiHandler, // Specific handler for condensing
this.condensingAbortController.signal, // Pass abort signal
)

if (error) {
// Don't show error if it was cancelled
if (!this.condensingAbortController.signal.aborted) {
this.say(
"condense_context_error",
error,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)
}
return
}

await this.overwriteApiConversationHistory(messages)
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
contextCondense,
)
return
} finally {
// Clean up
this.isCondensing = false
this.condensingAbortController = undefined
}
}

public cancelCondenseContext(): void {
if (this.condensingAbortController) {
this.condensingAbortController.abort()
}
await this.overwriteApiConversationHistory(messages)
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
contextCondense,
)
}

async say(
Expand Down Expand Up @@ -2256,8 +2295,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
let condensingApiHandler: ApiHandler | undefined

if (condensingApiConfigId && listApiConfigMeta && Array.isArray(listApiConfigMeta)) {
// Using type assertion for the id property to avoid implicit any.
const matchingConfig = listApiConfigMeta.find((config: any) => config.id === condensingApiConfigId)
const matchingConfig = listApiConfigMeta.find((config) => config.id === condensingApiConfigId)

if (matchingConfig) {
const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({
Expand Down
12 changes: 12 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,18 @@ export class ClineProvider
await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
}

/* Cancels the ongoing context condensing operation. */
async cancelCondenseContext() {
// Find all tasks in the stack and cancel any ongoing condensing operation
for (let i = this.clineStack.length - 1; i >= 0; i--) {
const task = this.clineStack[i]
if (task.isCondensing) {
task.cancelCondenseContext?.()
break
}
}
}

// this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
async deleteTaskWithId(id: string) {
try {
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,9 @@ export const webviewMessageHandler = async (
case "condenseTaskContextRequest":
provider.condenseTaskContext(message.text!)
break
case "cancelCondenseContext":
provider.cancelCondenseContext()
break
case "deleteTaskWithId":
provider.deleteTaskWithId(message.text!)
break
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ca/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/de/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"condensed_recently": "Context was condensed recently; skipping this attempt",
"condense_handler_invalid": "API handler for condensing context is invalid",
"condense_context_grew": "Context size increased during condensing; skipping this attempt",
"condense_cancelled": "Context condensing was cancelled",
"url_timeout": "The website took too long to load (timeout). This could be due to a slow connection, heavy website, or the site being temporarily unavailable. You can try again later or check if the URL is correct.",
"url_not_found": "The website address could not be found. Please check if the URL is correct and try again.",
"no_internet": "No internet connection. Please check your network connection and try again.",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/fr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/hi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/id/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/it/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ja/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading