Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b78e7e9
fix(task): persist deleted message costs for accurate total
abdulrahimpds Jan 29, 2026
5610aca
forgot this I suppose
abdulrahimpds Jan 29, 2026
7953bdd
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Jan 29, 2026
46e3509
Merge branch 'Kilo-Org:main' into fix/persist-delete-api-costs
abdulrahimpds Jan 31, 2026
d528e7a
Merge branch 'Kilo-Org:main' into fix/persist-delete-api-costs
abdulrahimpds Feb 4, 2026
60afc71
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 4, 2026
675507d
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 4, 2026
582ee6a
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 4, 2026
c9e2289
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 4, 2026
62bb752
Merge branch 'Kilo-Org:main' into fix/persist-delete-api-costs
abdulrahimpds Feb 5, 2026
3f16e2c
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 5, 2026
fefe908
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 5, 2026
942a830
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 5, 2026
3d4103b
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 5, 2026
29ee0fb
Merge branch 'main' into fix/persist-delete-api-costs
abdulrahimpds Feb 5, 2026
120dcc0
Merge branch 'main' into fix/persist-delete-api-costs
kevinvandijk Feb 12, 2026
71e18c4
Add markers
kevinvandijk Feb 12, 2026
90a4495
Add mocks for failing tests
kevinvandijk Feb 12, 2026
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
5 changes: 5 additions & 0 deletions .changeset/persist-deleted-api-costs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Fix: Persist total API cost after message deletion
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ export type ExtensionState = Pick<
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
currentTaskTodos?: TodoItem[] // Initial todos for the current task
currentTaskCumulativeCost?: number // kilocode_change: cumulative cost including deleted messages
apiConfiguration: ProviderSettings
uriScheme?: string
uiKind?: string // kilocode_change
Expand Down
10 changes: 9 additions & 1 deletion src/core/task-persistence/taskMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export type TaskMetadataOptions = {
* continue using this protocol even if user settings change.
*/
toolProtocol?: ToolProtocol
// kilocode_change start
/**
* cumulative total cost including deleted messages.
* if provided, this overrides the calculated totalCost from messages.
*/
cumulativeTotalCost?: number
// kilocode_change end
}

export async function taskMetadata({
Expand All @@ -44,6 +51,7 @@ export async function taskMetadata({
apiConfigName,
initialStatus,
toolProtocol,
cumulativeTotalCost, // kilocode_change
}: TaskMetadataOptions) {
const taskDir = await getTaskDirectoryPath(globalStoragePath, id)

Expand Down Expand Up @@ -114,7 +122,7 @@ export async function taskMetadata({
tokensOut: tokenUsage.totalTokensOut,
cacheWrites: tokenUsage.totalCacheWrites,
cacheReads: tokenUsage.totalCacheReads,
totalCost: tokenUsage.totalCost,
totalCost: cumulativeTotalCost !== undefined ? cumulativeTotalCost : tokenUsage.totalCost, // kilocode_change
size: taskDirSize,
workspace,
mode,
Expand Down
36 changes: 35 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
apiConversationHistory: ApiMessage[] = []
clineMessages: ClineMessage[] = []

/**
* cumulative cost of API calls from messages that were deleted during this session.
* this ensures the total cost displayed to the user reflects all API usage,
* even if messages are removed from the conversation history.
*/
private _deletedApiCost: number = 0 // kilocode_change

// Ask
private askResponse?: ClineAskResponse
private askResponseText?: string
Expand Down Expand Up @@ -1323,6 +1330,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
apiConfigName: this._taskApiConfigName, // Use the task's own provider profile, not the current provider profile.
initialStatus: this.initialStatus,
toolProtocol: this._taskToolProtocol, // Persist the locked tool protocol.
cumulativeTotalCost: this.getCumulativeTotalCost(), // kilocode_change: include deleted message costs.
})

// Emit token/tool usage updates using debounced function
Expand Down Expand Up @@ -4983,9 +4991,35 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return combineApiRequests(combineCommandSequences(messages))
}

// kilocod_change start
public getTokenUsage(): TokenUsage {
return getApiMetrics(this.combineMessages(this.clineMessages.slice(1)))
const metrics = getApiMetrics(this.combineMessages(this.clineMessages.slice(1)))
// add deleted API costs to the total cost
return {
...metrics,
totalCost: metrics.totalCost + this._deletedApiCost,
}
}

/**
* get cumulative total cost including deleted messages.
* this is the cost that should be persisted in history.
*/
public getCumulativeTotalCost(): number {
const metrics = getApiMetrics(this.combineMessages(this.clineMessages.slice(1)))
return metrics.totalCost + this._deletedApiCost
}

/**
* add cost from deleted messages to the cumulative total.
* called by message deletion handlers to preserve true session cost.
*/
public addDeletedApiCost(cost: number): void {
if (cost > 0) {
this._deletedApiCost += cost
}
}
// kilocod_change end

public recordToolUsage(toolName: ToolName) {
if (!this.toolUsage[toolName]) {
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2397,6 +2397,7 @@ export class ClineProvider
: undefined,
clineMessages: this.getCurrentTask()?.clineMessages || [],
currentTaskTodos: this.getCurrentTask()?.todoList || [],
currentTaskCumulativeCost: this.getCurrentTask()?.getCumulativeTotalCost(), // kilocode_change
messageQueue: this.getCurrentTask()?.messageQueueService?.messages,
taskHistoryFullLength: taskHistory.length, // kilocode_change
taskHistoryVersion: this.kiloCodeTaskHistoryVersion, // kilocode_change
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ vi.mock("../../task/Task", () => ({
updateApiConfiguration: vi.fn().mockImplementation(function (this: any, newConfig: any) {
this.apiConfiguration = newConfig
}),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}
// Define apiConfiguration as a property so tests can read it
Object.defineProperty(mockTask, "apiConfiguration", {
Expand Down
2 changes: 2 additions & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ vi.mock("../../task/Task", () => ({
setRootTask: vi.fn(),
taskId: options?.historyItem?.id || "test-task-id",
emit: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
})),
}))

Expand Down Expand Up @@ -379,6 +380,7 @@ describe("ClineProvider", () => {
setRootTask: vi.fn(),
taskId: options?.historyItem?.id || "test-task-id",
emit: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

Object.defineProperty(task, "messageManager", {
Expand Down
11 changes: 11 additions & 0 deletions src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ vi.mock("../../task/Task", () => ({
emit: vi.fn(),
parentTask: options.parentTask,
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
})),
}))

Expand Down Expand Up @@ -359,6 +360,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -812,6 +814,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -879,6 +882,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -931,6 +935,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -966,6 +971,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -1022,6 +1028,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -1069,6 +1076,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

const task2 = {
Expand All @@ -1079,6 +1087,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

const task3 = {
Expand All @@ -1089,6 +1098,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add tasks to provider stack
Expand Down Expand Up @@ -1231,6 +1241,7 @@ describe("ClineProvider - Sticky Mode", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}))

// Add all tasks to provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ vi.mock("../../task/Task", () => ({
setTaskApiConfigName: vi.fn(),
_taskApiConfigName: options.historyItem?.apiConfigName,
taskApiConfigName: options.historyItem?.apiConfigName,
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
})),
}))

