diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fcff11b2b..a29a98c40 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -3,7 +3,7 @@ ## [Unreleased] ### Added -- Added visual markers to assistant messages when prompt caching is invalidated +- Added a prompt-cache miss marker: a slim `โŠ˜ cache miss ยท tokens` divider above an assistant turn whose request lost the provider prompt cache. Detected from per-turn usage โ€” the previous turn cached a meaningful prefix but this request read none of it back and reprocessed the prompt (e.g. after a thinking-level / service-tier / model change, tool or system-prompt change, or a history rewrite). Reports the reprocessed token count. Toggle with the `display.cacheMissMarker` setting (Appearance โ†’ Display, on by default). ### Changed diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index fa0c791ce..76e09919f 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -849,6 +849,17 @@ export const SETTINGS_SCHEMA = { }, }, + "display.cacheMissMarker": { + type: "boolean", + default: true, + ui: { + tab: "appearance", + group: "Display", + label: "Cache Miss Marker", + description: "Show a divider above an assistant turn whose request lost (missed) the prompt cache", + }, + }, + showHardwareCursor: { type: "boolean", default: true, // will be computed based on platform if undefined diff --git a/packages/coding-agent/src/modes/components/chat-transcript-builder.ts b/packages/coding-agent/src/modes/components/chat-transcript-builder.ts index 6d5581042..513629dc0 100644 --- a/packages/coding-agent/src/modes/components/chat-transcript-builder.ts +++ b/packages/coding-agent/src/modes/components/chat-transcript-builder.ts @@ -252,8 +252,10 @@ export class ChatTranscriptBuilder { ); this.container.addChild(assistantComponent); - const invalidation = detectCacheInvalidation(this.#lastAssistantUsage, message.usage); - if (invalidation) assistantComponent.setCacheInvalidation(invalidation); + if (settings.get("display.cacheMissMarker")) { + const invalidation = detectCacheInvalidation(this.#lastAssistantUsage, message.usage); + if (invalidation) assistantComponent.setCacheInvalidation(invalidation); + } if (message.usage.cacheRead + message.usage.cacheWrite + message.usage.input > 0) { this.#lastAssistantUsage = message.usage; } diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index 2b160c367..6df61b15c 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -664,8 +664,10 @@ export class EventController { // meaningful prefix and this request read none of it back, flag the turn. const usage = event.message.usage; if (usage.cacheRead + usage.cacheWrite + usage.input > 0) { - const invalidation = detectCacheInvalidation(this.ctx.lastAssistantUsage, usage); - if (invalidation) this.ctx.streamingComponent.setCacheInvalidation(invalidation); + if (settings.get("display.cacheMissMarker")) { + const invalidation = detectCacheInvalidation(this.ctx.lastAssistantUsage, usage); + if (invalidation) this.ctx.streamingComponent.setCacheInvalidation(invalidation); + } this.ctx.lastAssistantUsage = usage; } this.#lastAssistantComponent = this.ctx.streamingComponent; diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 12652eb25..d3d47a29a 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -327,6 +327,13 @@ export class SelectorController { // InputController.toggleThinkingBlockVisibility). this.ctx.ui.resetDisplay(); break; + case "display.cacheMissMarker": + // Rebuild re-runs the usage-based detection under the new setting so + // markers appear/disappear; full reset retires any already committed + // to native scrollback (mirrors hideThinking). + this.ctx.rebuildChatFromMessages(); + this.ctx.ui.resetDisplay(); + break; case "tui.tight": setTuiTight(value as boolean); this.ctx.ui.invalidate(); diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index 4736771da..0a923a97e 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -111,6 +111,7 @@ export type SymbolKey = | "icon.agents" | "icon.job" | "icon.cache" + | "icon.cacheMiss" | "icon.input" | "icon.output" | "icon.host" @@ -310,6 +311,7 @@ const UNICODE_SYMBOLS: SymbolMap = { "icon.agents": "๐Ÿ‘ฅ", "icon.job": "โš™", "icon.cache": "๐Ÿ’พ", + "icon.cacheMiss": "โŠ˜", "icon.input": "โคต", "icon.output": "โคด", "icon.host": "๐Ÿ–ฅ", @@ -579,6 +581,8 @@ const NERD_SYMBOLS: SymbolMap = { "icon.job": "\uf013", // pick: ๏‡€ | alt: ๏‚  ๏ƒ‡ "icon.cache": "\uf1c0", + // pick: (fa-ban) | alt: โŠ˜ + "icon.cacheMiss": "\uf05e", // pick: ๏‚ | alt: ๏ก โ†’ "icon.input": "\uf090", // pick: ๏‚‹ | alt: ๏ก โ†’ @@ -810,6 +814,7 @@ const ASCII_SYMBOLS: SymbolMap = { "icon.agents": "AG", "icon.job": "bg", "icon.cache": "cache", + "icon.cacheMiss": "!", "icon.input": "in:", "icon.output": "out:", "icon.host": "host", @@ -1770,6 +1775,7 @@ export class Theme { agents: this.#symbols["icon.agents"], job: this.#symbols["icon.job"], cache: this.#symbols["icon.cache"], + cacheMiss: this.#symbols["icon.cacheMiss"], input: this.#symbols["icon.input"], output: this.#symbols["icon.output"], host: this.#symbols["icon.host"], diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index 59d96afad..6cf8ead56 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -412,8 +412,10 @@ export class UiHelpers { const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined; if (assistantComponent) { const usage = message.usage; - const invalidation = detectCacheInvalidation(this.ctx.lastAssistantUsage, usage); - if (invalidation) assistantComponent.setCacheInvalidation(invalidation); + if (this.ctx.settings.get("display.cacheMissMarker")) { + const invalidation = detectCacheInvalidation(this.ctx.lastAssistantUsage, usage); + if (invalidation) assistantComponent.setCacheInvalidation(invalidation); + } if (usage.cacheRead + usage.cacheWrite + usage.input > 0) { this.ctx.lastAssistantUsage = usage; }