Skip to content

feat: Insights, session-budget pacing, Fleet (cross-machine), and Activity tabs#33

Open
pehqge wants to merge 18 commits into
Rfluid:mainfrom
pehqge:integration
Open

feat: Insights, session-budget pacing, Fleet (cross-machine), and Activity tabs#33
pehqge wants to merge 18 commits into
Rfluid:mainfrom
pehqge:integration

Conversation

@pehqge

@pehqge pehqge commented Jun 6, 2026

Copy link
Copy Markdown

Summary

This PR adds four opt-in features to the modal — Insights, session-budget pacing, Fleet (cross-machine usage sync), and Activity (live process monitor) — plus a hover tooltip on the Tokens-per-day chart. Every feature is off by default and gated behind its own [config] section, so existing users see no change until they opt in. All logic lives in aura-core (headless, unit-tested); only rendering lives in aura. macOS + Linux are first-class; nothing platform-specific was added beyond the existing keychain/quota seams.

Note

Happy to split this into separate PRs (one per feature) if you'd prefer — they were developed independently and only share additive edits to config.rs / app.rs. See Testing & caveats at the bottom.


Insights tab

A new Insights tab ([insights] enabled, period-aware: All / 7d / 30d) that answers the "interesting" questions raw token totals don't.

What it shows

  • Top projects by token spend — with clean names (see below).
  • Top sessions by tokens, each tagged with its model tier (opus/sonnet/haiku) and an ultracode chip when detected.
  • Ultracode ROI — average tokens per ultracode session vs normal, with a Nx heavier multiplier.
  • Cache efficiencyread / (read+write) hit ratio ("X% of context served from cache").

How it works

  • Aggregation rides the existing single JSONL scan pass (reader/scan.rs) — no second scan, no extra I/O. New pure functions in reader/insights.rs (top_projects, top_sessions, ultracode_roi, cache_efficiency) feed a serializable InsightsSnapshot.
  • Clean project names come from the real cwd field Claude Code writes on every JSONL entry (basename(cwd)), not the lossy on-disk slug (-Users-pedro-Downloads-…), which is unrecoverable.
  • ultracode is a heuristic (the session JSONL contains a Workflow tool_use or the literal ultracode); model tier is exact. The tab footnotes this.

Forecast — session-budget pacing

Extends the existing Forecast tab ([pacing] enabled, Claude Code only) with a session-budget gauge: how much of the current 5h window you can spend without exhausting the weekly window before it resets.

How it works

  • Paces in token caps derived from the API-reported used_percentage, never a guessed cap: cap = tokens_in_window / (used_pct/100), computed for both the 5h and 7d windows from their real boundaries (resets_at − length_minutes).
  • Buckets history into actual 5h rate-limit windows (not raw JSONL session files — there are only ~5 windows/day) and learns the user's typical active windows/day (trimmed mean over active days, sessions ≥ active_session_min_tokens).
  • recommended_pct = (weekly_remaining_tokens / windows_left) / session_cap × 100. Guards: used_pct < 3% → "warming up" instead of a fabricated number; sessions_left floored to avoid divide-by-~0.
  • This replaces an earlier naive formula whose anchor algebraically cancelled the learned rate and just echoed "weekly remaining %".

Fleet — cross-machine usage sync

A new Fleet tab ([fleet] enabled, opt-in) that compares usage across every machine on the same Claude subscription — the 5h/weekly limits are account-wide and shared, so this shows each machine's share.

How it works

  • Transport: the free public ntfy.sh pub/sub broker (configurable broker_url for self-hosting). No account, no server to run.
  • End-to-end encrypted — the broker is untrusted. Each heartbeat is sealed with XChaCha20-Poly1305 under a key derived HKDF-SHA256(secret, "aura-fleet-v1"); the topic is "aura-" + base32(HMAC-SHA256(secret, "ntfy-topic"))[..16] (an 80-bit capability). The broker only ever sees ciphertext + an opaque topic.
  • Pairing: the OAuth token is opaque and per-machine, so there's no zero-config "same account" fingerprint. Pairing is a one-time code (generate on A, paste on B); the 256-bit secret lives in the OS keychain (dedicated aura-fleet-secret service — never touches Claude Code-credentials), never in config.toml.
  • Process-level sync: the FleetSync thread is owned by a FleetManager in the always-running tray loop (main.rs), not by the modal view — so it publishes/receives 24/7 with the modal closed, then stops cleanly when disabled or unpaired (reconcile() on startup / pair-leave / config change). One sleeping std::thread; wakes every heartbeat_secs (default 45) to publish + poll. Per-machine share is computed from tokens (the account %s are identical on every machine).

