@alyibrahim/claude-statusline 1.4.4 → 1.4.5

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,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # claude-statusline
3
+ <img src=".github/assets/logo.svg" alt="claude-statusline" width="600">
4
4
 
5
5
  [![CI](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml/badge.svg)](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@alyibrahim/claude-statusline)](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alyibrahim/claude-statusline",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
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
5
  "keywords": [
6
6
  "claude",
@@ -143,452 +143,23 @@ function handleHookEnd() {
143
143
  }
144
144
 
145
145
  async function handleHistory() {
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);
151
- }
152
-
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`;
159
- }
160
- if (s >= 60) return `${Math.floor(s / 60)}m`;
161
- return `${s}s`;
162
- }
163
-
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>`;
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>`;
146
+ const templatePath = path.join(__dirname, '../dashboard-design/dashboard.html');
147
+ const cssPath = path.join(__dirname, '../dashboard-design/styles.css');
148
+ const jsPath = path.join(__dirname, '../dashboard-design/script.js');
149
+
150
+ const template = fs.readFileSync(templatePath, 'utf8');
151
+ const css = fs.readFileSync(cssPath, 'utf8');
152
+ const js = fs.readFileSync(jsPath, 'utf8');
153
+
154
+ // Most-recent first, cap at 100
155
+ const sessions = readSessions().reverse().slice(0, 100);
156
+ const sessionsJson = JSON.stringify(sessions);
157
+
158
+ // Inject CSS, JS, and data into the template using the sentinel strings
159
+ const html = template
160
+ .replace('/*INJECT_CSS*/', css)
161
+ .replace('/*INJECT_DATA*/', sessionsJson)
162
+ .replace('/*INJECT_JS*/', js);
592
163
 
593
164
  const tempPath = path.join(os.tmpdir(), 'claude-statusline-dashboard.html');
594
165
  fs.writeFileSync(tempPath, html);