Kimi Code tokens live ~15 min and only the CLI renews them, so the load
state sits in .terminalFailure as its dominant steady state between CLI
uses. The Plan tab and tab-strip chip flapped to a reconnect screen every
cycle even with a good last snapshot on hand.
Extract the display decision into a pure KimiQuotaPresentation helper:
terminal-with-data shows the usage bars (flagged idle) and only the
no-data case falls through to reconnect. loadedBody stamps an 'as of
<time>' caption once a snapshot is older than 10 min, in every state that
renders it. The chip's kimiQuotaSummary downgrades terminal-with-data to
.stale so the bar and popover keep the last-known rows instead of the
reconnect card. Settings' connect/disconnect pane still explains the
terminal reason.
Adds KimiQuotaPresentationTests.
- Catch errors in 'sync setup' action: AuthError/DiscoveryError (login
timeout, port exhaustion, discovery failures) now print a clean one-line
message and exit 1 instead of crashing Node with an unhandled rejection
stack trace. Also guard the double rejection when all callback ports are
in use (both 'ready' and the callback promise reject).
- Windows: open the auth URL via 'rundll32 url.dll,FileProtocolHandler'
instead of 'cmd /c start'. cmd.exe splits its command line on '&', which
truncated the OAuth query string at the first parameter, so the IdP
received a bare /authorize request ('no parameters'). rundll32 receives
the URL as a plain process argument with no shell parsing.
- Theme the OAuth callback landing pages to match the dashboard (warm
paper background, ink text, forest-green success / terracotta failure),
fully inline with no network or asset dependencies.
AI-Origin: human
Extract StatusItemContextMenuPolicy (event mask, debounce, presentation
mode) and unit-test it so rightMouseUp + statusItemMenu cannot regress
to the flash/scroll-jump paths without a failing test.
Present the context menu on rightMouseUp (not down) so the matching
mouse-up no longer dismisses it, and open via statusItem.menu +
performClick so AppKit tracks the menu under the status item instead of
manual popUp (which scrolled the Today row away on mouse move).
Clears statusItem.menu in menuDidClose so left-click still opens the
popover. Debounce + legacy rightMouseUp path retained for macOS <= 26.
Closes#802
The desktop sessions resolver returned a single per-platform path, so
Microsoft Store (MSIX) installs of Claude Desktop were invisible: their
data lives under %LOCALAPPDATA%\Packages\<Claude package>\LocalCache\Roaming\Claude\local-agent-mode-sessions
and a filesystem junction workaround breaks Cowork's own file access
(reported and verified in #611).
getDesktopSessionsDir() becomes getDesktopSessionsDirs(): an ordered,
deduped candidate list (override, then classic APPDATA, then MSIX packages
matching Claude_* or *.Claude_*, existence-checked, lexicographically
sorted; .config on Linux). Results are memoized per env-input tuple so the
parser's per-file classification never rescans Packages. All call sites
scan every candidate; macOS, Linux and classic Windows behavior unchanged.
Fixes#611
Read ~/.kimi-code credentials directly and poll api.kimi.com
/coding/v1/usages for the weekly quota and rate-limit windows,
mirroring the existing Codex/Claude quota services.
- New KimiSubscriptionService with lenient decoding (string/number
values, enum-style time units, derived used from remaining) and
429 backoff
- Quota chip on the Kimi Code tab, Plan insight, and a Kimi tab in
Settings with connect/disconnect
- Independent refresh cadence anchor so Kimi-only setups don't poll
on every payload tick
- Expired tokens surface as terminal with automatic recovery once
the CLI refreshes the credential file
- Keep insights visible for quota-capable providers on empty days
- Widen Settings so six tabs don't collapse into the overflow menu
- Resolve k3/k3-agent/k2d6-agent model aliases to canonical Kimi names
- Discover sessions across all Kimi Code homes (CLI + desktop runtime)
- Accept conv-*/ctitle-* session directory naming, not just session_*
- Add Kimi Code provider tab with brand color to the menubar
- Show short model names (Kimi K3, Kimi K2.6) in the menubar payload
* sessions: fold subagent runs into PR attribution
Sidechain (subagent) session cost never reached the by-PR view, so a
session orchestrated on one model with subagent lanes on another showed
only the parent model on every PR row. Fold each sidechain's cost, calls,
models, and categories into the parent turn that spawned it, so it
inherits that turn's PR set under the existing turn-level state machine.
Linkage, in priority order: the spawn result's toolUseResult.agentId
pairs the child's agent id with the Agent/Task tool_use id that launched
it (recorded per turn), which is the true launch point and wins even when
the child's first activity landed during a later turn; else the child's
first-activity timestamp is bucketed into the containing turn span; else
the child folds into the parent's unattributed spend. Children of parents
that referenced no PR, and orphans whose parent is absent from the scan,
contribute nothing, unchanged.
Cache v6 to v7 (neither shipped, so one combined bump from v5): per-turn
spawnToolUseIds, per-file parentSessionId and agentSpawnLinks; the
validator and the append/compact paths thread them like prRefs. By-PR
footers now count parents plus folded subagent runs and the payload gains
an additive subagentSessions field. distinctCost now includes folded
subagent spend, documented in the payload comment.
* sessions: address adversarial review of subagent PR attribution
Rework child attribution to resolve each subagent to the PR its launching
turn was working on, using the parent's UNFILTERED turn data, and enforce
that every dollar is counted exactly once.
- Mutual exclusion: a child that referenced its own PR attributes
standalone and is never folded; a child with no links is folded only.
Fixes a double-charge where a self-linking child was both folded and
self-attributed.
- Recursion: a fold aggregates a child plus its non-self-linking
descendants (depth-first, cycle-guarded), so grandchildren spawned by
subagents reach the PR report.
- Global linkage: the subagent index keys by parentSessionId alone
(UUIDs are globally unique), so a child whose worktree resolves to a
different project still links.
- Date-range correctness: spawn-to-PR sets are built at assembly from the
full turn list, so a spawn in a pre-range turn attributes to the right
PR; a PR-linked parent whose own turns fall out of range is kept as a
0-cost fold anchor so its in-range child is not lost.
- Timestamp fallback compares epoch ms (mixed UTC offsets order right) and
is end-bounded: a child active after the parent's last turn is unlinked
(contributes nothing), matching orphan semantics.
- Cache adoption tries the newest prior versioned file (v6 then v5) so the
preceding build's expired-PR history survives the v7 bump; an invariant
note requires the list to cover every version that can exist on disk.
- Spawn-result pairing matches the tool_result block that carries the
agentId, not the first block, when a record batches several results.
- resolveSubagentAttribution is computed once and shared by aggregateByPr
and prLinkedTotals.
subagentSessions now counts folded subtrees (children plus descendants).
Verified on real data: attributed + unattributed reconciles to cost, and
parent-only cost plus folded-children cost equals the folded total to the
cent (no double-count).
* sessions: round-2 hardening of subagent PR attribution
Address a second adversarial review of the new machinery.
- ID collision: parents and a child's parent reference are keyed by
provider + sessionId, not bare sessionId. When two distinct parents
still share a key (true duplicate/imported data), the child folds into
NEITHER (deterministic skip, stays standalone): correctness over
coverage.
- Recursion dedup is global: one claimed-set spans all of a parent's
direct children, so a descendant reachable through two paths (a diamond
or duplicate id) folds exactly once and a parent-link cycle terminates.
- Cache adoption migrates every prior version oldest-to-newest and MERGES
per source path (newer wins per entry), so a sparse or partial newer
file no longer masks older-only expired-PR orphans.
- Fold anchors (0-cost PR-linked parents kept only for attribution) live
in a new ProjectSummary.subagentAnchors, never in `sessions`, so they no
longer contaminate session counts, averages, or any per-session report.
Folded PR rows take their date span from the contributing child activity
rather than the anchor's empty timestamps.
- One-pass buildPrAttribution computes rows and totals together; the
payload builder and CLI call it once. Drops the identity-keyed
memoization, which could return stale folds if the array was mutated.
- Ambiguous multi-block spawn-result pairing leaves the spawn link unset
on purpose; the child then folds via the timestamp fallback rather than
pairing with the wrong id or disappearing.
Every fix is mutation-verified. A fresh real-data drive re-proves the
no-double-count identity to the cent (parent-only cost plus folded cost
equals the folded total) and that the PR rows sum to attributedCost.
* sessions: round-3 hardening of subagent PR attribution
Third adversarial review pass.
- Ambiguity counts ALL candidate parents (and anchors) sharing a
provider+sessionId key, not just PR-bearing ones, and uses a
per-record fingerprint: a key carried by more than one DISTINCT record
folds its child/subtree into NEITHER (identical duplicates still fold
once). This unifies the parent-collision and duplicate-descendant rules
and is deterministic across input order.
- Project-rebuilding filters (by day, by date range, by config source)
now carry subagentAnchors through, and a date filter CONVERTS a spawn
parent whose in-range turns are all filtered out into an anchor so a
surviving in-range child still folds. Rebuilt sessions also keep their
PR + subagent-linkage metadata (prLinks, parentSessionId, spawnPrSets,
...), which buildSessionSummary otherwise drops, so by-PR and folding
work on a filtered slice (menubar/dashboard flow).
- Fold anchors leave ProjectSummary.sessions entirely and folded PR rows
take their span from the child, so 0-cost anchors never touch session
counts or averages.
- Ambiguous spawn pairing (parent named the agent but its exact launching
tool_use could not be paired) is recorded per parent; a late child of
such a pairing folds to the parent's last turn within a 30 minute grace
window, else stays unlinked. A truly-absent pairing gets no grace.
- Row session key is NUL-delimited and provider-prefixed, so a project
name or session id containing a space no longer collides and
undercounts distinct sessions.
Every fix mutation-verified. A fresh real-data drive re-proves the
no-double-count identity to the cent, and a day-filtered drive proves the
filter fix end to end (anchors created, subagents fold, identity holds).
* sessions: round-4 hardening of subagent PR attribution
Fourth adversarial review pass.
- sessionFingerprint now covers the COMPLETE linkage-relevant payload,
not just headline stats: a canonical (sorted-key) serialization of
agentSpawnLinks, spawnPrSets, prRefsAtRangeStart, ambiguousSpawnAgentIds,
parent/agent identity, and the per-turn prRefs timeline. Two records
that share an id and headline stats but map the child to different
spawns/PRs now fingerprint DISTINCT, so the ambiguity rule fires and
they fold into neither, deterministically rather than order-dependent
first-wins.
- A date/day filter recomputes prRefsAtRangeStart at the new slice
boundary by replaying the original full turn sequence, instead of
copying the wide range's value. A PR switch between the wide start and
the slice start (July 1 A, July 10 B, slice July 20) now carries B, not
a stale A; a turn exactly on the boundary stays in-slice and applies its
own refs. The recompute selects by timestamp, so it is order-independent.
Non-contiguous day selections are documented as treated contiguous from
the earliest selected day (a single session-level seed cannot represent
multiple segments; the menubar selection is a single day or a run).
- The anchor-carry path drops an anchor that duplicates a surviving
session id, so malformed merged input cannot double-count.
Also: rebuilt filtered sessions were losing their PR/subagent-linkage
metadata (a child its parentSessionId, a parent its prLinks), which
carryLinkageFields now restores, so by-PR and folding work on any filtered
slice.
Every fix mutation-verified. A fresh real-data drive re-proves the
no-double-count identity and reconciliation to the cent for both a
lifetime scan and a day-filtered slice (anchors created, subagents fold
through the filter).
* sessions: round-5 hardening of subagent PR attribution
Fifth adversarial review pass; closed-form fixes.
- sessionFingerprint serializes the COMPLETE fold-determining state via a
real recursive canonical encoder: session-level linkage AND, per turn in
sequence, timestamp, prRefs, cost, calls, savings, and per-model cost.
Object keys are sorted recursively and set-semantic arrays (PR-ref lists,
ambiguous ids, spawnPrSets values) are sorted, while the turn list keeps
order; the structure is emitted through JSON.stringify (no delimiter
concatenation). Two same-id parents that differ only in a turn timestamp
now fingerprint DISTINCT (fold neither), and records differing only in
set-array order fingerprint EQUAL (no false ambiguity).
- recomputeRangeStartPrRefs breaks an exact-same-millisecond tie
deterministically by the lexicographically-last sorted-ref key, so the
recomputed seed is stable regardless of turn order.
- A day filter seeds EACH selected day's first ref-less turn by replaying
the original full turn sequence up to that day's start (per-day seeding),
so a PR switch on an UNSELECTED day between two selected days carries to
the later day. Contiguous and non-contiguous selections are both correct.
- The anchor dedupe drops an anchor only when a surviving session shares
the full provider-aware, fingerprint-qualified identity (a proven
duplicate): a different-provider or different-record same-id session no
longer wrongly drops the anchor.
Also fixed a double-count the fingerprint test exposed: two duplicate
parent sessions share a key and the SAME resolved children, so folding is
now done once per parent key.
Every fix mutation-verified. Fresh real-data drives (lifetime, single-day,
and a NON-CONTIGUOUS day selection) re-prove no-double-count and
reconciliation to the cent.
---------
Co-authored-by: reviewer <review@local>
* sessions: attribute PR spend per turn instead of per session
The by-PR surfaces attributed a session's full cost to every PR it
referenced, so one orchestration session that touched many PRs rendered
identical full-session rows. Capture per-turn PR references during
transcript parsing and attribute each turn's cost to the PR set active at
that turn (split evenly across a multi-PR merge-sweep turn), carrying the
most recent PR set forward across turns that reference nothing. Turns
before the first reference form an unattributed bucket.
A session whose transcript expired before per-turn capture keeps its
session-level prLinks but has no per-turn refs; it falls back to an even
whole-session split, and any row carrying such a portion is flagged
approximate.
Rows are now summable: the CLI and app footers report the attributed sum
plus the unattributed remainder instead of a distinct-session total.
Bumps the session cache version so surviving transcripts re-parse and
populate the new per-turn field. Daily-cache versioning is untouched.
* sessions: harden PR attribution and add models + category breakdown
Addresses the review findings on the per-turn PR attribution and adds the
model and task-category surfaces.
Correctness:
- Cache migration: the 5 -> 6 session-cache bump now adopts the prior v5
file's expired-source PR entries instead of abandoning them, and the claude
scan preserves and surfaces PR-bearing orphans, so a session whose transcript
was deleted still appears as a legacy even-split instead of vanishing. The
daily cache is untouched.
- Date-range carry: the parser captures the PR set active at the start of the
in-range turn slice and seeds the state machine with it, so a PR referenced
before the window still owns its later, in-range, ref-less turns instead of
the session falling back to a whole-session approx split.
- Calls are split across a multi-PR turn by largest-remainder, keeping per-PR
counts whole (a 1-call, 2-PR turn no longer renders as 2 calls).
- The CLI and app footers reconcile to the rounded row values actually shown.
- Distinct sessions are keyed by project + sessionId, not sessionId alone.
- The app tolerates an older by-reference payload (no attributedCost): it keeps
the old non-summable footer and never renders NaN.
Features:
- Each PR contribution records the models of its calls and the turn's task
category (split by the same share on a multi-PR turn); legacy even-split rows
carry the model union but no category breakdown.
- Payload rows gain models (short names, cost-desc) and categories (label + cost,
cost-desc, omitted when empty); the payload gains otherPrCount/otherPrCost for
the PRs beyond the sent top 20.
- CLI --by-pr gains a Models column. The desktop table is now full-width with a
Models column and click-to-expand rows showing a per-category cost breakdown
with proportional bars, keyboard accessible, with an "Other (N more PRs)" row
when capped.
Tests: state-machine seed/models/categories/largest-remainder/dedup, the prRefs
round-trip through the real incremental-append path (continuation + straddle),
v5 adoption of an expired PR session, payload round-trip, and the desktop
expansion/models/old-payload cases.
* sessions: reconcile mixed PR rows, harden v5 adoption, bound models
Round-2 review follow-ups.
- Mixed legacy/live category breakdown: a PR row that combines an expired
legacy contribution (even-split, no turn data) with a live per-turn
contribution now emits an explicit "Legacy estimate (no per-turn detail)"
category carrying the legacy share, so the expansion reconciles to the row
cost instead of silently dropping it. A legacy-only row still shows no
breakdown.
- v5 adoption is now per cached file: one malformed entry is skipped instead
of rejecting the whole v5 cache and dropping every valid expired PR session.
- The desktop "Other (N more PRs)" line moved to the table footer as a muted,
separated summary rather than a sorted row.
- Row expansion resets when the PR set changes (period/provider/data), so a
stale expansion cannot linger on a row that is gone.
- Payload models per row are capped to the top 4 by attributed cost, with a
name-ascending tie-break; categories get the same stable tie-break.
Tests: mixed live+legacy reconciliation, legacy-only omission, model cap and
tie-break, corrupt-plus-valid v5 adoption, and the desktop expansion-reset.
* session-cache: validate optional agentType and failed during v5 adoption; app: reset PR expansion on period switch
A v5 entry that was valid except for a malformed optional field (agentType,
failed) passed per-file validation and flowed downstream as a non-string.
The expansion reset keyed only on the row URL set, so a period switch that
returned the same PRs kept a stale expansion open over changed numbers.
---------
Co-authored-by: reviewer <review@local>
Part 1 (src): expose two add-only, optional aggregations on the menubar
payload's `current`, computed on the unscoped all-provider path from the
surviving-session parse (carried history cannot contribute, as expected).
- current.pullRequests: aggregateByPr rows (top 20 by cost) plus the
multi-link-safe distinctCost/distinctSessions. Rows are by-reference, so
they are never summed.
- current.byBranch: new aggregateByBranch, per-branch spend (top 15 by cost)
that carries each session's last-seen git branch forward across its turns.
The cache stores a turn's branch only when it changes, so the parser now
resolves the branch at reconstruction (before the date slice) and records
SessionSummary.everHadBranch from the full transcript. That lets the report
keep a branch-bearing session's pre-branch spend in an explicit null row
even when the range clipped the anchor turn, while a provider that never
captures a branch still contributes nothing.
Part 2 (app): new "Pull requests" sidebar entry between Sessions and Spend
(shortcuts reflow to number 8 for Plans). It renders the pullRequests rows as
a refined table with the PR label linking out via openExternal, cost,
sessions, calls and an active-date span, plus a footnote stating the distinct
total and the by-reference attribution. A quiet explanatory line shows when no
PR links exist, never a fake table.
Tests: aggregateByBranch unit tests (carry-forward, null bucket, clipped
anchor, by-reference session counts) and the Pull requests component tests
(table, external link, footnote, both empty states). Nav-reflow assertions in
Sidebar and App tests updated.
Co-authored-by: reviewer <review@local>
* Resolve Fireworks-hosted models to friendly display names
Fireworks fleet models arrive as `accounts/fireworks/models/<slug>` and
were leaking the raw path/slug in reports and the dashboard (e.g.
`fireworks/mode…`, `glm-5p2`). getShortModelName's path fallback now takes
the last path segment and re-resolves it, so a known slug earns a friendly
name (GLM-5.2, Qwen 3.7 Plus, Kimi K2.7 Code, DeepSeek v4 Pro/Flash) while
an unmapped slug still falls through to the raw segment.
Display-only: getModelCosts prices off the full id, so dollar amounts are
unchanged. The By Model dashboard panel now renders through getShortModelName
too, so already-cached raw keys normalize without a cache rebuild.
* Merge By Model rows that resolve to the same display name
Once path-style ids normalize, a mixed-vintage cache can hold more than one
raw key for the same model (the full accounts/fireworks/models/<slug> path
from an older build and the bare slug/friendly name from a newer one). The
dashboard By Model panel aggregated by raw id, so those rendered as duplicate
rows once both mapped to the same display name.
Extract the aggregation into a pure, tested aggregateModelTotals() keyed by the
resolved display name so the rows merge into one. This also aligns the panel
with modelEfficiency, which is already keyed by getShortModelName.
The #782 titles threading added title to the sessions JSON row shape but
missed this emitter test's key list; caught by the first full-suite run
since the merge.
Co-authored-by: reviewer <review@local>
Compact one-row strip: correction rate with count, median time to first
edit, and the top reworked file, plus one coaching note derived locally
with the same thresholds and copy as the CLI's workflow-insights
buildCoachingNotes. Decodes two new optional payload blocks
(current.workflow and current.topReworkedFiles), so payloads from older
CLIs still parse. The section hides entirely when there is no signal and
individual stats never render as zero placeholders. It reads the store's
current payload, so it follows the selected agent tab automatically.
Adds unit tests for the duration formatter, note selection, model
derivation, and payload decoding.
Co-authored-by: reviewer <review@local>
Two Phase-1 desktop additions over the renderer's existing card grammar.
Overview workflow card: a panel below the daily chart with two stat tiles
(correction rate with count, median time to first edit) plus a top-rework
line and one coaching caption. The caption is derived locally with the CLI's
buildCoachingNotes thresholds (corrections, then file churn, then time to
first edit; first that fires). The card hides entirely when no real signal is
present, never renders zeros, and shows a "N% priced" chip only when pricing
coverage is a number below 1. Payload types gain optional workflow,
topReworkedFiles, and pricingCoverage fields so older payloads render as before.
Sessions view: the captured session title becomes the row headline with the id
demoted to the mono secondary line; untitled rows keep the project as the
headline (unchanged). Title is also searchable and added as an optional
SessionRow field so older CLIs render unchanged.
Co-authored-by: reviewer <review@local>
Same threading as prLinks: the cache-level title (last ai-title entry)
rides onto SessionSummary in both the Claude and generic provider
pipelines, and the sessions table gains a TITLE column (truncated at 38
chars, empty when a transcript never produced one, as subagent
transcripts typically do not). JSON rows carry it verbatim. Groundwork
for titles in the context browser, desktop Sessions view, and payload.
Co-authored-by: reviewer <review@local>
The desktop app's Life tab failed with 'invalid period': the renderer
and CLI both learned lifetime, but the electron IPC allowlist between
them did not. The menubar's period row also overflowed once Lifetime
joined it; labels compact to the desktop strip's forms (7D, 30D, 6M,
Life). Period selection is not persisted by raw value, so the label
change is safe. Follow-up worth doing: derive the IPC allowlist from
the renderer's period module so a new period cannot miss one layer of
three again.
Co-authored-by: reviewer <review@local>
The rich session capture (#758) stores every PR URL a transcript
references, but nothing displayed it. codeburn sessions --by-pr now
groups spend by pull request: cost, savings, sessions, calls, and the
date span per PR, sorted by cost, with a GitHub-shortened label and
json output. Attribution is by reference, so a session that mentions
several PRs counts fully toward each row; the footer reports the
distinct-session total so overlapping rows are never mistaken for a
grand total. prLinks now ride from the cached file onto SessionSummary
in both the Claude pipeline and the generic provider path.
Co-authored-by: reviewer <review@local>