Heads-up: MIN_HEARTBEAT_SECS = 20 because the loop makes 2 requests/cadence (publish + poll) and the public ntfy broker tolerates ~1 req/10s; a heartbeat published with X-Cache: no is never cached and thus never reaches a one-shot poll=1 subscriber, so caching is left on.


Activity — live Claude Code process monitor

A new Activity tab ([activity] enabled, opt-in) — a live monitor scoped to Claude Code only: which CLI session is eating the machine's CPU/RAM right now (useful when parallel agents freeze the box).

How it works

  • Uses sysinfo (default-features = false, system feature only — lean) to enumerate processes, classify Claude Code roots (claude CLI), and walk each root's subtree to capture what it spawned (MCP servers, cargo build, sub-agents — usually the real hogs).
  • Each session is labeled by project (the root process's cwd → basename) and active session (newest *.jsonl in ~/.claude/projects/<slug>), with total CPU% + RAM and its heaviest children.
  • Live, but cheap: re-samples every refresh_secs (default 3) only while the tab is on screen (mirrors the existing spinner-tick timer); zero cost off-screen. One long-lived sysinfo::System so CPU deltas are valid; first tick shows "measuring…".
  • Pure logic (classification, subtree, share math, session mapping) lives in aura-core/src/activity.rs and is unit-tested with synthetic process lists.

Tokens-per-day hover tooltips

The Tokens per day bar chart (Models tab) is now interactive.

How it works

  • Each bar gets an id + a hover highlight (accent brightened ~28%) and a .tooltip() showing the date, total tokens, and a per-model breakdown (top 3, compact figures).
  • The tooltip view fades + rises into place over ~160 ms (ease-out cubic, via GPUI's with_animation). No new dependencies.

Cross-platform

  • No new #[cfg] was added by these features beyond a dead-code guard. Secrets use macOS Keychain / Linux Secret Service (libsecret / GNOME Keyring / KWallet via keyring, pure-Rust crypto-rust) with a 0600 file fallback for headless boxes; Windows uses Credential Manager. security-framework is macOS-gated; keyring backends are per-target in Cargo.toml.
  • ntfy transport is ureq + rustls; the Fleet thread is plain std::thread; the process-level manager uses the same cx.spawn/background_executor tray loop that already runs on Linux.
  • Linux build note: the sync-secret-service keyring backend links the system dbus C lib (libdbus-sys), so a Linux build needs libdbus-1-dev present (same as this repo's existing ubuntu release CI).

Testing & caveats

  • cargo test -p aura-core → all green (232 tests incl. new pacing / insights / activity / fleet / build_heartbeat suites). cargo build -p aura clean.
  • Fleet was validated end-to-end on macOS: a standalone second-peer listener decrypted 27 consecutive heartbeats over the public ntfy broker with the aura modal never opened (continuous background publishing).
  • Not yet built/run on Linux by me (no Linux host available); the code is platform-neutral and the platform seams are unchanged, but a real Linux build + a mac↔linux pairing test are still pending. Flagging honestly.
  • Known minor quirk: heartbeats alternate carrying pct (API quota source) vs tokens (local-fallback source); harmless, can be unified by always computing local token sums.

🤖 Generated with Claude Code

pehqge and others added 18 commits June 5, 2026 20:31
Extend the Forecast tab with a session-budget gauge that recommends how
much of the current 5h window a Max user may burn so their weekly window
lands at/under 100% at reset — paced against a *learned* active-session
pattern rather than the raw 5h renewal cadence.

All math is in percentage of the weekly window (QuotaWindow.used_percentage,
the API-reported utilization), so it never guesses an unknown token cap and
stays correct across plan tiers.

Core:
- new quota/pacing.rs: SessionBudget, PacingStatus, ActivityPattern,
  SessionTokens, session_budget(), learn_pattern(), collect_session_tokens().
  learn_pattern uses a trimmed mean of active-session counts over active days
  only; session_budget floors sessions_left so it never divides by ~0 and
  emits an Insufficient state (no number) on thin history / non-API source.
- QuotaSnapshot gains an additive Option<ActivityPattern> pacing_pattern field
  (skipped from the wire format when absent).
- [pacing] config section: enabled (default false), active_session_min_tokens
  (50_000), history_days (14), fully wired into config_schema + describe/wizard.
- lexicon: pacing_* strings in both personas.

UI:
- app.rs refresh learns the pattern (Claude Code + Api source + enabled) on the
  existing ProjectsWatcher cadence — no new timer/thread — and render_forecast
  appends render_session_budget() below the projected windows, reusing the
  forecast card's bar/badge primitives and theme tokens only.

F3-independent: base SessionStat has no per-session token total, so F2 does its
own minimal token-per-session JSONL pass instead of depending on the F3 branch.
See BUILD_NOTES.md for the merge-sequencing note. Tests are written, not run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…badges)

Light up the previously-dead SessionStat with a new opt-in Insights tab that
answers the "interesting" questions: which project burned the most tokens,
which single session was the most expensive (and what mode it ran in), and how
token spend splits across model tiers.

Core (aura-core):
- scan.rs: extend SessionStat (session_id, project_dir, total_tokens,
  dominant_model, is_ultracode) + ScanAccum.tokens_by_project, all accumulated
  in the existing single scan pass — no second scan, no extra file I/O. Ultracode
  is a cheap byte-substring check (ULTRACODE_MARKERS) over the same buffer used
  for JSON parsing. Subagent tokens fold into their parent project.
- reader/insights.rs (new): pure aggregations — top_projects, top_sessions,
  mode_distribution, ModelTier, plus serializable ProjectStat / SessionInsight /
  ModeDistribution / InsightsSnapshot. Wired onto UsageSnapshot so the existing
  All/7d/30d period plumbing is reused.
- config.rs: [insights] section (enabled=false, top_n=5); tab hidden unless on.
- config_schema.rs / cli: descriptors, get/set, commented template, describe.
- lexicon: tab_insights (polite "Insights", goblin "Receipts").

UI (aura):
- app.rs: Insights AgentSection variant + conditional tab + render_insights
  (top projects bars, top sessions with tier/ultracode badges, mode distribution
  bar + counts, heuristic footnote). Theme tokens only, no hardcoded colors.
- format.rs: compact_tokens (18.2M / 9.1K).

Ultracode detection is a documented heuristic (rustdoc + UI footnote). AllTime
insights cover the delta scan only (StatsCache has no per-project breakdown).

Tests written for: top-projects ranking + token sums, top-sessions ordering,
ultracode detection (Workflow tool_use / literal / plain), dominant_model,
mode_distribution percentages (sum to 100), per-project + subagent folding,
compact_tokens, and [insights] config round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the Fleet feature: machines on the same Claude account pair once with a
one-time code, then auto-sync encrypted usage heartbeats over the free public
ntfy.sh pub/sub broker. A new Fleet tab shows each machine's share of the 5h
and weekly rate-limit windows.

The ntfy broker is treated as untrusted. Defense in depth:
- Topic = "aura-" + base32(HMAC-SHA256(secret, "ntfy-topic"))[:16] — a
  high-entropy capability, not a guessable name.
- Every payload sealed with XChaCha20-Poly1305 (random 24-byte nonce per
  message) under an HKDF-SHA256(secret, "aura-fleet-v1") key; the broker sees
  only ciphertext.
- Forged/tampered messages fail AEAD auth and are ignored; replays older than
  2x the heartbeat are dropped.
- The 256-bit pairing secret lives only in the OS keychain (dedicated service
  "aura-fleet-secret", never the Claude Code entry), with a 0600 file fallback
  on headless Linux. Never in config.toml or logs.

Per-machine share is computed from token counts (the account window %s are
global), then attributed against the account window %. No Claude tokens or
message content ever leave the machine.

New module tree crates/aura-core/src/net/ (pairing, crypto, fleet, transport,
secret_store, plus FleetSync background loop). All pure logic unit-tested
(written, not run); transport tested via an in-memory mock; one #[ignore] live
ntfy round-trip. [fleet] config section (off by default — the sync task never
spawns when disabled). Fleet tab + pairing UI in app.rs. docs/fleet.md and
BUILD_NOTES.md added.

Crates added to aura-core: chacha20poly1305 0.10 (new), hkdf/hmac/sha2/base64/
rand/zeroize/uuid (already in the lock tree), keyring sync-secret-service on
Linux. Pure-Rust, cross-compilable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	BUILD_NOTES.md
#	FEATURE_SPEC.md
#	crates/aura-core/src/config.rs
#	crates/aura-core/src/config_schema.rs
#	crates/aura/src/cli/config.rs
# Conflicts:
#	crates/aura-core/src/config.rs
#	crates/aura-core/src/config_schema.rs
#	crates/aura/src/app.rs
The session-budget gauge was wrong in two ways and just echoed the
weekly-remaining %.

BUG 1 (algebraic cancellation): learn_pattern set
avg_weekly_pct_per_active_session = 100/(active_per_day*7); session_budget
then divided weekly_remaining/sessions_left by it, cancelling active_per_day
so recommended_pct collapsed to ~weekly_remaining. The geometric anchor was a
no-op — deleted.

BUG 2 (impossible unit): the pattern counted active JSONL session *files* per
day, reaching ~47/week, but the 5h rate-limit window only renews ~5x/day. The
unit is now the 5h WINDOW: history is bucketed onto the live reset grid
(window_index = floor((ts - session.resets_at)/5h)); multiple sessions in one
window collapse to one. A window is active when its summed tokens clear
active_session_min_tokens. active_windows_per_day is the trimmed mean over
active days (naturally <= ~5).

Token caps are now inverted from real window boundaries instead of guessed:
weekly_cap = tokens_in(weekly_window_start, now) / (weekly_used%/100), same
for the 5h window, via a new reader helper sum_tokens_in_range (message-level,
reuses RawEntry parsing, skips subagents + mtime-prunes). A MIN_PCT_FOR_CAP
(3%) / zero-token guard returns Insufficient ("warming up") rather than divide
by ~0. budget_tokens = weekly_remaining_tokens / windows_left; recommended_pct
= budget_tokens / session_cap * 100. Caps are computed at the app refresh site
where reader + quota are both in scope (new pacing::compute_caps + Caps) and
threaded to the pure, unit-tested session_budget.

Tests rewritten for active-window trimmed mean, cap inversion + guard,
windows-left floor, sum_tokens_in_range, and the screenshot scenario
(weekly 15% used, ~30 windows left) now yielding a modest recommendation
instead of ~85%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e cards

Insights tab rework:
- Resolve project names from the real per-entry `cwd` Claude Code writes
  (basename, e.g. `reconhecimento`) instead of the lossy on-disk slug;
  fall back to the trimmed slug when no `cwd` was logged. `cwd` is captured
  from the first entry seen per session/project during the existing scan pass.
- Top Sessions: drop the misleading wall-clock duration (included idle gaps);
  rows now show tokens · project · badges, still sorted by tokens.
- Remove the Mode Distribution card and `ModeDistribution` aggregation
  (favorite model already lives in the Summary tab).
- Add an Ultracode ROI card: ultracode vs normal session counts + avg tokens
  and a heavier-by-N× multiplier (guarded when there are no normal sessions).
- Add a Cache Efficiency card: read/write cache totals + hit ratio
  ("N% of context served from cache"), guarded when there's no activity.

Tests: cwd→basename resolution incl. slug fallback, ultracode ROI averages +
zero-normal guard, cache hit-ratio + zero guard. Fixtures now write `cwd`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an opt-in "Activity" tab — a live process monitor scoped to Claude
Code. It enumerates running processes, classifies the `claude` CLI roots
(name == `claude`, `.exe`-tolerant; excludes the Claude.app desktop app
and aura itself), walks each root's full descendant subtree (MCP servers,
bash/build commands, sub-agents), and sums CPU% + RSS per session. Each
session is labelled by its project (basename of the root's cwd) and the
active `*.jsonl` session id under `~/.claude/projects/<slug>/`, where the
slug replaces every `/` and `.` with `-`. Sorted by total CPU% desc, with
the heaviest 1-3 child processes surfaced as readable culprit rows.

Pure logic lives in `aura-core::activity` and is unit-tested headless
against a synthetic process list + an injected tempdir (16 tests). The
OS-touching `ActivityMonitor` owns a long-lived `sysinfo::System` so CPU%
deltas compute across ticks; it's gated behind the default-on `activity`
Cargo feature. sysinfo is pinned to 0.31 (reusing the version already in
Cargo.lock) with `default-features = false, features = ["system"]` to stay
lean.

The GUI ticks the monitor only while the modal is open and the tab is
active, mirroring the spinner-tick pattern (cx.spawn + background timer,
self-rescheduling, gated on a generation token + active tab) — zero
background cost otherwise. First sample shows "measuring…" until a CPU
baseline exists.

Config: new `[activity]` section (`enabled` default false, `refresh_secs`
default 3, clamped to >=1). Tab hidden unless enabled and the agent is
Claude Code. The tab ignores the period selector (it's live). Docs in
docs/activity.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…closed

Fleet only published heartbeats the UI pushed via push_fleet_heartbeat,
which fires on a modal-open / file-watch refresh. With the modal closed
nothing published, so peers never saw each other unless someone sat with
the modal open — unacceptable for a passive monitor on an always-alive
tray app.

The sync loop now owns a heartbeat_source closure and builds a fresh
heartbeat every cadence by fetching the local Claude quota itself, with
no dependency on the UI. A UI-pushed publish_local still takes priority
for an instant update but is no longer required for publishing. The
self-built heartbeat is mirrored into FleetState as the local "self" row
so the user's own row renders with the modal closed. The first heartbeat
still fires immediately on spawn for instant pairing feedback.

The publish-decision is factored into a pure next_outbound helper so the
priority/cadence logic is unit-testable without a thread or transport.

Raise MIN_HEARTBEAT_SECS 10 -> 20: the loop makes two broker requests
per cadence (publish + poll); the public ntfy.sh broker tolerates
~1 req/10s, so a 20s floor keeps even an aggressive config at ~2 req/20s
= 1 req/10s and avoids HTTP 429.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FleetSync was owned by AuraView, which only exists while the modal
window is open — so Fleet stopped publishing/polling the moment the
modal closed, defeating the point of a passive cross-machine monitor.

Move Fleet ownership to a process-level FleetManager held in the
always-running tray spawn loop in main.rs (alongside the keepalive
window), so the single sleeping sync thread runs for the whole process
lifetime and is never dropped between modal opens.

- aura-core/net: add `build_heartbeat`, the single source of truth for
  turning a QuotaSnapshot into a Heartbeat (shared session/weekly needle
  logic), fixing the inconsistency where some heartbeats carried pct and
  others tokens. Unit-tested for API + local-fallback labels and the
  no-window default.
- runtime.rs: add shared Fleet handles — `fleet_state()` /
  `set_fleet_state` / `clear_fleet_state` (the modal reads peers from
  this) and a dirty signal (`mark_fleet_dirty` / `take_fleet_dirty`) the
  modal's Pair/Leave actions raise so the manager reconciles without a
  tray Show.
- main.rs: FleetManager::reconcile (re)starts/stops the sync to match
  config (enabled && paired); driven at startup, on each dirty tick, and
  on every tray Show after the config reload.
- app.rs: AuraView no longer owns Fleet — drop fleet_sync, the spawn/push
  helpers, and window_metrics; render_fleet reads peers from
  runtime::fleet_state(); Pair/Leave mutate the secret then mark dirty.

No platform-specific code added in the refactor. Silences the macOS/
Windows dead-code warning on secret_store::fallback_path (Linux/other
only) so a -D warnings build is clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Rfluid Rfluid self-requested a review June 6, 2026 17:36

@Rfluid Rfluid left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CI checks are failing. You can run scripts/pre-pr.sh locally to validate the changes before pushing. One caveat is that platform validation is not included in this script and must be verified separately.

I also think this PR could be split into smaller, more atomic PRs to make the review process easier. If you'd like to speed up the review and merge process, I'd recommend doing that.

@Rfluid Rfluid left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, please rebase your branch onto main and resolve any merge conflicts.

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.

2 participants