5 Commits
Author SHA1 Message Date
wizzy-yangandwizzy-yang 764bbfdad5 sessions, gui, tui, cli: the Sessions list is as long as the count beside it, and drawn in pages (#1019)
The Sessions page's list and its session count came from two different
lengths: the count is over every session (statsFor's Overview), while
GET /api/sessions fell back to sessions.Limit (200) when the page asked for
no number — and the page never asks for one. So the page said "238 sessions"
over a list of 200, and each agent whose sessions were oldest lost them
whole: Codex had 121 sessions here and the list showed 7, OpenCode 33 showed
2, ZCode 20 showed 3.

  * sessions.All (5000) is what a listing that means every session asks for,
    and the endpoint reads it when no number is asked of it; a caller that
    asks for one still gets it. It lives in internal/sessions so the page, the
    TUI and the CLI ask for the same number.
  * sessions.Limit stays 200 and is not raised: traceRecentFiles sizes the
    window gateway attribution is judged over by it (TraceSessionVisible
    decides whether a session has a readable local transcript), so raising it
    would change which calls count as magpie's.
  * The page draws that list in pages: SESS_PAGE (200) rows, and "Show N
    more" appends the next. A filter click or a keystroke in Search draws the
    first page and no more, so the cost does not grow with the history. The
    data stays complete, so the count and the list still agree. A list served
    4000 sessions long rebuilt every row on every keystroke — 275–320 ms a
    keystroke on a fast Mac with very small rows, worse on Windows — and now
    draws 200.
  * The note under the list counts the list the filters kept, not every
    session on the computer: it says "showing {drawn} of {n}" while pages are
    left and "{n} session"/"{n} sessions" once the whole list is drawn. Before
    it counted the whole history beside the drawn rows, so a search matching
    one session read "the list holds all 2600, 1 drawn" — as if 2599 were
    behind a "Show more" that isn't there. It also no longer says "drawn",
    which is how the page is built, not what a reader sees.
  * The CLI read sessions.List(0) — 200 — and then cut to its own --limit, so
    --limit 500 could never show more than 200 and the "%d more with --limit"
    foot was counted off the truncated list. It reads the whole set now; only
    o.limit rows are printed, so that costs a parse, not output.
  * The TUI read List(0) beside a head that says how many sessions there are,
    and then List(sessions.Limit+1) — 201, still not the whole set. It reads
    sessions.All now.

What the wider list costs is unchanged: every session file is read either way
to sort by last activity. Measured here with 384 sessions and 84.5k calls:
cold List(200) 10.3 s, List(5000) 10.5 s. What it adds is per-row work while
List holds the lock — a Codex session's titles stat, a pi session's omp
look-up, 25× the rows of JSON — hundreds of milliseconds, not seconds.

Tests, each failing without its change:
  * TestSessionsListIsAsLongAsTheCount (internal/gui): 205 Claude Code
    sessions as Claude Code keeps them; all 205 with no number asked, 5 with
    ?limit=5, 201 with ?limit=201. Fails with the list holding 200.
  * TestSessionsListIsNotCutAtTheLimit (internal/tui): 210 sessions, of which
    the ten past sessions.Limit are all in the list. Fails at 201.
  * TestSessionsMoreCountsTheWholeSet (root): the default --limit 20 over 212
    sessions says "192 more with --limit", and --limit 205 prints 205 rows.
    Fails saying 180 (counted off the truncated list).
  * sessions-paging.test.cjs (Chromium and WebKit, en and zh): 2600 sessions;
    the first page is 200 rows with "Show 200 more", the button appends to
    600 with the rows above left in place, a keystroke in Search draws a page
    in under 250 ms and starts the pages again, a search matching one session
    reads "1 session" and never the 2600 on the computer, and the note follows
    the list the filters kept (200 of 1111 after that search). Fails on
    origin/main (200 rows are not a page there) and with the pages taken out;
    counting every session again, or writing "1 sessions", fails it too.

Verified: go vet; go build (windows); go test ./internal/sessions
./internal/tui ./internal/gui . — only what origin/main fails (TestCursor,
TestGatewayFixQuitsOnlyAMagpie, TestPurposeAssetMatchesUsage,
TestUpdateThatWontStart, TestPluginListSaysMiddleware,
TestPluginOnOffRmShortName); node --check app.js and i18n.js; the browser
suites above plus sessions-overview, sessions-today, sessions-usage-open,
sessions-toolbar, sessions-calendar-fill, usage-sessions-agents-fit,
i18n-keys, usage-ledger and click-scroll.

Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com>
2026-10-07 00:16:13 +08:00
yetone 1565bb9ce1 usage: the cache hit rate is of the whole prompt, what was written to the cache in it too (Chapin on Discord: with Claude Code started anew at every AmpCode tool round, writing its prompt to the cache again each time, the Usage page still showed a hit rate near 100%) — the Usage page's Overview divided cache_read by input + cache_read, as did the Sessions tab's totals, the TUI's Sessions page and magpie sessions' hitRate; input is what was neither read from the cache nor written to it (the gateway's Usage.prompt() is input + cache_read + cache_write, and the sessions readers subtract both, as alma's and cline's do), so a prompt mostly re-written showed as mostly read. Each now divides by input + cache_read + cache_write, as the tray panel's Usage tab and the Requests tab's strip and rows already did. number-units.test.cjs checks the Overview's rate on its fake ledger (2B read of 1B + 2B + 30M written: 66%, where the old formula said 67%; fails on the old code), TestHitRateCountsCacheWrites checks hitRate (20% of 10 + 20 + 70), and the TUI Sessions page's fixture, which has writes, now says 67% where it said 69%. GUI: number-units, panel-usage, usage-ledger-chart, usage-refresh and mobile-web pass on WebKit; go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 14:14:43 +08:00
yetone 6aa99ef1c0 usage: costs show in RMB at a live rate, when Settings picks it (#212 L1cardo: 添加费用用人名币结算的选项,最好是能根据实时汇率来计算) — the Usage page, the tray panel, the TUI and the CLI all price a call in USD, magpie's list prices being dollars; Settings now has a Currency row (after Usage shows) offering $ USD, kept by default, and ¥ CNY, which converts every one of those at display time only — usage.Totals.Cost and the JSONL records it comes from stay in USD always, and a vendor's own balance (already in its own currency, a relay's ¥) is never touched.
The rate is internal/fx: a free, no-key endpoint (open.er-api.com/v6/latest/USD), cached in memory and in a small file beside magpie's other caches (XDG_CACHE_HOME/magpie/fxrate.json) for 12h, refetched only once that's stale, and falling back to the last rate it ever had — or a fixed 7.2 — while offline; the currency row's tooltip names the rate and when it was learned. usage.FormatCost carries the $/¥ formatting (2 decimals above $1, 3 below, whole numbers above 100) that usage_cli.go and internal/tui/pages.go used to each have their own copy of; both now read Settings' currency once per command/redraw rather than per row. internal/gui/api.go exposes the rate on /api/settings always and on /api/state only when cny is actually chosen, so usd users' hot path never touches the network for a rate nothing shows with.

Tested with new internal/fx/fx_test.go (a fake server: fetches and caches, answers from memory then from the cache file with no further calls, refetches once stale, falls back to a stale cache or Fallback when offline, ignores a reply with no CNY rate), TestFXInState in internal/gui/fx_test.go, TestFmtCostShowsChosenCurrency in internal/tui/usage_test.go, and a new internal/gui/tests/currency.test.cjs in Chromium and WebKit, English and Chinese (the Usage page's $ by default and ¥12.34×7.2 once cny is picked, the currency row's labels/state/tooltip, a real wheel scroll deep into Settings left alone by picking a currency, the choice surviving a reload). go build -tags nogui, GOOS=linux and GOOS=windows builds, and go test -tags nogui -count=1 ./... all pass clean.

A fetch that failed is not tried again for 10 minutes (the stale or fixed rate shown at once meanwhile), so offline each state() does not wait up to 4s on the network: TestGetDoesNotRetryAFailedFetchAtOnce.
2026-09-29 19:41:29 +08:00
yetone 44cd8fac4c sessions: OpenCode's and Pi's sessions are listed too (貌似没有opencode和pi的session,能支持一下么) — Usage → Sessions, magpie sessions and the TUI read OpenCode's opencode.db (or $OPENCODE_DB) read-only, or its older storage/ JSON files when there is no database, under $XDG_DATA_HOME/opencode or ~/.local/share/opencode, a subagent's session counted under the one that started it; and Pi's JSONL sessions under $PI_CODING_AGENT_DIR or ~/.pi/agent, plus $PI_CODING_AGENT_SESSION_DIR or settings.json's sessionDir, a forked session not counting again what it copied from its parent. Each gets its title, folder, times, tokens per model with cache reads and writes, cost, and a resume command (opencode --session <id>, pi --session <id>); the folders note names only the agents that are there 2026-09-28 18:45:07 +08:00
yetone 3dabb56ac9 sessions: history by day in the CLI and the TUI — magpie sessions lists the latest Claude Code and Codex sessions (when, agent, folder, first prompt, tokens, cost; --limit, --json with the resume commands), and with --days N|today|all (stats alone is 7) prints the range's totals (tokens, list-price cost, cache read and hit rate, active time and on how many days), a line a day with its tokens, cost, cache read, active time and a bar, then the top models and folders; --model and --folder narrow either, matched exactly, by base name or by a unique part, an unknown one naming those there are. The TUI gets a sessions page after usage (5; the library is 6 now) with the ranges today, 7, 30, 90 days and all (←→, t w m A), M and f to pick a model or a folder from those the range has, x to clear them, the totals, and a chart of the days in block characters, a week a column when the days don't fit, of tokens or, with c, cost. Both sum sessions.StatsFor as GET /api/sessions/stats does, through Stats.Sum, which rolls the days up under a model and a folder as the app's view does — active time not kept by model, so none under one 2026-09-28 17:06:33 +08:00