Expand Down Expand Up @@ -300,6 +301,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -365,6 +367,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down Expand Up @@ -421,6 +424,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

await provider.addClineToStack(mockTask as any)
Expand Down Expand Up @@ -610,6 +614,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Mock getGlobalState to return task history with our task
Expand Down Expand Up @@ -677,6 +682,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Create task 2 with profile B
Expand All @@ -691,6 +697,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task 1 to stack
Expand Down Expand Up @@ -775,6 +782,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
clineMessages: [],
apiConversationHistory: [],
updateApiConfiguration: vi.fn(),
getCumulativeTotalCost: vi.fn().mockReturnValue(0), // kilocode_change
}

// Add task to provider stack
Expand Down
22 changes: 22 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,28 @@ export const webviewMessageHandler = async (
vscode.window.showWarningMessage("No checkpoint found before this message")
}
} else {
// kilocode_change start: calculate the cost of messages being deleted before removing them
const messagesToDelete = currentCline.clineMessages.slice(messageIndex)
let deletedCost = 0
for (const msg of messagesToDelete) {
if (msg.say === "api_req_started" && msg.text) {
try {
const apiReqInfo = JSON.parse(msg.text)
if (apiReqInfo.cost && typeof apiReqInfo.cost === "number") {
deletedCost += apiReqInfo.cost
}
} catch {
// ignore parse errors
}
}
}

// add the deleted cost to the task's cumulative total
if (deletedCost > 0) {
currentCline.addDeletedApiCost(deletedCost)
}
// kilocode_change end

// For non-checkpoint deletes, preserve checkpoint associations for remaining messages
// Store checkpoints from messages that will be preserved
const preservedCheckpoints = new Map<number, any>()
Expand Down
15 changes: 14 additions & 1 deletion webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
clineMessages: messages,
currentTaskItem,
currentTaskTodos,
currentTaskCumulativeCost, // kilocode_change
taskHistoryFullLength, // kilocode_change
taskHistoryVersion, // kilocode_change
apiConfiguration,
Expand Down Expand Up @@ -176,7 +177,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])

// Has to be after api_req_finished are all reduced into api_req_started messages.
const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
// kilocode_change start
const apiMetrics = useMemo(() => {
const metrics = getApiMetrics(modifiedMessages)
// use cumulative cost from backend if available, otherwise fall back to calculated cost
if (currentTaskCumulativeCost !== undefined) {
return {
...metrics,
totalCost: currentTaskCumulativeCost,
}
}
return metrics
}, [modifiedMessages, currentTaskCumulativeCost])
// kilocode_change end

const [inputValue, setInputValue] = useState("")
const inputValueRef = useRef(inputValue)
Expand Down
Loading