[Condense] Add a button to condense the task context (#3623)

* [Condense] Add a button to condense the task context

* wip

* wip

* wip

* bring back delete size

* account for the system prompt in the context

* update tests to use systemPrompt

* add type

* translations

* nit

* update tests

* filter to the current task

* nit

* refactor

* nit

* non interactive option

* simplify chat summary UI

* changeset

* nit

* fix check-types

* throw
This commit is contained in:
Canyon Robins 2025-05-20 13:47:02 -07:00 committed by hannesrudolph
parent 4b974f623c
commit fd8825d042
33 changed files with 242 additions and 105 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Adds a button to intelligently condense the context window

View file

@ -97,13 +97,16 @@ describe("summarizeConversation", () => {
} as unknown as ApiHandler
})
// Default system prompt for tests
const defaultSystemPrompt = "You are a helpful assistant."
it("should not summarize when there are not enough messages", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
]
const result = await summarizeConversation(messages, mockApiHandler)
const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt)
expect(result.messages).toEqual(messages)
expect(result.cost).toBe(0)
expect(result.summary).toBe("")
@ -122,7 +125,7 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
const result = await summarizeConversation(messages, mockApiHandler)
const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt)
expect(result.messages).toEqual(messages)
expect(result.cost).toBe(0)
expect(result.summary).toBe("")
@ -141,7 +144,7 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
const result = await summarizeConversation(messages, mockApiHandler)
const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt)
// Check that the API was called correctly
expect(mockApiHandler.createMessage).toHaveBeenCalled()
@ -199,7 +202,7 @@ describe("summarizeConversation", () => {
return messages.map(({ role, content }: { role: string; content: any }) => ({ role, content }))
})
const result = await summarizeConversation(messages, mockApiHandler)
const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt)
// Should return original messages when summary is empty
expect(result.messages).toEqual(messages)
@ -222,7 +225,7 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
await summarizeConversation(messages, mockApiHandler)
await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt)
// Verify the final request message
const expectedFinalMessage = {

View file

@ -57,12 +57,13 @@ export type SummarizeResponse = {
*
* @param {ApiMessage[]} messages - The conversation messages
* @param {ApiHandler} apiHandler - The API handler to use for token counting.
* @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count
* @returns {SummarizeResponse} - The result of the summarization operation (see above)
*/
export async function summarizeConversation(
messages: ApiMessage[],
apiHandler: ApiHandler,
systemPrompt?: string,
systemPrompt: string,
): Promise<SummarizeResponse> {
const response: SummarizeResponse = { messages, cost: 0, summary: "" }
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(0, -N_MESSAGES_TO_KEEP))
@ -111,10 +112,10 @@ export async function summarizeConversation(
// Count the tokens in the context for the next API request
// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens
const contextMessages = outputTokens ? [...keepMessages] : [summaryMessage, ...keepMessages]
if (systemPrompt) {
contextMessages.unshift({ role: "user", content: systemPrompt })
}
const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt }
const contextMessages = outputTokens
? [systemPromptMessage, ...keepMessages]
: [systemPromptMessage, summaryMessage, ...keepMessages]
const contextBlocks = contextMessages.flatMap((message) =>
typeof message.content === "string" ? [{ text: message.content, type: "text" as const }] : message.content,
)

View file

@ -248,6 +248,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
// Check the new return type
@ -276,6 +277,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result).toEqual({
@ -302,6 +304,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo1.contextWindow,
maxTokens: modelInfo1.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
const result2 = await truncateConversationIfNeeded({
@ -310,6 +313,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo2.contextWindow,
maxTokens: modelInfo2.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result1.messages).toEqual(result2.messages)
@ -325,6 +329,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo1.contextWindow,
maxTokens: modelInfo1.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
const result4 = await truncateConversationIfNeeded({
@ -333,6 +338,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo2.contextWindow,
maxTokens: modelInfo2.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result3.messages).toEqual(result4.messages)
@ -363,6 +369,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo.contextWindow,
maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(resultWithSmall).toEqual({
messages: messagesWithSmallContent,
@ -392,6 +399,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo.contextWindow,
maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(resultWithLarge.messages).not.toEqual(messagesWithLargeContent) // Should truncate
expect(resultWithLarge.summary).toBe("")
@ -414,6 +422,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo.contextWindow,
maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(resultWithVeryLarge.messages).not.toEqual(messagesWithVeryLargeContent) // Should truncate
expect(resultWithVeryLarge.summary).toBe("")
@ -439,6 +448,7 @@ describe("truncateConversationIfNeeded", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result).toEqual({
messages: expectedResult,
@ -524,6 +534,7 @@ describe("truncateConversationIfNeeded", () => {
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
autoCondenseContext: true,
systemPrompt: "System prompt",
})
// Verify summarizeConversation was called
@ -559,6 +570,7 @@ describe("truncateConversationIfNeeded", () => {
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
autoCondenseContext: false,
systemPrompt: "System prompt",
})
// Verify summarizeConversation was not called
@ -612,6 +624,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result1).toEqual({
messages: messagesWithSmallContent,
@ -627,6 +640,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result2.messages).not.toEqual(messagesWithSmallContent)
expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction
@ -650,6 +664,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result1).toEqual({
messages: messagesWithSmallContent,
@ -665,6 +680,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result2.messages).not.toEqual(messagesWithSmallContent)
expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction
@ -687,6 +703,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result1.messages).toEqual(messagesWithSmallContent)
@ -697,6 +714,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result2).not.toEqual(messagesWithSmallContent)
expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction
@ -717,6 +735,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result1.messages).toEqual(messagesWithSmallContent)
@ -727,6 +746,7 @@ describe("getMaxTokens", () => {
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
apiHandler: mockApiHandler,
systemPrompt: "System prompt",
})
expect(result2).not.toEqual(messagesWithSmallContent)
expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction

View file

@ -64,7 +64,7 @@ type TruncateOptions = {
maxTokens?: number | null
apiHandler: ApiHandler
autoCondenseContext?: boolean
systemPrompt?: string
systemPrompt: string
}
type TruncateResponse = SummarizeResponse & { prevContextTokens: number }

View file

@ -76,7 +76,7 @@ import {
} from "../checkpoints"
import { processUserContentMentions } from "../mentions/processUserContentMentions"
import { ApiMessage } from "../task-persistence/apiMessages"
import { getMessagesSinceLastSummary } from "../condense"
import { getMessagesSinceLastSummary, summarizeConversation } from "../condense"
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
export type ClineEvents = {
@ -481,6 +481,39 @@ export class Task extends EventEmitter<ClineEvents> {
}
}
public async condenseContext(): Promise<void> {
const systemPrompt = await this.getSystemPrompt()
const {
messages,
summary,
cost,
newContextTokens = 0,
} = await summarizeConversation(this.apiConversationHistory, this.api, systemPrompt)
if (!summary) {
return
}
const lastMessageContent = this.apiConversationHistory.at(-1)?.content
await this.overwriteApiConversationHistory(messages)
const { contextTokens } = this.getTokenUsage()
const lastContent =
typeof lastMessageContent === "string"
? [{ type: "text" as const, text: lastMessageContent }]
: lastMessageContent
const lastMessageTokens = lastContent ? await this.api.countTokens(lastContent) : 0
const prevContextTokens = contextTokens + lastMessageTokens
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(
type: ClineSay,
text?: string,
@ -1368,35 +1401,9 @@ export class Task extends EventEmitter<ClineEvents> {
}
}
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
private async getSystemPrompt(): Promise<string> {
const { mcpEnabled } = (await this.providerRef.deref()?.getState()) ?? {}
let mcpHub: McpHub | undefined
const { apiConfiguration, mcpEnabled, autoApprovalEnabled, alwaysApproveResubmit, requestDelaySeconds } =
(await this.providerRef.deref()?.getState()) ?? {}
let rateLimitDelay = 0
// Only apply rate limiting if this isn't the first request
if (this.lastApiRequestTime) {
const now = Date.now()
const timeSinceLastRequest = now - this.lastApiRequestTime
const rateLimit = apiConfiguration?.rateLimitSeconds || 0
rateLimitDelay = Math.ceil(Math.max(0, rateLimit * 1000 - timeSinceLastRequest) / 1000)
}
// Only show rate limiting message if we're not retrying. If retrying, we'll include the delay there.
if (rateLimitDelay > 0 && retryAttempt === 0) {
// Show countdown timer
for (let i = rateLimitDelay; i > 0; i--) {
const delayMessage = `Rate limiting for ${i} seconds...`
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
await delay(1000)
}
}
// Update last request time before making the request
this.lastApiRequestTime = Date.now()
if (mcpEnabled ?? true) {
const provider = this.providerRef.deref()
@ -1432,7 +1439,7 @@ export class Task extends EventEmitter<ClineEvents> {
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
const systemPrompt = await (async () => {
return await (async () => {
const provider = this.providerRef.deref()
if (!provider) {
@ -1457,6 +1464,36 @@ export class Task extends EventEmitter<ClineEvents> {
rooIgnoreInstructions,
)
})()
}
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
const { apiConfiguration, autoApprovalEnabled, alwaysApproveResubmit, requestDelaySeconds, experiments } =
(await this.providerRef.deref()?.getState()) ?? {}
let rateLimitDelay = 0
// Only apply rate limiting if this isn't the first request
if (this.lastApiRequestTime) {
const now = Date.now()
const timeSinceLastRequest = now - this.lastApiRequestTime
const rateLimit = apiConfiguration?.rateLimitSeconds || 0
rateLimitDelay = Math.ceil(Math.max(0, rateLimit * 1000 - timeSinceLastRequest) / 1000)
}
// Only show rate limiting message if we're not retrying. If retrying, we'll include the delay there.
if (rateLimitDelay > 0 && retryAttempt === 0) {
// Show countdown timer
for (let i = rateLimitDelay; i > 0; i--) {
const delayMessage = `Rate limiting for ${i} seconds...`
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
await delay(1000)
}
}
// Update last request time before making the request
this.lastApiRequestTime = Date.now()
const systemPrompt = await this.getSystemPrompt()
const { contextTokens } = this.getTokenUsage()
if (contextTokens) {
@ -1495,7 +1532,7 @@ export class Task extends EventEmitter<ClineEvents> {
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
undefined /* options */,
{ isNonInteractive: true } /* options */,
contextCondense,
)
}

View file

@ -1111,6 +1111,22 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
await downloadTask(historyItem.ts, apiConversationHistory)
}
/* Condenses a task's message history to use fewer tokens. */
async condenseTaskContext(taskId: string) {
let task: Task | undefined
for (let i = this.clineStack.length - 1; i >= 0; i--) {
if (this.clineStack[i].taskId === taskId) {
task = this.clineStack[i]
break
}
}
if (!task) {
throw new Error(`Task with id ${taskId} not found in stack`)
}
await task.condenseContext()
await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
}
// this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
async deleteTaskWithId(id: string) {
try {

View file

@ -197,6 +197,9 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
case "showTaskWithId":
provider.showTaskWithId(message.text!)
break
case "condenseTaskContextRequest":
provider.condenseTaskContext(message.text!)
break
case "deleteTaskWithId":
provider.deleteTaskWithId(message.text!)
break

View file

@ -69,6 +69,7 @@ export interface ExtensionMessage {
| "setHistoryPreviewCollapsed"
| "commandExecutionStatus"
| "vsCodeSetting"
| "condenseTaskContextResponse"
text?: string
action?:
| "chatButtonClicked"

View file

@ -131,6 +131,7 @@ export interface WebviewMessage {
| "searchFiles"
| "toggleApiConfigPin"
| "setHistoryPreviewCollapsed"
| "condenseTaskContextRequest"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse

View file

@ -56,6 +56,8 @@ describe("ContextWindowProgress", () => {
totalCost: 0.001,
contextTokens: 1000,
onClose: jest.fn(),
buttonsDisabled: false,
handleCondenseContext: jest.fn((_taskId: string) => {}),
}
return render(

View file

@ -69,6 +69,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}`
const {
clineMessages: messages,
currentTaskItem,
taskHistory,
apiConfiguration,
mcpServers,
@ -138,6 +139,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const lastTtsRef = useRef<string>("")
const [wasStreaming, setWasStreaming] = useState<boolean>(false)
const [showCheckpointWarning, setShowCheckpointWarning] = useState<boolean>(false)
const [isCondensing, setIsCondensing] = useState<boolean>(false)
// UI layout depends on the last 2 messages
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
@ -619,15 +621,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
break
}
break
case "condenseTaskContextResponse":
if (message.text && message.text === currentTaskItem?.id) {
if (isCondensing && sendingDisabled) {
setSendingDisabled(false)
}
setIsCondensing(false)
}
break
}
// textAreaRef.current is not explicitly required here since React
// guarantees that ref will be stable across re-renders, and we're
// not using its value but its reference.
},
[
isCondensing,
isHidden,
sendingDisabled,
enableButtons,
currentTaskItem,
handleChatReset,
handleSendMessage,
handleSetChatBoxMessage,
@ -1242,6 +1255,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
},
}))
const handleCondenseContext = (taskId: string) => {
if (isCondensing || sendingDisabled) {
return
}
setIsCondensing(true)
setSendingDisabled(true)
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
}
return (
<div className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
{showAnnouncement && <Announcement hideAnnouncement={hideAnnouncement} />}
@ -1256,6 +1278,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
cacheReads={apiMetrics.totalCacheReads}
totalCost={apiMetrics.totalCost}
contextTokens={apiMetrics.contextTokens}
buttonsDisabled={sendingDisabled}
handleCondenseContext={handleCondenseContext}
onClose={handleTaskCloseButtonClick}
/>

View file

@ -27,10 +27,7 @@ export const ContextCondenseRow = ({ cost, prevContextTokens, newContextTokens,
{isExpanded && (
<div className="mt-2 ml-0 p-4 bg-vscode-editor-background rounded text-vscode-foreground text-sm">
<h3 className="font-bold mb-5 -mt-1 ml-2">{t("chat:contextCondense.conversationSummary")}</h3>
<div className="-ml-6">
<Markdown markdown={summary} />
</div>
<Markdown markdown={summary} />
</div>
)}
</div>

View file

@ -3,43 +3,54 @@ import prettyBytes from "pretty-bytes"
import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
import { Button } from "@/components/ui"
import { HistoryItem } from "@roo/shared/HistoryItem"
import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
import { IconButton } from "./IconButton"
export const TaskActions = ({ item }: { item: HistoryItem | undefined }) => {
interface TaskActionsProps {
item?: HistoryItem
buttonsDisabled: boolean
handleCondenseContext: (taskId: string) => void
}
export const TaskActions = ({ item, buttonsDisabled, handleCondenseContext }: TaskActionsProps) => {
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
const { t } = useTranslation()
return (
<div className="flex flex-row gap-1">
<Button
variant="ghost"
size="sm"
<IconButton
iconClass="codicon-desktop-download"
title={t("chat:task.export")}
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
<span className="codicon codicon-desktop-download" />
</Button>
disabled={buttonsDisabled}
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}
/>
{!!item?.size && item.size > 0 && (
<>
<Button
variant="ghost"
size="sm"
title={t("chat:task.delete")}
onClick={(e) => {
e.stopPropagation()
<IconButton
iconClass="codicon-file-zip"
title={t("chat:task.condenseContext")}
disabled={buttonsDisabled}
onClick={() => handleCondenseContext(item.id)}
/>
<div className="flex items-center">
<IconButton
iconClass="codicon-trash"
title={t("chat:task.delete")}
disabled={buttonsDisabled}
onClick={(e) => {
e.stopPropagation()
if (e.shiftKey) {
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
} else {
setDeleteTaskId(item.id)
}
}}>
<span className="codicon codicon-trash" />
{prettyBytes(item.size)}
</Button>
if (e.shiftKey) {
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
} else {
setDeleteTaskId(item.id)
}
}}
/>
<span className="ml-1 text-xs text-vscode-foreground opacity-85">{prettyBytes(item.size)}</span>
</div>
{deleteTaskId && (
<DeleteTaskDialog
taskId={deleteTaskId}

View file

@ -28,6 +28,8 @@ export interface TaskHeaderProps {
cacheReads?: number
totalCost: number
contextTokens: number
buttonsDisabled: boolean
handleCondenseContext: (taskId: string) => void
onClose: () => void
}
@ -40,6 +42,8 @@ const TaskHeader = ({
cacheReads,
totalCost,
contextTokens,
buttonsDisabled,
handleCondenseContext,
onClose,
}: TaskHeaderProps) => {
const { t } = useTranslation()
@ -152,7 +156,13 @@ const TaskHeader = ({
</span>
)}
</div>
{!totalCost && <TaskActions item={currentTaskItem} />}
{!totalCost && (
<TaskActions
item={currentTaskItem}
buttonsDisabled={buttonsDisabled}
handleCondenseContext={handleCondenseContext}
/>
)}
</div>
{doesModelSupportPromptCache &&
@ -181,7 +191,11 @@ const TaskHeader = ({
<span className="font-bold">{t("chat:task.apiCost")}</span>
<span>${totalCost?.toFixed(2)}</span>
</div>
<TaskActions item={currentTaskItem} />
<TaskActions
item={currentTaskItem}
buttonsDisabled={buttonsDisabled}
handleCondenseContext={handleCondenseContext}
/>
</div>
)}
</div>

View file

@ -40,6 +40,8 @@ describe("TaskHeader", () => {
doesModelSupportPromptCache: true,
totalCost: 0.05,
contextTokens: 200,
buttonsDisabled: false,
handleCondenseContext: jest.fn(),
onClose: jest.fn(),
}

View file

@ -10,7 +10,8 @@
"contextWindow": "Finestra de context:",
"closeAndStart": "Tancar tasca i iniciar-ne una de nova",
"export": "Exportar historial de tasques",
"delete": "Eliminar tasca (Shift + Clic per ometre confirmació)"
"delete": "Eliminar tasca (Shift + Clic per ometre confirmació)",
"condenseContext": "Condensar context de la tasca"
},
"unpin": "Desfixar",
"pin": "Fixar",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Context condensat",
"conversationSummary": "Resum de la conversa",
"tokens": "tokens"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Kontextfenster:",
"closeAndStart": "Aufgabe schließen und neue starten",
"export": "Aufgabenverlauf exportieren",
"delete": "Aufgabe löschen (Shift + Klick zum Überspringen der Bestätigung)"
"delete": "Aufgabe löschen (Shift + Klick zum Überspringen der Bestätigung)",
"condenseContext": "Task-Kontext komprimieren"
},
"unpin": "Lösen von oben",
"pin": "Anheften",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Kontext komprimiert",
"conversationSummary": "Gesprächszusammenfassung",
"tokens": "Tokens"
},
"followUpSuggest": {

View file

@ -7,6 +7,7 @@
"tokens": "Tokens:",
"cache": "Cache:",
"apiCost": "API Cost:",
"condenseContext": "Condense task context",
"contextWindow": "Context Length:",
"closeAndStart": "Close task and start a new one",
"export": "Export task history",
@ -131,7 +132,6 @@
},
"contextCondense": {
"title": "Context Condensed",
"conversationSummary": "Conversation Summary",
"tokens": "tokens"
},
"instructions": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Longitud del contexto:",
"closeAndStart": "Cerrar tarea e iniciar una nueva",
"export": "Exportar historial de tareas",
"delete": "Eliminar tarea (Shift + Clic para omitir confirmación)"
"delete": "Eliminar tarea (Shift + Clic para omitir confirmación)",
"condenseContext": "Condensar contexto de la tarea"
},
"unpin": "Desfijar",
"pin": "Fijar",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Contexto condensado",
"conversationSummary": "Resumen de la conversación",
"tokens": "tokens"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Durée du contexte :",
"closeAndStart": "Fermer la tâche et en commencer une nouvelle",
"export": "Exporter l'historique des tâches",
"delete": "Supprimer la tâche (Shift + Clic pour ignorer la confirmation)"
"delete": "Supprimer la tâche (Shift + Clic pour ignorer la confirmation)",
"condenseContext": "Condenser le contexte de la tâche"
},
"unpin": "Désépingler",
"pin": "Épingler",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Contexte condensé",
"conversationSummary": "Résumé de la conversation",
"tokens": "tokens"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "संदर्भ लंबाई:",
"closeAndStart": "कार्य बंद करें और नया शुरू करें",
"export": "कार्य इतिहास निर्यात करें",
"delete": "कार्य हटाएं (पुष्टि को छोड़ने के लिए Shift + क्लिक)"
"delete": "कार्य हटाएं (पुष्टि को छोड़ने के लिए Shift + क्लिक)",
"condenseContext": "कार्य संदर्भ संघनित करें"
},
"unpin": "पिन करें",
"pin": "अवपिन करें",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "संदर्भ संक्षिप्त किया गया",
"conversationSummary": "वार्तालाप का सारांश",
"tokens": "टोकन"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Lunghezza del contesto:",
"closeAndStart": "Chiudi attività e iniziane una nuova",
"export": "Esporta cronologia attività",
"delete": "Elimina attività (Shift + Clic per saltare la conferma)"
"delete": "Elimina attività (Shift + Clic per saltare la conferma)",
"condenseContext": "Condensa contesto attività"
},
"unpin": "Rilascia",
"pin": "Fissa",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Contesto condensato",
"conversationSummary": "Riepilogo della conversazione",
"tokens": "token"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "コンテキストウィンドウ:",
"closeAndStart": "タスクを閉じて新しいタスクを開始",
"export": "タスク履歴をエクスポート",
"delete": "タスクを削除Shift + クリックで確認をスキップ)"
"delete": "タスクを削除Shift + クリックで確認をスキップ)",
"condenseContext": "タスクコンテキストを圧縮"
},
"unpin": "ピン留めを解除",
"pin": "ピン留め",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "コンテキスト要約",
"conversationSummary": "会話の要約",
"tokens": "トークン"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "컨텍스트 창:",
"closeAndStart": "작업 닫고 새 작업 시작",
"export": "작업 기록 내보내기",
"delete": "작업 삭제 (Shift + 클릭으로 확인 생략)"
"delete": "작업 삭제 (Shift + 클릭으로 확인 생략)",
"condenseContext": "작업 컨텍스트 압축"
},
"unpin": "고정 해제하기",
"pin": "고정하기",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "컨텍스트 요약됨",
"conversationSummary": "대화 요약",
"tokens": "토큰"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Contextlengte:",
"closeAndStart": "Taak sluiten en een nieuwe starten",
"export": "Taakgeschiedenis exporteren",
"delete": "Taak verwijderen (Shift + Klik om bevestiging over te slaan)"
"delete": "Taak verwijderen (Shift + Klik om bevestiging over te slaan)",
"condenseContext": "Taakcontext samenvatten"
},
"unpin": "Losmaken",
"pin": "Vastmaken",
@ -210,7 +211,6 @@
},
"contextCondense": {
"title": "Context samengevat",
"conversationSummary": "Gespreksoverzicht",
"tokens": "tokens"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Okno kontekstu:",
"closeAndStart": "Zamknij zadanie i rozpocznij nowe",
"export": "Eksportuj historię zadań",
"delete": "Usuń zadanie (Shift + Kliknięcie, aby pominąć potwierdzenie)"
"delete": "Usuń zadanie (Shift + Kliknięcie, aby pominąć potwierdzenie)",
"condenseContext": "Skondensuj kontekst zadania"
},
"unpin": "Odepnij",
"pin": "Przypnij",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Kontekst skondensowany",
"conversationSummary": "Podsumowanie rozmowy",
"tokens": "tokeny"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Janela de contexto:",
"closeAndStart": "Fechar tarefa e iniciar nova",
"export": "Exportar histórico de tarefas",
"delete": "Excluir tarefa (Shift + Clique para pular confirmação)"
"delete": "Excluir tarefa (Shift + Clique para pular confirmação)",
"condenseContext": "Condensar contexto da tarefa"
},
"unpin": "Desfixar",
"pin": "Fixar",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Contexto condensado",
"conversationSummary": "Resumo da conversa",
"tokens": "tokens"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Длина контекста:",
"closeAndStart": "Закрыть задачу и начать новую",
"export": "Экспортировать историю задач",
"delete": "Удалить задачу (Shift + клик для пропуска подтверждения)"
"delete": "Удалить задачу (Shift + клик для пропуска подтверждения)",
"condenseContext": "Сжать контекст задачи"
},
"unpin": "Открепить",
"pin": "Закрепить",
@ -210,7 +211,6 @@
},
"contextCondense": {
"title": "Контекст сжат",
"conversationSummary": "Сводка разговора",
"tokens": "токены"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Bağlam Uzunluğu:",
"closeAndStart": "Görevi kapat ve yeni bir görev başlat",
"export": "Görev geçmişini dışa aktar",
"delete": "Görevi sil (Onayı atlamak için Shift + Tıkla)"
"delete": "Görevi sil (Onayı atlamak için Shift + Tıkla)",
"condenseContext": "Görev bağlamını yoğunlaştır"
},
"unpin": "Sabitlemeyi iptal et",
"pin": "Sabitle",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Bağlam Özetlendi",
"conversationSummary": "Konuşma Özeti",
"tokens": "token"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "Chiều dài bối cảnh:",
"closeAndStart": "Đóng nhiệm vụ và bắt đầu nhiệm vụ mới",
"export": "Xuất lịch sử nhiệm vụ",
"delete": "Xóa nhiệm vụ (Shift + Click để bỏ qua xác nhận)"
"delete": "Xóa nhiệm vụ (Shift + Click để bỏ qua xác nhận)",
"condenseContext": "Cô đọng ngữ cảnh tác vụ"
},
"unpin": "Bỏ ghim khỏi đầu",
"pin": "Ghim lên đầu",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "Ngữ cảnh đã tóm tắt",
"conversationSummary": "Tóm tắt cuộc hội thoại",
"tokens": "token"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "上下文长度:",
"closeAndStart": "关闭任务并开始新任务",
"export": "导出任务历史",
"delete": "删除任务Shift + 点击跳过确认)"
"delete": "删除任务Shift + 点击跳过确认)",
"condenseContext": "压缩任务上下文"
},
"unpin": "取消置顶",
"pin": "置顶",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "上下文已压缩",
"conversationSummary": "对话摘要",
"tokens": "tokens"
},
"followUpSuggest": {

View file

@ -10,7 +10,8 @@
"contextWindow": "上下文長度:",
"closeAndStart": "關閉現有工作並開始一項新的工作",
"export": "匯出工作紀錄",
"delete": "刪除工作(按住 Shift 並點選可跳過確認)"
"delete": "刪除工作(按住 Shift 並點選可跳過確認)",
"condenseContext": "壓縮工作上下文"
},
"unpin": "取消置頂",
"pin": "置頂",
@ -200,7 +201,6 @@
},
"contextCondense": {
"title": "上下文已壓縮",
"conversationSummary": "對話摘要",
"tokens": "tokens"
},
"followUpSuggest": {