Improve dashboard refresh coordination, scrolling, responsive layout, and data-density behavior, including the Windows-safe resize correction validated on the final head.
Issue #767 item 3 reports two different active-day counts on one dashboard
screen: the Daily Activity panel shows "of 37" (from a bounded live scan of
surviving session files) while the same period's headline can reflect more
days via the durable daily cache, which also counts days whose session files
have since expired. Each count is correct for what it measures - the panel
intentionally scans a fixed six-month window independent of the selected
period tab so scrolling always works, while the headline is scoped to the
active period tab.
Re-deriving the panel's count from the durable series was considered and
rejected: it would need a second buildDurablePeriod call scoped to six months
independent of the period tab, which is a behavior and perf change, not
polish. Kept the count unchanged and instead labelled the denominator -
dailyActivityFooter (src/dashboard.tsx) now renders "of N days scanned"
instead of a bare "of N", so the panel reads as "here's what the live scan
covered" rather than as a contradiction of the headline. Checked the
Optimize view's similar "Showing X-Y of Z" footer (line ~794): it counts
findings, not days, and isn't part of this ambiguity.
Also fixes an adjacent gap found while investigating: overview.ts's
non-interactive report already had a footnote for durable-cache carry-forward
("includes $X preserved from expired session logs") when carriedCostUSD > 0;
the interactive dashboard's Overview panel had no equivalent, so a headline
that included carried-forward cost had no explanation anywhere on screen.
Extracted the wording as carriedCostNote (format.ts) and surfaced
carriedCostUSD through DurableOverview so the TUI shows the same footnote.
This is separate from the active-day-count fix above - it explains cost
carry-forward, not day counts.
Fixesgetagentseal/codeburn#767 (item 3).
Review fixes on top of #805.
Discovery: getCachedCodexProject short-circuits again without opening the
file. The PR read the first line of every rollout to collect session_id,
which cost +129ms on an 827-file session directory with a warm cache.
Archived duplicates are now filtered by basename, which needs no file I/O.
Timing: on oversized mcp_tool_call_end lines a `duration_ms` key inside
invocation.arguments outranked the payload-level duration, inflating tool
wait. The depth-aware payload value wins; the naive scan stays as the
fallback for task_complete.
Layout: MIN_WIDE goes back to 90 so 90-129 column terminals keep the
two-column dashboard. The By Model panel drops the Tok/s column (and its
footnote) when the panel has fewer than 61 inner columns or when no model
recorded active timing, so narrow terminals do not truncate and non-Codex
users get no dead column.
Reuse one concrete six-month scan for interactive history and selected-period totals. This restores network-backed providers, removes concurrent refresh scans, and keeps non-interactive reports scoped to their requested period.
* feat(dashboard): add inline daily history paging
* refactor(dashboard): make daily activity directly scrollable
* fix(dashboard): keep the empty state for users with no history at all
Scrollable mode kept the dashboard shell up whenever full history exists,
but a truly-new user with zero data anywhere saw a zeroed skeleton instead
of the No usage data message. Extract the decision into showEmptyState so
the boundary is test-pinned: empty period with history renders the shell,
empty everything renders the empty state, and the loading pass never
flashes it.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* Fix mangled project paths in By Project and Top Sessions panels
shortProject() decoded Claude Code slugs by splitting on '-', which
broke directory names containing dashes ('foo-bar' became 'foo/bar').
Switch the dashboard to consume ProjectSummary.projectPath (the
canonical cwd already extracted by parser.ts) and rewrite shortProject
to operate on a real absolute path.
* shortProject: cache homedir, normalize Windows backslashes, fix stale test helper
---------
Co-authored-by: Abdallah Meghraoui <abdallah.meghraoui@outlook.com>
Turns whose only assistant tool is `Skill` collapse to category `general`
because `classifyByToolPattern` returns `'general'` and `refineByKeywords`
only operates on `coding`/`exploration`. In environments that lean on Claude
Code skills, the per-activity dashboard column flattens — every `/init`,
`/review`, `/security-review`, `/claude-api`, plus user-defined skills, all
land in `general` with no signal about which workflow ran.
Implements Option A from the issue:
- `ParsedApiCall.skills: string[]` populated in the Anthropic-path parser
via a new `extractSkillNames` helper that reads `input.skill || input.name`
from each `Skill` ToolUseBlock (mirrors `detectGhostSkills` extraction at
optimize.ts:765 so the two stay in sync).
- `ClassifiedTurn.subCategory?: string` set to the first skill name when the
resolved category is `general` AND any skill identifier was extracted.
Top-level category stays `general` — existing aggregations, exports, and
category-keyed code paths unchanged.
- `SessionSummary.skillBreakdown: Record<string, {turns,costUSD,editTurns,
oneShotTurns}>` populated in the same per-turn loop that builds
`categoryBreakdown`. Provider sessions (Codex/Cursor/etc.) keep `skills:
[]` — they don't expose the Skill tool surface today.
- Dashboard `ActivityBreakdown` renders top-N skill sub-rows beneath the
`general` row when present (indented `/skill-name`, dimmed). Other
categories render exactly as before; if no skills were invoked, the panel
is byte-identical to current output.
Existing 419 tests still pass. New `tests/classifier.test.ts` adds 8 cases:
single skill via `input.skill`, single via `input.name`, first-wins for
multi-skill turns, aggregation across multiple assistant calls in one turn,
no-name fallback (`subCategory` stays undefined), `Skill+Edit` promoting to
`coding` and dropping subCategory, non-Skill general turns, and a legacy
ParsedApiCall shape with `skills` field absent (forward-compat). Pre-fix
verification by stashing the source change reproduces 4/8 failures with the
exact "expected 'init', received undefined" diff; restoring → 8/8 pass.
Closes#203.
🤖 AI assistance disclosure: assistant-scaffolded by Claude (Opus 4.7);
author of record reviewed every line, ran the full vitest suite locally
(`npm test` → 32 files / 427 tests pass), `npx tsc --noEmit` clean, and
`npm run build` produces a clean ESM bundle.
- TopSessions: show '----------' placeholder when session.firstTimestamp
is empty (Copilot provider yields '' when timestamp is missing)
- DashboardContent: add comment explaining undefined days tri-state
- tests/dashboard.test.ts: cover top-5 selection (fewer than 5 sessions,
tied costs, descending sort) and avg/s returning '-' for zero sessions
Authored-by: AgentSeal <hello@agentseal.org>