Skip to content

Activity chart: metric selector + composite & cognitive-load scores - #307

Open
brendangooden wants to merge 8 commits into
melagiri:masterfrom
brendangooden:feat/activity-metric-selector-cognitive-load
Open

Activity chart: metric selector + composite & cognitive-load scores#307
brendangooden wants to merge 8 commits into
melagiri:masterfrom
brendangooden:feat/activity-metric-selector-cognitive-load

Conversation

@brendangooden

Copy link
Copy Markdown

What

Turns the dashboard Activity card from a fixed sessions/insights chart into a metric explorer, and adds two derived scores: a Composite activity score and a Cognitive load score.

Metric selector

A dropdown to plot any of: Composite score · Cognitive load · Cost · Tokens · Tool calls · Messages · Projects · Sessions, over 7d / 30d / 90d / All.

Composite score

Weighted blend — cost .30, tokens .25, tool calls .20, messages .15, projects .10 — on a fixed all-time reference, so a given day's score is stable when you change the range (only the y-axis zoom changes). Toggle between Weighted (single area) and Stacked (per-metric bars summing to the score).

Cognitive load (context-switching cost)

Volume metrics miss multitasking cost. This models it:

  • Each genuine human turn (type='user' with non-empty content — not automated tool-result messages) keeps its session "warm" for 15 minutes.
  • Load at each minute = (concurrent warm sessions) × (concurrent warm projects)² — projects compound, since juggling across repos is far more expensive than stacking sessions in one.
  • Daily load integrates that over active minutes. Idle/autonomous sessions emit no human turns, so they go cold and add nothing.
  • The tooltip surfaces peak concurrency, e.g. "Peak multitasking: 7 sessions across 3 projects."

How

Server — new GET /api/analytics/activity returns an all-time, gap-filled per-day series. Metric definitions match the CLI dashboard (tokens = input+output+cache_creation+cache_read). Cognitive load is computed from a single ordered pass over human-turn message timestamps joined to their project.

Dashboard — single-series metrics render as a gradient area, stacked composite as bars; y-axis zooms to the visible min/max; rich breakdown tooltip. Adds useActivity() hook, fetchActivity() client, ActivityDay type, and centralized per-metric colors. Reuses existing shadcn Card / recharts / theme for visual consistency.

Notes

  • No schema changes; reads existing sessions / messages tables.
  • Cognitive-load window (15 min) and the projects² weighting are easy to tune in computeCognitiveLoad.
  • Screenshots (weighted area, stacked bars, cognitive-load + tooltip) can be added to this description.

…vity chart

Replace the fixed sessions/insights Activity chart with a metric selector and
two new derived scores.

Server (`/api/analytics/activity`):
- New all-time, gap-filled per-day series (sessions, projects, messages,
  tool_calls, tokens, cost). Metric defs match the CLI dashboard
  (tokens = input+output+cache_creation+cache_read).
- Cognitive load per day: a human turn keeps a session "warm" for 15 min;
  load at each minute = concurrent warm sessions x concurrent warm projects^2
  (projects compound), integrated over active minutes. Human turns only
  (type='user' with non-empty content) so idle/autonomous sessions don't count.
  Also returns peak concurrent sessions/projects for the day.

Dashboard:
- Activity card now has a metric dropdown: Composite score, Cognitive load,
  Cost, Tokens, Tool calls, Messages, Projects, Sessions.
- Composite = weighted blend (cost .30, tokens .25, tool_calls .20,
  messages .15, projects .10) on a FIXED all-time reference, so a day's score
  is stable across range changes; Weighted (area) vs Stacked (bars) toggle.
- Single-series metrics render as a gradient area; stacked composite as bars.
- Y-axis zooms to the min/max shown. Tooltip shows the full per-day breakdown
  plus an interpretable "Peak multitasking: N sessions across M projects" line.
- New useActivity() hook, fetchActivity() client, ActivityDay type, and
  centralized per-metric colors.
Groups projects table rows into logical projects (name + normalized path,
collapsing path-hash/git-remote duplicates and folding worktree paths back
to their parent repo path), then computes a 60-day-inactivity state machine
(active/reactivated/dropped) per ISO week since the first-ever session.

Adds a shared mondayOfIsoWeek() helper to shared-aggregation.ts alongside
the existing parseIsoWeek/formatIsoWeek for the weekly bucketing.

No SQLite schema changes — reads existing projects and sessions tables only.
Additive types only (ProjectLifecycleWeek, ProjectLifecycleSummary,
ProjectsLifecycleResponse) — no changes to existing Session/Project types.
Combo chart (stacked cumulative Area for active/reactivated/dropped +
diverging Bar overlay for weekly started/dropped events) plus a sortable
status table, matching DashboardActivityChart's Card/Recharts conventions.
Registers /projects after /analytics in App.tsx routing/titles and in
Header.tsx's NAV_ITEMS (lands in the mobile 'More' sheet as the 5th item,
bottom tab bar unaffected).
A worktree's raw project row carries its own auto-generated folder name
(e.g. "keen-davinci-c306f0"), not its parent repo's name — path.basename(cwd)
in cli/src/parser/jsonl.ts. Grouping by name+path therefore could never fold
a worktree into its parent, contradicting the design intent.

Group by normalized path alone instead. For the logical project's display
name, prefer the most-active row whose name doesn't look machine-generated
(the .claude/worktrees/<name> basename, or the adjective-noun-hex sandbox
pattern used by tools like Lovable); fall back to the highest-session-count
name only if every row in the group looks auto-generated.

Verified against the real local DB: keen-davinci-c306f0 now correctly
folds into ubt-sales-app (84 sessions, up from 18) instead of appearing as
its own logical project. Total logical projects dropped from 80 to 67.
…ction

Replace the adjective-noun-hex name-pattern heuristic with a precise
path-containment check: prefer a raw row whose ORIGINAL path does not
contain a .claude/worktrees/ segment (the parent repo's own row, if one
was ever synced), falling back to highest-session-count only when every
row in the group is a worktree path. Simpler and more reliable than
fuzzy name matching, per team lead review.

Verified against the real local DB: ubt-sales-app still correctly absorbs
its keen-davinci-c306f0 worktree's sessions. Also confirmed ubt-tool-hub
has no non-worktree row at all in the real data (every session there was
recorded under one of 30+ randomly-generated worktree names) — added a
synthetic test for the optimistic-mclean-1ce467/ubt-tool-hub pair since
that exact scenario doesn't exist in the real DB to demo against.
…hart

feat(dashboard): Projects lifecycle chart + status table
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