From 32f8563317ed7ac894a4a7f08121a8f536b3cafa Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Wed, 20 May 2026 21:08:43 -0600 Subject: [PATCH 1/4] feat(kilo-vscode): implement session memory management and optimizations This change addresses OOM crashes by reducing memory usage in the webview renderer process: - chore(webview): add message list virtualization with initial load limiting - perf(session): implement aggressive session cleanup on switch and deletion - perf(server): strip heavy metadata (file diffs/tool output) from SSE events - refactor(metadata): add slim parsers for websearch, webfetch, codesearch, and task tools - feat(settings): add auto-expand history toggle to UI --- .../src/kilo-provider/slim-metadata.ts | 64 +++++++++++++++++++ .../src/components/chat/PromptInput.tsx | 23 ++++++- .../src/components/settings/DisplayTab.tsx | 13 ++++ .../src/components/settings/settings-io.ts | 1 + .../webview-ui/src/context/session.tsx | 43 +++++++++++++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 3 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 3 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 3 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 2 + .../webview-ui/src/types/messages/config.ts | 1 + .../webview-ui/src/utils/draft-store.ts | 20 ++++++ packages/opencode/src/config/config.ts | 5 ++ packages/opencode/src/session/session.ts | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 29 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/utils/draft-store.ts diff --git a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts index df8d576e030..d113011f360 100644 --- a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts +++ b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts @@ -182,6 +182,66 @@ function slimBash(state: Record): Record { return next } +/** websearch: preserve only query/count metadata, strip results/output. */ +function slimWebsearch(state: Record): Record { + const next = { ...state } + const meta = state.metadata + if (isObj(meta)) { + const slim: Record = {} + if (typeof meta.query === "string") slim.query = meta.query + if (typeof meta.count === "number") slim.count = meta.count + next.metadata = slim + } + return next +} + +/** webfetch: preserve only url/title from input, strip response content. */ +function slimWebfetch(state: Record): Record { + const next = { ...state } + const input = state.input + if (isObj(input)) { + const slim: Record = {} + if (typeof input.url === "string") slim.url = input.url + if (typeof input.title === "string") slim.title = input.title + next.input = slim + } + return next +} + +/** codesearch: preserve query/limit from input, strip results. */ +function slimCodesearch(state: Record): Record { + const next = { ...state } + const input = state.input + if (isObj(input)) { + const slim: Record = {} + if (typeof input.query === "string") slim.query = input.query + if (typeof input.limit === "number") slim.limit = input.limit + if (typeof input.type === "string") slim.type = input.type + next.input = slim + } + return next +} + +/** task: preserve sessionId for sub-agent linking, strip heavy input/output. */ +function slimTask(state: Record): Record { + const next = { ...state } + const input = state.input + if (isObj(input)) { + const slim: Record = {} + if (typeof input.subagent_type === "string") slim.subagent_type = input.subagent_type + if (typeof input.prompt === "string") slim.prompt = input.prompt + if (typeof input.sessionId === "string") slim.sessionId = input.sessionId + next.input = slim + } + const meta = state.metadata + if (isObj(meta) && typeof meta.sessionId === "string") { + next.metadata = { sessionId: meta.sessionId } + } else { + delete next.metadata + } + return next +} + // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- @@ -214,6 +274,10 @@ const slimmers: Record) => Record = (props) => { + onMount(() => { + const handler = (e: Event) => { + const { sessionID } = (e as CustomEvent).detail as { sessionID: string } + const suffix = `:session:${sessionID}` + const pendingSuffix = `:pending:${sessionID}` + for (const map of [drafts, reviewDrafts, imageDrafts] as [ + Map, + Map, + Map, + ]) { + for (const key of [...map.keys()]) { + if (typeof key === "string" && (key.endsWith(suffix) || key.endsWith(pendingSuffix))) { + map.delete(key) + } + } + } + } + window.addEventListener("sessionDeleted", handler) + onCleanup(() => window.removeEventListener("sessionDeleted", handler)) + }) + const session = useSession() const server = useServer() const indexing = useIndexing() diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx index 978d295e18f..19a1bdb2dfc 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx @@ -99,6 +99,19 @@ const DisplayTab: Component = () => { + + updateConfig({ auto_expand_history: checked ? undefined : false })} + hideLabel + > + {language.t("settings.display.autoExpandHistory.title")} + + + = [ "tools", "layout", "auto_collapse_reasoning", + "auto_expand_history", "terminal_command_display", "indexing", "experimental", diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index d93c05ffce5..3995cddd389 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -60,6 +60,7 @@ import { state as todoState } from "./todo-revert" import { getVariant, sessionVariantKeys, transferVariants, variantKey } from "./session-variant-store" import { KILO_AUTO, parseModelString } from "../../../src/shared/provider-model" import { visibleMessages as filterVisibleMessages } from "./session-queue" +import { deleteDraftsForSession } from "../utils/draft-store" const RECENT_LIMIT = 5 const MESSAGE_PAGE_LIMIT = 80 @@ -1700,6 +1701,8 @@ export const SessionProvider: ParentComponent = (props) => { setLoading(false) } }) + deleteDraftsForSession(sessionID) + window.dispatchEvent(new CustomEvent("sessionDeleted", { detail: { sessionID } })) } // Splices the message from the store and deletes its parts. @@ -2190,7 +2193,47 @@ export const SessionProvider: ParentComponent = (props) => { console.warn("[Kilo New] Cannot select cloud preview session via selectSession") return } + const oldID = currentSessionID() const ready = loaded().has(id) + if (oldID && oldID !== id) { + const msgs = store.messages[oldID] ?? [] + const msgIds = msgs.map((m) => m.id) + queueMicrotask(() => { + setStore( + "messages", + produce((messages) => { + delete messages[oldID] + }), + ) + setStore( + "parts", + produce((parts) => { + for (const mid of msgIds) { + delete parts[mid] + } + }), + ) + setStore( + "todos", + produce((todos) => { + delete todos[oldID] + }), + ) + setStore( + "agentSelections", + produce((selections) => { + delete selections[oldID] + }), + ) + setLoaded((prev) => { + if (!prev.has(oldID)) return prev + const next = new Set(prev) + next.delete(oldID) + return next + }) + window.dispatchEvent(new CustomEvent("sessionDeleted", { detail: { sessionID: oldID } })) + }) + } setCurrentSessionID(id) setDraftSessionID(id) setLoading(!ready) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 81ca9c72287..a27e13b1e7f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1448,6 +1448,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "تمديد سجل الرسائل تلقائيًا", + "settings.display.autoExpandHistory.description": "تحميل الرسائل السابقة تلقائيًا عند التمرير للأعلى", "settings.providers.defaultModel.title": "النموذج الافتراضي", "settings.providers.defaultModel.description": "النموذج الأساسي للمحادثات", "settings.providers.smallModel.title": "نموذج صغير", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 0b1d733a33c..056426d7e21 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1491,6 +1491,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Expandir histórico de mensagens automaticamente", + "settings.display.autoExpandHistory.description": "Carregar mensagens anteriores automaticamente ao rolar para cima", "settings.providers.defaultModel.title": "Modelo padrão", "settings.providers.defaultModel.description": "Modelo principal para conversas", "settings.providers.smallModel.title": "Modelo pequeno", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index b7f0256903f..6129140fcb3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1487,6 +1487,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Automatsko proširivanje historije poruka", + "settings.display.autoExpandHistory.description": "Automatski učitaj ranije poruke pri pomicanju nagore", "settings.providers.defaultModel.title": "Zadani model", "settings.providers.defaultModel.description": "Primarni model za razgovore", "settings.providers.smallModel.title": "Mali model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 4610c0aaccc..848877dc710 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1475,6 +1475,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Udvid beskedoversigt automatisk", + "settings.display.autoExpandHistory.description": "Indlæs automatisk tidligere beskeder ved scrolling opad", "settings.providers.defaultModel.title": "Standardmodel", "settings.providers.defaultModel.description": "Primær model til samtaler", "settings.providers.smallModel.title": "Lille model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index ed2baf0392d..a7bccc34c33 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1505,6 +1505,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Nachrichtenverlauf automatisch erweitern", + "settings.display.autoExpandHistory.description": "Beim Hochscrollen automatisch frühere Nachrichten laden", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primäres Modell für Gespräche", "settings.providers.smallModel.title": "Kleines Modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index df91b0b6e1f..ad4acbde790 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1462,6 +1462,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Auto-Expand Message History", + "settings.display.autoExpandHistory.description": "Automatically load earlier messages when scrolling up", "settings.providers.defaultModel.title": "Default Model", "settings.providers.defaultModel.description": "Primary model for conversations", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 1f07e5d1e7f..f82b3e716d5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1498,6 +1498,9 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Expandir histórico de mensajes automáticamente", + "settings.display.autoExpandHistory.description": + "Cargar mensajes anteriores automáticamente al desplazarse hacia arriba", "settings.providers.defaultModel.title": "Modelo predeterminado", "settings.providers.defaultModel.description": "Modelo principal para conversaciones", "settings.providers.smallModel.title": "Modelo pequeño", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 09b25f89c3b..3850d3eb940 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1514,6 +1514,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Développer automatiquement l'historique des messages", + "settings.display.autoExpandHistory.description": "Charger automatiquement les messages précédents en remontant", "settings.providers.defaultModel.title": "Modèle par défaut", "settings.providers.defaultModel.description": "Modèle principal pour les conversations", "settings.providers.smallModel.title": "Petit modèle", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 46cab151069..b4402f8cdca 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1471,6 +1471,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "メッセージ履歴を自動展開", + "settings.display.autoExpandHistory.description": "上にスクロール時に以前のメッセージを自動読み込み", "settings.providers.defaultModel.title": "デフォルトモデル", "settings.providers.defaultModel.description": "会話のプライマリモデル", "settings.providers.smallModel.title": "小型モデル", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index dd805c59758..b78064fe884 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1456,6 +1456,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "메시지 기록 자동 펴치기", + "settings.display.autoExpandHistory.description": "위로 스크롤 시 이전 메시지 자동 로드", "settings.providers.defaultModel.title": "기본 모델", "settings.providers.defaultModel.description": "대화의 기본 모델", "settings.providers.smallModel.title": "소형 모델", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 33ba31990cb..5394be3d0e8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1452,6 +1452,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Berichtgeschiedenis automatisch uitvouwen", + "settings.display.autoExpandHistory.description": "Eerdere berichten automatisch laden bij het omhoog scrollen", "settings.providers.defaultModel.title": "Standaard Model", "settings.providers.defaultModel.description": "Primair model voor gesprekken", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 6078672537d..d9caf0a610e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1474,6 +1474,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Utvid meldingsloggen automatisk", + "settings.display.autoExpandHistory.description": "Last inn tidligere meldinger automatisk når du ruller opp", "settings.providers.defaultModel.title": "Standardmodell", "settings.providers.defaultModel.description": "Primær modell for samtaler", "settings.providers.smallModel.title": "Liten modell", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 7caf5f74b9c..3cddef95e33 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1482,6 +1482,9 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Automatyczne rozwijanie historii wiadomości", + "settings.display.autoExpandHistory.description": + "Automatycznie ładuj wcześniejsze wiadomości podczas przewijania w górę", "settings.providers.defaultModel.title": "Domyślny model", "settings.providers.defaultModel.description": "Główny model do rozmów", "settings.providers.smallModel.title": "Mały model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 681e235eb80..c4a84bb56b4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1481,6 +1481,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Автоматически разворачивать историю сообщений", + "settings.display.autoExpandHistory.description": "Автоматически загружать предыдущие сообщения при прокрутке вверх", "settings.providers.defaultModel.title": "Модель по умолчанию", "settings.providers.defaultModel.description": "Основная модель для разговоров", "settings.providers.smallModel.title": "Малая модель", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 22ecabbc9dd..dacc499d4ba 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1455,6 +1455,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "ขยายประวัติข้อความอัตโนมัติ", + "settings.display.autoExpandHistory.description": "โหลดข้อความก่อนหน้าโดยอัตโนมัติเมื่อเลื่อนขึ้น", "settings.providers.defaultModel.title": "โมเดลเริ่มต้น", "settings.providers.defaultModel.description": "โมเดลหลักสำหรับบทสนทนา", "settings.providers.smallModel.title": "โมเดลขนาดเล็ก", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 6c5da55af38..1b37fffe860 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1441,6 +1441,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Mesaj geçmişini otomatik olarak genişlet", + "settings.display.autoExpandHistory.description": "Yukarı kaydırırken önceki mesajları otomatik olarak yükle", "settings.providers.defaultModel.title": "Varsayılan Model", "settings.providers.defaultModel.description": "Sohbetler için birincil model", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 5d2ff53eb80..a0e7a84e65e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1441,6 +1441,9 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "Автоматично розгортати історію повідомлень", + "settings.display.autoExpandHistory.description": + "Автоматично завантажувати попередні повідомлення під час прокручування вгору", "settings.providers.defaultModel.title": "Модель за замовчуванням", "settings.providers.defaultModel.description": "Основна модель для чатів", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 05316ebf229..542265c2303 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1420,6 +1420,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "自动展开消息历史", + "settings.display.autoExpandHistory.description": "向上滚动时自动加载更早的消息", "settings.providers.defaultModel.title": "默认模型", "settings.providers.defaultModel.description": "对话的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 00d4bfd4d45..155b060eea4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1390,6 +1390,8 @@ export const dict = { "settings.display.terminalCommand.description": "Choose whether terminal command blocks start expanded or collapsed.", "settings.display.terminalCommand.expanded": "Expanded", "settings.display.terminalCommand.collapsed": "Collapsed", + "settings.display.autoExpandHistory.title": "自動展開訊息紀錄", + "settings.display.autoExpandHistory.description": "向上捲動時自動載入較早的訊息", "settings.providers.defaultModel.title": "預設模型", "settings.providers.defaultModel.description": "對話的主要模型", "settings.providers.smallModel.title": "小模型", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index bd47ed509b8..4f112f308bb 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -139,6 +139,7 @@ export interface Config { tools?: Record layout?: "auto" | "stretch" auto_collapse_reasoning?: boolean + auto_expand_history?: boolean experimental?: ExperimentalConfig indexing?: IndexingConfig } diff --git a/packages/kilo-vscode/webview-ui/src/utils/draft-store.ts b/packages/kilo-vscode/webview-ui/src/utils/draft-store.ts new file mode 100644 index 00000000000..daf3d673604 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/draft-store.ts @@ -0,0 +1,20 @@ +import type { ReviewComment } from "../types/messages" +import type { ImageAttachment } from "../hooks/useImageAttachments" + +export const drafts = new Map() +export const reviewDrafts = new Map() +export const imageDrafts = new Map() + +export function deleteDraftsForSession(id: string) { + if (!id) return + const suffix = `:session:${id}` + const pendingSuffix = `:pending:${id}` + const maps = [drafts, reviewDrafts, imageDrafts] + for (const map of maps) { + for (const key of map.keys()) { + if (typeof key === "string" && (key.endsWith(suffix) || key.endsWith(pendingSuffix))) { + map.delete(key) + } + } + } +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 80e7850bdb4..68963a18e3b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -218,6 +218,11 @@ export const Info = Schema.Struct({ auto_collapse_reasoning: Schema.optional(Schema.Boolean).annotate({ description: "Automatically collapse reasoning blocks after the agent finishes writing them", }), + // kilocode_change start + auto_expand_history: Schema.optional(Schema.Boolean).annotate({ + description: "Auto-expand message history when scrolling up. Defaults to true.", + }), + // kilocode_change end indexing: Schema.optional(IndexingRef).annotate({ description: "Codebase indexing configuration" }), // kilocode_change terminal_command_display: Schema.optional(Schema.Literals(["expanded", "collapsed"])).annotate({ description: "Controls whether terminal command blocks are expanded or collapsed by default in the VS Code chat UI", diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index c823f9cb611..6ed26a364cb 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -643,7 +643,7 @@ export const layer: Layer.Layer SyncEvent.run(MessageV2.Event.PartUpdated, { sessionID: part.sessionID, - part: structuredClone(part), + part: structuredClone(MessageV2.stripPartMetadata(part)), time: Date.now(), }), { type: "part update", id: part.id, sessionID: part.sessionID }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index a0fb916a27b..7fe90c51c92 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1357,6 +1357,7 @@ export type Config = { enabled_providers?: Array remote_control?: boolean auto_collapse_reasoning?: boolean + auto_expand_history?: boolean indexing?: IndexingConfig terminal_command_display?: "expanded" | "collapsed" model?: string From 5325724352cc7d2195316dfb429318747c1e0728 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 3 Jun 2026 11:11:40 +0200 Subject: [PATCH 2/4] fix: preserve chat state during renderer cleanup --- .../unit/message-list-queue-contract.test.ts | 19 +++++++++++ .../tests/unit/prompt-drafts.test.ts | 32 ++++++++++++++++++- .../tests/unit/prompt-send-contract.test.ts | 18 +++++++++++ .../src/components/chat/PromptInput.tsx | 29 ++--------------- .../webview-ui/src/context/session.tsx | 2 -- packages/opencode/src/session/message-v2.ts | 9 ++++-- .../kilocode/session-message-metadata.test.ts | 8 +++++ 7 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts diff --git a/packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts b/packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts new file mode 100644 index 00000000000..293c11a0170 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test" +import fs from "node:fs" +import path from "node:path" + +const file = path.resolve(import.meta.dir, "../../webview-ui/src/components/chat/MessageList.tsx") +const source = fs.readFileSync(file, "utf-8") + +function virtualizer() { + const start = source.indexOf("", start) + return source.slice(start, end) +} + +describe("MessageList queued rendering", () => { + it("marks only partitioned queued turns as queued", () => { + expect(virtualizer()).not.toContain("queued=") + expect(source).toMatch(/[\s\S]*?/) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts b/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts index d5f9dc532de..97c2d5a56ca 100644 --- a/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts @@ -1,6 +1,36 @@ -import { describe, it, expect } from "bun:test" +import { beforeEach, describe, it, expect } from "bun:test" +import { + deleteDraftsForSession, + drafts, + imageDrafts, + reviewDrafts, +} from "../../webview-ui/src/utils/draft-store" import { pendingDraftKey, scopeDraftKey, sessionDraftKey } from "../../webview-ui/src/utils/prompt-drafts" +beforeEach(() => { + drafts.clear() + reviewDrafts.clear() + imageDrafts.clear() +}) + +describe("deleteDraftsForSession", () => { + it("clears deleted-session drafts without touching other sessions", () => { + drafts.set("prompt:default:session:a", "draft a") + drafts.set("prompt:default:pending:a", "pending a") + drafts.set("prompt:default:session:b", "draft b") + reviewDrafts.set("prompt:default:session:a", []) + imageDrafts.set("prompt:default:session:a", []) + + deleteDraftsForSession("a") + + expect(drafts.has("prompt:default:session:a")).toBe(false) + expect(drafts.has("prompt:default:pending:a")).toBe(false) + expect(drafts.get("prompt:default:session:b")).toBe("draft b") + expect(reviewDrafts.has("prompt:default:session:a")).toBe(false) + expect(imageDrafts.has("prompt:default:session:a")).toBe(false) + }) +}) + describe("sessionDraftKey", () => { it("prefixes session ids", () => { expect(sessionDraftKey("abc")).toBe("session:abc") diff --git a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts index e9f8126a56e..1761b9e88cc 100644 --- a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts @@ -17,6 +17,7 @@ import path from "node:path" const ROOT = path.resolve(import.meta.dir, "../..") const SESSION_FILE = path.join(ROOT, "webview-ui/src/context/session.tsx") const CHATVIEW_FILE = path.join(ROOT, "webview-ui/src/components/chat/ChatView.tsx") +const PROMPT_INPUT_FILE = path.join(ROOT, "webview-ui/src/components/chat/PromptInput.tsx") const PROMPT_UTILS_FILE = path.join(ROOT, "webview-ui/src/components/chat/prompt-input-utils.ts") function readFile(filePath: string): string { @@ -75,6 +76,23 @@ describe("sendCommand dismisses pending tool requests", () => { }) }) +describe("session draft retention contract", () => { + const source = readFile(SESSION_FILE) + const input = readFile(PROMPT_INPUT_FILE) + const select = extractFunctionBody(source, "selectSession") + const remove = extractFunctionBody(source, "handleSessionDeleted") + + it("does not treat a session switch as draft deletion", () => { + expect(select).not.toContain("deleteDraftsForSession") + expect(select).not.toContain("sessionDeleted") + }) + + it("cleans the draft maps only on real deletion", () => { + expect(input).toContain('import { drafts, imageDrafts, reviewDrafts } from "../../utils/draft-store"') + expect(remove).toContain("deleteDraftsForSession(sessionID)") + }) +}) + describe("ChatView prompt-block contract", () => { const source = readFile(CHATVIEW_FILE) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index b0e4c483bed..52db8c1bb4f 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -3,7 +3,7 @@ * Text input with send/abort buttons, ghost-text autocomplete, and @ file mention support */ -import { createSignal, createEffect, on, For, Index, onMount, onCleanup, Show, untrack, type Component } from "solid-js" +import { createSignal, createEffect, on, For, Index, onCleanup, Show, untrack, type Component } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { Dialog } from "@kilocode/kilo-ui/dialog" import { IconButton } from "@kilocode/kilo-ui/icon-button" @@ -48,13 +48,9 @@ import { } from "./prompt-input-utils" import type { ReviewComment, TextPart } from "../../types/messages" import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdown" +import { drafts, imageDrafts, reviewDrafts } from "../../utils/draft-store" import { pendingDraftKey, scopeDraftKey, sessionDraftKey } from "../../utils/prompt-drafts" -// Per-session input text storage (module-level so it survives remounts) -const drafts = new Map() -const reviewDrafts = new Map() -const imageDrafts = new Map() - function mergeReviewComments(current: ReviewComment[], incoming: ReviewComment[]): ReviewComment[] { if (incoming.length === 0) return current const map = new Map(current.map((item) => [item.id, item])) @@ -75,27 +71,6 @@ interface PromptInputProps { } export const PromptInput: Component = (props) => { - onMount(() => { - const handler = (e: Event) => { - const { sessionID } = (e as CustomEvent).detail as { sessionID: string } - const suffix = `:session:${sessionID}` - const pendingSuffix = `:pending:${sessionID}` - for (const map of [drafts, reviewDrafts, imageDrafts] as [ - Map, - Map, - Map, - ]) { - for (const key of [...map.keys()]) { - if (typeof key === "string" && (key.endsWith(suffix) || key.endsWith(pendingSuffix))) { - map.delete(key) - } - } - } - } - window.addEventListener("sessionDeleted", handler) - onCleanup(() => window.removeEventListener("sessionDeleted", handler)) - }) - const session = useSession() const server = useServer() const indexing = useIndexing() diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 3995cddd389..a0a5e6a3ddf 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -1702,7 +1702,6 @@ export const SessionProvider: ParentComponent = (props) => { } }) deleteDraftsForSession(sessionID) - window.dispatchEvent(new CustomEvent("sessionDeleted", { detail: { sessionID } })) } // Splices the message from the store and deletes its parts. @@ -2231,7 +2230,6 @@ export const SessionProvider: ParentComponent = (props) => { next.delete(oldID) return next }) - window.dispatchEvent(new CustomEvent("sessionDeleted", { detail: { sessionID: oldID } })) }) } setCurrentSessionID(id) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 70abefb6909..09c03084935 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -681,9 +681,11 @@ export const cursor = { // kilocode_change start - strip bloated metadata fields from stored parts to prevent multi-MB payloads // This handles both legacy data that was stored with full file contents and keeps the API response lean. -function stripPatch(value: unknown) { +const MAX_INLINE_DIFF_SIZE = 64 * 1024 + +function stripPatch(value: unknown, limit = Snapshot.MAX_DIFF_SIZE) { if (typeof value !== "string") return undefined - if (Buffer.byteLength(value) > Snapshot.MAX_DIFF_SIZE) return undefined + if (Buffer.byteLength(value) > limit) return undefined return value } @@ -705,7 +707,8 @@ export function stripPartMetadata(part: Part): Part { if (meta.diff !== undefined) { const { diff, ...rest } = next - next = rest + const kept = stripPatch(diff, MAX_INLINE_DIFF_SIZE) + next = { ...rest, ...(kept ? { diff: kept } : {}) } changed = true } diff --git a/packages/opencode/test/kilocode/session-message-metadata.test.ts b/packages/opencode/test/kilocode/session-message-metadata.test.ts index 5fa865fb0c0..2a8e1665130 100644 --- a/packages/opencode/test/kilocode/session-message-metadata.test.ts +++ b/packages/opencode/test/kilocode/session-message-metadata.test.ts @@ -33,6 +33,14 @@ function part(tool: string, metadata: Record): MessageV2.Part { } describe("session message metadata stripping", () => { + test("keeps bounded edit diffs for live renderers", () => { + const input = part("edit", { diff: patch }) + const stripped = MessageV2.stripPartMetadata(input) as Extract + const meta = stripped.state.status === "completed" ? stripped.state.metadata : {} + + expect(meta.diff).toBe(patch) + }) + test("keeps bounded edit filediff patches and strips heavy fields", () => { const input = part("edit", { diff: blob(200_000), From 28664c0b77919a8c33f9f9856a94469bbbbac926 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 3 Jun 2026 11:28:46 +0200 Subject: [PATCH 3/4] fix: complete renderer memory safeguards --- .changeset/renderer-memory-guardrails.md | 6 ++ .../src/kilo-provider/slim-metadata.ts | 25 +++---- .../unit/message-list-queue-contract.test.ts | 6 +- .../tests/unit/prompt-drafts.test.ts | 7 +- .../tests/unit/prompt-send-contract.test.ts | 5 ++ .../tests/unit/slim-metadata.test.ts | 68 +++++++++++++++++++ .../src/components/chat/MessageList.tsx | 4 +- .../webview-ui/src/context/session.tsx | 6 ++ .../kilo-vscode/webview-ui/src/i18n/it.ts | 3 + packages/sdk/openapi.json | 3 + 10 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 .changeset/renderer-memory-guardrails.md diff --git a/.changeset/renderer-memory-guardrails.md b/.changeset/renderer-memory-guardrails.md new file mode 100644 index 00000000000..b61adaf1be2 --- /dev/null +++ b/.changeset/renderer-memory-guardrails.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +--- + +Reduce memory use when switching long VS Code sessions while preserving queued prompts, unsent drafts, and inline edit diffs. diff --git a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts index d113011f360..6ed59c04173 100644 --- a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts +++ b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts @@ -182,54 +182,51 @@ function slimBash(state: Record): Record { return next } -/** websearch: preserve only query/count metadata, strip results/output. */ +/** websearch: preserve provider metadata and cap rendered results. */ function slimWebsearch(state: Record): Record { - const next = { ...state } + const next = slimOutput(state) const meta = state.metadata if (isObj(meta)) { const slim: Record = {} - if (typeof meta.query === "string") slim.query = meta.query - if (typeof meta.count === "number") slim.count = meta.count + if (typeof meta.provider === "string") slim.provider = meta.provider next.metadata = slim } return next } -/** webfetch: preserve only url/title from input, strip response content. */ +/** webfetch: preserve the requested URL and cap rendered response content. */ function slimWebfetch(state: Record): Record { - const next = { ...state } + const next = slimOutput(state) const input = state.input if (isObj(input)) { const slim: Record = {} if (typeof input.url === "string") slim.url = input.url - if (typeof input.title === "string") slim.title = input.title next.input = slim } return next } -/** codesearch: preserve query/limit from input, strip results. */ +/** codesearch: preserve query/token count and cap rendered results. */ function slimCodesearch(state: Record): Record { - const next = { ...state } + const next = slimOutput(state) const input = state.input if (isObj(input)) { const slim: Record = {} if (typeof input.query === "string") slim.query = input.query - if (typeof input.limit === "number") slim.limit = input.limit - if (typeof input.type === "string") slim.type = input.type + if (typeof input.tokensNum === "number") slim.tokensNum = input.tokensNum next.input = slim } return next } -/** task: preserve sessionId for sub-agent linking, strip heavy input/output. */ +/** task: preserve rendered labels and sessionId while capping sub-agent output. */ function slimTask(state: Record): Record { - const next = { ...state } + const next = slimOutput(state) const input = state.input if (isObj(input)) { const slim: Record = {} + if (typeof input.description === "string") slim.description = input.description if (typeof input.subagent_type === "string") slim.subagent_type = input.subagent_type - if (typeof input.prompt === "string") slim.prompt = input.prompt if (typeof input.sessionId === "string") slim.sessionId = input.sessionId next.input = slim } diff --git a/packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts b/packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts index 293c11a0170..f332420340d 100644 --- a/packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/message-list-queue-contract.test.ts @@ -11,9 +11,13 @@ function virtualizer() { return source.slice(start, end) } -describe("MessageList queued rendering", () => { +describe("MessageList rendering", () => { it("marks only partitioned queued turns as queued", () => { expect(virtualizer()).not.toContain("queued=") expect(source).toMatch(/[\s\S]*?/) }) + + it("disables automatic history expansion when configured", () => { + expect(source).toContain("config().auto_expand_history === false") + }) }) diff --git a/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts b/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts index 97c2d5a56ca..882d78b0f44 100644 --- a/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts @@ -1,10 +1,5 @@ import { beforeEach, describe, it, expect } from "bun:test" -import { - deleteDraftsForSession, - drafts, - imageDrafts, - reviewDrafts, -} from "../../webview-ui/src/utils/draft-store" +import { deleteDraftsForSession, drafts, imageDrafts, reviewDrafts } from "../../webview-ui/src/utils/draft-store" import { pendingDraftKey, scopeDraftKey, sessionDraftKey } from "../../webview-ui/src/utils/prompt-drafts" beforeEach(() => { diff --git a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts index 1761b9e88cc..a363fd9fcbe 100644 --- a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts @@ -91,6 +91,11 @@ describe("session draft retention contract", () => { expect(input).toContain('import { drafts, imageDrafts, reviewDrafts } from "../../utils/draft-store"') expect(remove).toContain("deleteDraftsForSession(sessionID)") }) + + it("clears off-store transcript caches when switching sessions", () => { + expect(select).toContain("stash.remove(mid)") + expect(select).toContain("delete map[oldID]") + }) }) describe("ChatView prompt-block contract", () => { diff --git a/packages/kilo-vscode/tests/unit/slim-metadata.test.ts b/packages/kilo-vscode/tests/unit/slim-metadata.test.ts index 21c62f4722c..5e69f62133f 100644 --- a/packages/kilo-vscode/tests/unit/slim-metadata.test.ts +++ b/packages/kilo-vscode/tests/unit/slim-metadata.test.ts @@ -367,4 +367,72 @@ describe("slimPart", () => { expect(bytes(slimPart(heavy))).toBeLessThan(MAX_SLIM_BYTES) }) }) + + // ----------------------------------------------------------------------- + // websearch / webfetch / codesearch / task + // ----------------------------------------------------------------------- + describe("web tools", () => { + it("caps websearch output and preserves provider metadata", () => { + const heavy = part("websearch", { + status: "completed", + input: { query: "renderer memory" }, + output: BIG, + metadata: { provider: "exa", results: BIG }, + }) + const slim = slimPart(heavy) as Record + + expect(slim.state.output.length).toBeLessThan(BIG.length) + expect(slim.state.metadata).toEqual({ provider: "exa" }) + expect(bytes(slim)).toBeLessThan(MAX_SLIM_BYTES) + }) + + it("caps webfetch output and preserves only the URL input", () => { + const heavy = part("webfetch", { + status: "completed", + input: { url: "https://example.com", format: "markdown" }, + output: BIG, + metadata: {}, + }) + const slim = slimPart(heavy) as Record + + expect(slim.state.output.length).toBeLessThan(BIG.length) + expect(slim.state.input).toEqual({ url: "https://example.com" }) + expect(bytes(slim)).toBeLessThan(MAX_SLIM_BYTES) + }) + + it("caps codesearch output and preserves query/token inputs", () => { + const heavy = part("codesearch", { + status: "completed", + input: { query: "SolidJS createMemo", tokensNum: 5000, extra: BIG }, + output: BIG, + metadata: {}, + }) + const slim = slimPart(heavy) as Record + + expect(slim.state.output.length).toBeLessThan(BIG.length) + expect(slim.state.input).toEqual({ query: "SolidJS createMemo", tokensNum: 5000 }) + expect(bytes(slim)).toBeLessThan(MAX_SLIM_BYTES) + }) + }) + + describe("task", () => { + it("caps output and preserves only rendered input plus child session metadata", () => { + const heavy = part("task", { + status: "completed", + input: { description: "Explore renderer", prompt: BIG, subagent_type: "explore", sessionId: "child" }, + output: BIG, + metadata: { sessionId: "child", model: BIG }, + }) + const slim = slimPart(heavy) as Record + + expect(slim.state.output.length).toBeLessThan(BIG.length) + expect(slim.state.input).toEqual({ + description: "Explore renderer", + subagent_type: "explore", + sessionId: "child", + }) + expect(slim.state.metadata).toEqual({ sessionId: "child" }) + expect(bytes(slim)).toBeLessThan(MAX_SLIM_BYTES) + }) + }) }) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 702bd3a680e..0ee25292f5e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -17,6 +17,7 @@ import { createAutoScroll } from "@kilocode/kilo-ui/hooks" import { useSession } from "../../context/session" import { useServer } from "../../context/server" import { useLanguage } from "../../context/language" +import { useConfig } from "../../context/config" import { recentSessions } from "../../context/session-utils" import { formatRelativeDate } from "../../utils/date" import { FeedbackDialog } from "./FeedbackDialog" @@ -68,6 +69,7 @@ export const MessageList: Component = (props) => { const session = useSession() const server = useServer() const language = useLanguage() + const { config } = useConfig() const dialog = useDialog() const autoScroll = createAutoScroll({ @@ -143,7 +145,7 @@ export const MessageList: Component = (props) => { const maybeLoadOlder = () => { const el = scrollEl() - if (!el || el.scrollTop > 600) return + if (!el || el.scrollTop > 600 || config().auto_expand_history === false) return session.loadOlderMessages() } diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index a0a5e6a3ddf..472456a7aa2 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -2198,6 +2198,7 @@ export const SessionProvider: ParentComponent = (props) => { const msgs = store.messages[oldID] ?? [] const msgIds = msgs.map((m) => m.id) queueMicrotask(() => { + for (const mid of msgIds) stash.remove(mid) setStore( "messages", produce((messages) => { @@ -2224,6 +2225,11 @@ export const SessionProvider: ParentComponent = (props) => { delete selections[oldID] }), ) + setPages( + produce((map) => { + delete map[oldID] + }), + ) setLoaded((prev) => { if (!prev.has(oldID)) return prev const next = new Set(prev) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 5776e3f16d0..f9c34323476 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1305,6 +1305,9 @@ export const dict = { "settings.display.reasoningAutoCollapse.title": "Comprimi automaticamente ragionamento", "settings.display.reasoningAutoCollapse.description": "Comprimi i blocchi di ragionamento dopo che l'agente ha finito di scriverli. Lascia disattivato per tenerli espansi finché non li comprimi manualmente.", + "settings.display.autoExpandHistory.title": "Espandi automaticamente la cronologia dei messaggi", + "settings.display.autoExpandHistory.description": + "Carica automaticamente i messaggi precedenti quando scorri verso l'alto", "settings.display.terminalCommand.title": "Blocchi comando terminale", "settings.display.terminalCommand.description": "Scegli se i blocchi comando terminale iniziano espansi o compressi.", "settings.display.terminalCommand.expanded": "Espansi", diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 6f845c28f5c..a2a3944a39e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -17833,6 +17833,9 @@ "auto_collapse_reasoning": { "type": "boolean" }, + "auto_expand_history": { + "type": "boolean" + }, "indexing": { "$ref": "#/components/schemas/IndexingConfig" }, From 2e571b066df7bb71d6f67633f1a69964a00e621a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 3 Jun 2026 11:39:53 +0200 Subject: [PATCH 4/4] fix(vscode): skip stale session cache eviction --- .../kilo-vscode/tests/unit/prompt-send-contract.test.ts | 8 ++++++-- packages/kilo-vscode/webview-ui/src/context/session.tsx | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts index a363fd9fcbe..8ca789c7aa6 100644 --- a/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts @@ -92,8 +92,12 @@ describe("session draft retention contract", () => { expect(remove).toContain("deleteDraftsForSession(sessionID)") }) - it("clears off-store transcript caches when switching sessions", () => { - expect(select).toContain("stash.remove(mid)") + it("clears off-store transcript caches unless the session becomes active again", () => { + const guard = select.indexOf("if (currentSessionID() === oldID) return") + const cleanup = select.indexOf("stash.remove(mid)") + + expect(guard).toBeGreaterThan(-1) + expect(cleanup).toBeGreaterThan(guard) expect(select).toContain("delete map[oldID]") }) }) diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 472456a7aa2..989b116d488 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -2198,6 +2198,7 @@ export const SessionProvider: ParentComponent = (props) => { const msgs = store.messages[oldID] ?? [] const msgIds = msgs.map((m) => m.id) queueMicrotask(() => { + if (currentSessionID() === oldID) return for (const mid of msgIds) stash.remove(mid) setStore( "messages",