@alyibrahim/claude-statusline 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <div align="center">
2
+
1
3
  # claude-statusline
2
4
 
3
5
  [![CI](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml/badge.svg)](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml)
@@ -8,12 +10,18 @@
8
10
 
9
11
  Runs as a **compiled Rust binary** (~5ms startup vs ~100ms for Node.js). Zero shell dependencies. One install command.
10
12
 
11
- ![statusline screenshot](https://raw.githubusercontent.com/AlyIbrahim1/claude-statusline/main/.github/image.png)
13
+ ![statusline screenshot](https://raw.githubusercontent.com/AlyIbrahim1/claude-statusline/main/.github/assets/statusline.png)
14
+
15
+ </div>
12
16
 
13
17
  ---
14
18
 
19
+ <div align="center">
20
+
15
21
  ## Install
16
22
 
23
+ </div>
24
+
17
25
  ```bash
18
26
  npm install -g @alyibrahim/claude-statusline
19
27
  ```
@@ -24,8 +32,12 @@ Done. The statusline configures itself automatically. Restart Claude Code to see
24
32
 
25
33
  ---
26
34
 
35
+ <div align="center">
36
+
27
37
  ## What you get
28
38
 
39
+ </div>
40
+
29
41
  **Line 1** — Model name · Effort level · Active subagents · Current task · Directory `(branch +commits)` · Context bar
30
42
 
31
43
  **Line 2** — Weekly token usage · 5h usage · Reset countdown *(Pro/Max)* — or — Session cost *(API key)* · Session tokens `X↓ Y↑`
@@ -44,24 +56,57 @@ Done. The statusline configures itself automatically. Restart Claude Code to see
44
56
 
45
57
  ---
46
58
 
47
- ## Platform support
59
+ <div align="center">
60
+
61
+ ## Session History
62
+
63
+ </div>
48
64
 
49
- The Rust binary is pre-built and installed automatically for your platform via npm `optionalDependencies`:
65
+ Track token usage, cost, and duration across every Claude Code session with a built-in analytics dashboard.
50
66
 
51
- | Platform | Package |
67
+ ![history dashboard](https://raw.githubusercontent.com/AlyIbrahim1/claude-statusline/main/.github/assets/history-dashboard.png)
68
+
69
+ Session history is **enabled by default** on setup. Each session records:
70
+
71
+ | Field | Details |
52
72
  |---|---|
53
- | Linux x64 | [`@alyibrahim/claude-statusline-linux-x64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-linux-x64) |
54
- | Linux arm64 | [`@alyibrahim/claude-statusline-linux-arm64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-linux-arm64) |
55
- | macOS x64 (Intel) | [`@alyibrahim/claude-statusline-darwin-x64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-darwin-x64) |
56
- | macOS arm64 (Apple Silicon) | [`@alyibrahim/claude-statusline-darwin-arm64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-darwin-arm64) |
57
- | Windows x64 | [`@alyibrahim/claude-statusline-win32-x64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-win32-x64) |
73
+ | Project | Directory name and path |
74
+ | Model | Which Claude model was used |
75
+ | Tokens | Input and output counts |
76
+ | Cost | USD cost (API key users) |
77
+ | Duration | Session length in seconds |
78
+ | Exit reason | How the session ended |
79
+
80
+ **Commands:**
81
+
82
+ ```bash
83
+ claude-statusline history # Open the analytics dashboard
84
+ claude-statusline enable-history # Enable session tracking
85
+ claude-statusline disable-history # Disable session tracking
86
+ ```
58
87
 
59
- npm picks the right one automatically. If your platform isn't listed, the JS fallback is used instead no action needed.
88
+ Data is stored at `~/.claude/statusline-history.jsonl`. The dashboard opens in your browser and supports project filtering and light/dark theme toggle.
60
89
 
61
90
  ---
62
91
 
92
+ <div align="center">
93
+
94
+ ## Platform support
95
+
96
+ </div>
97
+
98
+ Pre-built Rust binaries are available for **Linux x64/arm64, macOS x64/arm64, and Windows x64**. All Linux distributions (Ubuntu, Arch, Fedora, etc.) are supported. Any other platform falls back to the JS implementation automatically — no action needed.
99
+
100
+ See [PLATFORMS.md](PLATFORMS.md) for the full compatibility guide, per-platform install instructions, and feature availability table.
101
+
102
+ ---
103
+
104
+ <div align="center">
105
+
63
106
  ## Why this one
64
107
 
108
+ </div>
109
+
65
110
  | | claude-statusline | Others |
66
111
  |---|---|---|
67
112
  | Startup time | ~5ms (Rust binary) | ~100ms (Node.js cold-start every prompt) |
@@ -72,18 +117,27 @@ npm picks the right one automatically. If your platform isn't listed, the JS fal
72
117
  | Subagent counter | Counts active agents from todos dir | — |
73
118
  | Session tokens | Real-time via JSONL offset cache, split I/O (`X↓ Y↑`) | Stale stdin snapshot or none |
74
119
  | Session commits | Tracks git commits made this session | — |
120
+ | Session history | Analytics dashboard with per-project filtering, zero dependencies | — |
75
121
 
76
122
  ---
77
123
 
124
+ <div align="center">
125
+
78
126
  ## Requirements
79
127
 
128
+ </div>
129
+
80
130
  - **Node.js ≥16** — for install/uninstall scripts only (not needed at runtime on supported platforms)
81
131
  - **git** — optional, enables branch display
82
132
 
83
133
  ---
84
134
 
135
+ <div align="center">
136
+
85
137
  ## Uninstall
86
138
 
139
+ </div>
140
+
87
141
  ```bash
88
142
  claude-statusline uninstall
89
143
  npm uninstall -g @alyibrahim/claude-statusline
@@ -93,12 +147,20 @@ npm uninstall -g @alyibrahim/claude-statusline
93
147
 
94
148
  ---
95
149
 
150
+ <div align="center">
151
+
96
152
  ## Notes
97
153
 
154
+ </div>
155
+
98
156
  - Settings are written only to the `statusLine` key — all other `~/.claude/settings.json` keys are untouched
99
157
  - Respects `$CLAUDE_CONFIG_DIR` if set
100
158
  - Switched Node versions on an unsupported platform? Re-run `claude-statusline setup`
101
159
 
160
+ <div align="center">
161
+
102
162
  ## License
103
163
 
104
164
  MIT
165
+
166
+ </div>
package/package.json CHANGED
@@ -1,69 +1,68 @@
1
- {
2
- "name": "@alyibrahim/claude-statusline",
3
- "version": "1.4.0",
4
- "description": "Rich statusline for Claude Code — model, context bar, real-time token tracking, git branch, rate limits, and session stats. Rust binary, ~5ms startup.",
5
- "keywords": [
6
- "claude",
7
- "claude-code",
8
- "anthropic",
9
- "statusline",
10
- "status-bar",
11
- "terminal",
12
- "cli",
13
- "ai",
14
- "llm",
15
- "token-tracking",
16
- "context-window",
17
- "rate-limit",
18
- "git",
19
- "developer-tools",
20
- "productivity",
21
- "rust"
22
- ],
23
- "homepage": "https://github.com/AlyIbrahim1/claude-statusline#readme",
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/AlyIbrahim1/claude-statusline.git"
27
- },
28
- "bugs": {
29
- "url": "https://github.com/AlyIbrahim1/claude-statusline/issues"
30
- },
31
- "engines": {
32
- "node": ">=16"
33
- },
34
- "bin": {
35
- "claude-statusline": "bin/cli.js"
36
- },
37
- "scripts": {
38
- "postinstall": "node scripts/postinstall.js",
39
- "preuninstall": "node scripts/preuninstall.js",
40
- "test": "jest"
41
- },
42
- "jest": {
43
- "testPathIgnorePatterns": [
44
- "/node_modules/",
45
- "/.worktrees/"
46
- ]
47
- },
48
- "files": [
49
- "statusline.js",
50
- "bin/",
51
- "scripts/",
52
- "README.md"
53
- ],
54
- "license": "MIT",
55
- "dependencies": {
56
- "open": "^10.1.0"
57
- },
58
- "optionalDependencies": {
59
- "better-sqlite3": "^11.3.0",
60
- "@alyibrahim/claude-statusline-linux-x64": "1.4.0",
61
- "@alyibrahim/claude-statusline-linux-arm64": "1.4.0",
62
- "@alyibrahim/claude-statusline-darwin-x64": "1.4.0",
63
- "@alyibrahim/claude-statusline-darwin-arm64": "1.4.0",
64
- "@alyibrahim/claude-statusline-win32-x64": "1.4.0"
65
- },
66
- "devDependencies": {
67
- "jest": "^29.0.0"
68
- }
69
- }
1
+ {
2
+ "name": "@alyibrahim/claude-statusline",
3
+ "version": "1.4.2",
4
+ "description": "Rich statusline for Claude Code — model, context bar, real-time token tracking, git branch, rate limits, and session stats. Rust binary, ~5ms startup.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "anthropic",
9
+ "statusline",
10
+ "status-bar",
11
+ "terminal",
12
+ "cli",
13
+ "ai",
14
+ "llm",
15
+ "token-tracking",
16
+ "context-window",
17
+ "rate-limit",
18
+ "git",
19
+ "developer-tools",
20
+ "productivity",
21
+ "rust"
22
+ ],
23
+ "homepage": "https://github.com/AlyIbrahim1/claude-statusline#readme",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/AlyIbrahim1/claude-statusline.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/AlyIbrahim1/claude-statusline/issues"
30
+ },
31
+ "engines": {
32
+ "node": ">=16"
33
+ },
34
+ "bin": {
35
+ "claude-statusline": "bin/cli.js"
36
+ },
37
+ "scripts": {
38
+ "postinstall": "node scripts/postinstall.js",
39
+ "preuninstall": "node scripts/preuninstall.js",
40
+ "test": "jest"
41
+ },
42
+ "jest": {
43
+ "testPathIgnorePatterns": [
44
+ "/node_modules/",
45
+ "/.worktrees/"
46
+ ]
47
+ },
48
+ "files": [
49
+ "statusline.js",
50
+ "bin/",
51
+ "scripts/",
52
+ "README.md"
53
+ ],
54
+ "license": "MIT",
55
+ "dependencies": {
56
+ "open": "^10.1.0"
57
+ },
58
+ "optionalDependencies": {
59
+ "@alyibrahim/claude-statusline-linux-x64": "1.4.0",
60
+ "@alyibrahim/claude-statusline-linux-arm64": "1.4.0",
61
+ "@alyibrahim/claude-statusline-darwin-x64": "1.4.0",
62
+ "@alyibrahim/claude-statusline-darwin-arm64": "1.4.0",
63
+ "@alyibrahim/claude-statusline-win32-x64": "1.4.0"
64
+ },
65
+ "devDependencies": {
66
+ "jest": "^29.0.0"
67
+ }
68
+ }
@@ -1,50 +1,57 @@
1
+ 'use strict';
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const os = require('os');
4
5
  const open = require('open');
5
6
 
6
- // Returns undefined if better-sqlite3 is not available
7
- function getDb() {
7
+ const JSONL_PATH = path.join(
8
+ process.env.HOME || process.env.USERPROFILE || os.homedir(),
9
+ '.claude',
10
+ 'statusline-history.jsonl'
11
+ );
12
+
13
+ function now() {
14
+ return new Date().toISOString().replace('T', ' ').slice(0, 19);
15
+ }
16
+
17
+ function readSessions() {
18
+ if (!fs.existsSync(JSONL_PATH)) return [];
8
19
  try {
9
- const Database = require('better-sqlite3');
10
- const home = process.env.HOME || process.env.USERPROFILE || '.';
11
- const dbDir = path.join(home, '.claude');
12
- fs.mkdirSync(dbDir, { recursive: true });
13
-
14
- const db = new Database(path.join(dbDir, 'statusline-history.db'));
15
- db.exec(`
16
- CREATE TABLE IF NOT EXISTS sessions (
17
- id INTEGER PRIMARY KEY AUTOINCREMENT,
18
- session_id TEXT UNIQUE NOT NULL,
19
- project_dir TEXT NOT NULL,
20
- project_name TEXT NOT NULL,
21
- model TEXT NOT NULL,
22
- start_time DATETIME DEFAULT CURRENT_TIMESTAMP,
23
- end_time DATETIME DEFAULT CURRENT_TIMESTAMP,
24
- tokens_in INTEGER DEFAULT 0,
25
- tokens_out INTEGER DEFAULT 0,
26
- cost_usd REAL DEFAULT 0.0,
27
- duration_seconds INTEGER DEFAULT 0,
28
- exit_reason TEXT
29
- )
30
- `);
31
- return db;
20
+ return fs.readFileSync(JSONL_PATH, 'utf8')
21
+ .split('\n')
22
+ .filter(l => l.trim())
23
+ .map(l => JSON.parse(l));
32
24
  } catch (e) {
33
- return null;
25
+ return [];
34
26
  }
35
27
  }
36
28
 
37
- function handleHookStart() {
38
- const db = getDb();
39
- if (!db) return; // Silent fallback
29
+ function writeSessions(sessions) {
30
+ const tmp = JSONL_PATH + '.tmp';
31
+ fs.mkdirSync(path.dirname(JSONL_PATH), { recursive: true });
32
+ fs.writeFileSync(tmp, sessions.map(s => JSON.stringify(s)).join('\n') + '\n');
33
+ fs.renameSync(tmp, JSONL_PATH);
34
+ }
40
35
 
36
+ function handleHookStart() {
41
37
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
42
38
  const projectName = path.basename(projectDir);
43
- const tempSessionId = `pending-${projectName}-${Date.now()}`;
44
-
39
+ const session = {
40
+ session_id: `pending-${projectName}-${Date.now()}`,
41
+ project_dir: projectDir,
42
+ project_name: projectName,
43
+ model: 'pending',
44
+ start_time: now(),
45
+ end_time: now(),
46
+ tokens_in: 0,
47
+ tokens_out: 0,
48
+ cost_usd: 0,
49
+ duration_seconds: 0,
50
+ exit_reason: 'pending',
51
+ };
45
52
  try {
46
- const stmt = db.prepare('INSERT INTO sessions (session_id, project_dir, project_name, model, exit_reason) VALUES (?, ?, ?, ?, ?)');
47
- stmt.run(tempSessionId, projectDir, projectName, 'pending', 'pending');
53
+ fs.mkdirSync(path.dirname(JSONL_PATH), { recursive: true });
54
+ fs.appendFileSync(JSONL_PATH, JSON.stringify(session) + '\n');
48
55
  } catch (e) {}
49
56
  }
50
57
 
@@ -53,152 +60,540 @@ function handleHookEnd() {
53
60
  process.stdin.setEncoding('utf8');
54
61
  process.stdin.on('data', chunk => input += chunk);
55
62
  process.stdin.on('end', () => {
56
- const db = getDb();
57
- if (!db) return process.exit(0);
58
-
59
63
  let reason = 'unknown';
60
- try {
61
- const data = JSON.parse(input);
62
- reason = data.reason || 'unknown';
63
- } catch(e) {}
64
+ try { reason = JSON.parse(input).reason || 'unknown'; } catch (e) {}
64
65
 
65
66
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
66
- const home = process.env.HOME || process.env.USERPROFILE || '.';
67
- const slug = projectDir.replace(/[\/\\]/g, '-');
67
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
68
+ const slug = projectDir.replace(/[/\\]/g, '-');
68
69
  const projectsDir = path.join(home, '.claude', 'projects', slug);
69
70
 
70
- let newestFile = null;
71
- let newestTime = 0;
71
+ // Read session stats from the most recently modified JSONL in the project dir
72
+ let sessionId = null;
73
+ let totalIn = 0, totalOut = 0, cost = 0, model = '';
72
74
 
73
75
  if (fs.existsSync(projectsDir)) {
74
76
  try {
75
- const files = fs.readdirSync(projectsDir);
76
- for (const file of files) {
77
- if (file.endsWith('.jsonl')) {
78
- const p = path.join(projectsDir, file);
79
- const mtime = fs.statSync(p).mtimeMs;
80
- if (mtime > newestTime) {
81
- newestTime = mtime;
82
- newestFile = p;
83
- }
77
+ let newestTime = 0, newestFile = null;
78
+ for (const file of fs.readdirSync(projectsDir)) {
79
+ if (!file.endsWith('.jsonl')) continue;
80
+ const p = path.join(projectsDir, file);
81
+ const mtime = fs.statSync(p).mtimeMs;
82
+ if (mtime > newestTime) { newestTime = mtime; newestFile = p; }
83
+ }
84
+ if (newestFile) {
85
+ sessionId = path.basename(newestFile, '.jsonl');
86
+ const lines = fs.readFileSync(newestFile, 'utf8').split('\n');
87
+ for (const line of lines) {
88
+ if (!line.trim()) continue;
89
+ try {
90
+ const entry = JSON.parse(line);
91
+ if (entry.type === 'assistant' && entry.message?.usage) {
92
+ const u = entry.message.usage;
93
+ totalIn += (u.input_tokens || 0)
94
+ + Math.round((u.cache_read_input_tokens || 0) * 0.1)
95
+ + (u.cache_creation_input_tokens || 0);
96
+ totalOut += (u.output_tokens || 0);
97
+ if (!model) model = entry.message.model || '';
98
+ } else if (entry.type === 'cost') {
99
+ cost += (entry.cost_usd || 0);
100
+ } else if (entry.type === 'message_start' && !model) {
101
+ model = entry.message?.model || '';
102
+ }
103
+ } catch (e) {}
84
104
  }
85
105
  }
86
- } catch(e) {}
106
+ } catch (e) {}
87
107
  }
88
108
 
89
- if (newestFile) {
90
- const sessionId = path.basename(newestFile, '.jsonl');
91
- let totalIn = 0;
92
- let totalOut = 0;
93
- let cost = 0.0;
94
- let model = '';
109
+ if (!model) model = 'Claude';
95
110
 
96
- try {
97
- const content = fs.readFileSync(newestFile, 'utf8');
98
- const lines = content.split('\n');
99
- for (const line of lines) {
100
- if (!line.trim()) continue;
101
- try {
102
- const entry = JSON.parse(line);
103
- if (entry.type === 'assistant' && entry.message?.usage) {
104
- const u = entry.message.usage;
105
- totalIn += (u.input_tokens || 0) + Math.round((u.cache_read_input_tokens || 0) * 0.1) + (u.cache_creation_input_tokens || 0);
106
- totalOut += (u.output_tokens || 0);
107
- if (!model) model = entry.message.model || 'Claude';
108
- } else if (entry.type === 'cost') {
109
- cost += (entry.cost_usd || 0.0);
110
- } else if (entry.type === 'message_start' && !model) {
111
- model = entry.message?.model || 'Claude';
112
- }
113
- } catch(e) {}
111
+ // Update the most recent pending session for this project
112
+ try {
113
+ const sessions = readSessions();
114
+ let updatedIdx = -1;
115
+ for (let i = sessions.length - 1; i >= 0; i--) {
116
+ if (sessions[i].project_dir === projectDir && sessions[i].exit_reason === 'pending') {
117
+ updatedIdx = i;
118
+ break;
114
119
  }
115
- } catch(e) {}
120
+ }
116
121
 
117
- if (!model) model = 'Claude';
122
+ if (updatedIdx !== -1) {
123
+ const s = sessions[updatedIdx];
124
+ const startMs = new Date(s.start_time.replace(' ', 'T') + 'Z').getTime();
125
+ const durationSeconds = Math.round((Date.now() - startMs) / 1000);
126
+ sessions[updatedIdx] = {
127
+ ...s,
128
+ session_id: sessionId || s.session_id,
129
+ model,
130
+ end_time: now(),
131
+ tokens_in: totalIn,
132
+ tokens_out: totalOut,
133
+ cost_usd: cost,
134
+ duration_seconds: durationSeconds,
135
+ exit_reason: reason,
136
+ };
137
+ writeSessions(sessions);
138
+ }
139
+ } catch (e) {}
118
140
 
119
- try {
120
- const stmt = db.prepare(`
121
- UPDATE sessions
122
- SET session_id = ?, model = ?, end_time = CURRENT_TIMESTAMP,
123
- tokens_in = ?, tokens_out = ?, cost_usd = ?, exit_reason = ?,
124
- duration_seconds = CAST((julianday('now') - julianday(start_time)) * 86400 as integer)
125
- WHERE id = (SELECT id FROM sessions WHERE project_dir = ? AND exit_reason = 'pending' ORDER BY start_time DESC LIMIT 1)
126
- `);
127
- stmt.run(sessionId, model, totalIn, totalOut, cost, reason, projectDir);
128
- } catch (e) {}
129
- }
130
141
  process.exit(0);
131
142
  });
132
143
  }
133
144
 
134
145
  async function handleHistory() {
135
- const db = getDb();
136
- if (!db) {
137
- console.error('SQLite is not available. Please install @alyibrahim/claude-statusline with native modules, or ensure better-sqlite3 is successfully installed.');
138
- process.exit(1);
146
+ function fmt(n) {
147
+ n = Number(n) || 0;
148
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
149
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
150
+ return String(n);
139
151
  }
140
152
 
141
- let html = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Claude Statusline History</title>';
142
- html += `<style>
143
- body { font-family: 'Inter', sans-serif; background: #121212; color: #eee; margin: 0; padding: 40px; }
144
- h1 { font-size: 24px; font-weight: 600; margin-bottom: 20px; color: #fff; }
145
- .dashboard { max-width: 1000px; margin: 0 auto; }
146
- .totals { display: flex; gap: 20px; margin-bottom: 30px; }
147
- .card { background: #1e1e1e; padding: 20px; border-radius: 12px; flex: 1; border: 1px solid #333; }
148
- .card h3 { margin: 0 0 10px 0; font-size: 14px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; }
149
- .card p { margin: 0; font-size: 28px; font-weight: 600; color: #fff; }
150
- table { width: 100%; border-collapse: collapse; background: #1e1e1e; border-radius: 12px; overflow: hidden; border: 1px solid #333; }
151
- th, td { padding: 15px; text-align: left; border-bottom: 1px solid #333; }
152
- th { background: #252525; color: #aaa; font-weight: 500; font-size: 13px; text-transform: uppercase; }
153
- tr:last-child td { border-bottom: none; }
154
- .badge { background: #2b3a4a; color: #61afef; padding: 4px 8px; border-radius: 6px; font-size: 12px; font-weight: 600; }
155
- .model { color: #98c379; }
156
- .tokens { font-family: monospace; color: #e5c07b; }
157
- .cost { color: #d19a66; }
158
- </style></head><body>`;
159
-
160
- html += `<div class="dashboard"><h1>Claude Statusline Analytics</h1>`;
161
-
162
- try {
163
- const rows = db.prepare('SELECT project_name, model, start_time, duration_seconds, tokens_in, tokens_out, cost_usd, exit_reason FROM sessions ORDER BY start_time DESC LIMIT 100').all();
164
- let totalCost = 0.0, totalIn = 0, totalOut = 0;
165
-
166
- let rowsHtml = '';
167
- for (const s of rows) {
168
- if (s.exit_reason !== 'pending') {
169
- totalCost += (s.cost_usd || 0);
170
- totalIn += (s.tokens_in || 0);
171
- totalOut += (s.tokens_out || 0);
172
- }
173
- rowsHtml += `<tr>
174
- <td><span class="badge">${s.project_name}</span></td>
175
- <td class="model">${s.model}</td>
176
- <td>${s.start_time}</td>
177
- <td>${s.duration_seconds}s</td>
178
- <td class="tokens">${s.tokens_in}↓ ${s.tokens_out}↑</td>
179
- <td class="cost">$${Number(s.cost_usd).toFixed(4)}</td>
180
- <td>${s.exit_reason}</td>
181
- </tr>`;
153
+ function dur(s) {
154
+ s = Number(s) || 0;
155
+ if (s >= 3600) {
156
+ const h = Math.floor(s / 3600);
157
+ const m = Math.floor((s % 3600) / 60);
158
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
182
159
  }
160
+ if (s >= 60) return `${Math.floor(s / 60)}m`;
161
+ return `${s}s`;
162
+ }
183
163
 
184
- html += `<div class="totals">
185
- <div class="card"><h3>Total Input Tokens</h3><p>${totalIn}</p></div>
186
- <div class="card"><h3>Total Output Tokens</h3><p>${totalOut}</p></div>
187
- <div class="card"><h3>Total Spend</h3><p>$${totalCost.toFixed(2)}</p></div>
188
- </div>`;
189
-
190
- html += `<table><thead><tr>
191
- <th>Project</th><th>Model</th><th>Start Time</th><th>Duration</th><th>Tokens</th><th>Cost</th><th>Reason</th>
192
- </tr></thead><tbody>${rowsHtml}</tbody></table></div></body></html>`;
193
-
194
- const tempPath = path.join(os.tmpdir(), 'claude-statusline-dashboard.html');
195
- fs.writeFileSync(tempPath, html);
196
-
197
- console.log(`Opened dashboard at ${tempPath}`);
198
- await open(tempPath);
199
- } catch (e) {
200
- console.error(`Failed to load database: ${e.message}`);
164
+ const allSessions = readSessions().reverse().slice(0, 100);
165
+
166
+ const projectNames = [...new Set(allSessions.map(s => s.project_name))].sort();
167
+ const projectOptions = projectNames
168
+ .map(p => `<option value="${p}">${p}</option>`)
169
+ .join('\n ');
170
+
171
+ let totalCost = 0, totalIn = 0, totalOut = 0, sessionCount = 0;
172
+ let rowsHtml = '';
173
+
174
+ for (const s of allSessions) {
175
+ if (s.exit_reason !== 'pending') {
176
+ totalCost += (s.cost_usd || 0);
177
+ totalIn += (s.tokens_in || 0);
178
+ totalOut += (s.tokens_out || 0);
179
+ sessionCount++;
180
+ }
181
+ const isPending = s.exit_reason === 'pending';
182
+ const badgeClass = { normal: 'reason-badge normal', interrupt: 'reason-badge interrupt', pending: 'reason-badge pending' }[s.exit_reason] ?? 'reason-badge unknown';
183
+ const durCell = isPending ? '\u2014' : dur(s.duration_seconds);
184
+ const tokInCell = isPending ? '\u2014' : fmt(s.tokens_in);
185
+ const tokOutCell = isPending ? '\u2014' : fmt(s.tokens_out);
186
+ const costCell = isPending ? '\u2014' : `$${Number(s.cost_usd).toFixed(4)}`;
187
+
188
+ rowsHtml += `
189
+ <tr data-project="${s.project_name}" data-tok-in="${isPending ? 0 : s.tokens_in}" data-tok-out="${isPending ? 0 : s.tokens_out}" data-cost="${isPending ? 0 : s.cost_usd}" data-pending="${isPending ? 1 : 0}">
190
+ <td><span class="tag">${s.project_name}</span></td>
191
+ <td class="col-model">${s.model}</td>
192
+ <td class="col-ts">${s.start_time}</td>
193
+ <td class="col-dur">${durCell}</td>
194
+ <td class="col-tok">${tokInCell}</td>
195
+ <td class="col-tok">${tokOutCell}</td>
196
+ <td class="col-cost">${costCell}</td>
197
+ <td><span class="${badgeClass}">${s.exit_reason}</span></td>
198
+ </tr>`;
201
199
  }
200
+
201
+ const rowCount = allSessions.length;
202
+ const emptyRow = rowsHtml ? '' : '<tr><td colspan="8" style="text-align:center;padding:48px 20px;color:var(--text-3);font-size:13px;">No sessions recorded yet</td></tr>';
203
+
204
+ const html = `<!DOCTYPE html>
205
+ <html lang="en" data-theme="dark">
206
+ <head>
207
+ <meta charset="UTF-8">
208
+ <meta name="viewport" content="width=device-width, initial-scale=1">
209
+ <title>Claude Statusline \u2014 Session History</title>
210
+ <link rel="preconnect" href="https://fonts.googleapis.com">
211
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
212
+ <link href="https://fonts.googleapis.com/css2?family=Calistoga&family=Plus+Jakarta+Sans:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
213
+ <style>
214
+ [data-theme="dark"] {
215
+ --bg: #1a1916;
216
+ --bg-header: rgba(26,25,22,0.88);
217
+ --surface: #242220;
218
+ --surface-hover: #2a2825;
219
+ --surface-thead: #1e1c1a;
220
+ --border: #3a3733;
221
+ --border-subtle: #302e2b;
222
+ --accent: #D4673C;
223
+ --accent-mid: rgba(212,103,60,0.14);
224
+ --text: #E8E2DA;
225
+ --text-2: #A09890;
226
+ --text-3: #6B6460;
227
+ --green: #4CAF84;
228
+ --green-bg: rgba(76,175,132,0.15);
229
+ --amber: #D4893A;
230
+ --amber-bg: rgba(212,137,58,0.15);
231
+ --pending: #A99ED4;
232
+ --pending-bg: rgba(169,158,212,0.15);
233
+ --shadow-card: 0 1px 4px rgba(0,0,0,0.3), 0 0 0 1px var(--border);
234
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.4), 0 2px 4px rgba(0,0,0,0.2);
235
+ --toggle-bg: #3a3733;
236
+ --toggle-knob: #E8E2DA;
237
+ }
238
+ [data-theme="light"] {
239
+ --bg: #FAF9F6;
240
+ --bg-header: rgba(250,249,246,0.88);
241
+ --surface: #FFFFFF;
242
+ --surface-hover: #FDFCFA;
243
+ --surface-thead: #F4F2EE;
244
+ --border: #EAE4DD;
245
+ --border-subtle: #F0EBE5;
246
+ --accent: #C85A2E;
247
+ --accent-mid: rgba(200,90,46,0.10);
248
+ --text: #1C1410;
249
+ --text-2: #6B5D57;
250
+ --text-3: #9E8E87;
251
+ --green: #1F7A50;
252
+ --green-bg: rgba(31,122,80,0.10);
253
+ --amber: #925010;
254
+ --amber-bg: rgba(146,80,16,0.10);
255
+ --pending: #6B5FA8;
256
+ --pending-bg: rgba(107,95,168,0.10);
257
+ --shadow-card: 0 1px 3px rgba(28,20,16,0.07), 0 0 0 1px var(--border);
258
+ --shadow-md: 0 4px 16px rgba(28,20,16,0.10), 0 2px 4px rgba(28,20,16,0.06);
259
+ --toggle-bg: #EAE4DD;
260
+ --toggle-knob: #1C1410;
261
+ }
262
+ :root {
263
+ --radius: 12px;
264
+ --radius-sm: 8px;
265
+ --radius-tag: 6px;
266
+ --font-display:'Calistoga', Georgia, serif;
267
+ --font-body: 'Plus Jakarta Sans', system-ui, sans-serif;
268
+ --font-mono: 'JetBrains Mono', 'Courier New', monospace;
269
+ --ease: cubic-bezier(0.25, 0.46, 0.45, 0.94);
270
+ --header-h: 56px;
271
+ }
272
+ * { box-sizing: border-box; margin: 0; padding: 0; }
273
+ body {
274
+ font-family: var(--font-body);
275
+ background: var(--bg);
276
+ color: var(--text);
277
+ min-height: 100vh;
278
+ font-size: 14px;
279
+ line-height: 1.6;
280
+ -webkit-font-smoothing: antialiased;
281
+ transition: background 0.25s var(--ease), color 0.25s var(--ease);
282
+ }
283
+ .header {
284
+ position: sticky;
285
+ top: 0;
286
+ z-index: 100;
287
+ height: var(--header-h);
288
+ background: var(--bg-header);
289
+ border-bottom: 1px solid var(--border);
290
+ backdrop-filter: blur(12px);
291
+ -webkit-backdrop-filter: blur(12px);
292
+ display: flex;
293
+ align-items: center;
294
+ }
295
+ .header-inner {
296
+ width: 100%;
297
+ padding: 0 32px;
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: space-between;
301
+ gap: 16px;
302
+ }
303
+ .brand { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
304
+ .brand-icon {
305
+ width: 30px; height: 30px; border-radius: 7px; background: var(--accent);
306
+ display: flex; align-items: center; justify-content: center; flex-shrink: 0;
307
+ transition: background 0.25s var(--ease);
308
+ }
309
+ .brand-icon svg { width: 16px; height: 16px; fill: #fff; }
310
+ .brand-name {
311
+ font-family: var(--font-display); font-size: 17px; color: var(--text);
312
+ letter-spacing: -0.01em; line-height: 1; transition: color 0.25s var(--ease);
313
+ }
314
+ .brand-name span { color: var(--accent); transition: color 0.25s var(--ease); }
315
+ .header-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
316
+ .filter-wrap { position: relative; }
317
+ .filter-select {
318
+ appearance: none; -webkit-appearance: none;
319
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
320
+ color: var(--text-2); font-family: var(--font-body); font-size: 12px; font-weight: 500;
321
+ padding: 6px 28px 6px 10px; cursor: pointer; outline: none;
322
+ transition: border-color 0.15s, color 0.15s, background 0.25s var(--ease); min-width: 140px;
323
+ }
324
+ .filter-select:hover { border-color: var(--accent); color: var(--text); }
325
+ .filter-select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-mid); }
326
+ .filter-chevron {
327
+ position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
328
+ pointer-events: none; color: var(--text-3);
329
+ }
330
+ .gh-link {
331
+ display: flex; align-items: center; gap: 6px; padding: 6px 12px;
332
+ border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface);
333
+ color: var(--text-2); font-size: 12px; font-weight: 500; text-decoration: none;
334
+ transition: border-color 0.15s, color 0.15s, background 0.25s var(--ease); white-space: nowrap;
335
+ }
336
+ .gh-link:hover { border-color: var(--accent); color: var(--text); }
337
+ .gh-link svg { width: 14px; height: 14px; fill: currentColor; flex-shrink: 0; }
338
+ .theme-toggle {
339
+ width: 40px; height: 22px; border-radius: 11px; background: var(--toggle-bg);
340
+ border: none; cursor: pointer; position: relative; transition: background 0.25s var(--ease); flex-shrink: 0;
341
+ }
342
+ .theme-toggle::after {
343
+ content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
344
+ border-radius: 50%; background: var(--toggle-knob);
345
+ transition: transform 0.25s var(--ease), background 0.25s var(--ease);
346
+ }
347
+ [data-theme="light"] .theme-toggle::after { transform: translateX(18px); }
348
+ .wrap { max-width: 1160px; margin: 0 auto; padding: 36px 28px 64px; }
349
+ .page-title { text-align: center; margin-bottom: 36px; }
350
+ .page-title h1 {
351
+ font-family: var(--font-display); font-size: 28px; color: var(--text);
352
+ letter-spacing: -0.02em; line-height: 1.1; transition: color 0.25s var(--ease);
353
+ }
354
+ .page-title p {
355
+ font-size: 13px; color: var(--text-3); margin-top: 6px; font-weight: 400;
356
+ transition: color 0.25s var(--ease);
357
+ }
358
+ .section-label {
359
+ font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
360
+ color: var(--text-3); margin-bottom: 12px; transition: color 0.25s var(--ease);
361
+ }
362
+ .cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 36px; }
363
+ .card {
364
+ background: var(--surface); box-shadow: var(--shadow-card); border-radius: var(--radius);
365
+ padding: 22px 22px 18px;
366
+ transition: box-shadow 0.2s var(--ease), transform 0.2s var(--ease), background 0.25s var(--ease);
367
+ animation: fadeUp 0.4s var(--ease) both;
368
+ }
369
+ .card:nth-child(1) { animation-delay: 0.05s; }
370
+ .card:nth-child(2) { animation-delay: 0.10s; }
371
+ .card:nth-child(3) { animation-delay: 0.15s; }
372
+ .card:nth-child(4) { animation-delay: 0.20s; }
373
+ @keyframes fadeUp {
374
+ from { opacity: 0; transform: translateY(8px); }
375
+ to { opacity: 1; transform: translateY(0); }
376
+ }
377
+ .card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
378
+ .card-label {
379
+ font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
380
+ color: var(--text-3); margin-bottom: 10px; transition: color 0.25s var(--ease);
381
+ }
382
+ .card-value { font-family: var(--font-mono); font-size: 26px; font-weight: 500; line-height: 1; letter-spacing: -0.02em; }
383
+ .card-value.coral { color: var(--accent); }
384
+ .card-value.amber { color: var(--amber); }
385
+ .card-value.green { color: var(--green); }
386
+ .card-sub { font-size: 12px; color: var(--text-3); margin-top: 8px; font-weight: 400; transition: color 0.25s var(--ease); }
387
+ .table-section { animation: fadeUp 0.4s var(--ease) 0.25s both; }
388
+ .table-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
389
+ .table-title { font-size: 15px; font-weight: 600; color: var(--text); letter-spacing: -0.01em; transition: color 0.25s var(--ease); }
390
+ .table-count {
391
+ font-size: 12px; color: var(--text-3); background: var(--border-subtle);
392
+ padding: 3px 10px; border-radius: 20px; font-weight: 500;
393
+ transition: background 0.25s var(--ease), color 0.25s var(--ease);
394
+ }
395
+ .table-wrap {
396
+ background: var(--surface); box-shadow: var(--shadow-card); border-radius: var(--radius);
397
+ overflow: hidden; transition: background 0.25s var(--ease);
398
+ }
399
+ .table-scroll { overflow-x: auto; }
400
+ table { width: 100%; border-collapse: collapse; min-width: 860px; }
401
+ thead tr { background: var(--surface-thead); border-bottom: 2px solid var(--border); }
402
+ thead th {
403
+ padding: 12px 16px; text-align: left; font-size: 10px; font-weight: 600;
404
+ letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-3); white-space: nowrap;
405
+ transition: background 0.25s var(--ease), color 0.25s var(--ease);
406
+ }
407
+ thead th:first-child { color: var(--accent); opacity: 0.8; }
408
+ tbody tr { border-bottom: 1px solid var(--border-subtle); transition: background 0.12s var(--ease); }
409
+ tbody tr:last-child { border-bottom: none; }
410
+ tbody tr:nth-child(even) { background: rgba(255,255,255,0.02); }
411
+ [data-theme="light"] tbody tr:nth-child(even) { background: rgba(0,0,0,0.015); }
412
+ tbody tr:hover { background: var(--accent-mid); }
413
+ tbody td { padding: 11px 16px; font-size: 13px; white-space: nowrap; }
414
+ .tag {
415
+ display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px;
416
+ border-radius: var(--radius-tag); background: var(--accent-mid); color: var(--accent);
417
+ font-size: 12px; font-weight: 600; letter-spacing: -0.01em; max-width: 160px;
418
+ overflow: hidden; text-overflow: ellipsis;
419
+ transition: background 0.25s var(--ease), color 0.25s var(--ease);
420
+ }
421
+ .tag::before {
422
+ content: ''; width: 5px; height: 5px; border-radius: 50%;
423
+ background: currentColor; opacity: 0.6; flex-shrink: 0;
424
+ }
425
+ .col-model { font-family: var(--font-mono); font-size: 11px; color: var(--text-2); }
426
+ .col-ts { font-family: var(--font-mono); font-size: 11px; color: var(--text-3); }
427
+ .col-dur { font-family: var(--font-mono); font-size: 12px; color: var(--text); font-weight: 500; }
428
+ .col-tok { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--amber); }
429
+ .col-cost { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--green); }
430
+ .reason-badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; letter-spacing: 0.02em; }
431
+ .reason-badge.normal { background: var(--green-bg); color: var(--green); }
432
+ .reason-badge.interrupt { background: var(--amber-bg); color: var(--amber); }
433
+ .reason-badge.pending { background: var(--pending-bg); color: var(--pending); font-style: italic; }
434
+ .reason-badge.unknown { background: var(--border-subtle); color: var(--text-3); }
435
+ @media (max-width: 840px) {
436
+ .cards { grid-template-columns: repeat(2, 1fr); }
437
+ .wrap { padding: 24px 16px 48px; }
438
+ .header-inner { padding: 0 16px; }
439
+ .gh-link span { display: none; }
440
+ }
441
+ @media (max-width: 600px) {
442
+ .filter-select { min-width: 110px; }
443
+ .brand-name { font-size: 15px; }
444
+ }
445
+ @media (prefers-reduced-motion: reduce) {
446
+ .card, .table-section { animation: none; }
447
+ .card:hover { transform: none; }
448
+ * { transition-duration: 0ms !important; }
449
+ }
450
+ tbody tr.hidden { display: none; }
451
+ </style>
452
+ </head>
453
+ <body>
454
+
455
+ <header class="header">
456
+ <div class="header-inner">
457
+ <div class="brand">
458
+ <div class="brand-icon">
459
+ <svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
460
+ <path d="M9 1.5C4.86 1.5 1.5 4.86 1.5 9s3.36 7.5 7.5 7.5 7.5-3.36 7.5-7.5S13.14 1.5 9 1.5zm0 2.5a5 5 0 110 10A5 5 0 019 4zm0 2a3 3 0 100 6 3 3 0 000-6z"/>
461
+ </svg>
462
+ </div>
463
+ <div class="brand-name">claude<span>.</span>statusline</div>
464
+ </div>
465
+ <div class="header-controls">
466
+ <div class="filter-wrap">
467
+ <select class="filter-select" id="projectFilter" aria-label="Filter by project">
468
+ <option value="">All projects</option>
469
+ ${projectOptions}
470
+ </select>
471
+ <svg class="filter-chevron" width="10" height="10" viewBox="0 0 10 10" fill="none">
472
+ <path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
473
+ </svg>
474
+ </div>
475
+ <a class="gh-link" href="https://github.com/alyibrahim/claude-statusline" target="_blank" rel="noopener" aria-label="GitHub repository">
476
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
477
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
478
+ </svg>
479
+ <span>GitHub</span>
480
+ </a>
481
+ <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode" title="Toggle theme"></button>
482
+ </div>
483
+ </div>
484
+ </header>
485
+
486
+ <main class="wrap">
487
+ <div class="page-title">
488
+ <h1>Session History</h1>
489
+ <p>Claude Code usage across all projects</p>
490
+ </div>
491
+ <div class="section-label">Overview</div>
492
+ <div class="cards">
493
+ <div class="card">
494
+ <div class="card-label">Sessions</div>
495
+ <div class="card-value coral" id="statSessions">${sessionCount}</div>
496
+ <div class="card-sub">recorded</div>
497
+ </div>
498
+ <div class="card">
499
+ <div class="card-label">Tokens In</div>
500
+ <div class="card-value amber" id="statTokIn">${fmt(totalIn)}</div>
501
+ <div class="card-sub">input tokens</div>
502
+ </div>
503
+ <div class="card">
504
+ <div class="card-label">Tokens Out</div>
505
+ <div class="card-value amber" id="statTokOut">${fmt(totalOut)}</div>
506
+ <div class="card-sub">output tokens</div>
507
+ </div>
508
+ <div class="card">
509
+ <div class="card-label">Total Spend</div>
510
+ <div class="card-value green" id="statCost">$${totalCost.toFixed(2)}</div>
511
+ <div class="card-sub">USD</div>
512
+ </div>
513
+ </div>
514
+ <div class="table-section">
515
+ <div class="table-header">
516
+ <div class="table-title">Session Log</div>
517
+ <div class="table-count" id="rowCount">${rowCount} entr${rowCount === 1 ? 'y' : 'ies'}</div>
518
+ </div>
519
+ <div class="table-wrap">
520
+ <div class="table-scroll">
521
+ <table>
522
+ <thead>
523
+ <tr>
524
+ <th>Project</th>
525
+ <th>Model</th>
526
+ <th>Start Time</th>
527
+ <th>Duration</th>
528
+ <th>Tokens In</th>
529
+ <th>Tokens Out</th>
530
+ <th>Cost</th>
531
+ <th>Reason</th>
532
+ </tr>
533
+ </thead>
534
+ <tbody id="tableBody">${rowsHtml}${emptyRow}</tbody>
535
+ </table>
536
+ </div>
537
+ </div>
538
+ </div>
539
+ </main>
540
+
541
+ <script>
542
+ const toggle = document.getElementById('themeToggle');
543
+ const html = document.documentElement;
544
+ html.setAttribute('data-theme', localStorage.getItem('theme') || 'dark');
545
+ toggle.addEventListener('click', () => {
546
+ const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
547
+ html.setAttribute('data-theme', next);
548
+ localStorage.setItem('theme', next);
549
+ });
550
+
551
+ function fmtTokens(n) {
552
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
553
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
554
+ return String(n);
555
+ }
556
+
557
+ const filter = document.getElementById('projectFilter');
558
+ const rows = document.querySelectorAll('#tableBody tr');
559
+ const countEl = document.getElementById('rowCount');
560
+ const statSessions = document.getElementById('statSessions');
561
+ const statTokIn = document.getElementById('statTokIn');
562
+ const statTokOut = document.getElementById('statTokOut');
563
+ const statCost = document.getElementById('statCost');
564
+
565
+ function applyFilter() {
566
+ const val = filter.value;
567
+ let sessions = 0, tokIn = 0, tokOut = 0, cost = 0, visible = 0;
568
+ rows.forEach(row => {
569
+ const match = !val || row.dataset.project === val;
570
+ row.classList.toggle('hidden', !match);
571
+ if (match) {
572
+ visible++;
573
+ if (row.dataset.pending !== '1') {
574
+ sessions++;
575
+ tokIn += Number(row.dataset.tokIn) || 0;
576
+ tokOut += Number(row.dataset.tokOut) || 0;
577
+ cost += Number(row.dataset.cost) || 0;
578
+ }
579
+ }
580
+ });
581
+ countEl.textContent = visible + ' entr' + (visible === 1 ? 'y' : 'ies');
582
+ statSessions.textContent = sessions;
583
+ statTokIn.textContent = fmtTokens(tokIn);
584
+ statTokOut.textContent = fmtTokens(tokOut);
585
+ statCost.textContent = '$' + cost.toFixed(2);
586
+ }
587
+
588
+ filter.addEventListener('change', applyFilter);
589
+ </script>
590
+ </body>
591
+ </html>`;
592
+
593
+ const tempPath = path.join(os.tmpdir(), 'claude-statusline-dashboard.html');
594
+ fs.writeFileSync(tempPath, html);
595
+ console.log(`Opened dashboard at ${tempPath}`);
596
+ await open.default(tempPath);
202
597
  }
203
598
 
204
599
  module.exports = { handleHookStart, handleHookEnd, handleHistory };
package/scripts/setup.js CHANGED
@@ -59,11 +59,15 @@ function updateHooks(settings, command, enable) {
59
59
 
60
60
  function toggleHook(hookName, cmdString) {
61
61
  if (!settings.hooks[hookName]) settings.hooks[hookName] = [];
62
- settings.hooks[hookName] = settings.hooks[hookName].filter(h =>
63
- !(h.command && (h.command.includes('hook start') || h.command.includes('hook end')))
64
- );
62
+ // Remove any existing statusline hook entries (both old and new format)
63
+ settings.hooks[hookName] = settings.hooks[hookName].filter(h => {
64
+ if (h.hooks) {
65
+ return !h.hooks.some(inner => inner.command && (inner.command.includes('hook start') || inner.command.includes('hook end')));
66
+ }
67
+ return !(h.command && (h.command.includes('hook start') || h.command.includes('hook end')));
68
+ });
65
69
  if (enable) {
66
- settings.hooks[hookName].push({ type: 'command', command: cmdString });
70
+ settings.hooks[hookName].push({ matcher: '', hooks: [{ type: 'command', command: cmdString }] });
67
71
  }
68
72
  if (settings.hooks[hookName].length === 0) {
69
73
  delete settings.hooks[hookName];