From ae89304207878d061471fa5f2ec7e574fc7c8af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Wed, 11 Feb 2026 14:01:15 +0100 Subject: [PATCH] feat(vscode): add task header with cost, context usage, and compact button Add a sticky TaskHeader component above the chat messages showing session title, accumulated cost, context token usage, and a button to trigger context compaction (summarization). Wire cost and token data through the message pipeline (SSE message.updated events and initial message load) so the webview can compute session totals. Closes #166 --- packages/kilo-vscode/src/KiloProvider.ts | 37 ++++++++- .../src/services/cli-backend/http-client.ts | 12 +++ .../src/services/cli-backend/index.ts | 1 + .../src/services/cli-backend/types.ts | 11 +++ .../src/components/chat/ChatView.tsx | 2 + .../src/components/chat/TaskHeader.tsx | 75 +++++++++++++++++++ .../webview-ui/src/context/session.tsx | 46 ++++++++++++ .../webview-ui/src/styles/chat.css | 38 ++++++++++ .../webview-ui/src/types/messages.ts | 24 ++++++ 9 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2fd828add35..3cb4824dbd3 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -182,6 +182,9 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "requestProviders": await this.fetchAndSendProviders() break + case "compact": + await this.handleCompact(message.sessionID, message.providerID, message.modelID) + break } }) } @@ -332,13 +335,15 @@ export class KiloProvider implements vscode.WebviewViewProvider { const workspaceDir = this.getWorkspaceDirectory() const messagesData = await this.httpClient.getMessages(sessionID, workspaceDir) - // Convert to webview format + // Convert to webview format, including cost/tokens for assistant messages const messages = messagesData.map((m) => ({ id: m.info.id, sessionID: m.info.sessionID, role: m.info.role, parts: m.parts, createdAt: new Date(m.info.time.created).toISOString(), + cost: m.info.cost, + tokens: m.info.tokens, })) for (const message of messages) { @@ -504,6 +509,32 @@ export class KiloProvider implements vscode.WebviewViewProvider { } } + /** + * Handle compact (context summarization) request from the webview. + */ + private async handleCompact(sessionID?: string, providerID?: string, modelID?: string): Promise { + if (!this.httpClient) { + return + } + + const target = sessionID || this.currentSession?.id + if (!target) { + console.error("[Kilo New] KiloProvider: No sessionID for compact") + return + } + + try { + const workspaceDir = this.getWorkspaceDirectory() + await this.httpClient.summarize(target, providerID || "kilo", modelID || "kilo/auto", workspaceDir) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to compact session:", error) + this.postMessage({ + type: "error", + message: error instanceof Error ? error.message : "Failed to compact session", + }) + } + } + /** * Handle permission response from the webview. */ @@ -672,7 +703,7 @@ export class KiloProvider implements vscode.WebviewViewProvider { } case "message.updated": - // Message info updated + // Message info updated — forward cost/tokens for assistant messages this.postMessage({ type: "messageCreated", message: { @@ -680,6 +711,8 @@ export class KiloProvider implements vscode.WebviewViewProvider { sessionID: event.properties.info.sessionID, role: event.properties.info.role, createdAt: new Date(event.properties.info.time.created).toISOString(), + cost: event.properties.info.cost, + tokens: event.properties.info.tokens, }, }) break diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts index 59cc0096acf..6d16a26a4e0 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -192,6 +192,18 @@ export class HttpClient { return true } + /** + * Trigger context compaction (summarization) for a session. + */ + async summarize(sessionId: string, providerID: string, modelID: string, directory: string): Promise { + return this.request( + "POST", + `/session/${sessionId}/summarize`, + { providerID, modelID, auto: false }, + { directory, allowEmpty: true }, + ) + } + // ============================================ // Permission Methods // ============================================ diff --git a/packages/kilo-vscode/src/services/cli-backend/index.ts b/packages/kilo-vscode/src/services/cli-backend/index.ts index 4fd38068f07..db08cbf5e95 100644 --- a/packages/kilo-vscode/src/services/cli-backend/index.ts +++ b/packages/kilo-vscode/src/services/cli-backend/index.ts @@ -6,6 +6,7 @@ export type { SessionStatusInfo, MessageInfo, MessagePart, + TokenUsage, ToolState, PermissionRequest, SSEEvent, diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index d9636372603..bee18c1238c 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -18,6 +18,14 @@ export type SessionStatusInfo = | { type: "retry"; attempt: number; message: string; next: number } | { type: "busy" } +// Token usage shape returned by the server on assistant messages +export interface TokenUsage { + input: number + output: number + reasoning: number + cache: { read: number; write: number } +} + // Message types from MessageV2 export interface MessageInfo { id: string @@ -27,6 +35,9 @@ export interface MessageInfo { created: number completed?: number } + // Present on assistant messages + cost?: number + tokens?: TokenUsage } // Part types - simplified for UI display diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index 3a2b8b0226b..010df87f044 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -4,6 +4,7 @@ */ import { Component } from "solid-js" +import { TaskHeader } from "./TaskHeader" import { MessageList } from "./MessageList" import { PromptInput } from "./PromptInput" import { PermissionDialog } from "./PermissionDialog" @@ -11,6 +12,7 @@ import { PermissionDialog } from "./PermissionDialog" export const ChatView: Component = () => { return (
+
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx new file mode 100644 index 00000000000..477f3cff5ad --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -0,0 +1,75 @@ +/** + * TaskHeader component + * Sticky header above the chat messages showing session title, + * cost, context usage, and a compact button. + */ + +import { Component, Show, createMemo } from "solid-js" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { useSession } from "../../context/session" + +export const TaskHeader: Component = () => { + const session = useSession() + + const title = createMemo(() => session.currentSession()?.title) + const hasMessages = createMemo(() => session.messages().length > 0) + const visible = createMemo(() => !!(title() && hasMessages())) + const busy = createMemo(() => session.status() === "busy") + + const cost = createMemo(() => { + const total = session.totalCost() + if (total === 0) return undefined + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(total) + }) + + const context = createMemo(() => { + const usage = session.contextUsage() + if (!usage) return undefined + const tokens = usage.tokens.toLocaleString("en-US") + const pct = usage.percentage !== null ? `${usage.percentage}%` : undefined + return { tokens, pct } + }) + + return ( + +
+
+ {title()} +
+
+ + {(c) => ( + + {c()} + + )} + + + {(ctx) => ( + + {ctx().pct ?? ctx().tokens} + + )} + + + session.compact()} + aria-label="Compact session" + /> + +
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index ccf5e319cb8..113eb7cb2e0 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -29,6 +29,7 @@ import type { PermissionRequest, TodoItem, ModelSelection, + ContextUsage, ExtensionMessage, } from "../types/messages" @@ -69,9 +70,14 @@ interface SessionContextValue { selected: Accessor selectModel: (providerID: string, modelID: string) => void + // Cost and context usage for the current session + totalCost: Accessor + contextUsage: Accessor + // Actions sendMessage: (text: string, providerID?: string, modelID?: string) => void abort: () => void + compact: () => void respondToPermission: (permissionId: string, response: "once" | "always" | "reject") => void createSession: () => void loadSessions: () => void @@ -335,6 +341,22 @@ export const SessionProvider: ParentComponent = (props) => { }) } + function compact() { + const sessionID = currentSessionID() + if (!sessionID) { + console.warn("[Kilo New] Cannot compact: no current session") + return + } + + const sel = selected() + vscode.postMessage({ + type: "compact", + sessionID, + providerID: sel?.providerID, + modelID: sel?.modelID, + }) + } + function respondToPermission(permissionId: string, response: "once" | "always" | "reject") { // Resolve sessionID from the stored permission request const permission = permissions().find((p) => p.id === permissionId) @@ -405,6 +427,27 @@ export const SessionProvider: ParentComponent = (props) => { Object.values(store.sessions).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()), ) + // Total cost across all assistant messages in the current session + const totalCost = createMemo(() => { + return messages().reduce((sum, m) => sum + (m.role === "assistant" ? (m.cost ?? 0) : 0), 0) + }) + + // Context usage from the last assistant message that has token data + const contextUsage = createMemo(() => { + const msgs = messages() + for (let i = msgs.length - 1; i >= 0; i--) { + const m = msgs[i] + if (m.role !== "assistant" || !m.tokens) continue + const total = m.tokens.input + m.tokens.output + m.tokens.reasoning + m.tokens.cache.read + m.tokens.cache.write + if (total === 0) continue + const sel = selected() + const model = sel ? provider.findModel(sel) : undefined + const percentage = model?.contextLength ? Math.round((total / model.contextLength) * 100) : null + return { tokens: total, percentage } + } + return undefined + }) + const value: SessionContextValue = { currentSessionID, currentSession, @@ -417,8 +460,11 @@ export const SessionProvider: ParentComponent = (props) => { permissions, selected, selectModel, + totalCost, + contextUsage, sendMessage, abort, + compact, respondToPermission, createSession, loadSessions, diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index 254b3357099..bf756181127 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -20,6 +20,44 @@ min-height: 0; } +/* ============================================ + Task Header + ============================================ */ + +.task-header { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: var(--vscode-sideBar-background); + border-bottom: 1px solid var(--vscode-panel-border); + flex-shrink: 0; + min-height: 32px; +} + +.task-header-title { + flex: 1; + font-size: 12px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--vscode-foreground); +} + +.task-header-stats { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.task-header-stat { + font-size: 11px; + color: var(--vscode-descriptionForeground); + white-space: nowrap; +} + .chat-input { flex-shrink: 0; border-top: 1px solid var(--vscode-panel-border); diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 20b824a549a..4427a922e48 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -64,6 +64,20 @@ export interface PartDelta { textDelta?: string } +// Token usage for assistant messages +export interface TokenUsage { + input: number + output: number + reasoning: number + cache: { read: number; write: number } +} + +// Context usage derived from the last assistant message's tokens +export interface ContextUsage { + tokens: number + percentage: number | null +} + // Message structure (simplified for webview) export interface Message { id: string @@ -72,6 +86,8 @@ export interface Message { content?: string parts?: Part[] createdAt: string + cost?: number + tokens?: TokenUsage } // Session info (simplified for webview) @@ -345,6 +361,13 @@ export interface RequestProvidersMessage { type: "requestProviders" } +export interface CompactRequest { + type: "compact" + sessionID: string + providerID?: string + modelID?: string +} + export type WebviewMessage = | SendMessageRequest | AbortRequest @@ -359,6 +382,7 @@ export type WebviewMessage = | CancelLoginRequest | WebviewReadyRequest | RequestProvidersMessage + | CompactRequest // ============================================ // VS Code API type