Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- A session card's menu opens a browser for that session, above Open in, in a tab beside the session: its terminal on one side and the page on the other, with the divider, rotate and zoom of a split. The session's agent drives the same page: open a URL, read it, click, type, press a key, go back and forward, scroll and screenshot, from `browser_*` tools or from `lich browser`. The agent's own cursor shows where it acts: lich's arrow, in the session's card color or the theme's, with a badge naming the provider, glides to each click, field and scroll on the page and to back, forward, reload and the address on the pane's toolbar, playing even an agent's parallel calls in turn. You click, type and copy and paste with your own clipboard in the page while it does. Each click and key the agent sends answers with the page it left, in a compact form, so a long session spends about half the tokens it would reading the page back as JSON. When the agent opens a browser on its own, a toast in the bottom-right corner offers to show it. Open in a window moves the page to a Chromium window of its own, cookies and all. Closing the card closes the browser. The browser is the Chrome, Edge, Chromium, Brave, Vivaldi or Helium already installed. opencode's agent reaches it through `lich browser`; its own tool list does not include `browser_*`.
- The footer shows how much of the last turn was read back from the prompt cache, with a bar splitting the turn's input into cached, written to the cache and uncached. A turn after the cache expired reads 0% in amber, which is the turn billed at the full rate. It sits in the context window tooltip on Claude Code and Codex, and in the cost tooltip on oh-my-pi and opencode.

### Changed

Expand All @@ -18,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- A session browser's open, reload, back and forward return once the page is interactive, instead of waiting for every image and tracker to finish. That wait held the agent's next action for as long as the load event took, up to 30 seconds, after the page was already on screen. A screenshot is the viewport being watched.
- A Codex session's cost no longer bills the tokens written to the prompt cache twice. They were priced at the cache-write rate and again as uncached input.
- The mouse wheel in a terminal scrolls smoothly and as far as in other terminals. A notch moves three lines of the shell's history, or a full-screen program such as `less`, eased over a few frames instead of jumping some eight lines at once. A program that takes the mouse, such as nvim, Claude Code or Codex, gets one wheel step a notch and scrolls its own distance, where it used to get about eight and race through the conversation.

