agentlytics 0.1.0 → 0.1.1

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
@@ -13,11 +13,11 @@
13
13
  <a href="https://www.npmjs.com/package/agentlytics"><img src="https://img.shields.io/npm/v/agentlytics?color=6366f1&label=npm" alt="npm"></a>
14
14
  <a href="#supported-editors"><img src="https://img.shields.io/badge/editors-14-818cf8" alt="editors"></a>
15
15
  <a href="#license"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
16
- <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A518-brightgreen" alt="node"></a>
16
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A520.19%20%7C%20%E2%89%A522.12-brightgreen" alt="node"></a>
17
17
  </p>
18
18
 
19
19
  <p align="center">
20
- <img src="https://github.com/user-attachments/assets/fdb0acb2-db0f-4091-af23-949ca0fae9c8" alt="Agentlytics demo" width="100%">
20
+ <img src="misc/screenshot.png" alt="Agentlytics dashboard" width="100%">
21
21
  </p>
22
22
 
23
23
  ---
@@ -30,7 +30,7 @@ Agentlytics reads local chat history from every major AI coding assistant and pr
30
30
  npx agentlytics
31
31
  ```
32
32
 
33
- Opens at **http://localhost:4637**. Requires Node.js ≥ 18, macOS.
33
+ Opens at **http://localhost:4637**. Requires Node.js ≥ 20.19 or ≥ 22.12, macOS.
34
34
 
35
35
  To only build the cache database without starting the server:
36
36
 
package/editors/base.js CHANGED
@@ -1,5 +1,28 @@
1
+ const path = require('path');
2
+ const os = require('os');
1
3
  const chalk = require('chalk');
2
4
 
5
+ const HOME = os.homedir();
6
+
7
+ // --- Platform utilities ---
8
+
9
+ /**
10
+ * Get platform-specific app data directory path for VS Code-like editors.
11
+ * - macOS: ~/Library/Application Support/{appName}/User/...
12
+ * - Windows: ~/AppData/Roaming/{appName}/User/...
13
+ * - Linux: ~/.config/{appName}/User/...
14
+ */
15
+ function getAppDataPath(appName) {
16
+ switch (process.platform) {
17
+ case 'darwin':
18
+ return path.join(HOME, 'Library', 'Application Support', appName);
19
+ case 'win32':
20
+ return path.join(HOME, 'AppData', 'Roaming', appName);
21
+ default: // linux, etc.
22
+ return path.join(HOME, '.config', appName);
23
+ }
24
+ }
25
+
3
26
  // --- Formatting utilities shared across all editor adapters ---
4
27
 
5
28
  function formatArgs(args, maxLen = 300) {
@@ -117,6 +140,7 @@ function shortenPath(p, maxLen = 40) {
117
140
  */
118
141
 
119
142
  module.exports = {
143
+ getAppDataPath,
120
144
  formatArgs,
121
145
  formatToolCall,
122
146
  formatToolResult,
package/editors/cursor.js CHANGED
@@ -2,11 +2,13 @@ const Database = require('better-sqlite3');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
+ const { getAppDataPath } = require('./base');
5
6
 
6
7
  const HOME = os.homedir();
7
8
  const CURSOR_CHATS_DIR = path.join(HOME, '.cursor', 'chats');
8
- const WORKSPACE_STORAGE_DIR = path.join(HOME, 'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage');
9
- const GLOBAL_STORAGE_DB = path.join(HOME, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
9
+ const CURSOR_USER_DIR = path.join(getAppDataPath('Cursor'), 'User');
10
+ const WORKSPACE_STORAGE_DIR = path.join(CURSOR_USER_DIR, 'workspaceStorage');
11
+ const GLOBAL_STORAGE_DB = path.join(CURSOR_USER_DIR, 'globalStorage', 'state.vscdb');
10
12
 
11
13
  // ============================================================
12
14
  // Source 1: ~/.cursor/chats/<hash>/<chatId>/store.db (agent KV)
package/editors/vscode.js CHANGED
@@ -1,18 +1,17 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
-
5
- const HOME = os.homedir();
4
+ const { getAppDataPath } = require('./base');
6
5
 
7
6
  // VS Code variants: stable and insiders
8
7
  const VARIANTS = [
9
8
  {
10
9
  id: 'vscode',
11
- appSupport: path.join(HOME, 'Library', 'Application Support', 'Code'),
10
+ appSupport: getAppDataPath('Code'),
12
11
  },
13
12
  {
14
13
  id: 'vscode-insiders',
15
- appSupport: path.join(HOME, 'Library', 'Application Support', 'Code - Insiders'),
14
+ appSupport: getAppDataPath('Code - Insiders'),
16
15
  },
17
16
  ];
18
17
 
package/editors/zed.js CHANGED
@@ -2,9 +2,9 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const { execSync } = require('child_process');
5
+ const { getAppDataPath } = require('./base');
5
6
 
6
- const HOME = os.homedir();
7
- const THREADS_DB = path.join(HOME, 'Library', 'Application Support', 'Zed', 'threads', 'threads.db');
7
+ const THREADS_DB = path.join(getAppDataPath('Zed'), 'threads', 'threads.db');
8
8
 
9
9
  // ============================================================
10
10
  // Decompress zstd blob via CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,12 +1,10 @@
1
- import { editorColor, editorLabel } from '../lib/constants'
1
+ import { editorLabel } from '../lib/constants'
2
+ import EditorIcon from './EditorIcon'
2
3
 
3
4
  export default function EditorDot({ source, showLabel = false, size = 8 }) {
4
5
  return (
5
6
  <span className="inline-flex items-center gap-1.5">
6
- <span
7
- className="rounded-full flex-shrink-0"
8
- style={{ width: size, height: size, background: editorColor(source) }}
9
- />
7
+ <EditorIcon source={source} size={size} />
10
8
  {showLabel && <span className="text-[10px]" style={{ color: 'var(--c-text)' }}>{editorLabel(source)}</span>}
11
9
  </span>
12
10
  )
@@ -0,0 +1,63 @@
1
+ import { editorColor } from '../lib/constants'
2
+
3
+ const PATHS = {
4
+ cursor: 'M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23',
5
+ windsurf: 'M23.55 5.067c-1.2038-.002-2.1806.973-2.1806 2.1765v4.8676c0 .972-.8035 1.7594-1.7597 1.7594-.568 0-1.1352-.286-1.4718-.7659l-4.9713-7.1003c-.4125-.5896-1.0837-.941-1.8103-.941-1.1334 0-2.1533.9635-2.1533 2.153v4.8957c0 .972-.7969 1.7594-1.7596 1.7594-.57 0-1.1363-.286-1.4728-.7658L.4076 5.1598C.2822 4.9798 0 5.0688 0 5.2882v4.2452c0 .2147.0656.4228.1884.599l5.4748 7.8183c.3234.462.8006.8052 1.3509.9298 1.3771.313 2.6446-.747 2.6446-2.0977v-4.893c0-.972.7875-1.7593 1.7596-1.7593h.003a1.798 1.798 0 0 1 1.4718.7658l4.9723 7.0994c.4135.5905 1.05.941 1.8093.941 1.1587 0 2.1515-.9645 2.1515-2.153v-4.8948c0-.972.7875-1.7594 1.7596-1.7594h.194a.22.22 0 0 0 .2204-.2202v-4.622a.22.22 0 0 0-.2203-.2203Z',
6
+ claude: 'm4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z',
7
+ zed: 'M2.25 1.5a.75.75 0 0 0-.75.75v16.5H0V2.25A2.25 2.25 0 0 1 2.25 0h20.095c1.002 0 1.504 1.212.795 1.92L10.764 14.298h3.486V12.75h1.5v1.922a1.125 1.125 0 0 1-1.125 1.125H9.264l-2.578 2.578h11.689V9h1.5v9.375a1.5 1.5 0 0 1-1.5 1.5H5.185L2.562 22.5H21.75a.75.75 0 0 0 .75-.75V5.25H24v16.5A2.25 2.25 0 0 1 21.75 24H1.655C.653 24 .151 22.788.86 22.08L13.19 9.75H9.75v1.5h-1.5V9.375A1.125 1.125 0 0 1 9.375 8.25h5.314l2.625-2.625H5.625V15h-1.5V5.625a1.5 1.5 0 0 1 1.5-1.5h13.19L21.438 1.5z',
8
+ gemini: 'M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81',
9
+ copilot: 'M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z',
10
+ // VS Code - official bracket icon
11
+ vscode: 'M17.583.063a1.5 1.5 0 0 0-1.032.392 1.5 1.5 0 0 0-.001 0L7.04 9.708 2.81 6.442a1 1 0 0 0-1.32.098L.178 7.853a1 1 0 0 0 0 1.414l3.612 3.29-3.612 3.29a1 1 0 0 0 0 1.414L1.49 18.574a1 1 0 0 0 1.32.098L7.04 15.408l9.51 9.253a1.5 1.5 0 0 0 1.033.392A1.5 1.5 0 0 0 19.08 23.5V1.5A1.5 1.5 0 0 0 17.583.063M17.08 5.442l-5.5 7.115 5.5 7.115Z',
12
+ antigravity: 'M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z',
13
+ command: 'M6,2A4,4 0 0,1 10,6V8H14V6A4,4 0 0,1 18,2A4,4 0 0,1 22,6A4,4 0 0,1 18,10H16V14H18A4,4 0 0,1 22,18A4,4 0 0,1 18,22A4,4 0 0,1 14,18V16H10V18A4,4 0 0,1 6,22A4,4 0 0,1 2,18A4,4 0 0,1 6,14H8V10H6A4,4 0 0,1 2,6A4,4 0 0,1 6,2M16,18A2,2 0 0,0 18,20A2,2 0 0,0 20,18A2,2 0 0,0 18,16H16V18M14,10H10V14H14V10M6,16A2,2 0 0,0 4,18A2,2 0 0,0 6,20A2,2 0 0,0 8,18V16H6M8,6A2,2 0 0,0 6,4A2,2 0 0,0 4,6A2,2 0 0,0 6,8H8V6M18,8A2,2 0 0,0 20,6A2,2 0 0,0 18,4A2,2 0 0,0 16,6V8H18Z',
14
+ // Terminal icon for generic editors
15
+ terminal: 'M4 17.27V19h16v-1.73ZM4 5v1.73l7.07 4.55L4 15.82v1.73l10-6.46Z',
16
+ }
17
+
18
+ // Map editor IDs to icon paths
19
+ const EDITOR_ICONS = {
20
+ 'cursor': 'cursor',
21
+ 'cursor-agent': 'cursor',
22
+ 'windsurf': 'windsurf',
23
+ 'windsurf-next': 'windsurf',
24
+ 'antigravity': 'antigravity',
25
+ 'claude-code': 'claude',
26
+ 'claude': 'claude',
27
+ 'vscode': 'vscode',
28
+ 'vscode-insiders': 'vscode',
29
+ 'zed': 'zed',
30
+ 'gemini-cli': 'gemini',
31
+ 'copilot-cli': 'copilot',
32
+ 'opencode': 'terminal',
33
+ 'codex': 'terminal',
34
+ 'commandcode': 'command',
35
+ }
36
+
37
+ export default function EditorIcon({ source, size = 16, className = '' }) {
38
+ const pathKey = EDITOR_ICONS[source] || 'terminal'
39
+ const d = PATHS[pathKey]
40
+ const color = editorColor(source)
41
+
42
+ if (!d) {
43
+ return (
44
+ <span
45
+ className={`inline-block rounded-full flex-shrink-0 ${className}`}
46
+ style={{ width: size, height: size, background: color }}
47
+ />
48
+ )
49
+ }
50
+
51
+ return (
52
+ <svg
53
+ viewBox="0 0 24 24"
54
+ width={size}
55
+ height={size}
56
+ className={`inline-block flex-shrink-0 ${className}`}
57
+ fill={color}
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ >
60
+ <path d={d} />
61
+ </svg>
62
+ )
63
+ }
@@ -1,7 +1,7 @@
1
- export default function KpiCard({ label, value, sub }) {
1
+ export default function KpiCard({ label, value, sub, onClick }) {
2
2
  return (
3
- <div className="card px-3 py-2">
4
- <div className="text-base font-bold" style={{ color: 'var(--c-white)' }}>{value}</div>
3
+ <div className={`card px-3 py-2${onClick ? ' cursor-pointer hover:opacity-80 transition' : ''}`} onClick={onClick}>
4
+ <div className="text-base font-bold" style={{ color: onClick ? 'var(--c-accent)' : 'var(--c-white)' }}>{value}</div>
5
5
  <div className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{label}</div>
6
6
  {sub && <div className="text-[9px] mt-0.5" style={{ color: 'var(--c-text3)' }}>{sub}</div>}
7
7
  </div>
@@ -1,13 +1,14 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
- import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2 } from 'lucide-react'
3
+ import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2, AlertTriangle } from 'lucide-react'
4
4
  import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler } from 'chart.js'
5
5
  import { Doughnut, Bar, Line } from 'react-chartjs-2'
6
6
  import KpiCard from '../components/KpiCard'
7
7
  import ActivityHeatmap from '../components/ActivityHeatmap'
8
8
  import DateRangePicker from '../components/DateRangePicker'
9
9
  import { editorColor, editorLabel, formatNumber, dateRangeToApiParams } from '../lib/constants'
10
- import { fetchDailyActivity, fetchOverview as fetchOverviewApi, fetchDashboardStats, fetchShareImage } from '../lib/api'
10
+ import EditorIcon from '../components/EditorIcon'
11
+ import { fetchDailyActivity, fetchOverview as fetchOverviewApi, fetchDashboardStats, fetchShareImage, fetchChats } from '../lib/api'
11
12
  import { useTheme } from '../lib/theme'
12
13
  import SectionTitle from '../components/SectionTitle'
13
14
 
@@ -30,6 +31,7 @@ export default function Dashboard({ overview }) {
30
31
  const [dateRange, setDateRange] = useState(null)
31
32
  const { dark } = useTheme()
32
33
  const [sharing, setSharing] = useState(false)
34
+ const [largeContextChats, setLargeContextChats] = useState(null)
33
35
  const txtColor = dark ? '#888' : '#555'
34
36
  const txtDim = dark ? '#555' : '#999'
35
37
  const gridColor = dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)'
@@ -50,6 +52,14 @@ export default function Dashboard({ overview }) {
50
52
 
51
53
  useEffect(() => {
52
54
  const dateParams = dateRangeToApiParams(dateRange)
55
+ const chatParams = { limit: 500, named: false, ...dateParams }
56
+ if (selectedEditor) chatParams.editor = selectedEditor
57
+
58
+ fetchChats(chatParams).then(r => {
59
+ const big = (r.chats || []).filter(c => c.bubbleCount >= 100).sort((a, b) => b.bubbleCount - a.bubbleCount)
60
+ setLargeContextChats(big)
61
+ })
62
+
53
63
  if (!selectedEditor) {
54
64
  setFilteredData(null)
55
65
  fetchDailyActivity(dateParams).then(setDailyData)
@@ -218,76 +228,124 @@ export default function Dashboard({ overview }) {
218
228
  </button>
219
229
  </div>
220
230
 
221
- {/* Editor breakdown - top */}
231
+ {/* Editor breakdown - compact row */}
222
232
  <div className="card p-3">
223
- <SectionTitle>editors</SectionTitle>
224
- <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-2">
233
+ <div className="flex items-center flex-wrap gap-1.5">
225
234
  {allEditors.map(e => {
226
235
  const isSelected = selectedEditor === e.id
227
236
  return (
228
- <div
237
+ <button
229
238
  key={e.id}
230
- className="card px-3 py-3 text-center cursor-pointer transition"
239
+ className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] cursor-pointer transition rounded-sm"
231
240
  style={{
232
241
  border: isSelected ? `1.5px solid ${editorColor(e.id)}` : '1px solid var(--c-border)',
242
+ background: isSelected ? editorColor(e.id) + '15' : 'transparent',
233
243
  opacity: selectedEditor && !isSelected ? 0.4 : 1,
244
+ color: 'var(--c-text)',
234
245
  }}
235
246
  onClick={() => setSelectedEditor(isSelected ? null : e.id)}
236
247
  >
237
- <div className="w-2.5 h-2.5 rounded-full mx-auto mb-1.5" style={{ background: editorColor(e.id) }} />
238
- <div className="text-lg font-bold" style={{ color: 'var(--c-white)' }}>{e.count}</div>
239
- <div className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{editorLabel(e.id)}</div>
240
- </div>
248
+ <EditorIcon source={e.id} size={14} />
249
+ <span style={{ color: 'var(--c-text2)' }}>{editorLabel(e.id)}</span>
250
+ <span className="font-bold" style={{ color: 'var(--c-white)' }}>{e.count}</span>
251
+ </button>
241
252
  )
242
253
  })}
243
254
  </div>
244
255
  {selectedEditor && sel && (
245
- <div className="mt-3 flex items-center gap-2">
256
+ <div className="mt-2 flex items-center gap-2">
246
257
  <button onClick={() => navigate(`/sessions?editor=${selectedEditor}`)} className="flex items-center gap-1 text-[11px] px-2.5 py-1 transition" style={{ color: 'var(--c-accent)', border: '1px solid var(--c-border)' }}>
247
258
  Show Sessions <ArrowRight size={11} />
248
259
  </button>
249
260
  <button onClick={() => setSelectedEditor(null)} className="flex items-center gap-1 text-[11px] px-2.5 py-1 transition" style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}>
250
261
  <X size={9} /> Clear
251
262
  </button>
252
- <span className="text-[11px] ml-auto" style={{ color: 'var(--c-text)' }}>
253
- <span className="font-bold" style={{ color: editorColor(selectedEditor) }}>{editorLabel(selectedEditor)}</span>
254
- <span style={{ color: 'var(--c-text2)' }}> — {sel.count} sessions</span>
255
- </span>
256
263
  </div>
257
264
  )}
258
265
  </div>
259
266
 
260
267
 
261
- {/* KPIs row 1: Core stats */}
262
- <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-2">
263
- <KpiCard label="total sessions" value={formatNumber(d.totalChats)} sub={sel ? editorLabel(sel.id) : `${allEditors.length} editors`} />
264
- <KpiCard label="projects" value={d.topProjects.length} sub="unique folders" />
265
- <KpiCard label="time span" value={`${daysSpan}d`} sub={d.oldestChat ? `since ${new Date(d.oldestChat).toLocaleDateString()}` : ''} />
266
- <KpiCard label="this month" value={thisMonth ? thisMonth.count : 0} sub={thisMonth ? thisMonth.month : ''} />
268
+ {/* KPIs compact single row */}
269
+ <div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
270
+ <KpiCard label="sessions" value={formatNumber(d.totalChats)} sub={sel ? editorLabel(sel.id) : `${allEditors.length} editors`} onClick={() => navigate(selectedEditor ? `/sessions?editor=${selectedEditor}` : '/sessions')} />
271
+ <KpiCard label="projects" value={d.topProjects.length} sub={`${daysSpan}d span`} onClick={() => navigate('/projects')} />
272
+ <KpiCard label="this month" value={thisMonth ? thisMonth.count : 0} sub={thisMonth ? thisMonth.month : ''} onClick={() => navigate('/sessions')} />
267
273
  {stats && <>
268
- <KpiCard label="current streak" value={`${stats.streaks.current}d`} sub={<span className="flex items-center gap-0.5"><Flame size={8} className="text-orange-400" /> {stats.streaks.longest}d best</span>} />
269
- <KpiCard label="active days" value={stats.streaks.totalDays} sub={daysSpan > 0 ? `${((stats.streaks.totalDays / daysSpan) * 100).toFixed(0)}% of span` : ''} />
270
- <KpiCard label="avg msgs/session" value={avgMsgsPerSession} sub={<span className="flex items-center gap-0.5"><MessageSquare size={8} /> conversation depth</span>} />
271
- <KpiCard label="tool calls" value={formatNumber(stats.totalToolCalls)} sub={<span className="flex items-center gap-0.5"><Wrench size={8} /> total invocations</span>} />
274
+ <KpiCard label="avg depth" value={avgMsgsPerSession} sub={<span className="flex items-center gap-0.5"><MessageSquare size={8} /> msgs/session</span>} />
275
+ <KpiCard label="tool calls" value={formatNumber(stats.totalToolCalls)} sub={<span className="flex items-center gap-0.5"><Wrench size={8} /> total</span>} />
276
+ </>}
277
+ {tk && tk.input > 0 && <>
278
+ <KpiCard label="tokens in" value={formatNumber(tk.input)} sub="prompt" />
279
+ <KpiCard label="tokens out" value={formatNumber(tk.output)} sub={`${outputInputRatio}× ratio`} />
280
+ <KpiCard label="cache hit" value={`${cacheHitRate}%`} sub={formatNumber(tk.cacheRead)} />
281
+ <KpiCard label="you wrote" value={formatNumber(tk.userChars)} sub={`AI: ${formatNumber(tk.assistantChars)}`} />
272
282
  </>}
273
283
  </div>
274
284
 
275
- {/* Token economy KPIs */}
276
- {tk && tk.input > 0 && (
277
- <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2">
278
- <KpiCard label="input tokens" value={formatNumber(tk.input)} sub="total prompt" />
279
- <KpiCard label="output tokens" value={formatNumber(tk.output)} sub="total completion" />
280
- <KpiCard label="cache read" value={formatNumber(tk.cacheRead)} sub={`${cacheHitRate}% hit rate`} />
281
- <KpiCard label="cache write" value={formatNumber(tk.cacheWrite)} />
282
- <KpiCard label="output/input" value={`${outputInputRatio}×`} sub={<span className="flex items-center gap-0.5"><Zap size={8} /> efficiency ratio</span>} />
283
- <KpiCard label="you wrote" value={formatNumber(tk.userChars)} sub={`AI wrote ${formatNumber(tk.assistantChars)}`} />
284
- </div>
285
- )}
286
-
287
- {/* Activity Heatmap */}
285
+ {/* Activity Heatmap | Col 1 | Col 2 | Col 3 */}
288
286
  <div className="card p-3">
289
287
  <SectionTitle>agentic coding activity</SectionTitle>
290
- {dailyData ? <ActivityHeatmap dailyData={dailyData} /> : <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>loading...</div>}
288
+ <div className="flex gap-4">
289
+ <div className="min-w-0 flex-shrink-0">
290
+ {dailyData ? <ActivityHeatmap dailyData={dailyData} /> : <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>loading...</div>}
291
+ </div>
292
+ {stats && dailyData && (() => {
293
+ const activeDays = dailyData.filter(d => d.total > 0)
294
+ const busiest = activeDays.length > 0 ? activeDays.reduce((a, b) => a.total > b.total ? a : b) : null
295
+ const totalSessions = activeDays.reduce((s, d) => s + d.total, 0)
296
+ const avgPerDay = activeDays.length > 0 ? (totalSessions / activeDays.length).toFixed(1) : 0
297
+ return (
298
+ <div className="flex-1 grid grid-cols-3 gap-3 text-[10px] min-w-0" style={{ borderLeft: '1px solid var(--c-border)', paddingLeft: 16 }}>
299
+ <div className="space-y-2 min-w-0">
300
+ <div>
301
+ <div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">streaks</div>
302
+ <div className="flex items-center gap-1">
303
+ <Flame size={10} className="text-orange-400 flex-shrink-0" />
304
+ <span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.current}d</span>
305
+ <span style={{ color: 'var(--c-text3)' }}>now</span>
306
+ </div>
307
+ <div className="flex items-center gap-1 mt-0.5">
308
+ <Zap size={10} className="text-yellow-400 flex-shrink-0" />
309
+ <span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.longest}d</span>
310
+ <span style={{ color: 'var(--c-text3)' }}>best</span>
311
+ </div>
312
+ </div>
313
+ <div>
314
+ <div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">active days</div>
315
+ <span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.totalDays}</span>
316
+ <span className="ml-1" style={{ color: 'var(--c-text3)' }}>{avgPerDay}/day</span>
317
+ </div>
318
+ </div>
319
+ <div className="space-y-2 min-w-0">
320
+ {busiest && (
321
+ <div>
322
+ <div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">busiest day</div>
323
+ <div style={{ color: 'var(--c-white)' }} className="font-bold">{busiest.day}</div>
324
+ <div style={{ color: 'var(--c-text3)' }}>{busiest.total} sessions</div>
325
+ </div>
326
+ )}
327
+ <div>
328
+ <div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">peak hour</div>
329
+ <div style={{ color: 'var(--c-white)' }} className="font-bold">{String(stats.hourly.indexOf(Math.max(...stats.hourly))).padStart(2, '0')}:00</div>
330
+ <div style={{ color: 'var(--c-text3)' }}>{Math.max(...stats.hourly)} sessions</div>
331
+ </div>
332
+ </div>
333
+ <div className="min-w-0">
334
+ <div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">top modes</div>
335
+ <div className="space-y-1">
336
+ {modes.slice(0, 5).map(([mode, count]) => (
337
+ <div key={mode} className="flex items-center gap-1">
338
+ <div className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: MODE_COLORS[mode] || '#6b7280' }} />
339
+ <span className="truncate" style={{ color: 'var(--c-text)' }}>{mode}</span>
340
+ <span className="ml-auto font-bold flex-shrink-0" style={{ color: 'var(--c-white)' }}>{formatNumber(count)}</span>
341
+ </div>
342
+ ))}
343
+ </div>
344
+ </div>
345
+ </div>
346
+ )
347
+ })()}
348
+ </div>
291
349
  </div>
292
350
 
293
351
  {/* Monthly trend (stacked bar by editor) */}
@@ -405,56 +463,83 @@ export default function Dashboard({ overview }) {
405
463
  </div>
406
464
  </div>
407
465
 
408
- {/* Bottom row: Top models + Top tools */}
409
- {stats && (stats.topModels.length > 0 || stats.topTools.length > 0) && (
410
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
411
- {/* Top models */}
412
- {stats.topModels.length > 0 && (
413
- <div className="card p-3">
414
- <SectionTitle>top models</SectionTitle>
415
- <div className="space-y-1">
416
- {stats.topModels.map((m, i) => {
417
- const maxM = stats.topModels[0].count
418
- return (
419
- <div key={m.name} className="flex items-center gap-2">
420
- <span className="text-[9px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
421
- <div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
422
- <div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(m.count / maxM * 100).toFixed(1)}%`, background: i === 0 ? '#6366f1' : i === 1 ? '#818cf8' : '#a5b4fc40' }}>
423
- <span className="text-[8px] truncate" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{m.name}</span>
424
- </div>
466
+ {/* Bottom row: Top models + Top tools + Large context */}
467
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-2">
468
+ {stats && stats.topModels.length > 0 && (
469
+ <div className="card p-3">
470
+ <SectionTitle>top models</SectionTitle>
471
+ <div className="space-y-1">
472
+ {stats.topModels.map((m, i) => {
473
+ const maxM = stats.topModels[0].count
474
+ return (
475
+ <div key={m.name} className="flex items-center gap-2">
476
+ <span className="text-[9px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
477
+ <div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
478
+ <div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(m.count / maxM * 100).toFixed(1)}%`, background: i === 0 ? '#6366f1' : i === 1 ? '#818cf8' : '#a5b4fc40' }}>
479
+ <span className="text-[8px] truncate" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{m.name}</span>
425
480
  </div>
426
- <span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{m.count}</span>
427
481
  </div>
428
- )
429
- })}
430
- </div>
482
+ <span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{m.count}</span>
483
+ </div>
484
+ )
485
+ })}
431
486
  </div>
432
- )}
433
-
434
- {/* Top tools */}
435
- {stats.topTools.length > 0 && (
436
- <div className="card p-3">
437
- <SectionTitle>top tools <span style={{ color: 'var(--c-text3)' }}>({formatNumber(stats.totalToolCalls)} total)</span></SectionTitle>
438
- <div className="space-y-1">
439
- {stats.topTools.map((t, i) => {
440
- const maxT = stats.topTools[0].count
441
- return (
442
- <div key={t.name} className="flex items-center gap-2">
443
- <span className="text-[9px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
444
- <div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
445
- <div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(t.count / maxT * 100).toFixed(1)}%`, background: i === 0 ? '#10b981' : i === 1 ? '#34d399' : '#6ee7b740' }}>
446
- <span className="text-[8px] truncate font-mono" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{t.name}</span>
447
- </div>
487
+ </div>
488
+ )}
489
+
490
+ {stats && stats.topTools.length > 0 && (
491
+ <div className="card p-3">
492
+ <SectionTitle>top tools <span style={{ color: 'var(--c-text3)' }}>({formatNumber(stats.totalToolCalls)} total)</span></SectionTitle>
493
+ <div className="space-y-1">
494
+ {stats.topTools.map((t, i) => {
495
+ const maxT = stats.topTools[0].count
496
+ return (
497
+ <div key={t.name} className="flex items-center gap-2">
498
+ <span className="text-[9px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
499
+ <div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
500
+ <div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(t.count / maxT * 100).toFixed(1)}%`, background: i === 0 ? '#10b981' : i === 1 ? '#34d399' : '#6ee7b740' }}>
501
+ <span className="text-[8px] truncate font-mono" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{t.name}</span>
448
502
  </div>
449
- <span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{formatNumber(t.count)}</span>
450
503
  </div>
451
- )
452
- })}
453
- </div>
504
+ <span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{formatNumber(t.count)}</span>
505
+ </div>
506
+ )
507
+ })}
454
508
  </div>
455
- )}
456
- </div>
457
- )}
509
+ </div>
510
+ )}
511
+
512
+ {largeContextChats && largeContextChats.length > 0 && (
513
+ <div className="card p-3">
514
+ <SectionTitle>
515
+ <span className="inline-flex items-center gap-1">
516
+ <AlertTriangle size={10} className="text-amber-400" />
517
+ large context
518
+ <span style={{ color: 'var(--c-text3)' }}>({largeContextChats.length})</span>
519
+ </span>
520
+ </SectionTitle>
521
+ <div className="space-y-1">
522
+ {largeContextChats.slice(0, 10).map(c => (
523
+ <div
524
+ key={c.id}
525
+ className="flex items-center gap-1.5 px-1.5 py-1 rounded-sm cursor-pointer transition hover:opacity-80"
526
+ style={{ background: c.bubbleCount >= 500 ? 'rgba(239,68,68,0.06)' : 'rgba(245,158,11,0.06)' }}
527
+ onClick={() => navigate(`/sessions/${c.id}`)}
528
+ >
529
+ <EditorIcon source={c.source} size={10} />
530
+ <span className="text-[9px] truncate flex-1" style={{ color: 'var(--c-text)' }}>{c.name || 'Untitled'}</span>
531
+ <span
532
+ className="text-[9px] font-bold flex-shrink-0"
533
+ style={{ color: c.bubbleCount >= 500 ? '#ef4444' : '#f59e0b' }}
534
+ >
535
+ {c.bubbleCount}
536
+ </span>
537
+ </div>
538
+ ))}
539
+ </div>
540
+ </div>
541
+ )}
542
+ </div>
458
543
  </div>
459
544
  )
460
545
  }