diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cdc9e3..a16ebd7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,51 +2,117 @@
All notable changes to `@pi-vault/pi-usage` are documented here.
-## v0.5.1
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-- updated `@earendil-works/pi-coding-agent` to `^0.80.2`, `@earendil-works/pi-tui` to `^0.80.2`, `@types/node` to `^26.0.0`, and `vitest` to `^4.1.9`
+## [0.6.0] - 2026-07-05
-## v0.5.0
+### Added
-- added `usage.json` provider toggles so you can disable live providers you do not want queried
-- added dashboard insights for project, active skill, and MCP server breakdowns
-- grouped insight rows by category and cap each insight section with an overflow summary to keep the dashboard readable
-- improved offline session parsing to extract project names, active skills, MCP server names, and builtin tool usage more accurately
-- fixed dashboard readability around usage statistics spacing and grouped insight rendering
-- updated runtime requirements and tooling to Node `>=24.15.0`, refreshed dependencies, and aligned CI with Node 24 and pnpm 11.8.0
+- Tabbed overlay layout for the dashboard with three tabs: Usage Statistics, Current Usage, and Insights. The tab bar follows the same pattern as `@earendil-works/pi-extension-manager` for visual consistency.
+- Independent period selector for the Insights tab so the Insights period no longer follows the Usage Statistics period.
+- Per-tab contextual footer strings in `src/shared/constants.ts` so each tab shows only the keys that are relevant in that tab.
+- `DashboardTheme.inverse()` and `DashboardTheme.bg()` methods to support the new overlay rendering primitives.
+- New `src/tui/overlay-render.ts` module that owns `frame()`, `renderTabBar()`, and `pad()` utilities for drawing the overlay chrome.
-## v0.4.0
+### Changed
-- reorganized the extension around a dedicated `UsageCore`, with the package entrypoint reduced to a thin Pi adapter
-- split shared provider runtime behavior into reusable utilities for timeouts, JSON parsing, and percentage handling
-- broke the OpenCode Go provider into focused modules for dashboard scraping, SQLite reads, and quota window calculation
-- extracted dashboard formatting and table layout logic into smaller TUI modules
-- expanded automated coverage for orchestration, state projection, runtime utilities, dashboard formatting, and provider integrations
-- kept the public usage flow stable: `/usage`, `/usage:refresh`, exported events, and exported types remain intact
+- Refactored `src/tui/dashboard.ts` to render the new tabbed overlay layout. The `render()`, `handleInput()`, and `activeTab` state replace the previous stacked-section renderer.
+- Dashboard description in `package.json` updated to "Pi extension that tracks Pi usage across your sessions in one dashboard".
+- Updated `@biomejs/biome` to `^2.5.2`, `@earendil-works/pi-coding-agent` to `^0.80.3`, `@earendil-works/pi-tui` to `^0.80.3`, `@types/node` to `^26.1.0`, and `typebox` to `^1.3.3`.
+- Moved `typebox` from `peerDependencies` to `devDependencies` since it is only used for compile-time schema construction in this package.
-## v0.3.0
+### Removed
-- added StepFun live usage support
-- refreshed the README and provider setup guidance
-- refactored the source tree into the current `core`, `providers`, `shared`, and `tui` layout
-- updated dependencies, toolchain config, and Node requirements for the current runtime model
+- `v` keyboard shortcut for toggling the Insights panel — Insights is now its own tab, reached via `Tab` / `Shift-Tab`.
+- Legacy dashboard footer and border character constants from `src/shared/constants.ts` (replaced by per-tab footer strings and `overlay-render`).
+- Unused helper code that was only consumed by the legacy single-scroll renderer.
-## v0.2.0
+### Compatibility
-- added the `/usage:refresh` command for explicit live refreshes
-- added OpenRouter live usage and balance support
-- reworked the dashboard UI with Pi-themed styling, improved quota formatting, and current-provider tabs
-- improved MiniMax usage parsing and compatibility window handling
+- Node.js requirement unchanged: `>=24.15.0`.
+- Peer dependencies unchanged: `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`.
+- Public API unchanged: `/usage`, `/usage:refresh`, exported events, exported types all retain their previous surface.
+- `biome.json` schema URL updated to `https://biomejs.dev/schemas/2.5.2/schema.json` to match the new biome version.
-## v0.1.1
+## [0.5.1] - 2026-06-25
-- added the request/reply event API for consumers that need the latest usage snapshot
-- exported the event and type modules through `package.json`
-- added regression coverage for the event API surface
+### Changed
-## v0.1.0
+- Updated `@earendil-works/pi-coding-agent` to `^0.80.2`, `@earendil-works/pi-tui` to `^0.80.2`, `@types/node` to `^26.0.0`, and `vitest` to `^4.1.9`.
-- introduced the initial Pi usage dashboard with offline aggregation across local sessions
-- added live provider support for OpenAI/Codex, MiniMax, OpenCode Go, and Command Code
-- added the interactive TUI dashboard, provider registry, packaging metadata, release workflow, and test/lint/typecheck automation
-- published the first public package documentation and project license
+## [0.5.0] - 2026-06-22
+
+### Added
+
+- `usage.json` provider toggles so you can disable live providers you do not want queried.
+- Dashboard insights for project, active skill, and MCP server breakdowns.
+- Grouped insight rows by category, with an overflow summary per section to keep the dashboard readable.
+- Improved offline session parsing to extract project names, active skills, MCP server names, and builtin tool usage more accurately.
+
+### Fixed
+
+- Dashboard readability around usage statistics spacing and grouped insight rendering.
+
+### Compatibility
+
+- Runtime requirement updated to Node `>=24.15.0`.
+- CI aligned with Node 24 and pnpm 11.8.0.
+
+## [0.4.0] - 2026-06-14
+
+### Added
+
+- Expanded automated coverage for orchestration, state projection, runtime utilities, dashboard formatting, and provider integrations.
+
+### Changed
+
+- Reorganized the extension around a dedicated `UsageCore`, with the package entrypoint reduced to a thin Pi adapter.
+- Split shared provider runtime behavior into reusable utilities for timeouts, JSON parsing, and percentage handling.
+- Broke the OpenCode Go provider into focused modules for dashboard scraping, SQLite reads, and quota window calculation.
+- Extracted dashboard formatting and table layout logic into smaller TUI modules.
+
+### Compatibility
+
+- Public usage flow kept stable: `/usage`, `/usage:refresh`, exported events, and exported types remain intact.
+
+## [0.3.0] - 2026-06-08
+
+### Added
+
+- StepFun live usage support.
+
+### Changed
+
+- Refreshed the README and provider setup guidance.
+- Refactored the source tree into the current `core`, `providers`, `shared`, and `tui` layout.
+- Updated dependencies, toolchain config, and Node requirements for the current runtime model.
+
+## [0.2.0] - 2026-06-01
+
+### Added
+
+- The `/usage:refresh` command for explicit live refreshes.
+- OpenRouter live usage and balance support.
+
+### Changed
+
+- Reworked the dashboard UI with Pi-themed styling, improved quota formatting, and current-provider tabs.
+- Improved MiniMax usage parsing and compatibility window handling.
+
+## [0.1.1] - 2026-05-31
+
+### Added
+
+- The request/reply event API for consumers that need the latest usage snapshot.
+- Exported the event and type modules through `package.json`.
+- Regression coverage for the event API surface.
+
+## [0.1.0] - 2026-05-31
+
+### Added
+
+- Initial Pi usage dashboard with offline aggregation across local sessions.
+- Live provider support for OpenAI/Codex, MiniMax, OpenCode Go, and Command Code.
+- Interactive TUI dashboard, provider registry, packaging metadata, release workflow, and test/lint/typecheck automation.
+- First public package documentation and project license.
diff --git a/README.md b/README.md
index 22ab9a1..2a67fed 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,6 @@
Track Pi usage across your sessions in one dashboard. `@pi-vault/pi-usage` combines offline history with live provider snapshots so you can review costs, tokens, session activity, current quotas, and usage insights without leaving Pi.
-
-
## Install
```bash
@@ -21,49 +19,89 @@ Then reload Pi:
/reload
```
+## Quick Start
+
+Open the dashboard with cached data:
+
+```text
+/usage
+```
+
+Force a live refresh, rescan local history, and reopen:
+
+```text
+/usage:refresh
+```
+
## Commands
-- `/usage` opens the dashboard using cached live data when available. Use it for quick inspection.
-- `/usage:refresh` forces a live refresh, rescans local history, and then opens the dashboard.
+| Command | Purpose |
+| ----------------- | ---------------------------------------------------------------------------------------- |
+| `/usage` | Open the dashboard. Uses cached live data when available. Quick inspection. |
+| `/usage:refresh` | Force a live refresh, rescan local session history, then open the dashboard. |
+
+## Dashboard Tabs
+
+The dashboard renders as a tabbed overlay. Switch tabs with `Tab` / `Shift-Tab`.
+
+### Usage Statistics (default)
-## What the dashboard shows
+
-### Usage statistics
+Aggregates local Pi session history for the selected period.
-The top section aggregates local Pi session history for the selected period.
+- Period selector: `Today`, `This Week`, `Last Week`, `All Time`.
+- Aggregated provider/model table with expandable rows.
+- Total row for everything currently shown.
+- Per-row counts for sessions, messages, cost, total tokens, input, output, cache reads, and cache writes.
-- switch between `Today`, `This Week`, `Last Week`, and `All Time`
-- expand provider rows to inspect model-level usage
-- compare sessions, messages, cost, total tokens, input, output, cache reads, and cache writes
-- keep a running total row for everything currently shown
+### Current Usage
-### Current usage
+
-The lower section shows the supported providers. Configured providers can return live quota and balance data, while unconfigured ones may show `unavailable` or local fallback states.
+Shows supported live providers. Configured providers return live quota and balance data; unconfigured ones may show `unavailable` or a local fallback.
-- switch between `OpenAI/Codex`, `MiniMax`, `StepFun`, `OpenCode Go`, `Command Code`, and `OpenRouter`
-- view rolling-window quota bars like `5h` and weekly usage
-- see balance-style fields where the provider exposes them
-- get inline status for live, cached, stale, local, or unavailable data
+- Provider selector: `OpenAI/Codex`, `MiniMax`, `StepFun`, `OpenCode Go`, `Command Code`, `OpenRouter`.
+- Rolling-window quota bars (e.g. `5h`, weekly).
+- Balance-style fields where the provider exposes them.
+- Inline status: `live`, `cached`, `stale`, `local`, `unavailable`.
### Insights
-Press `v` to toggle insights for the selected period.
+
-- review the most expensive projects in your local session history
-- see active skill and MCP server breakdowns when that data is present
-- keep long sections readable through grouped insight categories and capped lists with overflow summaries
+Breakdowns for the selected period. Toggle visibility by switching to this tab.
-## How to use it
+- Most expensive projects in your local session history.
+- Active skill breakdown when that data is present.
+- MCP server breakdown when that data is present.
+- Grouped insight categories with capped lists and overflow summaries so long sections stay readable.
-### Keyboard shortcuts
+The Insights period selector is independent of the Usage Statistics period.
-- `[Tab/Shift-Tab]` switch provider tabs
-- `[Left/Right]` switch time period
-- `[Up/Down]` move through rows
-- `[Enter/Space]` expand or collapse provider rows
-- `[v]` toggle insights
-- `[q/Esc]` close the dashboard
+## Keyboard Shortcuts
+
+Global:
+
+- `[Tab]` next tab.
+- `[Shift-Tab]` previous tab.
+- `[q]` / `[Esc]` close the dashboard.
+
+Usage Statistics tab:
+
+- `[Left/Right]` switch period.
+- `[Up/Down]` move through rows.
+- `[Enter]` / `[Space]` expand or collapse the selected provider row.
+
+Current Usage tab:
+
+- `[Left/Right]` switch provider.
+
+Insights tab:
+
+- `[Left/Right]` switch period.
+
+The footer at the bottom of the dashboard shows the contextual shortcuts for the active tab.
## Configuration
@@ -72,10 +110,11 @@ Press `v` to toggle insights for the selected period.
Create `$PI_CODING_AGENT_DIR/extensions/usage.json` to disable specific live providers.
Default behavior:
-- if the file is missing, all providers stay enabled
-- if the file is `{}`, all providers stay enabled
-- if a provider is omitted, that provider stays enabled
-- if the JSON is malformed, `@pi-vault/pi-usage` ignores it and falls back to the default behavior
+
+- File missing: all providers stay enabled.
+- File is `{}`: all providers stay enabled.
+- Provider omitted: that provider stays enabled.
+- JSON malformed: `@pi-vault/pi-usage` ignores it and falls back to the default behavior.
Default example:
@@ -110,7 +149,7 @@ Disable MiniMax only:
### Provider setup
-Offline history works without extra setup. Provider cards appear for every supported live provider unless you disable them in `usage.json`. Providers you configure can return live data; others may show `unavailable` or local fallback states.
+Offline history works without extra setup. Provider cards appear for every supported live provider unless you disable them in `usage.json`. Providers you configure can return live data; others may show `unavailable` or a local fallback state.
#### OpenAI/Codex
diff --git a/biome.json b/biome.json
index a81c026..2b9db4d 100644
--- a/biome.json
+++ b/biome.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
+ "$schema": "https://biomejs.dev/schemas/2.5.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
diff --git a/docs/assets/current-usage.png b/docs/assets/current-usage.png
new file mode 100644
index 0000000..4f0fef9
Binary files /dev/null and b/docs/assets/current-usage.png differ
diff --git a/docs/assets/dashboard-ui.png b/docs/assets/dashboard-ui.png
deleted file mode 100644
index 67acca3..0000000
Binary files a/docs/assets/dashboard-ui.png and /dev/null differ
diff --git a/docs/assets/insights.png b/docs/assets/insights.png
new file mode 100644
index 0000000..ea17042
Binary files /dev/null and b/docs/assets/insights.png differ
diff --git a/docs/assets/usage-statistics.png b/docs/assets/usage-statistics.png
new file mode 100644
index 0000000..b0b5aea
Binary files /dev/null and b/docs/assets/usage-statistics.png differ
diff --git a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-1.md b/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-1.md
deleted file mode 100644
index bbceac3..0000000
--- a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-1.md
+++ /dev/null
@@ -1,205 +0,0 @@
-# Phase 1: Theme Foundation — Dashboard Overlay & Tabs
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Add `inverse` and `bg` methods to the `DashboardTheme` interface and its implementations, and add `"borderAccent"` and `"selectedBg"` to `DashboardColor`, so the theme API is ready for tab pill styling.
-
-**Parent plan:** `docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md`
-**Spec:** `docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md`
-
-**Preconditions:** None
-**Postconditions:** All tests pass. `DashboardTheme` exposes `inverse` and `bg`. `DashboardColor` includes `"borderAccent"` and `"selectedBg"`. `noTheme`, `fromPiTheme`, and the `makeAnsiTheme` test helper all implement the new methods. No behavior change to the dashboard.
-
----
-
-## Steps
-
-- [ ] **Step 1: Write failing tests for new theme methods**
-
-Create `tests/dashboard-theme.test.ts`:
-
-```typescript
-import { describe, expect, it } from "vitest";
-import {
- noTheme,
- fromPiTheme,
- type DashboardTheme,
-} from "../src/tui/dashboard-theme.ts";
-
-describe("DashboardTheme", () => {
- describe("noTheme", () => {
- it("inverse returns text unchanged", () => {
- expect(noTheme.inverse("hello")).toBe("hello");
- });
-
- it("bg returns text unchanged", () => {
- expect(noTheme.bg("selectedBg", "hello")).toBe("hello");
- });
- });
-
- describe("fromPiTheme", () => {
- it("delegates inverse to theme.inverse", () => {
- const piTheme = {
- fg: (_color: string, text: string) => text,
- bold: (text: string) => `${text}`,
- inverse: (text: string) => `${text}`,
- bg: (color: string, text: string) => `${text}`,
- };
- const theme = fromPiTheme(piTheme as never);
- expect(theme.inverse("test")).toBe("test");
- });
-
- it("delegates bg to theme.bg", () => {
- const piTheme = {
- fg: (_color: string, text: string) => text,
- bold: (text: string) => `${text}`,
- inverse: (text: string) => `${text}`,
- bg: (color: string, text: string) => `${text}`,
- };
- const theme = fromPiTheme(piTheme as never);
- expect(theme.bg("selectedBg", "test")).toBe("test");
- });
- });
-});
-```
-
-- [ ] **Step 2: Run tests to verify they fail**
-
-```bash
-npx vitest run tests/dashboard-theme.test.ts
-```
-
-Expected: FAIL — `noTheme.inverse` is not a function, `noTheme.bg` is not a function.
-
-- [ ] **Step 3: Add `inverse` and `bg` to DashboardTheme interface, all implementations, and the test helper**
-
-This step updates the interface, both production implementations, and the `makeAnsiTheme` test helper atomically so that TypeScript is never in a broken state.
-
-**3a. In `src/tui/dashboard-theme.ts`:**
-
-**Update the `DashboardTheme` interface** (replace the existing interface):
-
-```typescript
-export interface DashboardTheme {
- /** Foreground color for a themed string. */
- fg: (color: DashboardColor, text: string) => string;
- /** Background color for a themed string. */
- bg: (color: DashboardColor, text: string) => string;
- /** Bold modifier. */
- bold: (text: string) => string;
- /** Dim modifier. */
- dim: (text: string) => string;
- /** Inverse modifier (swap fg/bg). */
- inverse: (text: string) => string;
-}
-```
-
-**Update the `DashboardColor` JSDoc and type** (replace the existing comment and type):
-
-```typescript
-/**
- * Color roles referenced by the dashboard. Foreground names match
- * `ThemeColor` entries; `"selectedBg"` maps to the `ThemeBg` palette
- * and is only valid with `bg()`.
- */
-export type DashboardColor =
- | "accent"
- | "border"
- | "borderAccent"
- | "borderMuted"
- | "selectedBg"
- | "muted"
- | "dim"
- | "text";
-```
-
-**Update `noTheme`** (replace the existing const):
-
-```typescript
-export const noTheme: DashboardTheme = {
- fg: (_color, text) => text,
- bg: (_color, text) => text,
- bold: (text) => text,
- dim: (text) => text,
- inverse: (text) => text,
-};
-```
-
-**Update `fromPiTheme`** (replace the existing function body):
-
-```typescript
-export function fromPiTheme(theme: Theme): DashboardTheme {
- return {
- fg: (color, text) => theme.fg(color, text),
- bg: (color, text) => theme.bg(color as never, text),
- bold: (text) => theme.bold(text),
- dim: (text) => theme.fg("dim", text),
- inverse: (text) => theme.inverse(text),
- };
-}
-```
-
-Note: `bg` casts `color` to `never` because `DashboardColor` is a superset that includes fg-only colors. Only `"selectedBg"` is a valid `ThemeBg` key, but the cast keeps the adapter simple. The caller is responsible for passing valid bg colors.
-
-**3b. In `tests/dashboard.test.ts`:**
-
-Replace the `makeAnsiTheme` function with:
-
-```typescript
-function makeAnsiTheme(): DashboardTheme & {
- calls: { method: string; color?: string; text: string }[];
-} {
- const calls: { method: string; color?: string; text: string }[] = [];
- const wrap = (open: string) => (text: string) => {
- if (text.length === 0) return text;
- return `${open}${text}${ANSI_ESCAPE}[0m`;
- };
- return {
- calls,
- fg: (color, text) => {
- calls.push({ method: "fg", color, text });
- return wrap(`${ANSI_ESCAPE}[38;5;75m`)(text);
- },
- bg: (color, text) => {
- calls.push({ method: "bg", color, text });
- return wrap(`${ANSI_ESCAPE}[48;5;236m`)(text);
- },
- bold: (text) => {
- calls.push({ method: "bold", text });
- return `${ANSI_ESCAPE}[1m${text}${ANSI_ESCAPE}[22m`;
- },
- dim: (text) => {
- calls.push({ method: "dim", text });
- return wrap(`${ANSI_ESCAPE}[38;5;243m`)(text);
- },
- inverse: (text) => {
- calls.push({ method: "inverse", text });
- return `${ANSI_ESCAPE}[7m${text}${ANSI_ESCAPE}[27m`;
- },
- };
-}
-```
-
-- [ ] **Step 4: Run all tests**
-
-```bash
-npx vitest run
-```
-
-Expected: All tests pass — new theme tests green, existing dashboard tests unaffected.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add src/tui/dashboard-theme.ts tests/dashboard-theme.test.ts tests/dashboard.test.ts
-git commit -m "feat(tui): add inverse and bg methods to DashboardTheme
-
-Extend the theme adapter with inverse() for active tab pill styling
-and bg() for inactive tab pill background. Add selectedBg and
-borderAccent to DashboardColor. Update noTheme, fromPiTheme, and
-test helper.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
diff --git a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-2.md b/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-2.md
deleted file mode 100644
index c0fb02c..0000000
--- a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-2.md
+++ /dev/null
@@ -1,380 +0,0 @@
-# Phase 2: Render Utilities — Dashboard Overlay & Tabs
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Create `src/tui/overlay-render.ts` with `pad()`, `frame()`, `renderTabBar()`, `frameContentWidth()`, and a `DashboardTab` interface, replicated from the pi-extension-manager pattern using `DashboardTheme`.
-
-**Parent plan:** `docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md`
-**Spec:** `docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md`
-
-**Preconditions:** Phase 1 complete (`DashboardTheme` has `inverse`, `bg`; `DashboardColor` has `"borderAccent"`, `"selectedBg"`)
-**Postconditions:** All tests pass. `overlay-render.ts` exports `pad`, `frame`, `renderTabBar`, `frameContentWidth`, and `DashboardTab`. Dashboard is NOT modified.
-
----
-
-## Steps
-
-- [ ] **Step 1: Write failing tests for `pad`**
-
-Create `tests/overlay-render.test.ts` with the initial `pad` tests:
-
-```typescript
-import { describe, expect, it } from "vitest";
-import {
- pad,
- frame,
- renderTabBar,
- frameContentWidth,
-} from "../src/tui/overlay-render.ts";
-import { noTheme } from "../src/tui/dashboard-theme.ts";
-
-describe("pad", () => {
- it("pads short text with trailing spaces", () => {
- expect(pad("hi", 10)).toBe("hi ");
- });
-
- it("returns text unchanged when it fills the width", () => {
- expect(pad("hello", 5)).toBe("hello");
- });
-
- it("truncates text that exceeds the width", () => {
- const result = pad("hello world", 5);
- expect(result.length).toBeLessThanOrEqual(5);
- });
-
- it("handles zero width", () => {
- expect(pad("hi", 0)).toBe("");
- });
-});
-```
-
-- [ ] **Step 2: Write failing tests for `frame`**
-
-Append to `tests/overlay-render.test.ts`:
-
-```typescript
-describe("frameContentWidth", () => {
- it("subtracts borders and padding from total width", () => {
- // width - 2 (borders) - PADDING_X * 2 (4) = width - 6
- expect(frameContentWidth(20)).toBe(14);
- expect(frameContentWidth(10)).toBe(4);
- });
-
- it("returns 1 for very small widths", () => {
- expect(frameContentWidth(1)).toBe(1);
- expect(frameContentWidth(0)).toBe(1);
- });
-});
-
-describe("frame", () => {
- it("wraps content lines in a bordered box", () => {
- const lines = frame(["hello", "world"], 20, noTheme);
- // Top border
- expect(lines[0]).toContain("┏");
- expect(lines[0]).toContain("┓");
- // Bottom border
- expect(lines[lines.length - 1]).toContain("┗");
- expect(lines[lines.length - 1]).toContain("┛");
- // Content lines have vertical borders
- const contentLine = lines.find((l) => l.includes("hello"));
- expect(contentLine).toBeDefined();
- expect(contentLine).toContain("┃");
- });
-
- it("includes padding rows above and below content", () => {
- const lines = frame(["test"], 20, noTheme);
- // Structure: top border, 1 padding row, content, 1 padding row, bottom border
- expect(lines.length).toBe(5); // 1 top + 1 pad + 1 content + 1 pad + 1 bottom
- });
-
- it("truncates overflow with indicator when fixedInnerRows is set", () => {
- const content = ["line1", "line2", "line3", "line4", "line5"];
- const lines = frame(content, 30, noTheme, 3);
- const joined = lines.join("\n");
- expect(joined).toContain("line1");
- expect(joined).toContain("line2");
- expect(joined).toContain("more line(s)");
- expect(joined).not.toContain("line5");
- });
-
- it("renders a title in the top border when provided", () => {
- const lines = frame(["content"], 30, noTheme, undefined, "My Title");
- expect(lines[0]).toContain("My Title");
- expect(lines[0]).toContain("┏");
- });
-
- it("pads content to frameContentWidth", () => {
- const lines = frame(["hi"], 20, noTheme);
- // Content line: ┃ + 2 pad + content padded to contentWidth + 2 pad + ┃
- // All lines should be exactly 20 chars wide (in visible width)
- for (const line of lines) {
- expect(line.length).toBeLessThanOrEqual(20);
- }
- });
-});
-```
-
-- [ ] **Step 3: Write failing tests for `renderTabBar`**
-
-Append to `tests/overlay-render.test.ts`:
-
-```typescript
-describe("renderTabBar", () => {
- const tabs = [
- { id: "stats", label: "Usage Statistics" },
- { id: "current", label: "Current Usage" },
- { id: "insights", label: "Insights" },
- ];
-
- it("renders all tabs with active tab highlighted", () => {
- const result = renderTabBar(tabs, "stats", 80, noTheme);
- expect(result).toContain("Usage Statistics");
- expect(result).toContain("Current Usage");
- expect(result).toContain("Insights");
- });
-
- it("returns empty padding for no tabs", () => {
- const result = renderTabBar([], "stats", 20, noTheme);
- expect(result.trim()).toBe("");
- });
-
- it("shows overflow indicators when tabs exceed width", () => {
- const result = renderTabBar(tabs, "insights", 20, noTheme);
- // At 20 chars wide, not all tabs can fit
- expect(result).toContain("Insights");
- });
-
- it("pads result to the requested width", () => {
- const result = renderTabBar(tabs, "stats", 80, noTheme);
- // Result should be padded to exactly 80 visible chars
- expect(result.length).toBe(80);
- });
-});
-```
-
-- [ ] **Step 4: Run tests to verify they fail**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run tests/overlay-render.test.ts
-```
-
-Expected: FAIL -- module `../src/tui/overlay-render.ts` not found.
-
-- [ ] **Step 5: Implement `overlay-render.ts`**
-
-Create `src/tui/overlay-render.ts`:
-
-```typescript
-/**
- * Overlay rendering utilities for the usage dashboard.
- *
- * Replicates patterns from the pi-extension-manager's render helpers using
- * the dashboard's own DashboardTheme adapter so colors flow through Pi's
- * live theme.
- */
-import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
-import type { DashboardTheme, DashboardColor } from "./dashboard-theme.ts";
-
-// ── Layout constants ────────────────────────────────────────────────────
-
-const PADDING_X = 2;
-const PADDING_Y = 1;
-
-// ── Frame glyphs ────────────────────────────────────────────────────────
-
-const FRAME = { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" } as const;
-
-// ── Tab types ───────────────────────────────────────────────────────────
-
-export interface DashboardTab {
- id: string;
- label: string;
-}
-
-// ── Utilities ───────────────────────────────────────────────────────────
-
-/**
- * Pad `text` to exactly `width` visible columns. Truncates if wider.
- */
-export function pad(text: string, width: number): string {
- if (width <= 0) return "";
- const truncated = truncateToWidth(text, width, "");
- return `${truncated}${" ".repeat(Math.max(0, width - visibleWidth(truncated)))}`;
-}
-
-/**
- * Calculate the content width inside a frame (excluding borders and padding).
- */
-export function frameContentWidth(width: number): number {
- return Math.max(1, width - 2 - PADDING_X * 2);
-}
-
-// ── Frame ───────────────────────────────────────────────────────────────
-
-/**
- * Wrap content `lines` in a bordered frame box.
- *
- * Matches the pi-extension-manager's frame rendering:
- * - Top/bottom borders with optional title
- * - PADDING_Y blank rows above/below content
- * - PADDING_X space columns on each side of content
- * - Overflow truncation with "↓ N more line(s)" when fixedInnerRows is set
- */
-export function frame(
- lines: string[],
- width: number,
- theme: DashboardTheme,
- fixedInnerRows?: number,
- title = "",
-): string[] {
- const inner = Math.max(1, width - 2);
- const contentWidth = frameContentWidth(width);
- const border = (s: string) => theme.fg("borderAccent", s);
-
- let body = lines;
- if (fixedInnerRows !== undefined && body.length > fixedInnerRows) {
- const hidden = body.length - fixedInnerRows + 1;
- body = [
- ...body.slice(0, Math.max(0, fixedInnerRows - 1)),
- theme.fg("dim", `↓ ${hidden} more line(s)`),
- ].slice(0, fixedInnerRows);
- }
-
- const blank = `${border(FRAME.v)}${" ".repeat(inner)}${border(FRAME.v)}`;
-
- const top = (): string => {
- if (!title) {
- return `${border(FRAME.tl)}${border(FRAME.h.repeat(inner))}${border(FRAME.tr)}`;
- }
- const titleText = ` ${truncateToWidth(title, Math.max(1, inner - 2), "…")} `;
- const fill = Math.max(1, inner - visibleWidth(titleText));
- return `${border(FRAME.tl)}${theme.fg("accent", titleText)}${border(FRAME.h.repeat(fill))}${border(FRAME.tr)}`;
- };
-
- const out = [top()];
- for (let i = 0; i < PADDING_Y; i += 1) out.push(blank);
- for (const line of body) {
- out.push(
- `${border(FRAME.v)}${" ".repeat(PADDING_X)}${pad(line, contentWidth)}${" ".repeat(PADDING_X)}${border(FRAME.v)}`,
- );
- }
- for (let i = 0; i < PADDING_Y; i += 1) out.push(blank);
- out.push(
- `${border(FRAME.bl)}${border(FRAME.h.repeat(inner))}${border(FRAME.br)}`,
- );
- return out.map((line) => truncateToWidth(line, width, ""));
-}
-
-// ── Tab bar ─────────────────────────────────────────────────────────────
-
-function activePill(theme: DashboardTheme, label: string): string {
- return theme.fg("accent", theme.inverse(theme.bold(label)));
-}
-
-function inactivePill(theme: DashboardTheme, label: string): string {
- return theme.bg("selectedBg", theme.fg("accent", label));
-}
-
-/**
- * Render a tab bar with pill-styled active/inactive tabs.
- *
- * Dynamic visibility: expands tabs around the active tab to fit `width`,
- * showing ‹/› indicators when tabs overflow.
- */
-export function renderTabBar(
- tabs: DashboardTab[],
- activeId: string,
- width: number,
- theme: DashboardTheme,
-): string {
- const safeWidth = Math.max(1, width);
- if (tabs.length === 0) return " ".repeat(safeWidth);
-
- const activeIndex = Math.max(
- 0,
- tabs.findIndex((tab) => tab.id === activeId),
- );
- const widths = tabs.map((tab) => visibleWidth(tab.label) + 2); // " label "
-
- const sliceWidth = (s: number, e: number): number => {
- let total = 0;
- for (let i = s; i < e; i += 1) total += widths[i]!;
- total += Math.max(0, e - s - 1); // single-space gaps
- total += s > 0 ? 2 : 0; // "‹ "
- total += e < tabs.length ? 2 : 0; // " ›"
- return total;
- };
-
- let start = activeIndex;
- let end = activeIndex + 1;
- let preferRight = true;
- while (start > 0 || end < tabs.length) {
- let progressed = false;
- const tryRight = (): boolean => {
- if (end < tabs.length && sliceWidth(start, end + 1) <= safeWidth) {
- end += 1;
- return true;
- }
- return false;
- };
- const tryLeft = (): boolean => {
- if (start > 0 && sliceWidth(start - 1, end) <= safeWidth) {
- start -= 1;
- return true;
- }
- return false;
- };
- if (preferRight) {
- if (tryRight()) progressed = true;
- if (tryLeft()) progressed = true;
- } else {
- if (tryLeft()) progressed = true;
- if (tryRight()) progressed = true;
- }
- if (!progressed) break;
- preferRight = !preferRight;
- }
-
- const cells = tabs.slice(start, end).map((tab) => {
- const label = ` ${tab.label} `;
- return tab.id === activeId
- ? activePill(theme, label)
- : inactivePill(theme, label);
- });
- if (start > 0) cells.unshift(theme.fg("dim", "‹"));
- if (end < tabs.length) cells.push(theme.fg("dim", "›"));
- return pad(cells.join(" "), safeWidth);
-}
-```
-
-- [ ] **Step 6: Run overlay-render tests to verify they pass**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run tests/overlay-render.test.ts
-```
-
-Expected: All PASS.
-
-- [ ] **Step 7: Run full test suite**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run
-```
-
-Expected: All tests pass. The new file is additive -- nothing imports it yet.
-
-- [ ] **Step 8: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/tui/overlay-render.ts tests/overlay-render.test.ts
-git commit -m "feat(tui): add overlay rendering utilities
-
-Add frame(), renderTabBar(), and pad() in overlay-render.ts.
-Replicates pi-extension-manager patterns using DashboardTheme
-for Pi theme integration.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
diff --git a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-3.md b/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-3.md
deleted file mode 100644
index 1a86bbf..0000000
--- a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-3.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Phase 3: Constants Prep — Dashboard Overlay & Tabs
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Add per-tab `dashboardFooters` to `UI_STRINGS` in `src/shared/constants.ts` alongside the legacy `dashboardFooter` and `dashboardBorderChars` (which are removed in Phase 5 after the dashboard refactor).
-
-**Parent plan:** `docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md`
-**Spec:** `docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md`
-
-**Preconditions:** None
-**Postconditions:** All tests pass. `UI_STRINGS.dashboardFooters` exists with `statistics`, `current`, and `insights` keys. Legacy `dashboardFooter` and `dashboardBorderChars` remain untouched for backward compatibility (removed in Phase 5).
-
----
-
-## Steps
-
-- [ ] **Step 1: Add `dashboardFooters` object to `UI_STRINGS`**
-
-In `src/shared/constants.ts`, add the `dashboardFooters` property to `UI_STRINGS` immediately after `dashboardTitle`. The legacy `dashboardFooter` (singular) and `dashboardBorderChars` must remain — they are still referenced by the current dashboard code and will be removed in Phase 5.
-
-Replace the `UI_STRINGS` declaration with:
-
-```typescript
-export const UI_STRINGS = {
- dashboardTitle: "Pi Usage Dashboard",
- // New per-tab footers (used by the tabbed overlay starting Phase 4)
- dashboardFooters: {
- statistics: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[Up/Down] Row",
- "[Enter] Expand",
- "[q/Esc] Close",
- ].join(" • "),
- current: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Provider",
- "[q/Esc] Close",
- ].join(" • "),
- insights: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[q/Esc] Close",
- ].join(" • "),
- },
- // Legacy -- removed in Phase 5 when dashboard.ts stops referencing them
- dashboardFooter: [
- "[Tab/Shift-Tab] Provider",
- "[Left/Right] Period",
- "[Up/Down] Row",
- "[Enter/Space] Expand/Collapse",
- "[v] Insights",
- "[q/Esc] Close",
- ].join(" • "),
- dashboardBorderedSectionTitles: {
- usageStatistics: "Usage Statistics",
- currentUsage: "Current Usage",
- insights: "Insights",
- notes: "Notes",
- },
- dashboardBorderChars: {
- topLeft: "╭",
- topRight: "╮",
- bottomLeft: "╰",
- bottomRight: "╯",
- horizontal: "─",
- separatorLeft: "├",
- separatorRight: "┤",
- },
- dashboardDefaultPeriod: "allTime" as UsageWindow,
-} as const;
-```
-
-The resulting footer strings are:
-
-- `statistics`: `[Tab/Shift-Tab] Switch tab • [Left/Right] Period • [Up/Down] Row • [Enter] Expand • [q/Esc] Close`
-- `current`: `[Tab/Shift-Tab] Switch tab • [Left/Right] Provider • [q/Esc] Close`
-- `insights`: `[Tab/Shift-Tab] Switch tab • [Left/Right] Period • [q/Esc] Close`
-
-- [ ] **Step 2: Run all tests to verify no regressions**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run
-```
-
-Expected: All tests pass. The new `dashboardFooters` property is additive — no existing code references it yet, and the legacy properties are unchanged.
-
-- [ ] **Step 3: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/shared/constants.ts
-git commit -m "feat(constants): add per-tab footer strings
-
-Add dashboardFooters with context-aware key hints for each tab.
-Legacy dashboardFooter and dashboardBorderChars retained until
-the dashboard render pipeline is refactored.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
diff --git a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-4.md b/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-4.md
deleted file mode 100644
index ed9ec16..0000000
--- a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-4.md
+++ /dev/null
@@ -1,1444 +0,0 @@
-# Phase 4: Core Refactor + Tests — Dashboard Overlay & Tabs
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Refactor the dashboard component from stacked layout to tabbed overlay, and update all tests to match.
-
-**Parent plan:** `docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md`
-**Spec:** `docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md`
-
-**Preconditions:** Phases 1-3 complete (DashboardTheme has `inverse`/`bg`, `overlay-render.ts` exists with `frame`/`renderTabBar`/`pad`/`frameContentWidth`, `UI_STRINGS.dashboardFooters` exists with per-tab strings, `makeAnsiTheme` test helper has `inverse`/`bg`)
-**Postconditions:** All tests pass. Dashboard renders as tabbed overlay. `v` key removed. Tab/Shift-Tab switches tabs. Left/Right is contextual per tab.
-
-**Files modified:**
-
-| File | Action |
-| ------------------------- | ------------------------------------ |
-| `src/tui/dashboard.ts` | Refactor to tabbed overlay |
-| `tests/dashboard.test.ts` | Update all assertions for new layout |
-
----
-
-## Source Changes — `src/tui/dashboard.ts`
-
-### Step 1: Add imports and tab type/constants
-
-- [ ] Add the overlay-render import alongside existing imports:
-
-```typescript
-import {
- type DashboardTab,
- frame,
- frameContentWidth,
- renderTabBar,
-} from "./overlay-render.ts";
-```
-
-Then add the tab type and tab definition constants after the existing `SHIFT_TAB_KEY` / `DEFAULT_PERIOD_INDEX` block (before `normalizePlan`):
-
-```typescript
-type DashboardTabId = "statistics" | "current" | "insights";
-
-const DASHBOARD_TABS: DashboardTab[] = [
- {
- id: "statistics",
- label: UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics,
- },
- {
- id: "current",
- label: UI_STRINGS.dashboardBorderedSectionTitles.currentUsage,
- },
- { id: "insights", label: UI_STRINGS.dashboardBorderedSectionTitles.insights },
-];
-```
-
----
-
-### Step 2: Replace class state fields
-
-- [ ] In the `UsageDashboardComponent` class, remove the `showInsights` field and add `activeTab` + `insightsPeriodIndex`.
-
-**Remove:**
-
-```typescript
- private showInsights = false;
-```
-
-**Replace the field block with:**
-
-```typescript
- private activeTab: DashboardTabId = "statistics";
- private periodIndex = DEFAULT_PERIOD_INDEX;
- private insightsPeriodIndex = DEFAULT_PERIOD_INDEX;
- private rowIndex = 0;
- private expandedProvider: string | null = null;
- private currentUsageProviderIndex: number;
-```
-
-(The `periodIndex`, `rowIndex`, `expandedProvider`, and `currentUsageProviderIndex` fields stay; `showInsights` is removed; `activeTab` and `insightsPeriodIndex` are added.)
-
----
-
-### Step 3: Add switchTab helper
-
-- [ ] Add a `switchTab` method to the component class, after `moveProvider`:
-
-```typescript
- private switchTab(delta: number): void {
- const ids = DASHBOARD_TABS.map((t) => t.id);
- const currentIndex = ids.indexOf(this.activeTab);
- const next = (currentIndex + delta + ids.length) % ids.length;
- this.activeTab = ids[next] as DashboardTabId;
- }
-```
-
----
-
-### Step 4: Add per-tab input handlers
-
-- [ ] Add three private methods for per-tab contextual key handling. Place them after `switchTab`:
-
-```typescript
- private handleStatisticsInput(data: string): void {
- if (matchesKey(data, Key.left)) {
- this.movePeriod(-1);
- return;
- }
- if (matchesKey(data, Key.right)) {
- this.movePeriod(1);
- return;
- }
- const period = this.currentPeriod();
- if (!period) return;
- if (matchesKey(data, Key.down)) {
- this.rowIndex = Math.min(
- this.rowIndex + 1,
- Math.max(0, period.providers.length - 1),
- );
- }
- if (matchesKey(data, Key.up)) {
- this.rowIndex = Math.max(0, this.rowIndex - 1);
- }
- if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
- const provider = period.providers[this.rowIndex]?.key;
- if (!provider) return;
- this.expandedProvider =
- this.expandedProvider === provider ? null : provider;
- }
- }
-
- private handleCurrentUsageInput(data: string): void {
- if (matchesKey(data, Key.left)) {
- this.moveProvider(-1);
- return;
- }
- if (matchesKey(data, Key.right)) {
- this.moveProvider(1);
- return;
- }
- }
-
- private handleInsightsInput(data: string): void {
- if (matchesKey(data, Key.left)) {
- this.moveInsightsPeriod(-1);
- return;
- }
- if (matchesKey(data, Key.right)) {
- this.moveInsightsPeriod(1);
- return;
- }
- }
-
- private moveInsightsPeriod(delta: number): void {
- this.insightsPeriodIndex =
- (this.insightsPeriodIndex + delta + PERIODS.length) % PERIODS.length;
- }
-```
-
----
-
-### Step 5: Replace handleInput with tab-based routing
-
-- [ ] Replace the entire `handleInput` method body:
-
-```typescript
- handleInput(data: string): void {
- // Global keys
- if (data === "q" || matchesKey(data, Key.escape)) {
- this.invalidate();
- this.cancelScan?.();
- this.done();
- return;
- }
- if (matchesKey(data, Key.tab)) {
- this.switchTab(1);
- return;
- }
- if (matchesKey(data, SHIFT_TAB_KEY)) {
- this.switchTab(-1);
- return;
- }
-
- // Per-tab contextual keys
- switch (this.activeTab) {
- case "statistics":
- this.handleStatisticsInput(data);
- break;
- case "current":
- this.handleCurrentUsageInput(data);
- break;
- case "insights":
- this.handleInsightsInput(data);
- break;
- }
- }
-```
-
-The old `handleInput` body is fully replaced. The `v` key handler is gone. `Tab`/`Shift-Tab` now call `switchTab` instead of `moveProvider`. `Left`/`Right`/`Up`/`Down`/`Enter`/`Space` are routed through the per-tab handlers.
-
----
-
-### Step 6: Add renderInsightsTab method
-
-- [ ] Add after `renderInsightsByCategory`:
-
-```typescript
- private renderInsightsTab(w: number, lines: string[]): void {
- lines.push(
- ...this.renderTabs(
- PERIODS.map((period) => PERIOD_LABELS[period]),
- this.insightsPeriodIndex,
- w,
- ),
- );
- lines.push("");
-
- if (this.state.insights.length === 0) {
- lines.push(this.theme.dim("No insights yet."));
- } else {
- lines.push(...this.renderInsightsByCategory(w));
- }
- }
-```
-
----
-
-### Step 7: Rename renderUsageStatistics -> renderUsageStatisticsTab
-
-- [ ] Rename the method and remove the section title and the `showInsights` branch. The full replacement:
-
-```typescript
- private renderUsageStatisticsTab(w: number, lines: string[]): void {
- lines.push(
- ...this.renderTabs(
- PERIODS.map((period) => PERIOD_LABELS[period]),
- this.periodIndex,
- w,
- ),
- );
- if (this.state.loading) {
- lines.push(this.theme.dim("Loading session history..."));
- }
- lines.push("");
-
- const period = this.currentPeriod();
- if (!period || period.total.messageCount === 0) {
- lines.push(this.theme.dim("No local session usage found."));
- return;
- }
- const columns = tableColumns(w);
- const providerWidth = labelWidth(columns, w);
- lines.push(tableLine("Provider / Model", columns, providerWidth));
- lines.push(this.theme.fg("borderMuted", separator(columns, providerWidth)));
- period.providers.forEach((row, index) => {
- const expanded = this.expandedProvider === row.key;
- lines.push(
- this.renderProviderRow(
- row,
- index === this.rowIndex,
- expanded,
- columns,
- providerWidth,
- ),
- );
- if (expanded) {
- for (const model of period.modelsByProvider[row.key] ?? []) {
- lines.push(this.renderModelRow(model, columns, providerWidth));
- }
- }
- });
- lines.push(this.theme.fg("borderMuted", separator(columns, providerWidth)));
- lines.push(tableLine("Total", columns, providerWidth, period.total));
- lines.push("");
- lines.push(...this.renderLegend(w));
- }
-```
-
-**Removed vs. old `renderUsageStatistics`:**
-
-- Removed `lines.push(this.sectionTitle(...))` and the blank line after it (title is now in the tab bar)
-- Removed the entire `if (this.showInsights) { ... }` branch (Insights is a separate tab)
-
----
-
-### Step 8: Rename renderCurrentUsage -> renderCurrentUsageTab
-
-- [ ] Rename the method, remove the section title and header separator, and add `renderDiagnostics` call at the end. Full replacement:
-
-```typescript
- private renderCurrentUsageTab(w: number, lines: string[]): void {
- const providers = liveProviders(this.state);
- if (providers.length === 0) {
- lines.push(this.theme.dim("No live usage details."));
- return;
- }
- this.currentUsageProviderIndex = Math.min(
- this.currentUsageProviderIndex,
- Math.max(0, providers.length - 1),
- );
- lines.push(
- ...this.renderTabs(
- providers.map((provider) => provider.providerLabel),
- this.currentUsageProviderIndex,
- w,
- ),
- );
- lines.push("");
-
- const referenceTime = Math.max(
- this.state.generatedAt,
- ...providers.map((provider) => provider.fetchedAt),
- 0,
- );
- const selected = providers[this.currentUsageProviderIndex];
- lines.push(
- this.theme.fg(
- "accent",
- this.theme.bold(providerHeading(selected, referenceTime)),
- ),
- );
- if (selected.windows.length === 0 && selected.balances.length === 0) {
- lines.push(this.theme.dim("No live usage details."));
- } else {
- lines.push(...this.renderQuotaWindows(selected.windows));
- for (const balance of selected.balances) {
- const value =
- balance.unit === "USD"
- ? formatCurrency(balance.remaining ?? undefined)
- : formatAbbrev(balance.remaining ?? undefined);
- const unitSuffix = balance.unit === "USD" ? "" : ` ${balance.unit}`;
- const labelStyled = this.theme.dim(`${balance.label}:`);
- lines.push(`${labelStyled} ${value}${unitSuffix}`);
- }
- }
-
- // Diagnostics (previously a separate section, now housed in Current Usage)
- this.renderDiagnostics(lines);
- }
-```
-
-**Removed vs. old `renderCurrentUsage`:**
-
-- Removed `lines.push(this.sectionTitle(...))` (title is in the tab bar)
-- Removed `lines.push(this.currentUsageHeaderSeparator(w))` (no separator in tab layout)
-- Added `this.renderDiagnostics(lines)` at the end (was called separately in old `render()`)
-
----
-
-### Step 9: Replace render() to use frame + tab bar + per-tab content
-
-- [ ] Replace the entire `render` method:
-
-```typescript
- render(width: number): string[] {
- const w = Math.max(8, width);
- const contentWidth = frameContentWidth(w);
- const lines: string[] = [];
-
- // Tab bar
- lines.push(
- renderTabBar(DASHBOARD_TABS, this.activeTab, contentWidth, this.theme),
- );
- lines.push("");
-
- // Active tab content
- switch (this.activeTab) {
- case "statistics":
- this.renderUsageStatisticsTab(contentWidth, lines);
- break;
- case "current":
- this.renderCurrentUsageTab(contentWidth, lines);
- break;
- case "insights":
- this.renderInsightsTab(contentWidth, lines);
- break;
- }
-
- // Footer
- lines.push("");
- lines.push(this.renderFooter(contentWidth));
-
- return frame(lines, w, this.theme);
- }
-```
-
----
-
-### Step 10: Update renderFooter to be context-aware
-
-- [ ] Replace the `renderFooter` method:
-
-```typescript
- private renderFooter(width: number): string {
- const footerKey =
- this.activeTab === "statistics"
- ? "statistics"
- : this.activeTab === "current"
- ? "current"
- : "insights";
- return this.theme.dim(
- truncateVisible(UI_STRINGS.dashboardFooters[footerKey], width),
- );
- }
-```
-
----
-
-### Step 11: Remove dead code
-
-- [ ] Remove the following methods and free functions that are no longer called:
-
-**Remove free functions** (before the `UsageDashboardOptions` interface):
-
-```typescript
-function horizontalBorder(width: number, left: string, right: string): string {
- if (width <= 2) return left + right;
- return `${left}${"─".repeat(width - 2)}${right}`;
-}
-
-function borderSeparator(width: number): string {
- if (width <= 2) return "├┤";
- return `├${"─".repeat(width - 2)}┤`;
-}
-```
-
-**Remove private methods** from `UsageDashboardComponent`:
-
-```typescript
- private sectionTitle(text: string): string {
- return this.theme.fg("accent", this.theme.bold(text));
- }
-
- private borderLine(width: number): string {
- return this.theme.fg(
- "border",
- horizontalBorder(
- width,
- UI_STRINGS.dashboardBorderChars.topLeft,
- UI_STRINGS.dashboardBorderChars.topRight,
- ),
- );
- }
-
- private currentUsageHeaderSeparator(width: number): string {
- return this.theme.fg("borderMuted", borderSeparator(width));
- }
-
- private bottomBorder(width: number): string {
- return this.theme.fg(
- "border",
- horizontalBorder(
- width,
- UI_STRINGS.dashboardBorderChars.bottomLeft,
- UI_STRINGS.dashboardBorderChars.bottomRight,
- ),
- );
- }
-```
-
-**Update `renderDiagnostics`** to inline the old `sectionTitle` call (since `sectionTitle` is removed):
-
-```typescript
- private renderDiagnostics(lines: string[]): void {
- const providers = liveProviders(this.state);
- const diagnosticNotes = providers.flatMap((provider) =>
- providerDiagnostics(provider).map(
- (diagnostic) => `* ${provider.providerLabel}: ${diagnostic}`,
- ),
- );
- if (diagnosticNotes.length === 0) return;
- lines.push("");
- lines.push(
- this.theme.fg(
- "accent",
- this.theme.bold(UI_STRINGS.dashboardBorderedSectionTitles.notes),
- ),
- );
- for (const note of diagnosticNotes) {
- lines.push(this.theme.dim(note));
- }
- }
-```
-
-The only change is replacing `this.sectionTitle(UI_STRINGS.dashboardBorderedSectionTitles.notes)` with the inlined `this.theme.fg("accent", this.theme.bold(...))`.
-
----
-
-### Step 12: Update openDashboard with overlay options
-
-- [ ] Replace the `openDashboard` function to pass overlay options:
-
-```typescript
-export async function openDashboard(
- ctx: ExtensionCommandContext,
- state: UsageCoreState,
- cancelScan?: () => void,
-): Promise {
- await ctx.ui.custom(
- (tui, theme, _keys, done) => {
- const piTheme = theme as unknown as Parameters[0];
- const dashboardTheme: DashboardTheme =
- piTheme &&
- typeof (piTheme as { fg?: unknown }).fg === "function" &&
- typeof (piTheme as { bold?: unknown }).bold === "function"
- ? fromPiTheme(piTheme)
- : noTheme;
- return new UsageDashboardComponent(state, done, {
- tui,
- theme: dashboardTheme,
- cancelScan,
- });
- },
- {
- overlay: true,
- overlayOptions: {
- anchor: "center",
- maxHeight: "85%",
- width: "92%",
- },
- },
- );
-}
-```
-
-The callback body is unchanged. The second argument `{ overlay: true, overlayOptions: { ... } }` is new.
-
----
-
-### Step 13: Verify build compiles
-
-- [ ] Run:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx tsc --noEmit
-```
-
-Expected: No type errors. If references to removed methods or old constants remain, fix them before proceeding.
-
----
-
-## Test Changes — `tests/dashboard.test.ts`
-
-> **Convention:** Content is now inside a `frame()` box. Lines look like `┃ content ┃`. Tests that used `l.startsWith("text")` must change to `l.includes("text")`. The `indexOf("[")` alignment checks still work because frame padding is uniform across all content lines.
-
-### Step 14: Update test -- "renders Usage Statistics tab by default"
-
-- [ ] Replace the `"renders usage statistics + current usage with selected provider details"` test:
-
-```typescript
-it("renders Usage Statistics tab by default with table and legend", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- const out = c.render(140).join("\n");
-
- // Frame borders
- expect(out).toContain("\u2501"); // ━
- expect(out).toContain("\u250F"); // ┏
- expect(out).toContain("\u251B"); // ┛
-
- // Tab bar shows all three tabs
- expect(out).toContain("Usage Statistics");
- expect(out).toContain("Current Usage");
- expect(out).toContain("Insights");
-
- // Default period is All Time
- expect(out).toContain("[All Time]");
- expect(out).toContain("Provider / Model");
- expect(out).toContain("openai-codex");
- expect(out).toContain("428k");
-
- // Legend
- expect(out).toContain(
- "Tokens = Input + Output + CacheW \u2022 \u2191In = Input + CacheW \u2022 \u2193Out = Output \u2022 CacheR = Cache Read \u2022 CacheW = Cache Write",
- );
-
- // Current Usage content should NOT be visible on the Statistics tab
- expect(out).not.toContain("Command Code (Go) \u2022 live \u2022 4s old");
- expect(out).not.toContain("57% left");
-
- // No legacy layout artifacts
- expect(out).not.toContain("\u256D"); // ╭ old border
- expect(out).not.toContain("\u256F"); // ╯ old border
-});
-```
-
----
-
-### Step 15: Add test -- "renders Current Usage tab with provider details and diagnostics"
-
-- [ ] Add after the previous test:
-
-```typescript
-it("renders Current Usage tab with provider details and diagnostics", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const out = c.render(140).join("\n");
-
- // Provider details
- expect(out).toContain("Command Code (Go) \u2022 live \u2022 4s old");
- expect(out).toContain("57% left");
- expect(out).toContain(expectedResetText(Date.parse("2026-06-07T11:47:00")));
- expect(out).toContain("$4.29/$10.00");
-
- // Diagnostics appear in Current Usage tab
- expect(out).toContain("Subscription endpoint unavailable.");
- expect(out).toContain("Live cache is unavailable.");
-
- // Usage Statistics table should NOT be visible
- expect(out).not.toContain("Provider / Model");
- expect(out).not.toContain("[All Time]");
-});
-```
-
----
-
-### Step 16: Add test -- "renders Insights tab with insights grouped by category"
-
-- [ ] Add after the previous test:
-
-```typescript
-it("renders Insights tab with insights grouped by category", () => {
- const state = mkState();
- state.insights = [
- { category: "project", label: "career-ops", cost: 9, detail: "90.0%" },
- { category: "project", label: "dotfiles", cost: 1, detail: "10.0%" },
- {
- category: "cost",
- label: "Large context",
- cost: 5,
- detail: "50.0% over 150k context",
- },
- ];
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Insights tab (Tab twice)
- c.handleInput("\t");
- c.handleInput("\t");
- const out = c.render(100).join("\n");
-
- expect(out).toContain("Projects");
- expect(out).toContain("career-ops");
- expect(out).toContain("90.0%");
- expect(out).toContain("Cost patterns");
- expect(out).toContain("Large context");
-
- // Should have its own period selector
- expect(out).toContain("[All Time]");
-
- // Usage Statistics content should NOT be visible
- expect(out).not.toContain("Provider / Model");
-});
-```
-
----
-
-### Step 16b: Add test -- "Insights tab period selector is independent from Statistics tab"
-
-- [ ] Add after the previous test:
-
-```typescript
-it("has independent period selector for Insights tab", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
-
- // Change Statistics tab period to Today
- c.handleInput("\u001b[D"); // Left (All Time → Last Week)
- c.handleInput("\u001b[D"); // Left (Last Week → This Week)
- c.handleInput("\u001b[D"); // Left (This Week → Today)
- expect(c.render(120).join("\n")).toContain("[Today]");
-
- // Switch to Insights tab
- c.handleInput("\t");
- c.handleInput("\t");
- const insightsOut = c.render(120).join("\n");
- // Insights should still be on All Time (independent period)
- expect(insightsOut).toContain("[All Time]");
-
- // Change Insights period
- c.handleInput("\u001b[D"); // Left
- expect(c.render(120).join("\n")).toContain("[Last Week]");
-
- // Switch back to Statistics tab and verify its period is still Today
- c.handleInput("\t"); // Insights → Statistics (wraps)
- expect(c.render(120).join("\n")).toContain("[Today]");
-});
-```
-
----
-
-### Step 17: Update test -- quota bar alignment (add tab switch)
-
-- [ ] Replace `"aligns quota bars by shared label width across available windows"`:
-
-```typescript
-it("aligns quota bars by shared label width across available windows", () => {
- const state = mkState();
- setWindows(state, [
- {
- key: "5h",
- label: "5h",
- usedPercent: 50,
- resetAt: Date.now() + 3600000,
- },
- {
- key: "weekly",
- label: "Weekly",
- usedPercent: 10,
- resetAt: Date.now() + 86400000 * 7,
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const lines = c.render(200);
-
- const line5h = lines.find(
- (l) => l.includes("5h") && l.includes("% left") && l.includes("["),
- );
- const lineWeekly = lines.find(
- (l) => l.includes("Weekly") && l.includes("% left") && l.includes("["),
- );
-
- expect(line5h).toBeDefined();
- expect(lineWeekly).toBeDefined();
-
- // Opening brackets must align vertically
- const bracket5h = line5h?.indexOf("[") ?? -1;
- const bracketWeekly = lineWeekly?.indexOf("[") ?? -1;
- expect(bracket5h).toBe(bracketWeekly);
-
- // Shorter label is padded to match the longest available-window label
- expect(line5h).toMatch(/5h\s+:/);
-});
-```
-
----
-
-### Step 18: Update test -- fractional usedPercent (add tab switch)
-
-- [ ] Replace `"rounds fractional usedPercent to integer remaining percentage"`:
-
-```typescript
-it("rounds fractional usedPercent to integer remaining percentage", () => {
- const state = mkState();
- setWindows(state, [
- {
- key: "cycle",
- label: "Cycle",
- usedPercent: 43.7,
- resetAt: Date.now() + 3600000,
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const out = c.render(140).join("\n");
-
- // 100 - 43.7 = 56.3, rounded to 56
- expect(out).toContain("56% left");
- expect(out).not.toContain("56.3%");
-});
-```
-
----
-
-### Step 19: Update test -- same-day reset (add tab switch)
-
-- [ ] Replace `"formats same-day reset as HH:mm only"`:
-
-```typescript
-it("formats same-day reset as HH:mm only", () => {
- const now = new Date();
- const sameDayReset = new Date(
- now.getFullYear(),
- now.getMonth(),
- now.getDate(),
- 14,
- 30,
- ).getTime();
-
- const state = mkState();
- setWindows(state, [
- {
- key: "cycle",
- label: "Cycle",
- usedPercent: 50,
- resetAt: sameDayReset,
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const out = c.render(140).join("\n");
-
- expect(out).toContain("(resets 14:30)");
- expect(out).not.toContain(" on ");
-});
-```
-
----
-
-### Step 20: Update test -- cross-day reset (add tab switch)
-
-- [ ] Replace `"formats cross-day reset as HH:mm on D MMM"`:
-
-```typescript
-it("formats cross-day reset as HH:mm on D MMM", () => {
- const state = mkState();
- setWindows(state, [
- {
- key: "cycle",
- label: "Cycle",
- usedPercent: 50,
- resetAt: Date.parse("2026-06-07T11:47:00"),
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const out = c.render(140).join("\n");
-
- expect(out).toContain(expectedResetText(Date.parse("2026-06-07T11:47:00")));
-});
-```
-
----
-
-### Step 21: Update test -- reset unavailable (add tab switch)
-
-- [ ] Replace `"renders reset unavailable when resetAt is absent"`:
-
-```typescript
-it("renders reset unavailable when resetAt is absent", () => {
- const state = mkState();
- setWindows(state, [
- {
- key: "cycle",
- label: "Cycle",
- usedPercent: 50,
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const out = c.render(140).join("\n");
-
- expect(out).toContain("(reset unavailable)");
-});
-```
-
----
-
-### Step 22: Update test -- unavailable windows (add tab switch)
-
-- [ ] Replace `"renders unavailable windows without bar and does not affect alignment"`:
-
-```typescript
-it("renders unavailable windows without bar and does not affect alignment", () => {
- const state = mkState();
- setWindows(state, [
- {
- key: "5h",
- label: "5h",
- usedPercent: 50,
- resetAt: Date.now() + 3600000,
- },
- {
- key: "daily",
- label: "Daily",
- usedPercent: 30,
- resetAt: Date.now() + 86400000,
- },
- {
- key: "verylong",
- label: "VeryLongName",
- usedPercent: 10,
- unavailableReason: "Not applicable",
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const lines = c.render(200);
-
- const line5h = lines.find(
- (l) => l.includes("5h") && l.includes("% left") && l.includes("["),
- );
- const lineDaily = lines.find(
- (l) => l.includes("Daily") && l.includes("% left") && l.includes("["),
- );
- const lineLong = lines.find(
- (l) => l.includes("VeryLongName") && l.includes("Not applicable"),
- );
-
- expect(line5h).toBeDefined();
- expect(lineDaily).toBeDefined();
- expect(lineLong).toBeDefined();
-
- // Unavailable window has no bar or percentage
- expect(lineLong).not.toContain("% left");
-
- // Available windows' bars align (maxLabelWidth from "5h" and "Daily" only)
- const bracket5h = line5h?.indexOf("[") ?? -1;
- const bracketDaily = lineDaily?.indexOf("[") ?? -1;
- expect(bracket5h).toBe(bracketDaily);
-
- // "5h" is padded to "Daily" width (5 chars), not "VeryLongName" width
- expect(line5h).toMatch(/5h\s+:/);
-});
-```
-
----
-
-### Step 23: Update test -- quota row without ratio (add tab switch)
-
-- [ ] Replace `"renders quota row without ratio when used/limit/unit are incomplete"`:
-
-```typescript
-it("renders quota row without ratio when used/limit/unit are incomplete", () => {
- const state = mkState();
- setWindows(state, [
- {
- key: "cycle",
- label: "Cycle",
- usedPercent: 50,
- resetAt: Date.now() + 3600000,
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const out = c.render(140).join("\n");
-
- expect(out).toContain("50% left");
- // No ratio suffix should appear
- expect(out).not.toContain(" - $");
- expect(out).not.toContain(" requests");
-});
-```
-
----
-
-### Step 24: Update test -- provider navigation (Current Usage tab)
-
-- [ ] Replace `"wraps joined legend and supports tab-based provider navigation"`:
-
-```typescript
-it("supports provider navigation with left/right in Current Usage tab", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
-
- // Left arrow cycles providers backward: Command Code (4) -> OpenCode Go (3)
- c.handleInput("\u001b[D"); // Left
- let out = c.render(120).join("\n");
- expect(out).toContain("[OpenCode Go]");
- expect(out).toContain("Credits: $12.50");
-
- // Right arrow cycles forward: OpenCode Go (3) -> Command Code (4)
- c.handleInput("\u001b[C"); // Right
- out = c.render(120).join("\n");
- expect(out).toContain("[Command Code]");
-});
-```
-
----
-
-### Step 25: Update test -- enter/space expand and period changes (remove v)
-
-- [ ] Replace `"uses enter/space for expand, v for insights, and left/right for period changes"`:
-
-```typescript
-it("uses enter/space for expand and left/right for period changes in Statistics tab", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
-
- // Enter expands the selected provider row to reveal its model rows.
- c.handleInput("\r");
- expect(c.render(120).join("\n")).toContain("gpt-5");
-
- // Left/Right change the period. Default is All Time (index 3); one Left
- // press moves to Last Week.
- c.handleInput("\u001b[D");
- expect(c.render(120).join("\n")).toContain("[Last Week]");
-
- // Two more Right presses move through This Week back to Today.
- c.handleInput("\u001b[C");
- c.handleInput("\u001b[C");
- expect(c.render(120).join("\n")).toContain("[Today]");
-
- // Period changes reset the selected row back to 0.
- expect(c.render(120).join("\n")).toContain("openai-codex");
-});
-```
-
----
-
-### Step 26: Update test -- insights grouped by category (tab switch instead of v)
-
-- [ ] Replace `"renders insights grouped by category"`:
-
-```typescript
-it("renders insights grouped by category in Insights tab", () => {
- const state = mkState();
- state.insights = [
- { category: "project", label: "career-ops", cost: 9, detail: "90.0%" },
- { category: "project", label: "dotfiles", cost: 1, detail: "10.0%" },
- {
- category: "cost",
- label: "Large context",
- cost: 5,
- detail: "50.0% over 150k context",
- },
- ];
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Insights tab (Tab twice)
- c.handleInput("\t");
- c.handleInput("\t");
- const lines = c.render(100);
- const out = lines.join("\n");
- expect(out).toContain("Projects");
- expect(out).toContain("career-ops");
- expect(out).toContain("90.0%");
- expect(out).toContain("Cost patterns");
- expect(out).toContain("Large context");
- // Verify table structure for project category
- const projectsIdx = lines.findIndex((l) => l.includes("Projects"));
- expect(projectsIdx).toBeGreaterThan(-1);
- expect(lines[projectsIdx]).toContain("% of usage");
- expect(lines[projectsIdx + 1]).toContain("career-ops");
- // Verify bullet-list format for cost category
- expect(out).toContain(" - Large context:");
-});
-```
-
----
-
-### Step 27: Update test -- defaults insights without category (tab switch instead of v)
-
-- [ ] Replace `"defaults insights without category to cost patterns"`:
-
-```typescript
-it("defaults insights without category to cost patterns", () => {
- const state = mkState();
- state.insights = [{ label: "No category", cost: 1, detail: "test" }];
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Insights tab
- c.handleInput("\t");
- c.handleInput("\t");
- const out = c.render(100).join("\n");
- expect(out).toContain("Cost patterns");
- expect(out).toContain(" - No category:");
-});
-```
-
----
-
-### Step 28: Delete test -- "renders empty line between Usage Statistics title and period tabs"
-
-- [ ] Remove the entire `it("renders empty line between Usage Statistics title and period tabs", ...)` test block. The section title is now in the tab bar, not a standalone line.
-
----
-
-### Step 29: Update themed test -- frame borders and tab bar styling
-
-- [ ] Replace `"wraps section titles, borders, and dimmed helpers with ANSI escape codes"`:
-
-```typescript
-it("renders frame borders and tab bar with themed styling", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
- const lines = c.render(140);
- const out = lines.join("\n");
-
- // Frame uses ┏ and ┛ (from overlay-render frame glyphs)
- expect(out).toContain("\u250F"); // ┏
- expect(out).toContain("\u251B"); // ┛
-
- // Tab bar active pill uses inverse+bold for Usage Statistics
- expect(
- theme.calls.some(
- (c) => c.method === "bold" && c.text.includes("Usage Statistics"),
- ),
- ).toBe(true);
- expect(
- theme.calls.some(
- (c) => c.method === "inverse" && c.text.includes("Usage Statistics"),
- ),
- ).toBe(true);
-
- // Footer should be dimmed with per-tab content
- expect(out).toContain("[Tab/Shift-Tab] Switch tab");
- expect(
- theme.calls.some(
- (c) =>
- c.method === "dim" && c.text.includes("[Tab/Shift-Tab] Switch tab"),
- ),
- ).toBe(true);
-});
-```
-
----
-
-### Step 30: Update themed test -- inactive tabs with bg styling
-
-- [ ] Replace `"dims the inactive provider tabs in Current Usage"`:
-
-```typescript
-it("renders inactive main tabs with bg styling", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
- c.render(140);
-
- // Inactive tabs use bg("selectedBg", fg("accent", label))
- const bgCalls = theme.calls
- .filter((c) => c.method === "bg")
- .map((c) => c.text);
- // Current Usage and Insights should have bg calls (they're inactive)
- expect(bgCalls.some((t) => t.includes("Current Usage"))).toBe(true);
- expect(bgCalls.some((t) => t.includes("Insights"))).toBe(true);
-});
-```
-
----
-
-### Step 31: Update themed test -- disclosure arrow (adjust for frame)
-
-- [ ] Replace `"highlights the selected disclosure arrow + provider label and dims the rest"`:
-
-```typescript
-it("highlights the selected disclosure arrow and dims the rest", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
- const lines = c.render(140);
-
- const providerLine = lines.find(
- (l) => l.includes("openai-codex") && l.includes("\u25B8"),
- );
- expect(providerLine).toBeDefined();
-
- const plain = stripAnsi(providerLine ?? "");
- // Line is inside a frame (\u2503...\u2503); verify no stray > cursor in content
- const content = plain.replace(/^\u2503\s*/, "");
- expect(content.startsWith(">")).toBe(false);
- expect(plain).toContain("\u25B8"); // ▸
- expect(plain).toContain("openai-codex");
- expect(
- theme.calls.some(
- (c) =>
- c.method === "fg" &&
- c.color === "accent" &&
- c.text.includes("openai-codex"),
- ),
- ).toBe(true);
-});
-```
-
----
-
-### Step 32: Update themed test -- quota bar alignment by visible width (add tab switch)
-
-- [ ] Replace `"aligns themed quota bars by visible width"`:
-
-```typescript
-it("aligns themed quota bars by visible width", () => {
- const theme = makeAnsiTheme();
- const state = mkState();
- setWindows(state, [
- {
- key: "5h",
- label: "5h",
- usedPercent: 50,
- resetAt: Date.now() + 3600000,
- },
- {
- key: "weekly",
- label: "Weekly",
- usedPercent: 10,
- resetAt: Date.now() + 86400000 * 7,
- },
- ]);
-
- const c = new UsageDashboardComponent(state, () => undefined, { theme });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const lines = c.render(200);
- const line5h = lines.find(
- (l) =>
- stripAnsi(l).includes("5h") && l.includes("[") && l.includes("% left"),
- );
- const lineWeekly = lines.find(
- (l) =>
- stripAnsi(l).includes("Weekly") &&
- l.includes("[") &&
- l.includes("% left"),
- );
-
- expect(line5h).toBeDefined();
- expect(lineWeekly).toBeDefined();
-
- // The opening bracket (after padding) aligns vertically; frame adds
- // uniform padding so relative alignment is preserved.
- const bracketIndex = (line: string) => stripAnsi(line).indexOf("[");
- expect(bracketIndex(line5h ?? "")).toBe(bracketIndex(lineWeekly ?? ""));
-});
-```
-
----
-
-### Step 33: Update themed test -- quota remaining-bar fill (add tab switch)
-
-- [ ] Replace `"highlights the quota remaining-bar fill and percentage"`:
-
-```typescript
-it("highlights the quota remaining-bar fill and percentage", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- c.render(140);
-
- // The fill glyphs should be wrapped in accent styling.
- const filledAccent = theme.calls.find(
- (c) =>
- c.method === "fg" && c.color === "accent" && c.text.includes("\u2588"),
- );
- expect(filledAccent).toBeDefined();
-
- // The percentage text should be accent-wrapped.
- const percentAccent = theme.calls.find(
- (c) => c.method === "fg" && c.color === "accent" && c.text === "57% left",
- );
- expect(percentAccent).toBeDefined();
-});
-```
-
----
-
-### Step 34: Update themed test -- dims formula legend, reset, ratio (split across tabs)
-
-- [ ] Replace `"dims the formula legend, reset text, and ratio text"`:
-
-```typescript
-it("dims the formula legend on Statistics tab and reset/ratio on Current Usage tab", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
-
- // Statistics tab: legend segments should be dimmed
- c.render(140);
- const dimmed = theme.calls
- .filter((c) => c.method === "dim")
- .map((c) => c.text);
- expect(dimmed).toContain("Tokens = Input + Output + CacheW");
- expect(dimmed).toContain("CacheR = Cache Read");
-
- // Switch to Current Usage tab: reset and ratio should be dimmed
- c.handleInput("\t");
- c.render(140);
- const allDimmed = theme.calls
- .filter((c) => c.method === "dim")
- .map((c) => c.text);
- expect(
- allDimmed.some(
- (text) =>
- text.startsWith("(resets ") || text.includes("reset unavailable"),
- ),
- ).toBe(true);
- expect(allDimmed).toContain("$4.29/$10.00");
-});
-```
-
----
-
-### Step 35: Update themed test -- footer context-aware per tab
-
-- [ ] Replace `"renders the footer in [Shortcut] Action format with dimmed styling"`:
-
-```typescript
-it("renders context-aware footer per tab", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
-
- // Statistics tab footer
- let out = c.render(160).join("\n");
- let stripped = stripAnsi(out);
- expect(stripped).toContain("[Tab/Shift-Tab] Switch tab");
- expect(stripped).toContain("[Left/Right] Period");
- expect(stripped).toContain("[Up/Down] Row");
- expect(stripped).toContain("[Enter] Expand");
- expect(stripped).toContain("[q/Esc] Close");
-
- // Current Usage tab footer
- c.handleInput("\t");
- out = c.render(160).join("\n");
- stripped = stripAnsi(out);
- expect(stripped).toContain("[Tab/Shift-Tab] Switch tab");
- expect(stripped).toContain("[Left/Right] Provider");
- expect(stripped).not.toContain("[Up/Down] Row");
-
- // Insights tab footer
- c.handleInput("\t");
- out = c.render(160).join("\n");
- stripped = stripAnsi(out);
- expect(stripped).toContain("[Tab/Shift-Tab] Switch tab");
- expect(stripped).toContain("[Left/Right] Period");
- expect(stripped).not.toContain("[Up/Down] Row");
-});
-```
-
----
-
-### Step 36: Update themed test -- ANSI truncation (frame handles it now)
-
-- [ ] Replace `"strips ANSI before applying final truncation so visible width is preserved"`:
-
-```typescript
-it("strips ANSI before applying final truncation so visible width is preserved", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
- // Render with a narrow width -- every visible line must not exceed it
- // even when ANSI escapes are present. frame() handles truncation.
- const lines = c.render(40);
- for (const line of lines) {
- const visible = stripAnsi(line).length;
- expect(visible).toBeLessThanOrEqual(40);
- }
-});
-```
-
----
-
-### Step 37: Update responsive test -- narrow widths (check frame borders)
-
-- [ ] Replace `"renders at very narrow widths without breaking the table"`:
-
-```typescript
-it("renders at very narrow widths without breaking the frame", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- const lines = c.render(30);
- for (const line of lines) {
- const visible = line.replace(ANSI_PATTERN, "").length;
- expect(visible).toBeLessThanOrEqual(30);
- }
- // Frame borders should be present
- const out = lines.join("\n");
- expect(out).toContain("\u250F"); // ┏
- expect(out).toContain("\u251B"); // ┛
-});
-```
-
----
-
-## Verification
-
-### Step 38: Run all tests
-
-- [ ] Run:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run
-```
-
-Expected: All tests pass.
-
----
-
-### Step 39: Fix any remaining failures
-
-- [ ] If any tests fail, diagnose and fix. Common issues:
- - **Line-finding assertions:** Tests using `startsWith` or `indexOf` may need updating for frame-wrapped lines. Use `includes` instead.
- - **Width calculations:** Content width is now `frameContentWidth(w) = w - 6`, so table breakpoints may shift at boundary widths. Adjust render width or assertion if needed.
- - **Tab state:** Tests that render Current Usage content must call `c.handleInput("\t")` first. Tests that render Insights content must call `c.handleInput("\t")` twice.
- - **Missing `inverse`/`bg` on theme:** If `makeAnsiTheme` doesn't have `inverse`/`bg`, Phase 1 was not applied correctly. Verify preconditions.
-
----
-
-### Step 40: Commit
-
-- [ ] Run:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/tui/dashboard.ts tests/dashboard.test.ts
-git commit -m "feat(tui): refactor dashboard to tabbed overlay + update tests
-
-Replace vertical stacking with 3 tabs: Usage Statistics, Current
-Usage, Insights. Use frame() and renderTabBar() from overlay-render.
-Tab/Shift-Tab switches tabs, Left/Right is contextual per tab.
-Diagnostics moved into Current Usage tab. Enable overlay mode with
-centered anchor, 85% maxHeight, 92% width. Update all dashboard
-tests for new layout.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
diff --git a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-5.md b/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-5.md
deleted file mode 100644
index 8bf2d4c..0000000
--- a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-5.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Phase 5: Legacy Cleanup — Dashboard Overlay & Tabs
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Remove the legacy `dashboardFooter` (singular) and `dashboardBorderChars` properties from `UI_STRINGS`, which were kept for backward compatibility during Phase 3 and are no longer referenced after Phase 4.
-
-**Parent plan:** `docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md`
-**Spec:** `docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md`
-
-**Preconditions:** Phase 4 (core refactor + test updates) is complete. The dashboard no longer references `UI_STRINGS.dashboardFooter` or `UI_STRINGS.dashboardBorderChars`. All tests pass.
-**Postconditions:** `UI_STRINGS` contains only the new per-tab `dashboardFooters` object. No dead constants remain. All tests and type checks pass.
-
----
-
-## Step 1: Remove `dashboardFooter` and `dashboardBorderChars` from `UI_STRINGS`
-
-- [ ] Edit `src/shared/constants.ts` and remove the `dashboardFooter` and `dashboardBorderChars` properties from the `UI_STRINGS` object.
-
-The final `UI_STRINGS` must be exactly:
-
-```typescript
-export const UI_STRINGS = {
- dashboardTitle: "Pi Usage Dashboard",
- dashboardFooters: {
- statistics: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[Up/Down] Row",
- "[Enter] Expand",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- current: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Provider",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- insights: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- },
- dashboardBorderedSectionTitles: {
- usageStatistics: "Usage Statistics",
- currentUsage: "Current Usage",
- insights: "Insights",
- notes: "Notes",
- },
- dashboardDefaultPeriod: "allTime" as UsageWindow,
-} as const;
-```
-
-Properties to remove:
-
-1. `dashboardFooter` (singular) — the old single-string footer with `[Tab/Shift-Tab] Provider`, `[Enter/Space] Expand/Collapse`, `[v] Insights` hints. Superseded by `dashboardFooters` (plural, per-tab).
-2. `dashboardBorderChars` — the old `╭╮╰╯─├┤` border characters. Superseded by `frame()` in `src/tui/overlay-render.ts` which uses `┏┓┗┛━┃`.
-
----
-
-## Step 2: Grep for stale references
-
-- [ ] Verify no code still references the removed properties.
-
-Run:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-grep -rn "dashboardFooter[^s]" src/ tests/ || echo "No stale dashboardFooter refs"
-grep -rn "dashboardBorderChars" src/ tests/ || echo "No stale dashboardBorderChars refs"
-```
-
-Expected: Both commands print the "No stale" message. No files reference either property.
-
-If references are found, fix them before proceeding:
-- References to `UI_STRINGS.dashboardFooter` should be replaced with `UI_STRINGS.dashboardFooters[tabId]` (this should already have been done in Phase 4).
-- References to `UI_STRINGS.dashboardBorderChars` should be removed entirely (border rendering is now handled by `frame()` in `overlay-render.ts`).
-
----
-
-## Step 3: Type check
-
-- [ ] Verify the project compiles with no type errors.
-
-Run:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx tsc --noEmit
-```
-
-Expected: Clean exit, no type errors.
-
----
-
-## Step 4: Run all tests
-
-- [ ] Verify all tests pass after the removal.
-
-Run:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run
-```
-
-Expected: All tests pass. No test should reference the removed properties (they were updated in Phase 4's Task 6).
-
----
-
-## Step 5: Commit
-
-- [ ] Stage and commit the change.
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/shared/constants.ts
-git commit -m "chore(constants): remove legacy dashboard footer and border chars
-
-Remove dashboardFooter (singular) and dashboardBorderChars from
-UI_STRINGS. These were kept for backward compatibility during Phase 3
-and are superseded by dashboardFooters (per-tab) and the frame()
-utility in overlay-render.ts.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
diff --git a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-6.md b/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-6.md
deleted file mode 100644
index d140bba..0000000
--- a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs-phase-6.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# Phase 6: Verification — Dashboard Overlay & Tabs
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Final verification sweep confirming no stale references, no type errors, and all tests pass after the full dashboard overlay refactor.
-
-**Parent plan:** `docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md`
-**Spec:** `docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md`
-
-**Preconditions:** Phases 1-5 are complete. The dashboard is a tabbed overlay using `frame()` and `renderTabBar()`. Legacy constants have been removed.
-**Postconditions:** The codebase is confirmed clean — no dead references, no type errors, all tests green. The refactor is fully complete.
-
----
-
-## Step 1: Run full test suite
-
-- [ ] Run all tests and confirm they pass.
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run
-```
-
-Expected: All tests pass with exit code 0. No skipped or failing tests.
-
----
-
-## Step 2: Type check
-
-- [ ] Run the TypeScript compiler in check-only mode.
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage && npx tsc --noEmit
-```
-
-Expected: Clean exit, no type errors.
-
----
-
-## Step 3: Grep for stale references to removed identifiers
-
-- [ ] Verify no code references identifiers that were removed during the refactor.
-
-Run each command and confirm no matches are found:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-
-# Legacy singular footer (should be dashboardFooters plural)
-grep -rn "dashboardFooter[^s]" src/ tests/ || echo "OK: No stale dashboardFooter refs"
-
-# Legacy border characters (replaced by frame() in overlay-render.ts)
-grep -rn "dashboardBorderChars" src/ tests/ || echo "OK: No stale dashboardBorderChars refs"
-
-# Legacy insights toggle field (replaced by activeTab === "insights")
-grep -rn "showInsights" src/ tests/ || echo "OK: No stale showInsights refs"
-
-# Legacy "v" key handler in dashboard (insights is now a tab, not a toggle)
-grep -rn '"v"' src/tui/dashboard.ts || echo "OK: No v-key handler in dashboard"
-```
-
-Expected: All four commands print their "OK" message. No matches found.
-
-If any stale references are found, fix them before proceeding to Step 4.
-
----
-
-## Step 4: Verify overlay options are present in `dashboard.ts`
-
-- [ ] Confirm the overlay configuration is correctly set in the `openDashboard` function.
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-grep -A5 "overlay:" src/tui/dashboard.ts
-```
-
-Expected output should include:
-
-```
-overlay: true,
-overlayOptions: {
- anchor: "center",
- maxHeight: "85%",
- width: "92%",
-}
-```
-
----
-
-## Step 5: Commit any remaining fixes (if needed)
-
-- [ ] If any issues were found and fixed in Steps 1-4, stage and commit them.
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add -A
-git commit -m "fix(tui): address remaining issues from tabbed overlay refactor
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
-
-If no fixes were needed, skip this step — the refactor is complete.
diff --git a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md b/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md
deleted file mode 100644
index 385fbff..0000000
--- a/docs/superpowers/plans/2026-07-05-dashboard-overlay-tabs.md
+++ /dev/null
@@ -1,1614 +0,0 @@
-# Dashboard Overlay & Tabs Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Refactor the pi-usage dashboard from a vertically-stacked custom component into a centered overlay with 3 tabbed panes (Usage Statistics, Current Usage, Insights).
-
-**Architecture:** Single `UsageDashboardComponent` class restructured around tab state. New `overlay-render.ts` provides `frame()`, `renderTabBar()`, and `pad()` utilities replicated from the pi-extension-manager pattern. The overlay uses `ctx.ui.custom()` with `{ overlay: true, overlayOptions: { anchor: "center", maxHeight: "85%", width: "92%" } }`.
-
-**Tech Stack:** TypeScript, `@earendil-works/pi-tui` (Component, Key, matchesKey, visibleWidth, truncateToWidth), `@earendil-works/pi-coding-agent` (Theme), Vitest for tests.
-
-**Reference repos:**
-
-- pi-extension-manager: `/Users/lanh/Developer/pi-packages/vanillagreencom-vstack/pi-extensions/pi-extension-manager` -- overlay pattern, frame/tab bar rendering, pill styling
-- pi TUI framework: `/Users/lanh/Developer/pi-packages/pi` -- Component/Theme/TUI/OverlayOptions interfaces
-
-**Spec:** `docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md`
-
----
-
-## Execution Phases
-
-Each phase is atomic -- all tests pass after completion. Phases 1-3 touch independent files and can run in parallel.
-
-| Phase | Tasks | Description | Result |
-|-------|-------|-------------|--------|
-| **1. Theme foundation** | Task 1 | Add `inverse` + `bg` to `DashboardTheme`, update adapters and test helper | Theme API ready for tab pills |
-| **2. Render utilities** | Task 2 | Create `overlay-render.ts` with `frame()`, `renderTabBar()`, `pad()` + tests | Utilities exist, dashboard unchanged |
-| **3. Constants prep** | Task 3 | Add per-tab `dashboardFooters` alongside legacy `dashboardFooter` | New constants available, legacy kept |
-| **4. Core refactor + tests** | Tasks 4 + 6 | Refactor dashboard state/render/input to tabs + overlay. Update all dashboard tests. | Dashboard is tabbed overlay, tests green |
-| **5. Legacy cleanup** | Task 5 | Remove old `dashboardFooter`, `dashboardBorderChars` | No dead code |
-| **6. Verification** | Task 7 | Full suite, type check, grep for stale references | Confirmed clean |
-
----
-
-## File Map
-
-| File | Action | Responsibility |
-| ------------------------------- | ------ | ------------------------------------------------------------------------ |
-| `src/tui/dashboard-theme.ts` | Modify | Add `inverse`, `bg` methods; add `"borderAccent"`, `"selectedBg"` colors |
-| `src/tui/overlay-render.ts` | Create | `frame()`, `renderTabBar()`, `pad()` utilities |
-| `src/shared/constants.ts` | Modify | Per-tab footer strings, updated border chars |
-| `src/tui/dashboard.ts` | Modify | Tab state, refactored `render()` and `handleInput()`, overlay options |
-| `tests/dashboard-theme.test.ts` | Create | Tests for new theme methods |
-| `tests/overlay-render.test.ts` | Create | Tests for frame, tab bar, pad |
-| `tests/dashboard.test.ts` | Modify | Update all assertions for tabbed overlay output |
-
----
-
-## Task 1: Extend DashboardTheme with `inverse` and `bg`
-
-**Files:**
-
-- Modify: `src/tui/dashboard-theme.ts`
-- Create: `tests/dashboard-theme.test.ts`
-
-- [ ] **Step 1: Write failing tests for new theme methods**
-
-Create `tests/dashboard-theme.test.ts`:
-
-```typescript
-import { describe, expect, it } from "vitest";
-import {
- noTheme,
- fromPiTheme,
- type DashboardTheme,
-} from "../src/tui/dashboard-theme.ts";
-
-describe("DashboardTheme", () => {
- describe("noTheme", () => {
- it("inverse returns text unchanged", () => {
- expect(noTheme.inverse("hello")).toBe("hello");
- });
-
- it("bg returns text unchanged", () => {
- expect(noTheme.bg("selectedBg", "hello")).toBe("hello");
- });
- });
-
- describe("fromPiTheme", () => {
- it("delegates inverse to theme.inverse", () => {
- const piTheme = {
- fg: (_color: string, text: string) => text,
- bold: (text: string) => `${text}`,
- inverse: (text: string) => `${text}`,
- bg: (color: string, text: string) => `${text}`,
- };
- const theme = fromPiTheme(piTheme as never);
- expect(theme.inverse("test")).toBe("test");
- });
-
- it("delegates bg to theme.bg", () => {
- const piTheme = {
- fg: (_color: string, text: string) => text,
- bold: (text: string) => `${text}`,
- inverse: (text: string) => `${text}`,
- bg: (color: string, text: string) => `${text}`,
- };
- const theme = fromPiTheme(piTheme as never);
- expect(theme.bg("selectedBg", "test")).toBe("test");
- });
- });
-});
-```
-
-- [ ] **Step 2: Run tests to verify they fail**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run tests/dashboard-theme.test.ts`
-
-Expected: FAIL -- `noTheme.inverse` is not a function, `noTheme.bg` is not a function.
-
-- [ ] **Step 3: Add `inverse` and `bg` to DashboardTheme interface and implementations**
-
-In `src/tui/dashboard-theme.ts`, update the `DashboardTheme` interface:
-
-```typescript
-export interface DashboardTheme {
- /** Foreground color for a themed string. */
- fg: (color: DashboardColor, text: string) => string;
- /** Background color for a themed string. */
- bg: (color: DashboardColor, text: string) => string;
- /** Bold modifier. */
- bold: (text: string) => string;
- /** Dim modifier. */
- dim: (text: string) => string;
- /** Inverse modifier (swap fg/bg). */
- inverse: (text: string) => string;
-}
-```
-
-Update `DashboardColor` to include the new color roles:
-
-```typescript
-export type DashboardColor =
- | "accent"
- | "border"
- | "borderAccent"
- | "borderMuted"
- | "selectedBg"
- | "muted"
- | "dim"
- | "text";
-```
-
-Update `noTheme`:
-
-```typescript
-export const noTheme: DashboardTheme = {
- fg: (_color, text) => text,
- bg: (_color, text) => text,
- bold: (text) => text,
- dim: (text) => text,
- inverse: (text) => text,
-};
-```
-
-Update `fromPiTheme`:
-
-```typescript
-export function fromPiTheme(theme: Theme): DashboardTheme {
- return {
- fg: (color, text) => theme.fg(color, text),
- bg: (color, text) => theme.bg(color as never, text),
- bold: (text) => theme.bold(text),
- dim: (text) => theme.fg("dim", text),
- inverse: (text) => theme.inverse(text),
- };
-}
-```
-
-Note: `bg` casts to `never` because `DashboardColor` is a superset that includes fg-only colors. Only `"selectedBg"` is a valid `ThemeBg` key, but the cast keeps the adapter simple. The caller is responsible for passing valid bg colors.
-
-- [ ] **Step 4: Run new tests to verify they pass**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run tests/dashboard-theme.test.ts`
-
-Expected: PASS
-
-- [ ] **Step 5: Run all existing tests to verify no regressions**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run`
-
-Expected: All tests pass. The existing code still compiles because the new methods are additive.
-
-- [ ] **Step 6: Update `makeAnsiTheme` test helper for the new methods**
-
-In `tests/dashboard.test.ts`, update the `makeAnsiTheme` function to include `inverse` and `bg`:
-
-```typescript
-function makeAnsiTheme(): DashboardTheme & {
- calls: { method: string; color?: string; text: string }[];
-} {
- const calls: { method: string; color?: string; text: string }[] = [];
- const wrap = (open: string) => (text: string) => {
- if (text.length === 0) return text;
- return `${open}${text}${ANSI_ESCAPE}[0m`;
- };
- return {
- calls,
- fg: (color, text) => {
- calls.push({ method: "fg", color, text });
- return wrap(`${ANSI_ESCAPE}[38;5;75m`)(text);
- },
- bg: (color, text) => {
- calls.push({ method: "bg", color, text });
- return wrap(`${ANSI_ESCAPE}[48;5;236m`)(text);
- },
- bold: (text) => {
- calls.push({ method: "bold", text });
- return `${ANSI_ESCAPE}[1m${text}${ANSI_ESCAPE}[22m`;
- },
- dim: (text) => {
- calls.push({ method: "dim", text });
- return wrap(`${ANSI_ESCAPE}[38;5;243m`)(text);
- },
- inverse: (text) => {
- calls.push({ method: "inverse", text });
- return `${ANSI_ESCAPE}[7m${text}${ANSI_ESCAPE}[27m`;
- },
- };
-}
-```
-
-- [ ] **Step 7: Run all tests again**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run`
-
-Expected: All tests pass.
-
-- [ ] **Step 8: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/tui/dashboard-theme.ts tests/dashboard-theme.test.ts tests/dashboard.test.ts
-git commit -m "feat(tui): add inverse and bg methods to DashboardTheme
-
-Extend the theme adapter with inverse() for active tab pill styling
-and bg() for inactive tab pill background. Add selectedBg and
-borderAccent to DashboardColor. Update noTheme, fromPiTheme, and
-test helper.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
-
----
-
-## Task 2: Create overlay-render.ts with frame, tab bar, and pad utilities
-
-**Files:**
-
-- Create: `src/tui/overlay-render.ts`
-- Create: `tests/overlay-render.test.ts`
-
-**Reference:** The pi-extension-manager's `extensions/manager/render.ts` (frame, renderTabBar, pad, pill helpers) and `extensions/manager/types.ts` (POPUP_PADDING_X/Y constants). Replicate the patterns using `DashboardTheme` instead of Pi's `Theme` directly.
-
-- [ ] **Step 1: Write failing tests for `pad`**
-
-Create `tests/overlay-render.test.ts`:
-
-```typescript
-import { describe, expect, it } from "vitest";
-import { pad, frame, renderTabBar } from "../src/tui/overlay-render.ts";
-import { noTheme } from "../src/tui/dashboard-theme.ts";
-
-describe("pad", () => {
- it("pads short text with trailing spaces", () => {
- expect(pad("hi", 10)).toBe("hi ");
- });
-
- it("returns text unchanged when it fills the width", () => {
- expect(pad("hello", 5)).toBe("hello");
- });
-
- it("truncates text that exceeds the width", () => {
- const result = pad("hello world", 5);
- expect(result.length).toBeLessThanOrEqual(5);
- });
-
- it("handles zero width", () => {
- expect(pad("hi", 0)).toBe("");
- });
-});
-```
-
-- [ ] **Step 2: Write failing tests for `frame`**
-
-Append to `tests/overlay-render.test.ts`:
-
-```typescript
-describe("frame", () => {
- it("wraps content lines in a bordered box", () => {
- const lines = frame(["hello", "world"], 20, noTheme);
- // Top border
- expect(lines[0]).toContain("┏");
- expect(lines[0]).toContain("┓");
- // Bottom border
- expect(lines[lines.length - 1]).toContain("┗");
- expect(lines[lines.length - 1]).toContain("┛");
- // Content lines have vertical borders
- const contentLine = lines.find((l) => l.includes("hello"));
- expect(contentLine).toBeDefined();
- expect(contentLine).toContain("┃");
- });
-
- it("includes padding rows above and below content", () => {
- const lines = frame(["test"], 20, noTheme);
- // Structure: top border, 1 padding row, content, 1 padding row, bottom border
- expect(lines.length).toBe(5); // 1 top + 1 pad + 1 content + 1 pad + 1 bottom
- });
-
- it("truncates overflow with indicator when fixedInnerRows is set", () => {
- const content = ["line1", "line2", "line3", "line4", "line5"];
- const lines = frame(content, 30, noTheme, 3);
- const joined = lines.join("\n");
- expect(joined).toContain("line1");
- expect(joined).toContain("line2");
- expect(joined).toContain("more line(s)");
- expect(joined).not.toContain("line5");
- });
-
- it("renders a title in the top border when provided", () => {
- const lines = frame(["content"], 30, noTheme, undefined, "My Title");
- expect(lines[0]).toContain("My Title");
- expect(lines[0]).toContain("┏");
- });
-
- it("pads content to frameContentWidth", () => {
- const lines = frame(["hi"], 20, noTheme);
- // Content line: ┃ + 2 pad + content padded to contentWidth + 2 pad + ┃
- // All lines should be exactly 20 chars wide (in visible width)
- for (const line of lines) {
- expect(line.length).toBeLessThanOrEqual(20);
- }
- });
-});
-```
-
-- [ ] **Step 3: Write failing tests for `renderTabBar`**
-
-Append to `tests/overlay-render.test.ts`:
-
-```typescript
-describe("renderTabBar", () => {
- const tabs = [
- { id: "stats", label: "Usage Statistics" },
- { id: "current", label: "Current Usage" },
- { id: "insights", label: "Insights" },
- ];
-
- it("renders all tabs with active tab highlighted", () => {
- const result = renderTabBar(tabs, "stats", 80, noTheme);
- expect(result).toContain("Usage Statistics");
- expect(result).toContain("Current Usage");
- expect(result).toContain("Insights");
- });
-
- it("returns empty padding for no tabs", () => {
- const result = renderTabBar([], "stats", 20, noTheme);
- expect(result.trim()).toBe("");
- });
-
- it("shows overflow indicators when tabs exceed width", () => {
- const result = renderTabBar(tabs, "insights", 20, noTheme);
- // At 20 chars wide, not all tabs can fit
- expect(result).toContain("Insights");
- });
-
- it("pads result to the requested width", () => {
- const result = renderTabBar(tabs, "stats", 80, noTheme);
- // Result should be padded to exactly 80 visible chars
- expect(result.length).toBe(80);
- });
-});
-```
-
-- [ ] **Step 4: Run tests to verify they fail**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run tests/overlay-render.test.ts`
-
-Expected: FAIL -- module `../src/tui/overlay-render.ts` not found.
-
-- [ ] **Step 5: Implement `overlay-render.ts`**
-
-Create `src/tui/overlay-render.ts`:
-
-```typescript
-/**
- * Overlay rendering utilities for the usage dashboard.
- *
- * Replicates patterns from the pi-extension-manager's render helpers using
- * the dashboard's own DashboardTheme adapter so colors flow through Pi's
- * live theme.
- */
-import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
-import type { DashboardTheme, DashboardColor } from "./dashboard-theme.ts";
-
-// ── Layout constants ────────────────────────────────────────────────────
-
-const PADDING_X = 2;
-const PADDING_Y = 1;
-
-// ── Frame glyphs ────────────────────────────────────────────────────────
-
-const FRAME = { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" } as const;
-
-// ── Tab types ───────────────────────────────────────────────────────────
-
-export interface DashboardTab {
- id: string;
- label: string;
-}
-
-// ── Utilities ───────────────────────────────────────────────────────────
-
-/**
- * Pad `text` to exactly `width` visible columns. Truncates if wider.
- */
-export function pad(text: string, width: number): string {
- if (width <= 0) return "";
- const truncated = truncateToWidth(text, width, "");
- return `${truncated}${" ".repeat(Math.max(0, width - visibleWidth(truncated)))}`;
-}
-
-/**
- * Calculate the content width inside a frame (excluding borders and padding).
- */
-export function frameContentWidth(width: number): number {
- return Math.max(1, width - 2 - PADDING_X * 2);
-}
-
-// ── Frame ───────────────────────────────────────────────────────────────
-
-/**
- * Wrap content `lines` in a bordered frame box.
- *
- * Matches the pi-extension-manager's frame rendering:
- * - Top/bottom borders with optional title
- * - PADDING_Y blank rows above/below content
- * - PADDING_X space columns on each side of content
- * - Overflow truncation with "↓ N more line(s)" when fixedInnerRows is set
- */
-export function frame(
- lines: string[],
- width: number,
- theme: DashboardTheme,
- fixedInnerRows?: number,
- title = "",
-): string[] {
- const inner = Math.max(1, width - 2);
- const contentWidth = frameContentWidth(width);
- const border = (s: string) => theme.fg("borderAccent", s);
-
- let body = lines;
- if (fixedInnerRows !== undefined && body.length > fixedInnerRows) {
- const hidden = body.length - fixedInnerRows + 1;
- body = [
- ...body.slice(0, Math.max(0, fixedInnerRows - 1)),
- theme.fg("dim", `↓ ${hidden} more line(s)`),
- ].slice(0, fixedInnerRows);
- }
-
- const blank = `${border(FRAME.v)}${" ".repeat(inner)}${border(FRAME.v)}`;
-
- const top = (): string => {
- if (!title) {
- return `${border(FRAME.tl)}${border(FRAME.h.repeat(inner))}${border(FRAME.tr)}`;
- }
- const titleText = ` ${truncateToWidth(title, Math.max(1, inner - 2), "…")} `;
- const fill = Math.max(1, inner - visibleWidth(titleText));
- return `${border(FRAME.tl)}${theme.fg("accent", titleText)}${border(FRAME.h.repeat(fill))}${border(FRAME.tr)}`;
- };
-
- const out = [top()];
- for (let i = 0; i < PADDING_Y; i += 1) out.push(blank);
- for (const line of body) {
- out.push(
- `${border(FRAME.v)}${" ".repeat(PADDING_X)}${pad(line, contentWidth)}${" ".repeat(PADDING_X)}${border(FRAME.v)}`,
- );
- }
- for (let i = 0; i < PADDING_Y; i += 1) out.push(blank);
- out.push(
- `${border(FRAME.bl)}${border(FRAME.h.repeat(inner))}${border(FRAME.br)}`,
- );
- return out.map((line) => truncateToWidth(line, width, ""));
-}
-
-// ── Tab bar ─────────────────────────────────────────────────────────────
-
-function activePill(theme: DashboardTheme, label: string): string {
- return theme.fg("accent", theme.inverse(theme.bold(label)));
-}
-
-function inactivePill(theme: DashboardTheme, label: string): string {
- return theme.bg("selectedBg", theme.fg("accent", label));
-}
-
-/**
- * Render a tab bar with pill-styled active/inactive tabs.
- *
- * Dynamic visibility: expands tabs around the active tab to fit `width`,
- * showing ‹/› indicators when tabs overflow.
- */
-export function renderTabBar(
- tabs: DashboardTab[],
- activeId: string,
- width: number,
- theme: DashboardTheme,
-): string {
- const safeWidth = Math.max(1, width);
- if (tabs.length === 0) return " ".repeat(safeWidth);
-
- const activeIndex = Math.max(
- 0,
- tabs.findIndex((tab) => tab.id === activeId),
- );
- const widths = tabs.map((tab) => visibleWidth(tab.label) + 2); // " label "
-
- const sliceWidth = (s: number, e: number): number => {
- let total = 0;
- for (let i = s; i < e; i += 1) total += widths[i]!;
- total += Math.max(0, e - s - 1); // single-space gaps
- total += s > 0 ? 2 : 0; // "‹ "
- total += e < tabs.length ? 2 : 0; // " ›"
- return total;
- };
-
- let start = activeIndex;
- let end = activeIndex + 1;
- let preferRight = true;
- while (start > 0 || end < tabs.length) {
- let progressed = false;
- const tryRight = (): boolean => {
- if (end < tabs.length && sliceWidth(start, end + 1) <= safeWidth) {
- end += 1;
- return true;
- }
- return false;
- };
- const tryLeft = (): boolean => {
- if (start > 0 && sliceWidth(start - 1, end) <= safeWidth) {
- start -= 1;
- return true;
- }
- return false;
- };
- if (preferRight) {
- if (tryRight()) progressed = true;
- if (tryLeft()) progressed = true;
- } else {
- if (tryLeft()) progressed = true;
- if (tryRight()) progressed = true;
- }
- if (!progressed) break;
- preferRight = !preferRight;
- }
-
- const cells = tabs.slice(start, end).map((tab) => {
- const label = ` ${tab.label} `;
- return tab.id === activeId
- ? activePill(theme, label)
- : inactivePill(theme, label);
- });
- if (start > 0) cells.unshift(theme.fg("dim", "‹"));
- if (end < tabs.length) cells.push(theme.fg("dim", "›"));
- return pad(cells.join(" "), safeWidth);
-}
-```
-
-- [ ] **Step 6: Run tests to verify they pass**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run tests/overlay-render.test.ts`
-
-Expected: All PASS.
-
-- [ ] **Step 7: Run full test suite**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run`
-
-Expected: All tests pass. The new file is additive -- nothing imports it yet.
-
-- [ ] **Step 8: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/tui/overlay-render.ts tests/overlay-render.test.ts
-git commit -m "feat(tui): add overlay rendering utilities
-
-Add frame(), renderTabBar(), and pad() in overlay-render.ts.
-Replicates pi-extension-manager patterns using DashboardTheme
-for Pi theme integration.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
-
----
-
-## Task 3: Update constants for tabbed layout
-
-**Files:**
-
-- Modify: `src/shared/constants.ts`
-
-- [ ] **Step 1: Update `UI_STRINGS` with per-tab footers and frame chars**
-
-In `src/shared/constants.ts`, replace the footer and border definitions:
-
-```typescript
-export const UI_STRINGS = {
- dashboardTitle: "Pi Usage Dashboard",
- dashboardFooters: {
- statistics: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[Up/Down] Row",
- "[Enter] Expand",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- current: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Provider",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- insights: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- },
- dashboardBorderedSectionTitles: {
- usageStatistics: "Usage Statistics",
- currentUsage: "Current Usage",
- insights: "Insights",
- notes: "Notes",
- },
- dashboardDefaultPeriod: "allTime" as UsageWindow,
-} as const;
-```
-
-Note: The old `dashboardFooter` (singular) and `dashboardBorderChars` are removed. Border rendering is now handled by `frame()` in `overlay-render.ts`.
-
-- [ ] **Step 2: Fix any compile errors from the removal**
-
-Check `src/tui/dashboard.ts` for references to `UI_STRINGS.dashboardFooter` and `UI_STRINGS.dashboardBorderChars`. These will cause type errors -- that's expected. They will be fixed in Task 4 when the dashboard render pipeline is refactored. For now, temporarily keep the old properties alongside the new ones to avoid breaking the build:
-
-```typescript
-export const UI_STRINGS = {
- dashboardTitle: "Pi Usage Dashboard",
- // New per-tab footers
- dashboardFooters: {
- statistics: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[Up/Down] Row",
- "[Enter] Expand",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- current: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Provider",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- insights: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- },
- // Legacy -- removed in Task 4 when dashboard.ts stops referencing them
- dashboardFooter: [
- "[Tab/Shift-Tab] Provider",
- "[Left/Right] Period",
- "[Up/Down] Row",
- "[Enter/Space] Expand/Collapse",
- "[v] Insights",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- dashboardBorderedSectionTitles: {
- usageStatistics: "Usage Statistics",
- currentUsage: "Current Usage",
- insights: "Insights",
- notes: "Notes",
- },
- dashboardBorderChars: {
- topLeft: "\u256D",
- topRight: "\u256E",
- bottomLeft: "\u2570",
- bottomRight: "\u256F",
- horizontal: "\u2500",
- separatorLeft: "\u251C",
- separatorRight: "\u2524",
- },
- dashboardDefaultPeriod: "allTime" as UsageWindow,
-} as const;
-```
-
-- [ ] **Step 3: Run all tests**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run`
-
-Expected: All tests pass (legacy properties kept for backward compatibility).
-
-- [ ] **Step 4: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/shared/constants.ts
-git commit -m "feat(constants): add per-tab footer strings
-
-Add dashboardFooters with context-aware key hints for each tab.
-Legacy dashboardFooter and dashboardBorderChars retained until
-the dashboard render pipeline is refactored.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
-
----
-
-## Task 4: Refactor dashboard component to tabbed overlay
-
-This is the main refactor. The component gains tab state, the render pipeline uses `frame()` + `renderTabBar()`, and `handleInput()` is restructured for tab-based navigation.
-
-**Files:**
-
-- Modify: `src/tui/dashboard.ts`
-
-- [ ] **Step 1: Add tab state and type**
-
-At the top of `src/tui/dashboard.ts`, add the tab type and tab definitions:
-
-```typescript
-import {
- type DashboardTab,
- frame,
- frameContentWidth,
- renderTabBar,
-} from "./overlay-render.ts";
-import { UI_STRINGS } from "../shared/constants.ts";
-
-type DashboardTabId = "statistics" | "current" | "insights";
-
-const DASHBOARD_TABS: DashboardTab[] = [
- {
- id: "statistics",
- label: UI_STRINGS.dashboardBorderedSectionTitles.usageStatistics,
- },
- {
- id: "current",
- label: UI_STRINGS.dashboardBorderedSectionTitles.currentUsage,
- },
- { id: "insights", label: UI_STRINGS.dashboardBorderedSectionTitles.insights },
-];
-```
-
-In the `UsageDashboardComponent` class, replace the `showInsights` field and add `activeTab` + `insightsPeriodIndex`:
-
-```typescript
-export class UsageDashboardComponent implements Component {
- private activeTab: DashboardTabId = "statistics";
- private periodIndex = DEFAULT_PERIOD_INDEX;
- private insightsPeriodIndex = DEFAULT_PERIOD_INDEX;
- private rowIndex = 0;
- private expandedProvider: string | null = null;
- private currentUsageProviderIndex: number;
- // ... rest unchanged
-```
-
-Remove the `private showInsights = false;` line.
-
-- [ ] **Step 2: Add tab switching helper**
-
-Add a `switchTab` method to the component class:
-
-```typescript
-private switchTab(delta: number): void {
- const ids = DASHBOARD_TABS.map((t) => t.id);
- const currentIndex = ids.indexOf(this.activeTab);
- const next = (currentIndex + delta + ids.length) % ids.length;
- this.activeTab = ids[next] as DashboardTabId;
-}
-```
-
-- [ ] **Step 3: Refactor `handleInput` for tab-based navigation**
-
-Replace the `handleInput` method:
-
-```typescript
-handleInput(data: string): void {
- // Global keys
- if (data === "q" || matchesKey(data, Key.escape)) {
- this.invalidate();
- this.cancelScan?.();
- this.done();
- return;
- }
- if (matchesKey(data, Key.tab)) {
- this.switchTab(1);
- return;
- }
- if (matchesKey(data, SHIFT_TAB_KEY)) {
- this.switchTab(-1);
- return;
- }
-
- // Per-tab contextual keys
- switch (this.activeTab) {
- case "statistics":
- this.handleStatisticsInput(data);
- break;
- case "current":
- this.handleCurrentUsageInput(data);
- break;
- case "insights":
- this.handleInsightsInput(data);
- break;
- }
-}
-
-private handleStatisticsInput(data: string): void {
- if (matchesKey(data, Key.left)) {
- this.movePeriod(-1);
- return;
- }
- if (matchesKey(data, Key.right)) {
- this.movePeriod(1);
- return;
- }
- const period = this.currentPeriod();
- if (!period) return;
- if (matchesKey(data, Key.down)) {
- this.rowIndex = Math.min(
- this.rowIndex + 1,
- Math.max(0, period.providers.length - 1),
- );
- }
- if (matchesKey(data, Key.up)) {
- this.rowIndex = Math.max(0, this.rowIndex - 1);
- }
- if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
- const provider = period.providers[this.rowIndex]?.key;
- if (!provider) return;
- this.expandedProvider =
- this.expandedProvider === provider ? null : provider;
- }
-}
-
-private handleCurrentUsageInput(data: string): void {
- if (matchesKey(data, Key.left)) {
- this.moveProvider(-1);
- return;
- }
- if (matchesKey(data, Key.right)) {
- this.moveProvider(1);
- return;
- }
-}
-
-private handleInsightsInput(data: string): void {
- if (matchesKey(data, Key.left)) {
- this.moveInsightsPeriod(-1);
- return;
- }
- if (matchesKey(data, Key.right)) {
- this.moveInsightsPeriod(1);
- return;
- }
-}
-
-private moveInsightsPeriod(delta: number): void {
- this.insightsPeriodIndex =
- (this.insightsPeriodIndex + delta + PERIODS.length) % PERIODS.length;
-}
-```
-
-- [ ] **Step 4: Refactor `render()` to use frame + tab bar + per-tab content**
-
-Replace the `render` method:
-
-```typescript
-render(width: number): string[] {
- const w = Math.max(8, width);
- const contentWidth = frameContentWidth(w);
- const lines: string[] = [];
-
- // Tab bar
- lines.push(renderTabBar(DASHBOARD_TABS, this.activeTab, contentWidth, this.theme));
- lines.push("");
-
- // Active tab content
- switch (this.activeTab) {
- case "statistics":
- this.renderUsageStatisticsTab(contentWidth, lines);
- break;
- case "current":
- this.renderCurrentUsageTab(contentWidth, lines);
- break;
- case "insights":
- this.renderInsightsTab(contentWidth, lines);
- break;
- }
-
- // Footer
- lines.push("");
- lines.push(this.renderFooter(contentWidth));
-
- return frame(lines, w, this.theme);
-}
-```
-
-- [ ] **Step 5: Rename section renderers to tab renderers**
-
-Rename `renderUsageStatistics` to `renderUsageStatisticsTab` and remove the insights toggle:
-
-```typescript
-private renderUsageStatisticsTab(w: number, lines: string[]): void {
- lines.push(
- ...this.renderTabs(
- PERIODS.map((period) => PERIOD_LABELS[period]),
- this.periodIndex,
- w,
- ),
- );
- if (this.state.loading) {
- lines.push(this.theme.dim("Loading session history..."));
- }
- lines.push("");
-
- const period = this.currentPeriod();
- if (!period || period.total.messageCount === 0) {
- lines.push(this.theme.dim("No local session usage found."));
- return;
- }
- const columns = tableColumns(w);
- const providerWidth = labelWidth(columns, w);
- lines.push(tableLine("Provider / Model", columns, providerWidth));
- lines.push(this.theme.fg("borderMuted", separator(columns, providerWidth)));
- period.providers.forEach((row, index) => {
- const expanded = this.expandedProvider === row.key;
- lines.push(
- this.renderProviderRow(
- row,
- index === this.rowIndex,
- expanded,
- columns,
- providerWidth,
- ),
- );
- if (expanded) {
- for (const model of period.modelsByProvider[row.key] ?? []) {
- lines.push(this.renderModelRow(model, columns, providerWidth));
- }
- }
- });
- lines.push(this.theme.fg("borderMuted", separator(columns, providerWidth)));
- lines.push(tableLine("Total", columns, providerWidth, period.total));
- lines.push("");
- lines.push(...this.renderLegend(w));
-}
-```
-
-Rename `renderCurrentUsage` to `renderCurrentUsageTab` and move diagnostics into it:
-
-```typescript
-private renderCurrentUsageTab(w: number, lines: string[]): void {
- const providers = liveProviders(this.state);
- if (providers.length === 0) {
- lines.push(this.theme.dim("No live usage details."));
- return;
- }
- this.currentUsageProviderIndex = Math.min(
- this.currentUsageProviderIndex,
- Math.max(0, providers.length - 1),
- );
- lines.push(
- ...this.renderTabs(
- providers.map((provider) => provider.providerLabel),
- this.currentUsageProviderIndex,
- w,
- ),
- );
- lines.push("");
-
- const referenceTime = Math.max(
- this.state.generatedAt,
- ...providers.map((provider) => provider.fetchedAt),
- 0,
- );
- const selected = providers[this.currentUsageProviderIndex];
- lines.push(
- this.theme.fg(
- "accent",
- this.theme.bold(providerHeading(selected, referenceTime)),
- ),
- );
- if (selected.windows.length === 0 && selected.balances.length === 0) {
- lines.push(this.theme.dim("No live usage details."));
- } else {
- lines.push(...this.renderQuotaWindows(selected.windows));
- for (const balance of selected.balances) {
- const value =
- balance.unit === "USD"
- ? formatCurrency(balance.remaining ?? undefined)
- : formatAbbrev(balance.remaining ?? undefined);
- const unitSuffix = balance.unit === "USD" ? "" : ` ${balance.unit}`;
- const labelStyled = this.theme.dim(`${balance.label}:`);
- lines.push(`${labelStyled} ${value}${unitSuffix}`);
- }
- }
-
- // Diagnostics (previously a separate section)
- this.renderDiagnostics(lines);
-}
-```
-
-Add a new `renderInsightsTab`:
-
-```typescript
-private renderInsightsTab(w: number, lines: string[]): void {
- lines.push(
- ...this.renderTabs(
- PERIODS.map((period) => PERIOD_LABELS[period]),
- this.insightsPeriodIndex,
- w,
- ),
- );
- lines.push("");
-
- if (this.state.insights.length === 0) {
- lines.push(this.theme.dim("No insights yet."));
- } else {
- lines.push(...this.renderInsightsByCategory(w));
- }
-}
-```
-
-- [ ] **Step 6: Update `renderFooter` to be context-aware**
-
-Replace the `renderFooter` method:
-
-```typescript
-private renderFooter(width: number): string {
- const footerKey = this.activeTab === "statistics"
- ? "statistics"
- : this.activeTab === "current"
- ? "current"
- : "insights";
- return this.theme.dim(truncateVisible(UI_STRINGS.dashboardFooters[footerKey], width));
-}
-```
-
-- [ ] **Step 7: Remove dead code**
-
-Remove these methods/fields that are no longer needed:
-
-- `borderLine(width)` method
-- `currentUsageHeaderSeparator(width)` method
-- `bottomBorder(width)` method
-- `sectionTitle(text)` method (section titles are now tab names in the tab bar)
-- The old `render()` method body (already replaced in step 4)
-- Remove imports of `horizontalBorder` and `borderSeparator` helper functions
-- Remove the `horizontalBorder` and `borderSeparator` free functions
-
-- [ ] **Step 8: Update `openDashboard` to use overlay options**
-
-In the `openDashboard` function at the bottom of `dashboard.ts`:
-
-```typescript
-export async function openDashboard(
- ctx: ExtensionCommandContext,
- state: UsageCoreState,
- cancelScan?: () => void,
-): Promise {
- await ctx.ui.custom(
- (tui, theme, _keys, done) => {
- const piTheme = theme as unknown as Parameters[0];
- const dashboardTheme: DashboardTheme =
- piTheme &&
- typeof (piTheme as { fg?: unknown }).fg === "function" &&
- typeof (piTheme as { bold?: unknown }).bold === "function"
- ? fromPiTheme(piTheme)
- : noTheme;
- return new UsageDashboardComponent(state, done, {
- tui,
- theme: dashboardTheme,
- cancelScan,
- });
- },
- {
- overlay: true,
- overlayOptions: {
- anchor: "center",
- maxHeight: "85%",
- width: "92%",
- },
- },
- );
-}
-```
-
-- [ ] **Step 9: Verify the build compiles**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx tsc --noEmit`
-
-Expected: No type errors. If any references to the old `dashboardFooter` or `dashboardBorderChars` remain, fix them.
-
-- [ ] **Step 10: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/tui/dashboard.ts
-git commit -m "feat(tui): refactor dashboard to tabbed overlay
-
-Replace vertical stacking with 3 tabs: Usage Statistics, Current
-Usage, Insights. Use frame() and renderTabBar() from overlay-render.
-Tab/Shift-Tab switches tabs, Left/Right is contextual per tab.
-Diagnostics moved into Current Usage tab. Enable overlay mode with
-centered anchor, 85% maxHeight, 92% width.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
-
----
-
-## Task 5: Remove legacy constants
-
-**Files:**
-
-- Modify: `src/shared/constants.ts`
-
-- [ ] **Step 1: Remove legacy footer and border char properties**
-
-In `src/shared/constants.ts`, remove the `dashboardFooter` and `dashboardBorderChars` properties from `UI_STRINGS` that were kept for backward compatibility in Task 3:
-
-```typescript
-export const UI_STRINGS = {
- dashboardTitle: "Pi Usage Dashboard",
- dashboardFooters: {
- statistics: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[Up/Down] Row",
- "[Enter] Expand",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- current: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Provider",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- insights: [
- "[Tab/Shift-Tab] Switch tab",
- "[Left/Right] Period",
- "[q/Esc] Close",
- ].join(" \u2022 "),
- },
- dashboardBorderedSectionTitles: {
- usageStatistics: "Usage Statistics",
- currentUsage: "Current Usage",
- insights: "Insights",
- notes: "Notes",
- },
- dashboardDefaultPeriod: "allTime" as UsageWindow,
-} as const;
-```
-
-- [ ] **Step 2: Verify no remaining references**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && grep -r "dashboardFooter[^s]" src/ tests/ || echo "No references"` and `grep -r "dashboardBorderChars" src/ tests/ || echo "No references"`
-
-Expected: No references found (tests not yet updated may still reference them -- if so, do not remove yet and defer to Task 6).
-
-- [ ] **Step 3: Verify build compiles**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx tsc --noEmit`
-
-Expected: No type errors.
-
-- [ ] **Step 4: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add src/shared/constants.ts
-git commit -m "chore(constants): remove legacy dashboard footer and border chars
-
-These are superseded by dashboardFooters (per-tab) and the frame()
-utility in overlay-render.ts.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
-
----
-
-## Task 6: Update dashboard tests
-
-**Files:**
-
-- Modify: `tests/dashboard.test.ts`
-
-The tests need to account for:
-
-1. Output is now wrapped in a `frame()` -- top/bottom borders are `┏━┓` / `┗━┛` instead of `╭─╮` / `╰─╯`
-2. A tab bar appears at the top with `Usage Statistics`, `Current Usage`, `Insights`
-3. Only the active tab's content is rendered (no stacking of all sections)
-4. `Tab`/`Shift-Tab` switches tabs instead of cycling providers
-5. Provider cycling uses `Left`/`Right` within the Current Usage tab
-6. `v` key no longer toggles insights
-7. Insights is a separate tab with its own period selector
-8. Footer is context-aware per tab
-9. Diagnostics appear inside the Current Usage tab
-
-- [ ] **Step 1: Update the main render test**
-
-Replace the `"renders usage statistics + current usage with selected provider details"` test:
-
-```typescript
-it("renders Usage Statistics tab by default with table and legend", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- const out = c.render(140).join("\n");
-
- // Frame borders
- expect(out).toContain("┏");
- expect(out).toContain("┛");
-
- // Tab bar with Usage Statistics active
- expect(out).toContain("Usage Statistics");
- expect(out).toContain("Current Usage");
- expect(out).toContain("Insights");
-
- // Default period is All Time
- expect(out).toContain("[All Time]");
- expect(out).toContain("Provider / Model");
- expect(out).toContain("openai-codex");
- expect(out).toContain("428k");
-
- expect(out).toContain("Tokens = Input + Output + CacheW");
-
- // Current Usage content should NOT be visible on the Statistics tab
- expect(out).not.toContain("Command Code (Go) \u2022 live \u2022 4s old");
- expect(out).not.toContain("57% left");
-});
-```
-
-- [ ] **Step 2: Add test for Current Usage tab**
-
-```typescript
-it("renders Current Usage tab with provider details and diagnostics", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
- const out = c.render(140).join("\n");
-
- expect(out).toContain("Command Code (Go) \u2022 live \u2022 4s old");
- expect(out).toContain("57% left");
- expect(out).toContain(expectedResetText(Date.parse("2026-06-07T11:47:00")));
-
- // Diagnostics should appear in Current Usage tab
- expect(out).toContain("Subscription endpoint unavailable.");
-
- // Usage Statistics table should NOT be visible
- expect(out).not.toContain("Provider / Model");
- expect(out).not.toContain("[All Time]");
-});
-```
-
-- [ ] **Step 3: Add test for Insights tab**
-
-```typescript
-it("renders Insights tab with insights grouped by category", () => {
- const state = mkState();
- state.insights = [
- { category: "project", label: "career-ops", cost: 9, detail: "90.0%" },
- { category: "project", label: "dotfiles", cost: 1, detail: "10.0%" },
- {
- category: "cost",
- label: "Large context",
- cost: 5,
- detail: "50.0% over 150k context",
- },
- ];
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Insights tab (Tab twice)
- c.handleInput("\t");
- c.handleInput("\t");
- const out = c.render(100).join("\n");
-
- expect(out).toContain("Projects");
- expect(out).toContain("career-ops");
- expect(out).toContain("90.0%");
- expect(out).toContain("Cost patterns");
- expect(out).toContain("Large context");
-
- // Should have its own period selector
- expect(out).toContain("[All Time]");
-});
-```
-
-- [ ] **Step 4: Update tab/provider navigation tests**
-
-Replace the `"wraps joined legend and supports tab-based provider navigation"` test:
-
-```typescript
-it("supports provider navigation with left/right in Current Usage tab", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- // Switch to Current Usage tab
- c.handleInput("\t");
-
- // Left arrow cycles providers
- c.handleInput("\u001b[D"); // Left
- let out = c.render(120).join("\n");
- expect(out).toContain("OpenRouter");
-
- // Right arrow cycles back
- c.handleInput("\u001b[C"); // Right
- out = c.render(120).join("\n");
- expect(out).toContain("Command Code (Go)");
-});
-```
-
-- [ ] **Step 5: Update the insights toggle test**
-
-Replace the `"uses enter/space for expand, v for insights, and left/right for period changes"` test:
-
-```typescript
-it("uses enter/space for expand and left/right for period changes in Statistics tab", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
-
- // Enter expands the selected provider row to reveal its model rows.
- c.handleInput("\r");
- expect(c.render(120).join("\n")).toContain("gpt-5");
-
- // Left/Right change the period. Default is All Time (index 3); one Left
- // press moves to Last Week.
- c.handleInput("\u001b[D");
- expect(c.render(120).join("\n")).toContain("[Last Week]");
-
- // Two more Right presses move through This Week back to Today.
- c.handleInput("\u001b[C");
- c.handleInput("\u001b[C");
- expect(c.render(120).join("\n")).toContain("[Today]");
-});
-```
-
-- [ ] **Step 6: Update the insights grouping test**
-
-Replace the `"renders insights grouped by category"` test. This now uses tab switching instead of `v`:
-
-```typescript
-it("renders insights grouped by category in Insights tab", () => {
- const state = mkState();
- state.insights = [
- { category: "project", label: "career-ops", cost: 9, detail: "90.0%" },
- { category: "project", label: "dotfiles", cost: 1, detail: "10.0%" },
- {
- category: "cost",
- label: "Large context",
- cost: 5,
- detail: "50.0% over 150k context",
- },
- ];
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Insights tab
- c.handleInput("\t");
- c.handleInput("\t");
- const lines = c.render(100);
- const out = lines.join("\n");
- expect(out).toContain("Projects");
- expect(out).toContain("career-ops");
- expect(out).toContain("90.0%");
- expect(out).toContain("Cost patterns");
- expect(out).toContain("Large context");
- expect(out).toContain(" - Large context:");
-});
-```
-
-- [ ] **Step 7: Update the "defaults insights without category" test**
-
-```typescript
-it("defaults insights without category to cost patterns", () => {
- const state = mkState();
- state.insights = [{ label: "No category", cost: 1, detail: "test" }];
- const c = new UsageDashboardComponent(state, () => undefined, {
- theme: noTheme,
- });
- // Switch to Insights tab
- c.handleInput("\t");
- c.handleInput("\t");
- const out = c.render(100).join("\n");
- expect(out).toContain("Cost patterns");
- expect(out).toContain(" - No category:");
-});
-```
-
-- [ ] **Step 8: Update themed styling tests**
-
-In the `"dashboard themed styling"` describe block:
-
-Update `"wraps section titles, borders, and dimmed helpers with ANSI escape codes"`:
-
-```typescript
-it("renders frame borders and tab bar with themed styling", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
- const lines = c.render(140);
- const out = lines.join("\n");
-
- // Frame uses ┏ and ┛ (from overlay-render frame glyphs)
- expect(out).toContain("┏");
- expect(out).toContain("┛");
-
- // Tab bar active pill uses inverse+bold for Usage Statistics
- expect(
- theme.calls.some(
- (c) => c.method === "bold" && c.text.includes("Usage Statistics"),
- ),
- ).toBe(true);
- expect(
- theme.calls.some(
- (c) => c.method === "inverse" && c.text.includes("Usage Statistics"),
- ),
- ).toBe(true);
-
- // Footer should be dimmed with per-tab content
- expect(out).toContain("[Tab/Shift-Tab] Switch tab");
- expect(
- theme.calls.some(
- (c) =>
- c.method === "dim" && c.text.includes("[Tab/Shift-Tab] Switch tab"),
- ),
- ).toBe(true);
-});
-```
-
-Update `"dims the inactive provider tabs in Current Usage"`:
-
-```typescript
-it("renders inactive main tabs with bg styling", () => {
- const theme = makeAnsiTheme();
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme,
- });
- c.render(140);
-
- // Inactive tabs use bg("selectedBg", fg("accent", label))
- const bgCalls = theme.calls
- .filter((c) => c.method === "bg")
- .map((c) => c.text);
- // Current Usage and Insights should have bg calls (they're inactive)
- expect(bgCalls.some((t) => t.includes("Current Usage"))).toBe(true);
- expect(bgCalls.some((t) => t.includes("Insights"))).toBe(true);
-});
-```
-
-- [ ] **Step 9: Update the footer test**
-
-Replace `"renders the footer in [Shortcut] Action format with dimmed styling"`:
-
-```typescript
-it("renders context-aware footer per tab", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
-
- // Statistics tab footer
- let out = c.render(160).join("\n");
- let stripped = stripAnsi(out);
- expect(stripped).toContain("[Tab/Shift-Tab] Switch tab");
- expect(stripped).toContain("[Left/Right] Period");
- expect(stripped).toContain("[Up/Down] Row");
- expect(stripped).toContain("[Enter] Expand");
- expect(stripped).toContain("[q/Esc] Close");
-
- // Current Usage tab footer
- c.handleInput("\t");
- out = c.render(160).join("\n");
- stripped = stripAnsi(out);
- expect(stripped).toContain("[Tab/Shift-Tab] Switch tab");
- expect(stripped).toContain("[Left/Right] Provider");
- expect(stripped).not.toContain("[Up/Down] Row");
-
- // Insights tab footer
- c.handleInput("\t");
- out = c.render(160).join("\n");
- stripped = stripAnsi(out);
- expect(stripped).toContain("[Tab/Shift-Tab] Switch tab");
- expect(stripped).toContain("[Left/Right] Period");
- expect(stripped).not.toContain("[Up/Down] Row");
-});
-```
-
-- [ ] **Step 10: Update responsive layout tests**
-
-Update `"renders at very narrow widths without breaking the table"`:
-
-```typescript
-it("renders at very narrow widths without breaking the frame", () => {
- const c = new UsageDashboardComponent(mkState(), () => undefined, {
- theme: noTheme,
- });
- const lines = c.render(30);
- for (const line of lines) {
- const visible = line.replace(ANSI_PATTERN, "").length;
- expect(visible).toBeLessThanOrEqual(30);
- }
- // Frame borders should be present
- expect(lines.join("\n")).toContain("┏");
- expect(lines.join("\n")).toContain("┛");
-});
-```
-
-- [ ] **Step 11: Remove or update tests that reference the `v` key or old layout**
-
-Remove the test `"renders empty line between Usage Statistics title and period tabs"` -- the section title is now in the tab bar, not a separate line.
-
-Remove the `"dims the inactive provider tabs in Current Usage"` test that checks for provider tab dimming on the Statistics tab -- provider tabs are now only visible on the Current Usage tab.
-
-Update any other tests that assert the old stacked layout (both `"Usage Statistics"` and `"Current Usage"` visible simultaneously).
-
-- [ ] **Step 12: Run all tests**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run`
-
-Expected: All tests pass. If any fail, fix the assertions to match the new tabbed output structure.
-
-- [ ] **Step 13: Commit**
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add tests/dashboard.test.ts
-git commit -m "test(tui): update dashboard tests for tabbed overlay
-
-Update all render and input assertions for the new tab-based layout.
-Remove v-key toggle tests. Add tab switching, per-tab content, and
-context-aware footer tests.
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
-
----
-
-## Task 7: Final verification and cleanup
-
-**Files:**
-
-- All modified files
-
-- [ ] **Step 1: Run full test suite**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx vitest run`
-
-Expected: All tests pass.
-
-- [ ] **Step 2: Type check**
-
-Run: `cd /Users/lanh/Developer/pi-vault/pi-usage && npx tsc --noEmit`
-
-Expected: No type errors.
-
-- [ ] **Step 3: Verify no dead references to removed constants**
-
-Run:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-grep -rn "dashboardFooter[^s]" src/ tests/ || echo "No stale footer refs"
-grep -rn "dashboardBorderChars" src/ tests/ || echo "No stale border refs"
-grep -rn "showInsights" src/ tests/ || echo "No stale showInsights refs"
-grep -rn '"v"' src/tui/dashboard.ts || echo "No v-key handler"
-```
-
-Expected: No references found for any of these.
-
-- [ ] **Step 4: Verify overlay options are present**
-
-Run: `grep -A5 "overlay:" src/tui/dashboard.ts`
-
-Expected: Shows `overlay: true` with `overlayOptions: { anchor: "center", maxHeight: "85%", width: "92%" }`.
-
-- [ ] **Step 5: Commit any remaining fixes**
-
-If any fixes were needed, commit them:
-
-```bash
-cd /Users/lanh/Developer/pi-vault/pi-usage
-git add -A
-git commit -m "fix(tui): address remaining issues from tabbed overlay refactor
-
-Generated with [Devin](https://devin.ai)
-
-Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>"
-```
diff --git a/docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md b/docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md
deleted file mode 100644
index 6e4d117..0000000
--- a/docs/superpowers/specs/2026-07-05-dashboard-overlay-tabs-design.md
+++ /dev/null
@@ -1,197 +0,0 @@
-# Dashboard Overlay & Tabs Refactor
-
-**Date:** 2026-07-05
-**Status:** Approved
-**Scope:** `src/tui/` layer only -- no public API changes
-
-## Summary
-
-Refactor the pi-usage dashboard from a vertically-stacked full-custom component into a centered overlay with 3 tabbed panes: Usage Statistics, Current Usage, and Insights. Adopt the overlay and rendering patterns from pi-extension-manager (replicated locally, not imported).
-
-## Motivation
-
-The current dashboard renders all sections stacked vertically in a single scroll, with Insights toggled via the `v` key replacing Usage Statistics. This makes the UI dense and requires the user to mentally separate unrelated data. The pi-extension-manager already establishes an overlay + tab pattern that users are familiar with. Aligning pi-usage to this pattern improves consistency and usability.
-
-## Architecture
-
-### Component Structure
-
-Single `UsageDashboardComponent` class retained (no multi-component decomposition). Internal restructuring around tab state:
-
-**State changes:**
-
-- Add `activeTab: 'statistics' | 'current' | 'insights'` (default: `'statistics'`)
-- Add `insightsPeriodIndex: number` -- independent period selector for the Insights tab
-- Remove `showInsights: boolean` (replaced by tab state)
-- Retain `periodIndex`, `rowIndex`, `expandedProvider`, `currentUsageProviderIndex` unchanged
-
-**Render pipeline:**
-
-```
-render(width)
- -> frame(contentLines, width, theme, fixedInnerRows)
- -> renderTabBar(tabs, activeTab, innerWidth, theme)
- -> renderActiveTabContent(innerWidth)
- -> renderUsageStatisticsTab(w, lines) // tab 0
- -> renderCurrentUsageTab(w, lines) // tab 1
- -> renderInsightsTab(w, lines) // tab 2
- -> renderFooter(innerWidth) // context-aware keys
-```
-
-### Overlay Configuration
-
-The `openDashboard` function passes overlay options to `ctx.ui.custom()`:
-
-```typescript
-await ctx.ui.custom(
- (tui, theme, _keys, done) => { ... },
- {
- overlay: true,
- overlayOptions: {
- anchor: "center",
- maxHeight: "85%",
- width: "92%",
- },
- },
-);
-```
-
-These dimensions match the pi-extension-manager for visual consistency.
-
-## Tab Definitions
-
-### Tab 1: Usage Statistics (default)
-
-Content:
-
-- Period selector tabs (Today, This Week, Last Week, All Time)
-- Aggregated provider/model table with expandable rows
-- Total row
-- Token legend
-
-Contextual keys:
-
-- Left/Right: switch period
-- Up/Down: navigate provider rows
-- Enter/Space: expand/collapse selected provider
-
-### Tab 2: Current Usage
-
-Content:
-
-- Provider selector tabs (live providers only)
-- Provider heading (name, plan, status, age)
-- Quota windows with progress bars
-- Balances
-- Diagnostics/Notes (previously a separate section, now housed here)
-
-Contextual keys:
-
-- Left/Right: switch provider
-
-### Tab 3: Insights
-
-Content:
-
-- Period selector tabs (same as Usage Statistics, independent state)
-- Insights grouped by category (Projects, Skills, MCP servers, Cost patterns)
-
-Contextual keys:
-
-- Left/Right: switch period
-
-## Key Bindings
-
-### Global (all tabs)
-
-| Key | Action |
-| --------- | ----------------------------- |
-| Tab | Next tab |
-| Shift-Tab | Previous tab |
-| q / Esc | Close dashboard (cancel scan) |
-
-### Per-tab contextual keys
-
-| Key | Usage Statistics | Current Usage | Insights |
-| ----------- | ---------------- | --------------- | ------------- |
-| Left/Right | Switch period | Switch provider | Switch period |
-| Up/Down | Navigate rows | (unused) | (unused) |
-| Enter/Space | Expand/collapse | (unused) | (unused) |
-
-### Removed keys
-
-- `v` -- no longer needed; Insights is its own tab
-
-### Footer text (context-aware)
-
-- Usage Statistics: `[Tab/Shift-Tab] Switch tab [Left/Right] Period [Up/Down] Row [Enter] Expand [q] Close`
-- Current Usage: `[Tab/Shift-Tab] Switch tab [Left/Right] Provider [q] Close`
-- Insights: `[Tab/Shift-Tab] Switch tab [Left/Right] Period [q] Close`
-
-## New File: `src/tui/overlay-render.ts`
-
-Local replications of pi-extension-manager rendering utilities. These do not import from the manager -- they replicate the patterns using pi-usage's existing `DashboardTheme` adapter.
-
-### `frame(lines, width, theme, fixedInnerRows?, title?)`
-
-Draws a bordered frame around content lines. Uses the manager's box-drawing characters (`┏━┓┃┗┛`) for visual consistency with other Pi overlays. The existing `UI_STRINGS.dashboardBorderChars` (`╭╮╰╯`) are replaced. Configurable padding: 2px horizontal, 1px vertical. Truncates overflow with `"down-arrow N more line(s)"` indicator when content exceeds `fixedInnerRows`.
-
-### `renderTabBar(tabs, activeTab, width, theme)`
-
-Renders pill-styled tabs within available width:
-
-- Active tab: `theme.fg("accent", theme.inverse(theme.bold(label)))` -- inverted pill
-- Inactive tabs: `theme.bg("selectedBg", theme.fg("accent", label))` -- subtle background pill
-- Dynamic visibility with `‹` / `›` overflow indicators (handles narrow terminals)
-
-These pull colors through the `DashboardTheme` adapter, which delegates to Pi's live theme, ensuring visual consistency with the Pi theme in use.
-
-### `pad(text, width)`
-
-Pads text to exact visible width, ANSI-aware. Wraps `truncateToWidth` + space padding.
-
-## Theme Changes
-
-### `DashboardTheme` interface
-
-Add two methods:
-
-- `inverse(text: string): string` -- for active tab pill styling
-- `bg(color: DashboardColor, text: string): string` -- for inactive tab pill background
-
-### `fromPiTheme()` adapter
-
-Add `inverse` and `bg` delegations to Pi's `theme.inverse()` and `theme.bg()`. Colors flow through Pi's live theme -- no hardcoded values. The `DashboardColor` type gains `"selectedBg"` for the inactive pill background.
-
-### `noTheme` passthrough
-
-Add `inverse` and `bg` as identity functions for unit tests.
-
-## Files Changed
-
-| File | Change |
-| ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
-| `src/tui/overlay-render.ts` | New file -- `frame()`, `renderTabBar()`, `pad()` |
-| `src/tui/dashboard.ts` | Refactor `render()`, `handleInput()`, add tab state, use `frame()` and `renderTabBar()` |
-| `src/tui/dashboard-theme.ts` | Add `inverse` and `bg` to `DashboardTheme`, `fromPiTheme`, `noTheme`; add `"selectedBg"` to `DashboardColor` |
-| `src/shared/constants.ts` | Update `UI_STRINGS.dashboardFooter` to per-tab footers, remove `v` key reference |
-| `tests/` | Update rendering and input tests for new tab structure |
-
-## Files Unchanged
-
-| File | Reason |
-| ---------------------------- | -------------------------------------------------------------------------- |
-| `src/index.ts` | `openDashboard` signature unchanged; only overlay options added internally |
-| `src/tui/formatters.ts` | All formatters reused as-is |
-| `src/tui/table-layout.ts` | Table layout reused as-is within Usage Statistics tab |
-| `src/tui/dashboard-model.ts` | Data model unchanged |
-| `src/core/*` | Core logic untouched |
-| `src/providers/*` | Provider implementations untouched |
-| `src/shared/types.ts` | Types unchanged |
-
-## Testing
-
-- Existing rendering tests updated to assert per-tab output (tab bar present, only active tab's content rendered)
-- Existing `handleInput` tests updated for new key semantics (Tab switches tabs, `v` removed)
-- `noTheme` passthrough extended with `inverse` so unit tests work without a real Pi theme
-- No new test infrastructure needed -- same `render(width)` + `handleInput(data)` testing pattern
diff --git a/package.json b/package.json
index f47f23f..f11dcb4 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "@pi-vault/pi-usage",
- "version": "0.5.1",
+ "version": "0.6.0",
"type": "module",
- "description": "Pi extension that displays aggregated usage statistics across all sessions",
+ "description": "Pi extension that tracks Pi usage across your sessions in one dashboard",
"author": "Lanh Hoang ",
"license": "MIT",
"homepage": "https://github.com/pi-vault/pi-usage#readme",
@@ -47,16 +47,17 @@
"CHANGELOG.md",
"README.md"
],
- "peerDependencies": {
- "@earendil-works/pi-coding-agent": "*",
- "@earendil-works/pi-tui": "*"
- },
"devDependencies": {
- "@biomejs/biome": "^2.5.0",
- "@earendil-works/pi-coding-agent": "^0.80.2",
- "@earendil-works/pi-tui": "^0.80.2",
- "@types/node": "^26.0.0",
+ "@biomejs/biome": "^2.5.2",
+ "@earendil-works/pi-coding-agent": "^0.80.3",
+ "@earendil-works/pi-tui": "^0.80.3",
+ "@types/node": "^26.1.0",
+ "typebox": "^1.3.3",
"typescript": "^6.0.3",
"vitest": "^4.1.9"
+ },
+ "peerDependencies": {
+ "@earendil-works/pi-coding-agent": "*",
+ "@earendil-works/pi-tui": "*"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0312663..08063d9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,23 +9,26 @@ importers:
.:
devDependencies:
'@biomejs/biome':
- specifier: ^2.5.0
- version: 2.5.0
+ specifier: ^2.5.2
+ version: 2.5.2
'@earendil-works/pi-coding-agent':
- specifier: ^0.80.2
- version: 0.80.2(ws@8.21.0)(zod@4.4.3)
+ specifier: ^0.80.3
+ version: 0.80.3(ws@8.21.0)(zod@4.4.3)
'@earendil-works/pi-tui':
- specifier: ^0.80.2
- version: 0.80.2
+ specifier: ^0.80.3
+ version: 0.80.3
'@types/node':
- specifier: ^26.0.0
- version: 26.0.0
+ specifier: ^26.1.0
+ version: 26.1.0
+ typebox:
+ specifier: ^1.3.3
+ version: 1.3.3
typescript:
specifier: ^6.0.3
version: 6.0.3
vitest:
specifier: ^4.1.9
- version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@26.0.0)(vite@8.0.14(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.9.0))
+ version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@26.1.0)(vite@8.0.14(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0))
packages:
@@ -143,79 +146,79 @@ packages:
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
- '@biomejs/biome@2.5.0':
- resolution: {integrity: sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==}
+ '@biomejs/biome@2.5.2':
+ resolution: {integrity: sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==}
engines: {node: '>=14.21.3'}
hasBin: true
- '@biomejs/cli-darwin-arm64@2.5.0':
- resolution: {integrity: sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==}
+ '@biomejs/cli-darwin-arm64@2.5.2':
+ resolution: {integrity: sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
- '@biomejs/cli-darwin-x64@2.5.0':
- resolution: {integrity: sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==}
+ '@biomejs/cli-darwin-x64@2.5.2':
+ resolution: {integrity: sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
- '@biomejs/cli-linux-arm64-musl@2.5.0':
- resolution: {integrity: sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==}
+ '@biomejs/cli-linux-arm64-musl@2.5.2':
+ resolution: {integrity: sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@biomejs/cli-linux-arm64@2.5.0':
- resolution: {integrity: sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==}
+ '@biomejs/cli-linux-arm64@2.5.2':
+ resolution: {integrity: sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@biomejs/cli-linux-x64-musl@2.5.0':
- resolution: {integrity: sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==}
+ '@biomejs/cli-linux-x64-musl@2.5.2':
+ resolution: {integrity: sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@biomejs/cli-linux-x64@2.5.0':
- resolution: {integrity: sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==}
+ '@biomejs/cli-linux-x64@2.5.2':
+ resolution: {integrity: sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@biomejs/cli-win32-arm64@2.5.0':
- resolution: {integrity: sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==}
+ '@biomejs/cli-win32-arm64@2.5.2':
+ resolution: {integrity: sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
- '@biomejs/cli-win32-x64@2.5.0':
- resolution: {integrity: sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==}
+ '@biomejs/cli-win32-x64@2.5.2':
+ resolution: {integrity: sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
- '@earendil-works/pi-agent-core@0.80.2':
- resolution: {integrity: sha512-BF9WPhixIFjT6Kp3Iz3H6ugkU+4AWotM8py96XE5pIK0arJbQKMWbR+dXSWWDEmR5yc/aFQODnuyowOEzMGO7Q==}
+ '@earendil-works/pi-agent-core@0.80.3':
+ resolution: {integrity: sha512-3qw0/GeRQBU/nlGjDe5Yb7ePKTmoxefx2YxyKMFAviFUMXpFexBG/hS7mBtwFahFvzrrTPPoRT6sFIDjwoDWPQ==}
engines: {node: '>=22.19.0'}
- '@earendil-works/pi-ai@0.80.2':
- resolution: {integrity: sha512-5GNKfdrRJ4uZ5Zd9iudoXggi/BbUcKnD/xfRHtdR+7q4vWqPvfx8auFuaT+ewGBVI8K4wj87eigFQ/iCSuy9RQ==}
+ '@earendil-works/pi-ai@0.80.3':
+ resolution: {integrity: sha512-jPZLMeGL5kkMSEAwAklfXTMHqZvfhsJtCCpKGIr5Duk7mc0n4skjB1dugk7y0z3z8ZHIUCmPAWHdyDqgUz5vdA==}
engines: {node: '>=22.19.0'}
hasBin: true
- '@earendil-works/pi-coding-agent@0.80.2':
- resolution: {integrity: sha512-m9v7OUit0s9LklWfh61ca/XY5INjUzjtYtNZwy3cNvyjOLk3IpBgghP8aAp0iH35rLaiRwuuWiJ8t88ODMWY+A==}
+ '@earendil-works/pi-coding-agent@0.80.3':
+ resolution: {integrity: sha512-TIggw9gCXpA+Ph7OjdTA7ka2NPwTVuPmy39KDSyUzaKq8VvHfMGR7vtRz4JB7Um/RMRblmzhu4p9tUCk6MTgGA==}
engines: {node: '>=22.19.0'}
hasBin: true
- '@earendil-works/pi-tui@0.80.2':
- resolution: {integrity: sha512-OvOAMIbXiC9OSse17YMiXIsI9AS5XM/ZV8N/k+UzdlRpPILDQYmLElevgGW92kkXR8qHBClIdzhCjuzlBGvphA==}
+ '@earendil-works/pi-tui@0.80.3':
+ resolution: {integrity: sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==}
engines: {node: '>=22.19.0'}
'@emnapi/core@1.10.0':
@@ -521,12 +524,12 @@ packages:
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
- '@types/node@25.9.4':
- resolution: {integrity: sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==}
-
'@types/node@26.0.0':
resolution: {integrity: sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==}
+ '@types/node@26.1.0':
+ resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==}
+
'@types/retry@0.12.0':
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
@@ -975,14 +978,14 @@ packages:
typebox@1.1.38:
resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==}
+ typebox@1.3.3:
+ resolution: {integrity: sha512-URXGUE31PJDQC+PtRMJeLdF4kmmOdFoVPikPCtV2oOIhUpNpppEdIz7W8bH8cFYPYHdDpaRvqwdegMTmHliudg==}
+
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
- undici-types@7.24.6:
- resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
-
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
@@ -1350,44 +1353,44 @@ snapshots:
'@babel/runtime@7.29.7': {}
- '@biomejs/biome@2.5.0':
+ '@biomejs/biome@2.5.2':
optionalDependencies:
- '@biomejs/cli-darwin-arm64': 2.5.0
- '@biomejs/cli-darwin-x64': 2.5.0
- '@biomejs/cli-linux-arm64': 2.5.0
- '@biomejs/cli-linux-arm64-musl': 2.5.0
- '@biomejs/cli-linux-x64': 2.5.0
- '@biomejs/cli-linux-x64-musl': 2.5.0
- '@biomejs/cli-win32-arm64': 2.5.0
- '@biomejs/cli-win32-x64': 2.5.0
-
- '@biomejs/cli-darwin-arm64@2.5.0':
+ '@biomejs/cli-darwin-arm64': 2.5.2
+ '@biomejs/cli-darwin-x64': 2.5.2
+ '@biomejs/cli-linux-arm64': 2.5.2
+ '@biomejs/cli-linux-arm64-musl': 2.5.2
+ '@biomejs/cli-linux-x64': 2.5.2
+ '@biomejs/cli-linux-x64-musl': 2.5.2
+ '@biomejs/cli-win32-arm64': 2.5.2
+ '@biomejs/cli-win32-x64': 2.5.2
+
+ '@biomejs/cli-darwin-arm64@2.5.2':
optional: true
- '@biomejs/cli-darwin-x64@2.5.0':
+ '@biomejs/cli-darwin-x64@2.5.2':
optional: true
- '@biomejs/cli-linux-arm64-musl@2.5.0':
+ '@biomejs/cli-linux-arm64-musl@2.5.2':
optional: true
- '@biomejs/cli-linux-arm64@2.5.0':
+ '@biomejs/cli-linux-arm64@2.5.2':
optional: true
- '@biomejs/cli-linux-x64-musl@2.5.0':
+ '@biomejs/cli-linux-x64-musl@2.5.2':
optional: true
- '@biomejs/cli-linux-x64@2.5.0':
+ '@biomejs/cli-linux-x64@2.5.2':
optional: true
- '@biomejs/cli-win32-arm64@2.5.0':
+ '@biomejs/cli-win32-arm64@2.5.2':
optional: true
- '@biomejs/cli-win32-x64@2.5.0':
+ '@biomejs/cli-win32-x64@2.5.2':
optional: true
- '@earendil-works/pi-agent-core@0.80.2(ws@8.21.0)(zod@4.4.3)':
+ '@earendil-works/pi-agent-core@0.80.3(ws@8.21.0)(zod@4.4.3)':
dependencies:
- '@earendil-works/pi-ai': 0.80.2(ws@8.21.0)(zod@4.4.3)
+ '@earendil-works/pi-ai': 0.80.3(ws@8.21.0)(zod@4.4.3)
ignore: 7.0.5
typebox: 1.1.38
yaml: 2.9.0
@@ -1399,7 +1402,7 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-ai@0.80.2(ws@8.21.0)(zod@4.4.3)':
+ '@earendil-works/pi-ai@0.80.3(ws@8.21.0)(zod@4.4.3)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@4.4.3)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
@@ -1420,11 +1423,11 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-coding-agent@0.80.2(ws@8.21.0)(zod@4.4.3)':
+ '@earendil-works/pi-coding-agent@0.80.3(ws@8.21.0)(zod@4.4.3)':
dependencies:
- '@earendil-works/pi-agent-core': 0.80.2(ws@8.21.0)(zod@4.4.3)
- '@earendil-works/pi-ai': 0.80.2(ws@8.21.0)(zod@4.4.3)
- '@earendil-works/pi-tui': 0.80.2
+ '@earendil-works/pi-agent-core': 0.80.3(ws@8.21.0)(zod@4.4.3)
+ '@earendil-works/pi-ai': 0.80.3(ws@8.21.0)(zod@4.4.3)
+ '@earendil-works/pi-tui': 0.80.3
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
cross-spawn: 7.0.6
@@ -1450,7 +1453,7 @@ snapshots:
- ws
- zod
- '@earendil-works/pi-tui@0.80.2':
+ '@earendil-works/pi-tui@0.80.3':
dependencies:
get-east-asian-width: 1.6.0
marked: 18.0.5
@@ -1700,11 +1703,11 @@ snapshots:
'@types/estree@1.0.9': {}
- '@types/node@25.9.4':
+ '@types/node@26.0.0':
dependencies:
- undici-types: 7.24.6
+ undici-types: 8.3.0
- '@types/node@26.0.0':
+ '@types/node@26.1.0':
dependencies:
undici-types: 8.3.0
@@ -1719,13 +1722,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
- '@vitest/mocker@4.1.9(vite@8.0.14(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.9.0))':
+ '@vitest/mocker@4.1.9(vite@8.0.14(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.0.14(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.9.0)
+ vite: 8.0.14(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.9':
dependencies:
@@ -2048,7 +2051,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.1
- '@types/node': 25.9.4
+ '@types/node': 26.0.0
long: 5.3.2
retry@0.12.0: {}
@@ -2115,15 +2118,15 @@ snapshots:
typebox@1.1.38: {}
- typescript@6.0.3: {}
+ typebox@1.3.3: {}
- undici-types@7.24.6: {}
+ typescript@6.0.3: {}
undici-types@8.3.0: {}
undici@8.5.0: {}
- vite@8.0.14(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.9.0):
+ vite@8.0.14(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -2131,15 +2134,15 @@ snapshots:
rolldown: 1.0.2
tinyglobby: 0.2.16
optionalDependencies:
- '@types/node': 26.0.0
+ '@types/node': 26.1.0
fsevents: 2.3.3
jiti: 2.7.0
yaml: 2.9.0
- vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@26.0.0)(vite@8.0.14(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.9.0)):
+ vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@26.1.0)(vite@8.0.14(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.9
- '@vitest/mocker': 4.1.9(vite@8.0.14(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.9.0))
+ '@vitest/mocker': 4.1.9(vite@8.0.14(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.9
'@vitest/runner': 4.1.9
'@vitest/snapshot': 4.1.9
@@ -2156,11 +2159,11 @@ snapshots:
tinyexec: 1.2.3
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
- vite: 8.0.14(@types/node@26.0.0)(jiti@2.7.0)(yaml@2.9.0)
+ vite: 8.0.14(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
- '@types/node': 26.0.0
+ '@types/node': 26.1.0
transitivePeerDependencies:
- msw
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 598bc8a..1a6b9d1 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,8 +1,3 @@
allowBuilds:
"@google/genai": true
protobufjs: true
-minimumReleaseAgeExclude:
- - "@earendil-works/pi-agent-core@0.79.0"
- - "@earendil-works/pi-ai@0.79.0"
- - "@earendil-works/pi-coding-agent@0.79.0"
- - "@earendil-works/pi-tui@0.79.0"