## [0.79.0] - 2026-10-24
Expand Down
1 change: 1 addition & 0 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ porque lee lo que cada CLI deja escrito, y no hay dos que registren lo mismo.
| Lo que obtienes | Claude Code | Codex | Antigravity | opencode | oh-my-pi | Crush | Cursor CLI | Kiro CLI |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Ventana de contexto en el pie de ventana | sí | sí | no | no | no | no | no | sí |
| Cuánto del último turno salió de la caché de prompts, en el pie de ventana | sí | sí | no | en el tooltip del costo | en el tooltip del costo | no | no | no |
| El tooltip de la tarjeta nombra el modelo en uso, y con cuál se abrió si cambió | sí | sí | no | no | no | no | no | sí |
| Costo en el pie de ventana | sí | sí | no | sí | sí | sí | no | créditos |
| Cuánto queda de tu plan | sí | sí | sí | solo OpenCode Go | no | no | sí | no |
Expand Down
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ lich は各 CLI が書き出す情報を読んでいて、何を書き出すか
| 得られるもの | Claude Code | Codex | Antigravity | opencode | oh-my-pi | Crush | Cursor CLI | Kiro CLI |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| フッターにコンテキストウィンドウを表示 | あり | あり | なし | なし | なし | なし | なし | あり |
| 直前のターンのうちプロンプトキャッシュから読んだ割合をフッターに表示 | あり | あり | なし | コストのツールチップ | コストのツールチップ | なし | なし | なし |
| カードのツールチップに実行中のモデルを表示(開始時から変わっていれば開始時のモデルも) | あり | あり | なし | なし | なし | なし | なし | あり |
| フッターに費用を表示 | あり | あり | なし | あり | あり | あり | なし | クレジット |
| プランの残量を表示 | あり | あり | あり | OpenCode Go のみ | なし | なし | あり | なし |
Expand Down
1 change: 1 addition & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ lich로 할 수 있는 일:
| 기능 | Claude Code | Codex | Antigravity | opencode | oh-my-pi | Crush | Cursor CLI | Kiro CLI |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| 하단 바에 컨텍스트 윈도우 표시 | 예 | 예 | 아니요 | 아니요 | 아니요 | 아니요 | 아니요 | 예 |
| 직전 턴 중 프롬프트 캐시에서 읽은 비율을 하단 바에 표시 | 예 | 예 | 아니요 | 비용 툴팁에 | 비용 툴팁에 | 아니요 | 아니요 | 아니요 |
| 카드 툴팁에 실행 중인 모델 표시 (시작할 때와 바뀌었으면 시작 모델도) | 예 | 예 | 아니요 | 아니요 | 아니요 | 아니요 | 아니요 | 예 |
| 하단 바에 비용 표시 | 예 | 예 | 아니요 | 예 | 예 | 예 | 아니요 | 크레딧 |
| 남은 플랜 사용량 | 예 | 예 | 예 | OpenCode Go만 | 아니요 | 아니요 | 예 | 아니요 |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ reads what each CLI writes down and no two of them write down the same things.
| What you get | Claude Code | Codex | Antigravity | opencode | oh-my-pi | Crush | Cursor CLI | Kiro CLI |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Context window in the footer | yes | yes | no | no | no | no | no | yes |
| How much of the last turn came from the prompt cache, in the footer | yes | yes | no | in the cost tooltip | in the cost tooltip | no | no | no |
| The card's tooltip names the model running, and the one it opened on if it changed | yes | yes | no | no | no | no | no | yes |
| Cost in the footer | yes | yes | no | yes | yes | yes | no | credits |
| How much of your plan is left | yes | yes | yes | OpenCode Go only | no | no | yes | no |
Expand Down
1 change: 1 addition & 0 deletions README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ mesmas coisas.
| Recurso | Claude Code | Codex | Antigravity | opencode | oh-my-pi | Crush | Cursor CLI | Kiro CLI |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Janela de contexto no rodapé | sim | sim | não | não | não | não | não | sim |
| Quanto do último turno veio do cache de prompt, no rodapé | sim | sim | não | no tooltip de custo | no tooltip de custo | não | não | não |
| O tooltip do card mostra o modelo em uso, e com qual abriu se ele mudou | sim | sim | não | não | não | não | não | sim |
| Custo no rodapé | sim | sim | não | sim | sim | sim | não | créditos |
| Quanto ainda resta do seu plano | sim | sim | sim | só OpenCode Go | não | não | sim | não |
Expand Down
1 change: 1 addition & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
| Что вы получаете | Claude Code | Codex | Antigravity | opencode | oh-my-pi | Crush | Cursor CLI | Kiro CLI |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Окно контекста в нижней панели | да | да | нет | нет | нет | нет | нет | да |
| Какая доля последнего хода пришла из кэша промптов, в нижней панели | да | да | нет | в подсказке стоимости | в подсказке стоимости | нет | нет | нет |
| Подсказка карточки называет работающую модель и ту, на которой сессия открылась, если она сменилась | да | да | нет | нет | нет | нет | нет | да |
| Стоимость в нижней панели | да | да | нет | да | да | да | нет | кредиты |
| Сколько осталось от лимитов подписки | да | да | да | только OpenCode Go | нет | нет | да | нет |
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
| 功能 | Claude Code | Codex | Antigravity | opencode | oh-my-pi | Crush | Cursor CLI | Kiro CLI |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| 底栏显示上下文窗口 | 有 | 有 | 无 | 无 | 无 | 无 | 无 | 有 |
| 在底栏显示上一轮有多少来自提示缓存 | 有 | 有 | 无 | 在费用提示框中 | 在费用提示框中 | 无 | 无 | 无 |
| 卡片提示显示正在运行的模型,若与打开时不同也显示打开时的模型 | 有 | 有 | 无 | 无 | 无 | 无 | 无 | 有 |
| 底栏显示花费 | 有 | 有 | 无 | 有 | 有 | 有 | 无 | 按点数计 |
| 套餐剩余额度 | 有 | 有 | 有 | 仅 OpenCode Go | 无 | 无 | 有 | 无 |
Expand Down
31 changes: 23 additions & 8 deletions api/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,30 @@ type ResumeAvailableOptions struct {
// off, which is not a failure but the design above, and for a miss the next turn
// heals. Without it a session that cannot be priced and a session that is not
// being priced look identical, and an empty footer reads as zero spend.
//
// Cache is where the last turn's input came from, and is nil whenever the
// provider files no split of it: Crush, Kiro, Cursor and Antigravity, and a
// Claude conversation whose last event is a compaction, since the turn before it
// no longer describes the window. On oh-my-pi and opencode it rides beside the
// cost with no window, so the footer puts it in the cost tooltip.
type SessionUsage struct {
ID string `json:"id"`
Percent int `json:"percent"`
Tokens int `json:"tokens"`
Window int `json:"window"`
Model string `json:"model"`
Effort string `json:"effort"`
CostUSD *float64 `json:"costUsd,omitempty"`
CostMiss string `json:"costMiss,omitempty"`
ID string `json:"id"`
Percent int `json:"percent"`
Tokens int `json:"tokens"`
Window int `json:"window"`
Model string `json:"model"`
Effort string `json:"effort"`
CostUSD *float64 `json:"costUsd,omitempty"`
CostMiss string `json:"costMiss,omitempty"`
Cache *SessionCache `json:"cache,omitempty"`
}

// SessionCache splits one turn's input tokens three ways: Read came from the
// prompt cache, Write was written into it, and Uncached was neither.
type SessionCache struct {
Read int `json:"read"`
Write int `json:"write"`
Uncached int `json:"uncached"`
}

// TurnState is what the window missed about one live session while it was not
Expand Down
7 changes: 7 additions & 0 deletions api/testdata/contract.golden
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,7 @@ SessionUsage {
effort string
costUsd,omitempty *float64
costMiss,omitempty string
cache,omitempty *SessionCache
}

SessionsColored {
Expand Down Expand Up @@ -1890,6 +1891,12 @@ PluginProfileFailure {
error string
}

SessionCache {
read int
write int
uncached int
}

BrowserRect {
x int
y int
Expand Down
5 changes: 4 additions & 1 deletion docs/ceilings.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ work when nobody knows it and that the call site never shows. The mechanism and
mind, the footer is silently wrong in one direction or the other, with nothing on screen saying so. Their usage
event carries a zero window, which is what tells the footer to drop the ring (`contextReadout`,
`native/cmd/lich-native/footer_readout.go`). A zero from any of the three is an answer and not a gap: it is what
they record for a free model and for a subscription. Codex is the one provider lich prices itself, from a slice of
they record for a free model and for a subscription. The last turn's cache line (`api.SessionCache`) sits in the context
tooltip on Claude Code and Codex, and in the cost tooltip on oh-my-pi and opencode, which record the split but
no window. Crush folds its cache counts into the prompt and completion totals, Kiro files every token count as
zero, and Cursor and Antigravity have no usage reader, so the four show none. Codex is the one provider lich prices itself, from a slice of
rates baked into the release (`internal/pricing/prices.json`), so an offline Codex session shows a cost at the
rate that shipped — **stale by however long it has been since the release**, until the refresh
(`internal/pricing/pricing.go`) lands and overrides it. What is left with no price at all is a model newer than
Expand Down
4 changes: 3 additions & 1 deletion docs/providers/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ two-cards-one-conversation warning arrives at once.
## Transcript & cost

The full readout: per-turn token counts, and the window its mod reports, else one named from the model
(`windowForModel`). The running model the card's tooltip names is the assistant's `message.model`.
(`windowForModel`). The running model the card's tooltip names is the assistant's `message.model`. The cache line
under the window splits the same line's input into `cache_read_input_tokens`, `cache_creation_input_tokens` and
`input_tokens`, and goes away after a compaction until the next assistant line.

A conversation lich spawned is told from a plain `claude` run by the name lich spawns it under
(`relay.IsRosterName` on the first `custom-title`), which covers the conversations whose rows were deleted before
Expand Down
5 changes: 4 additions & 1 deletion docs/providers/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ cards before the two-cards-one-conversation warning arrives.
## Transcript & cost

The full readout: the window is 95% of the rollout's default or configured `model_context_window`, and the
tokens a running `total_token_usage`. The running model the card's tooltip names is `turn_context.model`.
tokens a running `total_token_usage`. The running model the card's tooltip names is `turn_context.model`. The
cache line under the window reads `last_token_usage`: `cached_input_tokens` and `cache_write_input_tokens` are
both part of `input_tokens` (Codex fills them from the Responses API's `input_tokens_details`), and the cost is
priced from the same split.

Codex is the one provider lich prices itself. `internal/pricing/prices.json` bakes a fixed slice of OpenAI rates
beside the Claude ones, read from the same LiteLLM table the refresh (`internal/pricing/pricing.go`) reads.
Expand Down
4 changes: 3 additions & 1 deletion docs/providers/omp.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ two-cards-one-conversation warning arrives at the switch.
## Transcript & cost

Cost only: a USD total on every assistant turn, and no line carries a context window, so the footer shows the
figure and no ring. The figure is omp's own arithmetic, never re-priced here: the sum of its assistant turns, the
figure and no ring. The cost tooltip also carries the newest assistant turn's cache line, from its
`usage.input`, `cacheRead` and `cacheWrite`: omp takes the cache out of `input` before writing it
(`calculateOpenAIUsageAccounting`), so the three add up to the turn's input. The figure is omp's own arithmetic, never re-priced here: the sum of its assistant turns, the
same walk its own status line makes, so a `task` sub-agent's spend is missing from lich's figure exactly as it is
from omp's. A turn written with no total at all withholds the session's whole number and borrows the
`unpriced-model` marker, whose tooltip then blames a network that would not have helped. That branch has never
Expand Down
5 changes: 4 additions & 1 deletion docs/providers/opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ the card's row keeps the old id and a restart resumes the conversation the card

Cost only: `session.cost` per conversation, with no window recorded beside it, so the footer shows the figure and
no ring. opencode files each sub-agent as a session of its own and does *not* roll it into the parent, so the read
walks the `parent_id` chain. That is measured behaviour of opencode's schema, not a promise it made.
walks the `parent_id` chain. That is measured behaviour of opencode's schema, not a promise it made. The cost
tooltip also carries the newest finished assistant message's cache line, from `tokens.input`, `tokens.cache.read`
and `tokens.cache.write` on that `message` row, whose input already leaves the cache out. A sub-agent's turns are
its own session's and are not read.

## Plugin install

Expand Down
9 changes: 8 additions & 1 deletion internal/browser/chrome.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ const domSettle = 1500 * time.Millisecond
// the profile a promotion reopens, short enough that a hung one is killed.
const closeWait = 5 * time.Second

// launchWait bounds how long a starting Chrome may take to print its DevTools
// URL. chromedp's default of 20s is shorter than a cold start: on a GitHub
// Ubuntu runner the first launch of the suite passed 20s, the second took 6s
// and the third 1s, each before Chrome's own sandbox check, so the time goes to
// loading the browser off a cold disk, which a slow machine pays too.
const launchWait = 60 * time.Second

// chromeLaunch starts a Chromium-family browser via CDP. ExecPath is
// chromium.FindBrowser (system Chrome/Edge/… — never lich-shell); UserDataDir
// is this ephemeral dir, never lich/chromium-profile. Attaching to the UI
Expand All @@ -38,7 +45,7 @@ func chromeLaunch(dir string, headed bool) (Page, error) {
return nil, err
}
opts := append([]chromedp.ExecAllocatorOption{}, chromedp.DefaultExecAllocatorOptions[:]...)
opts = append(opts, chromedp.ExecPath(exe), chromedp.UserDataDir(dir))
opts = append(opts, chromedp.ExecPath(exe), chromedp.UserDataDir(dir), chromedp.WSURLReadTimeout(launchWait))
if headed {
opts = append(opts, chromedp.Flag("headless", false))
}
Expand Down
4 changes: 4 additions & 0 deletions internal/terminal/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func (s *Service) usageFrom(id string, src usageSource) (usageEvent, bool) {
Window: u.window,
Model: u.model,
Effort: u.effort,
Cache: u.cache,
}
if !supported {
event.Cache = lastTurnCache(src)
}
cost, miss, priced := s.sessionCost(id, src)
switch {
Expand Down
Loading
Loading