Skip to content

feat(footer): show the last turn's prompt cache hit - #945

Merged
omartelo merged 5 commits into
mainfrom
feat/context-cache-tooltip
Oct 27, 2026
Merged

omartelo merged 5 commits into
mainfrom
feat/context-cache-tooltip

Conversation

@omartelo

@omartelo omartelo commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The footer now says how much of the last turn's input came back from the prompt cache:

  • Cache: 98% hit, green from 50% and amber below, so a turn after the cache expired (0%, amber) stands out as the one billed at the full rate.
  • A bar under it splits the turn's input into cached (muted), written to the cache (amber) and uncached (foreground).

It sits in the context window tooltip on Claude Code and Codex, and in the cost tooltip on oh-my-pi and opencode, which record the split but no window.

Also fixes the Codex cost billing a cache write twice: cache_write_input_tokens is part of input_tokens (Codex fills it from the Responses API's input_tokens_details; its own parses_cache_write_token_usage test has 40 cached + 60 written of 100 input), but it was priced at the cache-write rate and again as uncached input. Cost and the cache bar now share one split, codexCache.

How

  • api.SessionUsage gains Cache *SessionCache {read, write, uncached}; contract.golden moved with it.
  • Claude Code: the same assistant line the ring reads (cache_read_input_tokens, cache_creation_input_tokens, input_tokens). A compaction drops the split until the next assistant line, since the turn before it no longer describes the window.
  • Codex: last_token_usage, both cache counters taken out of input_tokens.
  • oh-my-pi: the newest assistant line's usage.input, cacheRead, cacheWrite (omp already takes the cache out of input, calculateOpenAIUsageAccounting).
  • opencode: the newest finished assistant message row of the session, tokens.input, tokens.cache.read, tokens.cache.write; a sub-agent's own session is not read.
  • Native: footer_cache.go draws the line and the three-part bar, under the context token line or under the cost lines when there is no window; one new catalog key, shell.footerSession.cacheHit, in all seven locales.

Provider gap (Invariant 5)

Crush folds its cache counts into its totals, Kiro files every token count as zero, and Cursor and Antigravity have no usage reader, so the four show no cache line. Written down in docs/ceilings.md, the Provider support table of every README, and docs/providers/.

Tests changed

  • TestCodexTranscriptCostPricesTheRunningTotal asserted the double count, which Codex never promised: its uncached share is now 575 of 1000 input, not 600.
  • TestSessionUsageReportsTheTranscriptTail, TestSessionUsageReportsCodexRollout, TestScanCodexContextUsage: the event contract gained Cache.

Test plan

  • go test ./internal/terminal ./api: split per provider, compaction clears it, opencode skips an unfinished turn and a sub-agent's, a missing message table reads as no line, the cost-only event carries it, Codex cost with a cache write.
  • go test ./cmd/lich-native: decode keeps a valid split and drops a malformed or negative one; cacheHit rounding and the empty cases.
  • Local gate (backend and native/): gofmt, vet, test.
  • Rendered both tooltips headless, dark and light: context at warm (98%), mixed (62%) and cold (0%), cost on a provider with no window.
  • The opencode query run read-only against a real opencode.db.

…oltip

The context window tooltip gains a line with the share of the last turn's
input read back from the prompt cache, in green from 50% and amber below,
over a bar splitting that input into cached, written and uncached.

The session-usage event carries the split as api.SessionCache, read from
the assistant line's usage on Claude Code and from last_token_usage on
Codex. It is nil for every other provider and for a Claude conversation
whose last event is a compaction.

Tests: TestSessionUsageReportsTheTranscriptTail,
TestSessionUsageReportsCodexRollout and TestScanCodexContextUsage expect
the new Cache field because the contract changed.
cache_write_input_tokens is part of input_tokens: Codex fills it from the
Responses API's input_tokens_details (its parses_cache_write_token_usage
test has 40 cached + 60 written of 100 input). The cost priced it once at
the cache-write rate and again inside the uncached input. Cost and the
context tooltip's cache bar now share one split, codexCache.

TestCodexTranscriptCostPricesTheRunningTotal asserted the double count,
which Codex never promised: its uncached share is now 575, not 600.
…opencode

Both record the last turn's cache split but no window, so the line rides on
the cost tooltip instead of a context one. oh-my-pi's comes from the newest
assistant line's usage (input, cacheRead, cacheWrite), opencode's from the
newest finished assistant message row (tokens.input, tokens.cache.*); both
already leave the cache out of input.
@omartelo omartelo changed the title feat(footer): show the last turn's prompt cache hit in the context tooltip feat(footer): show the last turn's prompt cache hit Oct 27, 2026
…oltip

# Conflicts:
#	CHANGELOG.md
#	api/testdata/contract.golden
chromedp gives a launching Chrome 20s to print its DevTools URL. On a
GitHub Ubuntu runner the suite's first launch passed that, failing
TestChromeClearReachesTheFieldsListener with "websocket url timeout
reached" on two runs; in the run that predates the sandbox sysctl the
second launch took 6s and the third 1s to reach Chrome's sandbox check,
so the time goes to loading the browser off a cold disk, not to lich.
@omartelo
omartelo merged commit 17ba815 into main Oct 27, 2026
7 checks passed
@omartelo
omartelo deleted the feat/context-cache-tooltip branch October 27, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant