From 1dff8c12cd9e44113ed57001c9f3bdeea32dacd6 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:42:48 +0500 Subject: [PATCH 01/29] Add design spec for MCP & Skill badges in session cards and conversation view --- .../2026-04-06-mcp-skill-badges-design.md | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md diff --git a/docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md b/docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md new file mode 100644 index 0000000..151cd0a --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md @@ -0,0 +1,85 @@ +# MCP & Skill Badges + +Display MCP servers and Skills used in sessions as colored badges — on session cards (list view) and on individual messages (conversation view). + +## Data sources + +Session JSONL files already contain `tool_use` content blocks in assistant messages: + +- **MCP**: `{ "type": "tool_use", "name": "mcp____", "input": {...} }` +- **Skill**: `{ "type": "tool_use", "name": "Skill", "input": { "skill": "" } }` + +Both `loadSessions()` and `loadSessionDetail()` already read these JSONL files — the tool_use blocks are just currently discarded by `extractContent()`. + +## Backend changes (data.js) + +### loadSessions() — session enrichment (~line 284) + +Where `detail_messages` is counted by iterating JSONL lines, additionally collect: + +- `s.mcp_servers: string[]` — unique MCP server names extracted from tool names matching `mcp____` (take `` part) +- `s.skills: string[]` — unique skill names from `Skill` tool_use blocks via `input.skill` + +Both arrays default to `[]`. + +### loadSessionDetail() — message-level tools + +When parsing assistant messages, for each message additionally collect a `tools` array: + +```js +{ + role: 'assistant', + content: '...', + tools: [ + { type: 'mcp', server: 'chrome-devtools-mcp', tool: 'take_screenshot' }, + { type: 'skill', skill: 'figma:figma-use' } + ] +} +``` + +Only include tools where `name.startsWith('mcp__')` or `name === 'Skill'`. Deduplicate within a single message (same tool name = one entry). + +## Frontend changes (app.js) + +### Session cards — card-top badges + +After the existing `tool-badge tool-` span, render all MCP servers and skills: + +```html +chrome-devtools +figma-use +``` + +Show all badges, no limit. + +### Conversation view — message badges + +On each assistant message that has `tools.length > 0`, render a badge row under `msg-role`: + +```html +
+ take_screenshot + navigate_page + figma-use +
+``` + +Card-level badges show server name (compact). Message-level badges show tool/skill name (detailed). + +## CSS (styles.css) + +Two new badge variants using existing `tool-badge` base class: + +```css +.badge-mcp { background: rgba(251, 146, 60, 0.2); color: #fb923c; border-color: #fb923c; } +.badge-skill { background: rgba(139, 92, 246, 0.2); color: #8b5cf6; border-color: #8b5cf6; } +.msg-tools { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } +``` + +Orange for MCP, purple for Skills. Both follow the existing `tool-badge` pattern (small, rounded, border). + +## Scope + +- Only MCP and Skill badges. No Agent, no built-in tools (Bash, Read, etc.). +- No new API endpoints — data added to existing responses. +- No new dependencies. From 0e88dae0b9d32954c54caa2351a0c98d307ff021 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:45:11 +0500 Subject: [PATCH 02/29] Add implementation plan for MCP & Skill badges --- .../plans/2026-04-06-mcp-skill-badges.md | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-06-mcp-skill-badges.md diff --git a/docs/superpowers/plans/2026-04-06-mcp-skill-badges.md b/docs/superpowers/plans/2026-04-06-mcp-skill-badges.md new file mode 100644 index 0000000..97a6c28 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-mcp-skill-badges.md @@ -0,0 +1,397 @@ +# MCP & Skill Badges Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Show MCP servers and Skills used in sessions as colored badges on session cards and conversation messages. + +**Architecture:** Extend the existing JSONL parsing in `data.js` to extract `tool_use` blocks for MCP (`mcp__*`) and Skill. Pass extracted data through existing API responses. Frontend renders badges using the existing `tool-badge` CSS pattern. + +**Tech Stack:** Node.js (backend), plain browser JS (frontend), CSS + +--- + +### Task 1: Backend — Extract MCP/Skill data in loadSessions() + +**Files:** +- Modify: `src/data.js:360-384` (Enrich Claude sessions with detail file info) + +The existing enrichment loop already reads every JSONL line and parses JSON to count `detail_messages`. We add MCP/Skill extraction to this same loop — zero additional I/O. + +- [ ] **Step 1: Add MCP/Skill collection to the enrichment loop** + +In `src/data.js`, find the enrichment block (line ~360-384). Replace the inner try block that counts messages: + +```js +// Current code (lines 368-378): + try { + let msgCount = 0; + const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean); + for (const sl of sLines) { + try { + const entry = JSON.parse(sl); + if (entry.type === 'user' || entry.type === 'assistant') msgCount++; + } catch {} + } + s.detail_messages = msgCount; + } catch { s.detail_messages = 0; } +``` + +Replace with: + +```js + try { + let msgCount = 0; + const mcpSet = new Set(); + const skillSet = new Set(); + const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean); + for (const sl of sLines) { + try { + const entry = JSON.parse(sl); + if (entry.type === 'user' || entry.type === 'assistant') msgCount++; + if (entry.type === 'assistant') { + const content = (entry.message || {}).content; + if (Array.isArray(content)) { + for (const block of content) { + if (block.type !== 'tool_use') continue; + const name = block.name || ''; + if (name.startsWith('mcp__')) { + const parts = name.split('__'); + if (parts.length >= 3) mcpSet.add(parts[1]); + } else if (name === 'Skill') { + const skill = (block.input || {}).skill; + if (skill) skillSet.add(skill); + } + } + } + } + } catch {} + } + s.detail_messages = msgCount; + s.mcp_servers = Array.from(mcpSet); + s.skills = Array.from(skillSet); + } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; } +``` + +- [ ] **Step 2: Set defaults for non-Claude sessions** + +Right after the `else` branch (line ~380) that sets `s.has_detail = false`, add defaults. Also ensure the defaults exist for Codex/OpenCode/Kiro sessions that skip this loop. + +Find this block: + +```js + } else { + s.has_detail = false; + s.file_size = 0; + s.detail_messages = 0; + } +``` + +Replace with: + +```js + } else { + s.has_detail = false; + s.file_size = 0; + s.detail_messages = 0; + s.mcp_servers = []; + s.skills = []; + } +``` + +- [ ] **Step 3: Verify backend output** + +Run the server and open in browser: + +```bash +cd /Users/apple/Desktop/codedash && node bin/cli.js run +``` + +Then in another terminal: + +```bash +curl -s http://localhost:3456/api/sessions | python3 -c " +import json, sys +sessions = json.load(sys.stdin) +for s in sessions[:50]: + mcp = s.get('mcp_servers', []) + skills = s.get('skills', []) + if mcp or skills: + print(f\"{s['id'][:8]}: mcp={mcp}, skills={skills}\") +" +``` + +Expected: sessions that used MCP/Skill tools show their names in the arrays. + +- [ ] **Step 4: Commit** + +```bash +git add src/data.js +git commit -m "feat: extract MCP servers and Skills from session JSONL in loadSessions()" +``` + +--- + +### Task 2: Backend — Extract tools per message in loadSessionDetail() + +**Files:** +- Modify: `src/data.js:398-441` (loadSessionDetail function) + +- [ ] **Step 1: Add tools extraction to Claude message parsing** + +In `loadSessionDetail()`, find the Claude format branch (line ~419-425): + +```js + if (found.format === 'claude') { + if (entry.type === 'user' || entry.type === 'assistant') { + const content = extractContent((entry.message || {}).content); + if (content) { + messages.push({ role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' }); + } + } +``` + +Replace with: + +```js + if (found.format === 'claude') { + if (entry.type === 'user' || entry.type === 'assistant') { + const content = extractContent((entry.message || {}).content); + if (content) { + const msg = { role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' }; + if (entry.type === 'assistant') { + const rawContent = (entry.message || {}).content; + if (Array.isArray(rawContent)) { + const tools = extractTools(rawContent); + if (tools.length > 0) msg.tools = tools; + } + } + messages.push(msg); + } + } +``` + +- [ ] **Step 2: Add the extractTools helper function** + +Add this function right after the existing `extractContent()` function (after line ~630): + +```js +function extractTools(contentBlocks) { + const tools = []; + const seen = new Set(); + for (const block of contentBlocks) { + if (block.type !== 'tool_use') continue; + const name = block.name || ''; + if (name.startsWith('mcp__')) { + const parts = name.split('__'); + if (parts.length >= 3) { + const key = 'mcp:' + parts[1] + ':' + parts.slice(2).join('__'); + if (!seen.has(key)) { + seen.add(key); + tools.push({ type: 'mcp', server: parts[1], tool: parts.slice(2).join('__') }); + } + } + } else if (name === 'Skill') { + const skill = (block.input || {}).skill; + if (skill && !seen.has('skill:' + skill)) { + seen.add('skill:' + skill); + tools.push({ type: 'skill', skill: skill }); + } + } + } + return tools; +} +``` + +- [ ] **Step 3: Verify detail API** + +```bash +# Pick a session ID that has MCP/Skill usage from Task 1 verification +curl -s "http://localhost:3456/api/session/?project=" | python3 -c " +import json, sys +data = json.load(sys.stdin) +for m in data.get('messages', []): + tools = m.get('tools', []) + if tools: + print(f\"{m['role']}: {tools}\") +" +``` + +Expected: assistant messages that used MCP/Skill tools have a `tools` array. + +- [ ] **Step 4: Commit** + +```bash +git add src/data.js +git commit -m "feat: extract per-message MCP/Skill tools in loadSessionDetail()" +``` + +--- + +### Task 3: CSS — Badge styles + +**Files:** +- Modify: `src/frontend/styles.css:1049-1053` (after `.tool-kiro` block) + +- [ ] **Step 1: Add badge-mcp, badge-skill, and msg-tools styles** + +In `styles.css`, after the `.tool-kiro` block (line ~1049-1052), before the `/* -- Groups */` comment, add: + +```css +.badge-mcp { + background: rgba(251, 146, 60, 0.15); + color: var(--accent-orange); +} + +.badge-skill { + background: rgba(139, 92, 246, 0.15); + color: var(--accent-purple); +} + +.msg-tools { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} +``` + +- [ ] **Step 2: Check that `--accent-orange` and `--accent-purple` CSS variables exist** + +Search `styles.css` for these variables. If they don't exist, add them to the `:root` block. If they already exist (used by `.tool-kiro` and `.tool-opencode`), no action needed. + +Run: + +```bash +grep -n 'accent-orange\|accent-purple' src/frontend/styles.css +``` + +Expected: variables are already defined (used by `.tool-kiro` and `.tool-opencode`). If not, add to `:root`. + +- [ ] **Step 3: Commit** + +```bash +git add src/frontend/styles.css +git commit -m "feat: add CSS for MCP and Skill badge styles" +``` + +--- + +### Task 4: Frontend — Badges on session cards + +**Files:** +- Modify: `src/frontend/app.js:466-474` (renderCard function, card-top section) +- Modify: `src/frontend/app.js:523-524` (renderListCard function) + +- [ ] **Step 1: Add badges to renderCard()** + +In `renderCard()`, find line ~468 where `tool-badge` is rendered: + +```js + html += '' + escHtml(s.tool) + ''; +``` + +Right after this line, add: + +```js + if (s.mcp_servers && s.mcp_servers.length > 0) { + s.mcp_servers.forEach(function(m) { + html += '' + escHtml(m) + ''; + }); + } + if (s.skills && s.skills.length > 0) { + s.skills.forEach(function(sk) { + html += '' + escHtml(sk) + ''; + }); + } +``` + +- [ ] **Step 2: Add badges to renderListCard()** + +In `renderListCard()`, find line ~524: + +```js + html += '' + escHtml(s.tool) + ''; +``` + +Right after this line, add the same badge code: + +```js + if (s.mcp_servers && s.mcp_servers.length > 0) { + s.mcp_servers.forEach(function(m) { + html += '' + escHtml(m) + ''; + }); + } + if (s.skills && s.skills.length > 0) { + s.skills.forEach(function(sk) { + html += '' + escHtml(sk) + ''; + }); + } +``` + +- [ ] **Step 3: Verify in browser** + +Reload the dashboard. Session cards that used MCP/Skills should show orange/purple badges next to the tool badge (e.g., `CLAUDE` `CHROME-DEVTOOLS` `FIGMA-USE`). + +- [ ] **Step 4: Commit** + +```bash +git add src/frontend/app.js +git commit -m "feat: render MCP/Skill badges on session cards" +``` + +--- + +### Task 5: Frontend — Badges in conversation view + +**Files:** +- Modify: `src/frontend/app.js:1128-1135` (showDetail message rendering) + +- [ ] **Step 1: Add tool badges to assistant messages in conversation view** + +In the `showDetail()` function, find the message rendering loop (line ~1128-1135): + +```js + data.messages.forEach(function(m) { + var roleClass = m.role === 'user' ? 'msg-user' : 'msg-assistant'; + var roleLabel = m.role === 'user' ? 'You' : 'Assistant'; + msgsHtml += '
'; + msgsHtml += '
' + roleLabel + '
'; + msgsHtml += '
' + escHtml(m.content) + '
'; + msgsHtml += '
'; + }); +``` + +Replace with: + +```js + data.messages.forEach(function(m) { + var roleClass = m.role === 'user' ? 'msg-user' : 'msg-assistant'; + var roleLabel = m.role === 'user' ? 'You' : 'Assistant'; + msgsHtml += '
'; + msgsHtml += '
' + roleLabel + '
'; + if (m.tools && m.tools.length > 0) { + msgsHtml += '
'; + m.tools.forEach(function(t) { + if (t.type === 'mcp') { + msgsHtml += '' + escHtml(t.tool) + ''; + } else if (t.type === 'skill') { + msgsHtml += '' + escHtml(t.skill) + ''; + } + }); + msgsHtml += '
'; + } + msgsHtml += '
' + escHtml(m.content) + '
'; + msgsHtml += '
'; + }); +``` + +- [ ] **Step 2: Verify in browser** + +Click on a session that used MCP/Skill tools. In the conversation view, assistant messages should show tool badges between the role label and message content. + +- [ ] **Step 3: Commit** + +```bash +git add src/frontend/app.js +git commit -m "feat: render MCP/Skill badges in conversation view" +``` From 61be24fe4b96f03c70cd89cffa94f17f92d5ef5d Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:47:23 +0500 Subject: [PATCH 03/29] feat: extract MCP servers and Skills from session JSONL in loadSessions() --- src/data.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/data.js b/src/data.js index 28d8a5d..5e1cfce 100644 --- a/src/data.js +++ b/src/data.js @@ -367,19 +367,41 @@ function loadSessions() { s.file_size = fs.statSync(sessionFile).size; try { let msgCount = 0; + const mcpSet = new Set(); + const skillSet = new Set(); const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean); for (const sl of sLines) { try { const entry = JSON.parse(sl); if (entry.type === 'user' || entry.type === 'assistant') msgCount++; + if (entry.type === 'assistant') { + const content = (entry.message || {}).content; + if (Array.isArray(content)) { + for (const block of content) { + if (block.type !== 'tool_use') continue; + const name = block.name || ''; + if (name.startsWith('mcp__')) { + const parts = name.split('__'); + if (parts.length >= 3) mcpSet.add(parts[1]); + } else if (name === 'Skill') { + const skill = (block.input || {}).skill; + if (skill) skillSet.add(skill); + } + } + } + } } catch {} } s.detail_messages = msgCount; - } catch { s.detail_messages = 0; } + s.mcp_servers = Array.from(mcpSet); + s.skills = Array.from(skillSet); + } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; } } else { s.has_detail = false; s.file_size = 0; s.detail_messages = 0; + s.mcp_servers = []; + s.skills = []; } } From 4f0fa9fb5797cfafc9b6bb7e5fea2de7097d5115 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:48:50 +0500 Subject: [PATCH 04/29] feat: extract per-message MCP/Skill tools in loadSessionDetail() --- src/data.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/data.js b/src/data.js index 5e1cfce..08acad9 100644 --- a/src/data.js +++ b/src/data.js @@ -442,7 +442,15 @@ function loadSessionDetail(sessionId, project) { if (entry.type === 'user' || entry.type === 'assistant') { const content = extractContent((entry.message || {}).content); if (content) { - messages.push({ role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' }); + const msg = { role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' }; + if (entry.type === 'assistant') { + const rawContent = (entry.message || {}).content; + if (Array.isArray(rawContent)) { + const tools = extractTools(rawContent); + if (tools.length > 0) msg.tools = tools; + } + } + messages.push(msg); } } } else { @@ -639,6 +647,32 @@ function isSystemMessage(text) { return false; } +function extractTools(contentBlocks) { + const tools = []; + const seen = new Set(); + for (const block of contentBlocks) { + if (block.type !== 'tool_use') continue; + const name = block.name || ''; + if (name.startsWith('mcp__')) { + const parts = name.split('__'); + if (parts.length >= 3) { + const key = 'mcp:' + parts[1] + ':' + parts.slice(2).join('__'); + if (!seen.has(key)) { + seen.add(key); + tools.push({ type: 'mcp', server: parts[1], tool: parts.slice(2).join('__') }); + } + } + } else if (name === 'Skill') { + const skill = (block.input || {}).skill; + if (skill && !seen.has('skill:' + skill)) { + seen.add('skill:' + skill); + tools.push({ type: 'skill', skill: skill }); + } + } + } + return tools; +} + function extractContent(raw) { if (!raw) return ''; if (typeof raw === 'string') return raw; From a87c6ec65a2a7a38b7193d9ce229052bf60ba4df Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:50:13 +0500 Subject: [PATCH 05/29] feat: add CSS for MCP and Skill badge styles --- src/frontend/styles.css | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/frontend/styles.css b/src/frontend/styles.css index 88278fc..d7ccc6a 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -1051,6 +1051,23 @@ body { color: var(--accent-orange); } +.badge-mcp { + background: rgba(251, 146, 60, 0.15); + color: var(--accent-orange); +} + +.badge-skill { + background: rgba(139, 92, 246, 0.15); + color: var(--accent-purple); +} + +.msg-tools { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} + /* ── Groups ─────────────────────────────────────────────────── */ .group { From e8ff457dca19a0c021f3430c95c6903e639890cd Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:50:56 +0500 Subject: [PATCH 06/29] feat: render MCP/Skill badges on session cards --- src/frontend/app.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/frontend/app.js b/src/frontend/app.js index be84c84..a39f5e0 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -466,6 +466,16 @@ function renderCard(s, idx) { html += '
'; html += ''; html += '' + escHtml(s.tool) + ''; + if (s.mcp_servers && s.mcp_servers.length > 0) { + s.mcp_servers.forEach(function(m) { + html += '' + escHtml(m) + ''; + }); + } + if (s.skills && s.skills.length > 0) { + s.skills.forEach(function(sk) { + html += '' + escHtml(sk) + ''; + }); + } html += '' + escHtml(projName) + ''; html += '' + timeAgo(s.last_ts) + ''; if (costStr) { @@ -522,6 +532,16 @@ function renderListCard(s, idx) { var html = '
'; html += '' + escHtml(s.tool) + ''; + if (s.mcp_servers && s.mcp_servers.length > 0) { + s.mcp_servers.forEach(function(m) { + html += '' + escHtml(m) + ''; + }); + } + if (s.skills && s.skills.length > 0) { + s.skills.forEach(function(sk) { + html += '' + escHtml(sk) + ''; + }); + } html += '' + escHtml(projName) + ''; html += '' + escHtml((s.first_message || '').slice(0, 80)) + ''; html += '' + s.messages + ' msgs'; From d47b4d758eeb5d231fc2225ba77048efafa260da Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:51:42 +0500 Subject: [PATCH 07/29] feat: render MCP/Skill badges in conversation view --- src/frontend/app.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/frontend/app.js b/src/frontend/app.js index a39f5e0..346d781 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -1150,6 +1150,17 @@ async function openDetail(s) { var roleLabel = m.role === 'user' ? 'You' : 'Assistant'; msgsHtml += '
'; msgsHtml += '
' + roleLabel + '
'; + if (m.tools && m.tools.length > 0) { + msgsHtml += '
'; + m.tools.forEach(function(t) { + if (t.type === 'mcp') { + msgsHtml += '' + escHtml(t.tool) + ''; + } else if (t.type === 'skill') { + msgsHtml += '' + escHtml(t.skill) + ''; + } + }); + msgsHtml += '
'; + } msgsHtml += '
' + escHtml(m.content) + '
'; msgsHtml += '
'; }); From d82d6b947138b8840aa6193724abad61fbadc075 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:54:07 +0500 Subject: [PATCH 08/29] fix: ensure mcp_servers/skills defaults for non-Claude sessions --- src/data.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/data.js b/src/data.js index 08acad9..b5057d5 100644 --- a/src/data.js +++ b/src/data.js @@ -405,6 +405,12 @@ function loadSessions() { } } + // Ensure all sessions have mcp_servers/skills (non-Claude sessions skip enrichment loop) + for (const s of Object.values(sessions)) { + if (!s.mcp_servers) s.mcp_servers = []; + if (!s.skills) s.skills = []; + } + const result = Object.values(sessions).sort((a, b) => b.last_ts - a.last_ts); for (const s of result) { From 4aae637f1f4366c84f863aee6f02871d5c2693c1 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 02:58:08 +0500 Subject: [PATCH 09/29] =?UTF-8?q?fix:=20strip=20plugin=20prefix=20from=20s?= =?UTF-8?q?kill=20names=20(superpowers:writing-plans=20=E2=86=92=20writing?= =?UTF-8?q?-plans)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data.js b/src/data.js index b5057d5..2780d8c 100644 --- a/src/data.js +++ b/src/data.js @@ -385,7 +385,7 @@ function loadSessions() { if (parts.length >= 3) mcpSet.add(parts[1]); } else if (name === 'Skill') { const skill = (block.input || {}).skill; - if (skill) skillSet.add(skill); + if (skill) skillSet.add(skill.includes(':') ? skill.split(':').pop() : skill); } } } @@ -672,7 +672,7 @@ function extractTools(contentBlocks) { const skill = (block.input || {}).skill; if (skill && !seen.has('skill:' + skill)) { seen.add('skill:' + skill); - tools.push({ type: 'skill', skill: skill }); + tools.push({ type: 'skill', skill: skill.includes(':') ? skill.split(':').pop() : skill }); } } } From c505cb551125ae3c8c3bfd52836c5d1e679571ea Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 03:01:23 +0500 Subject: [PATCH 10/29] =?UTF-8?q?fix:=20show=20plugin=20name=20instead=20o?= =?UTF-8?q?f=20skill=20name=20(superpowers:writing-plans=20=E2=86=92=20sup?= =?UTF-8?q?erpowers)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data.js b/src/data.js index 2780d8c..e4aab4f 100644 --- a/src/data.js +++ b/src/data.js @@ -385,7 +385,7 @@ function loadSessions() { if (parts.length >= 3) mcpSet.add(parts[1]); } else if (name === 'Skill') { const skill = (block.input || {}).skill; - if (skill) skillSet.add(skill.includes(':') ? skill.split(':').pop() : skill); + if (skill) skillSet.add(skill.includes(':') ? skill.split(':')[0] : skill); } } } @@ -672,7 +672,7 @@ function extractTools(contentBlocks) { const skill = (block.input || {}).skill; if (skill && !seen.has('skill:' + skill)) { seen.add('skill:' + skill); - tools.push({ type: 'skill', skill: skill.includes(':') ? skill.split(':').pop() : skill }); + tools.push({ type: 'skill', skill: skill.includes(':') ? skill.split(':')[0] : skill }); } } } From 51e18f2ad98edaed18c64210b80d031876d11ec2 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 03:05:02 +0500 Subject: [PATCH 11/29] feat: extract MCP/Skill badges for Codex sessions --- src/data.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/data.js b/src/data.js index e4aab4f..d8b3516 100644 --- a/src/data.js +++ b/src/data.js @@ -262,6 +262,26 @@ function scanCodexSessions() { } } catch {} + // Extract MCP/Skill from Codex session file + const mcpSet = new Set(); + const skillSet = new Set(); + try { + const sLines = fs.readFileSync(f, 'utf8').split('\n').filter(Boolean); + for (const sl of sLines) { + try { + const entry = JSON.parse(sl); + if (entry.type === 'response_item' && entry.payload) { + const pType = entry.payload.type; + const pName = entry.payload.name || ''; + if (pType === 'function_call' && pName.startsWith('mcp__')) { + const parts = pName.split('__'); + if (parts.length >= 3) mcpSet.add(parts[1]); + } + } + } catch {} + } + } catch {} + const existing = sessions.find(s => s.id === sid); if (existing) { existing.has_detail = true; @@ -270,6 +290,8 @@ function scanCodexSessions() { existing.project = cwd; existing.project_short = cwd.replace(os.homedir(), '~'); } + if (mcpSet.size > 0) existing.mcp_servers = Array.from(mcpSet); + if (skillSet.size > 0) existing.skills = Array.from(skillSet); } else { sessions.push({ id: sid, @@ -283,6 +305,8 @@ function scanCodexSessions() { has_detail: true, file_size: stat.size, detail_messages: 0, + mcp_servers: Array.from(mcpSet), + skills: Array.from(skillSet), }); } } @@ -461,6 +485,7 @@ function loadSessionDetail(sessionId, project) { } } else { if (entry.type === 'response_item' && entry.payload) { + const pType = entry.payload.type; const role = entry.payload.role; if (role === 'user' || role === 'assistant') { const content = extractContent(entry.payload.content); @@ -468,11 +493,36 @@ function loadSessionDetail(sessionId, project) { messages.push({ role: role, content: content.slice(0, 2000), uuid: '' }); } } + // Collect Codex function_call tools for the last assistant message + if (pType === 'function_call') { + const pName = entry.payload.name || ''; + const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null; + if (lastMsg && lastMsg.role === 'assistant') { + if (!lastMsg.tools) lastMsg.tools = []; + if (pName.startsWith('mcp__')) { + const parts = pName.split('__'); + if (parts.length >= 3) { + const key = 'mcp:' + parts[1] + ':' + parts.slice(2).join('__'); + if (!lastMsg._toolSeen) lastMsg._toolSeen = new Set(); + if (!lastMsg._toolSeen.has(key)) { + lastMsg._toolSeen.add(key); + lastMsg.tools.push({ type: 'mcp', server: parts[1], tool: parts.slice(2).join('__') }); + } + } + } + } + } } } } catch {} } + // Clean up internal dedup markers + for (const m of messages) { + if (m._toolSeen) delete m._toolSeen; + if (m.tools && m.tools.length === 0) delete m.tools; + } + return { messages: messages.slice(0, 200) }; } From cc5abaaf27fc14fd7914d7d734752b4df3757f50 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 03:14:52 +0500 Subject: [PATCH 12/29] Add design spec for skeleton preloader and sidebar font fix --- .../2026-04-06-preloader-sidebar-design.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md diff --git a/docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md b/docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md new file mode 100644 index 0000000..c70ab9d --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md @@ -0,0 +1,37 @@ +# Skeleton Preloader & Sidebar Font Fix + +## Sidebar — Unified font size + +Currently 3 different font sizes in sidebar items: 14px (nav), 12px (.small — Install Agents, Changelog), 10px (section headers). + +**Fix:** All sidebar items → 13px. Remove `.small` class usage. Section headers (Agents, Install Agents) stay 10px uppercase — they are labels, not navigation items. + +### Changes + +- `styles.css`: Change `.sidebar-item` font-size from 14px to 13px +- `styles.css`: Remove `.sidebar-item.small` rules (font-size: 12px, padding: 6px, small svg) +- `index.html`: Remove `class="small"` from Install Agents items and Changelog item + +## Skeleton Preloader + +When dashboard first loads, the content area is empty while `loadSessions()` fetches data. Show skeleton placeholder cards with shimmer animation. + +### Implementation + +- `styles.css`: Add `@keyframes skeleton-shimmer` animation, `.skeleton-card` styles +- `app.js`: In `init()`, render 6 skeleton cards into `#content` before `loadSessions()`. After `loadSessions()` completes, `render()` replaces them with real content (already happens — `applyFilters()` calls `render()` which sets `content.innerHTML`). + +### Skeleton card structure + +Mimics real card layout: +- Top row: badge placeholder + project placeholder + time placeholder +- Body: 2 text line placeholders +- Footer: 3 small metadata placeholders + +Shimmer: `linear-gradient(90deg, transparent 25%, rgba(255,255,255,0.06) 50%, transparent 75%)` animated left-to-right, 1.5s infinite. + +## Scope + +- No new API endpoints +- No new dependencies +- Pure CSS + minimal JS changes From baf158e8713bef46c41c8cbdd3faf14ef086e9f2 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 03:15:40 +0500 Subject: [PATCH 13/29] feat: add skeleton preloader + unify sidebar font sizes to 13px --- src/frontend/app.js | 24 ++++++++++++++++++++++++ src/frontend/index.html | 10 +++++----- src/frontend/styles.css | 25 ++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/frontend/app.js b/src/frontend/app.js index 346d781..3eb4d8d 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -2018,6 +2018,30 @@ function dismissUpdate() { // ── Initialization ───────────────────────────────────────────── (function init() { + // Show skeleton preloader + var content = document.getElementById('content'); + if (content) { + var skeletonHtml = ''; + for (var i = 0; i < 6; i++) { + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + skeletonHtml += '
'; + } + content.innerHTML = skeletonHtml; + } + // Load data loadSessions(); loadTerminals(); diff --git a/src/frontend/index.html b/src/frontend/index.html index 0687c18..3c27c32 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -62,19 +62,19 @@
- '; // Action buttons @@ -1158,9 +1174,13 @@ async function openDetail(s) { data.messages.forEach(function(m) { var roleClass = m.role === 'user' ? 'msg-user' : 'msg-assistant'; var roleLabel = m.role === 'user' ? 'You' : 'Assistant'; - msgsHtml += '
'; + var hasTools = m.tools && m.tools.length > 0; + msgsHtml += '
'; + msgsHtml += '
'; msgsHtml += '
' + roleLabel + '
'; - if (m.tools && m.tools.length > 0) { + msgsHtml += '
' + escHtml(m.content) + '
'; + msgsHtml += '
'; + if (hasTools) { msgsHtml += '
'; m.tools.forEach(function(t) { if (t.type === 'mcp') { @@ -1171,7 +1191,6 @@ async function openDetail(s) { }); msgsHtml += '
'; } - msgsHtml += '
' + escHtml(m.content) + '
'; msgsHtml += '
'; }); msgContainer.innerHTML = msgsHtml; diff --git a/src/frontend/styles.css b/src/frontend/styles.css index 03ef960..e41bd43 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -642,9 +642,6 @@ body { } .detail-info { - display: grid; - grid-template-columns: auto 1fr; - gap: 8px 16px; font-size: 13px; margin-bottom: 16px; } @@ -1037,11 +1034,21 @@ body { color: var(--accent-purple); } +.message.has-tools { + padding: 0; + overflow: hidden; +} + +.message.has-tools .msg-inner { + padding: 10px 14px; +} + .msg-tools { display: flex; flex-wrap: wrap; - gap: 4px; - margin-top: 4px; + gap: 6px; + padding: 6px 14px; + border-top: 1px solid var(--border); } /* ── Settings page ──────────────────────────────────────────── */ From 35cc300b270a6da9b34071d51757bb1c7c5a8cff Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 03:47:31 +0500 Subject: [PATCH 18/29] feat: custom calendar date picker with range selection and presets --- src/frontend/app.js | 172 +++++++++++++++++++++++++++++++++++++++- src/frontend/index.html | 7 +- src/frontend/styles.css | 99 ++++++++++++++++++++--- 3 files changed, 261 insertions(+), 17 deletions(-) diff --git a/src/frontend/app.js b/src/frontend/app.js index b9996e1..400edda 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -393,9 +393,177 @@ function onTagFilter(val) { } function onDateFilter() { - dateFrom = document.getElementById('dateFrom').value || ''; - dateTo = document.getElementById('dateTo').value || ''; applyFilters(); + updateDateBtn(); +} + +// ── Calendar ───────────────────────────────────────────────── + +var calYear = new Date().getFullYear(); +var calMonth = new Date().getMonth(); +var calStart = null; +var calEnd = null; +var calSelecting = false; + +function toggleCalendar() { + var popup = document.getElementById('calendarPopup'); + if (!popup) return; + if (popup.classList.contains('open')) { + popup.classList.remove('open'); + return; + } + renderCalendar(); + popup.classList.add('open'); + // Close on outside click + setTimeout(function() { + document.addEventListener('click', closeCalendarOutside, { once: true }); + }, 0); +} + +function closeCalendarOutside(e) { + var popup = document.getElementById('calendarPopup'); + var btn = document.getElementById('dateBtn'); + if (popup && !popup.contains(e.target) && btn && !btn.contains(e.target)) { + popup.classList.remove('open'); + } else if (popup && popup.classList.contains('open')) { + document.addEventListener('click', closeCalendarOutside, { once: true }); + } +} + +function renderCalendar() { + var popup = document.getElementById('calendarPopup'); + if (!popup) return; + + var monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December']; + var firstDay = new Date(calYear, calMonth, 1); + var lastDay = new Date(calYear, calMonth + 1, 0); + var startWeekday = (firstDay.getDay() + 6) % 7; // Monday=0 + var daysInMonth = lastDay.getDate(); + var today = new Date(); + var todayStr = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0') + '-' + String(today.getDate()).padStart(2,'0'); + + var html = '
'; + html += ''; + html += '' + monthNames[calMonth] + ' ' + calYear + ''; + html += ''; + html += '
'; + + html += '
MoTuWeThFrSaSu
'; + html += '
'; + + // Previous month padding + var prevLastDay = new Date(calYear, calMonth, 0).getDate(); + for (var i = startWeekday - 1; i >= 0; i--) { + html += '
' + (prevLastDay - i) + '
'; + } + + // Current month days + for (var d = 1; d <= daysInMonth; d++) { + var dateStr = calYear + '-' + String(calMonth+1).padStart(2,'0') + '-' + String(d).padStart(2,'0'); + var cls = 'cal-day'; + if (dateStr === todayStr) cls += ' today'; + if (calStart && calEnd) { + if (dateStr === calStart) cls += ' range-start'; + if (dateStr === calEnd) cls += ' range-end'; + if (dateStr > calStart && dateStr < calEnd) cls += ' in-range'; + if (calStart === calEnd && dateStr === calStart) cls += ' range-start range-end'; + } else if (calStart && dateStr === calStart) { + cls += ' range-start range-end'; + } + html += '
' + d + '
'; + } + + // Next month padding + var totalCells = startWeekday + daysInMonth; + var remaining = (7 - (totalCells % 7)) % 7; + for (var n = 1; n <= remaining; n++) { + html += '
' + n + '
'; + } + html += '
'; + + // Presets + html += '
'; + var presets = [['All',''],['Today','0'],['7d','7'],['30d','30'],['90d','90']]; + presets.forEach(function(p) { + html += ''; + }); + html += '
'; + + popup.innerHTML = html; +} + +function calNav(dir) { + calMonth += dir; + if (calMonth < 0) { calMonth = 11; calYear--; } + if (calMonth > 11) { calMonth = 0; calYear++; } + renderCalendar(); +} + +function calPickDay(dateStr) { + if (!calSelecting) { + // First click — set start + calStart = dateStr; + calEnd = null; + calSelecting = true; + } else { + // Second click — set end + if (dateStr < calStart) { + calEnd = calStart; + calStart = dateStr; + } else { + calEnd = dateStr; + } + calSelecting = false; + } + renderCalendar(); + // Apply filter + dateFrom = calStart || ''; + dateTo = calEnd || calStart || ''; + onDateFilter(); +} + +function calPreset(days) { + calSelecting = false; + if (!days) { + calStart = null; + calEnd = null; + dateFrom = ''; + dateTo = ''; + } else { + var now = new Date(); + calEnd = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + if (days === '0') { + calStart = calEnd; + } else { + var from = new Date(now.getTime() - parseInt(days) * 86400000); + calStart = from.getFullYear() + '-' + String(from.getMonth()+1).padStart(2,'0') + '-' + String(from.getDate()).padStart(2,'0'); + } + dateFrom = calStart; + dateTo = calEnd; + } + renderCalendar(); + onDateFilter(); + // Close popup + var popup = document.getElementById('calendarPopup'); + if (popup) popup.classList.remove('open'); +} + +function updateDateBtn() { + var btn = document.getElementById('dateBtn'); + var label = document.getElementById('dateBtnLabel'); + if (!btn || !label) return; + if (!dateFrom && !dateTo) { + label.textContent = 'All time'; + btn.classList.remove('has-filter'); + } else if (dateFrom === dateTo) { + label.textContent = dateFrom; + btn.classList.add('has-filter'); + } else { + var f = dateFrom.slice(5) || ''; + var t = dateTo.slice(5) || ''; + label.textContent = f + ' \u2014 ' + t; + btn.classList.add('has-filter'); + } } function toggleGroup() { diff --git a/src/frontend/index.html b/src/frontend/index.html index 81ffecd..1172dac 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -108,8 +108,11 @@ - - + +
@@ -124,40 +146,30 @@
-
- Session Detail +

-
-
-

Delete Session?

-

This will permanently delete the session file and remove it from history.

-
-
- - +
+ +
+
+

+

+

+
+ +
- - -
- - +
+
diff --git a/src/frontend/styles.css b/src/frontend/styles.css index 6faddfe..155051d 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -135,7 +135,18 @@ body { } .sidebar-item:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); } .sidebar-item.active { color: var(--text-primary); background: rgba(255,255,255,0.08); } -.sidebar-item svg { width: 18px; height: 18px; opacity: 0.7; } +.icon { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.icon-brand { width: 20px; height: 20px; } +.icon-sm { width: 14px; height: 14px; } +.sidebar-item .icon { opacity: 0.7; } .sidebar-divider { height: 1px; background: var(--border); margin: 12px 20px; } From 3859382d8c6e417d26ac05db1e0e5077cd5aef91 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 03:58:40 +0500 Subject: [PATCH 20/29] chore: add .superpowers/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aafcb34..2e086c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ .DS_Store *.log +.superpowers/ From 230f70e63329c4bbd6759a94cc32ef0e1f6484c1 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 04:04:23 +0500 Subject: [PATCH 21/29] chore: remove superpowers specs/plans from repo --- .../plans/2026-04-06-mcp-skill-badges.md | 397 ------------------ .../2026-04-06-card-footer-settings-design.md | 69 --- .../2026-04-06-mcp-skill-badges-design.md | 85 ---- .../2026-04-06-preloader-sidebar-design.md | 37 -- 4 files changed, 588 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-06-mcp-skill-badges.md delete mode 100644 docs/superpowers/specs/2026-04-06-card-footer-settings-design.md delete mode 100644 docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md delete mode 100644 docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md diff --git a/docs/superpowers/plans/2026-04-06-mcp-skill-badges.md b/docs/superpowers/plans/2026-04-06-mcp-skill-badges.md deleted file mode 100644 index 97a6c28..0000000 --- a/docs/superpowers/plans/2026-04-06-mcp-skill-badges.md +++ /dev/null @@ -1,397 +0,0 @@ -# MCP & Skill Badges Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Show MCP servers and Skills used in sessions as colored badges on session cards and conversation messages. - -**Architecture:** Extend the existing JSONL parsing in `data.js` to extract `tool_use` blocks for MCP (`mcp__*`) and Skill. Pass extracted data through existing API responses. Frontend renders badges using the existing `tool-badge` CSS pattern. - -**Tech Stack:** Node.js (backend), plain browser JS (frontend), CSS - ---- - -### Task 1: Backend — Extract MCP/Skill data in loadSessions() - -**Files:** -- Modify: `src/data.js:360-384` (Enrich Claude sessions with detail file info) - -The existing enrichment loop already reads every JSONL line and parses JSON to count `detail_messages`. We add MCP/Skill extraction to this same loop — zero additional I/O. - -- [ ] **Step 1: Add MCP/Skill collection to the enrichment loop** - -In `src/data.js`, find the enrichment block (line ~360-384). Replace the inner try block that counts messages: - -```js -// Current code (lines 368-378): - try { - let msgCount = 0; - const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean); - for (const sl of sLines) { - try { - const entry = JSON.parse(sl); - if (entry.type === 'user' || entry.type === 'assistant') msgCount++; - } catch {} - } - s.detail_messages = msgCount; - } catch { s.detail_messages = 0; } -``` - -Replace with: - -```js - try { - let msgCount = 0; - const mcpSet = new Set(); - const skillSet = new Set(); - const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean); - for (const sl of sLines) { - try { - const entry = JSON.parse(sl); - if (entry.type === 'user' || entry.type === 'assistant') msgCount++; - if (entry.type === 'assistant') { - const content = (entry.message || {}).content; - if (Array.isArray(content)) { - for (const block of content) { - if (block.type !== 'tool_use') continue; - const name = block.name || ''; - if (name.startsWith('mcp__')) { - const parts = name.split('__'); - if (parts.length >= 3) mcpSet.add(parts[1]); - } else if (name === 'Skill') { - const skill = (block.input || {}).skill; - if (skill) skillSet.add(skill); - } - } - } - } - } catch {} - } - s.detail_messages = msgCount; - s.mcp_servers = Array.from(mcpSet); - s.skills = Array.from(skillSet); - } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; } -``` - -- [ ] **Step 2: Set defaults for non-Claude sessions** - -Right after the `else` branch (line ~380) that sets `s.has_detail = false`, add defaults. Also ensure the defaults exist for Codex/OpenCode/Kiro sessions that skip this loop. - -Find this block: - -```js - } else { - s.has_detail = false; - s.file_size = 0; - s.detail_messages = 0; - } -``` - -Replace with: - -```js - } else { - s.has_detail = false; - s.file_size = 0; - s.detail_messages = 0; - s.mcp_servers = []; - s.skills = []; - } -``` - -- [ ] **Step 3: Verify backend output** - -Run the server and open in browser: - -```bash -cd /Users/apple/Desktop/codedash && node bin/cli.js run -``` - -Then in another terminal: - -```bash -curl -s http://localhost:3456/api/sessions | python3 -c " -import json, sys -sessions = json.load(sys.stdin) -for s in sessions[:50]: - mcp = s.get('mcp_servers', []) - skills = s.get('skills', []) - if mcp or skills: - print(f\"{s['id'][:8]}: mcp={mcp}, skills={skills}\") -" -``` - -Expected: sessions that used MCP/Skill tools show their names in the arrays. - -- [ ] **Step 4: Commit** - -```bash -git add src/data.js -git commit -m "feat: extract MCP servers and Skills from session JSONL in loadSessions()" -``` - ---- - -### Task 2: Backend — Extract tools per message in loadSessionDetail() - -**Files:** -- Modify: `src/data.js:398-441` (loadSessionDetail function) - -- [ ] **Step 1: Add tools extraction to Claude message parsing** - -In `loadSessionDetail()`, find the Claude format branch (line ~419-425): - -```js - if (found.format === 'claude') { - if (entry.type === 'user' || entry.type === 'assistant') { - const content = extractContent((entry.message || {}).content); - if (content) { - messages.push({ role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' }); - } - } -``` - -Replace with: - -```js - if (found.format === 'claude') { - if (entry.type === 'user' || entry.type === 'assistant') { - const content = extractContent((entry.message || {}).content); - if (content) { - const msg = { role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' }; - if (entry.type === 'assistant') { - const rawContent = (entry.message || {}).content; - if (Array.isArray(rawContent)) { - const tools = extractTools(rawContent); - if (tools.length > 0) msg.tools = tools; - } - } - messages.push(msg); - } - } -``` - -- [ ] **Step 2: Add the extractTools helper function** - -Add this function right after the existing `extractContent()` function (after line ~630): - -```js -function extractTools(contentBlocks) { - const tools = []; - const seen = new Set(); - for (const block of contentBlocks) { - if (block.type !== 'tool_use') continue; - const name = block.name || ''; - if (name.startsWith('mcp__')) { - const parts = name.split('__'); - if (parts.length >= 3) { - const key = 'mcp:' + parts[1] + ':' + parts.slice(2).join('__'); - if (!seen.has(key)) { - seen.add(key); - tools.push({ type: 'mcp', server: parts[1], tool: parts.slice(2).join('__') }); - } - } - } else if (name === 'Skill') { - const skill = (block.input || {}).skill; - if (skill && !seen.has('skill:' + skill)) { - seen.add('skill:' + skill); - tools.push({ type: 'skill', skill: skill }); - } - } - } - return tools; -} -``` - -- [ ] **Step 3: Verify detail API** - -```bash -# Pick a session ID that has MCP/Skill usage from Task 1 verification -curl -s "http://localhost:3456/api/session/?project=" | python3 -c " -import json, sys -data = json.load(sys.stdin) -for m in data.get('messages', []): - tools = m.get('tools', []) - if tools: - print(f\"{m['role']}: {tools}\") -" -``` - -Expected: assistant messages that used MCP/Skill tools have a `tools` array. - -- [ ] **Step 4: Commit** - -```bash -git add src/data.js -git commit -m "feat: extract per-message MCP/Skill tools in loadSessionDetail()" -``` - ---- - -### Task 3: CSS — Badge styles - -**Files:** -- Modify: `src/frontend/styles.css:1049-1053` (after `.tool-kiro` block) - -- [ ] **Step 1: Add badge-mcp, badge-skill, and msg-tools styles** - -In `styles.css`, after the `.tool-kiro` block (line ~1049-1052), before the `/* -- Groups */` comment, add: - -```css -.badge-mcp { - background: rgba(251, 146, 60, 0.15); - color: var(--accent-orange); -} - -.badge-skill { - background: rgba(139, 92, 246, 0.15); - color: var(--accent-purple); -} - -.msg-tools { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 4px; -} -``` - -- [ ] **Step 2: Check that `--accent-orange` and `--accent-purple` CSS variables exist** - -Search `styles.css` for these variables. If they don't exist, add them to the `:root` block. If they already exist (used by `.tool-kiro` and `.tool-opencode`), no action needed. - -Run: - -```bash -grep -n 'accent-orange\|accent-purple' src/frontend/styles.css -``` - -Expected: variables are already defined (used by `.tool-kiro` and `.tool-opencode`). If not, add to `:root`. - -- [ ] **Step 3: Commit** - -```bash -git add src/frontend/styles.css -git commit -m "feat: add CSS for MCP and Skill badge styles" -``` - ---- - -### Task 4: Frontend — Badges on session cards - -**Files:** -- Modify: `src/frontend/app.js:466-474` (renderCard function, card-top section) -- Modify: `src/frontend/app.js:523-524` (renderListCard function) - -- [ ] **Step 1: Add badges to renderCard()** - -In `renderCard()`, find line ~468 where `tool-badge` is rendered: - -```js - html += '' + escHtml(s.tool) + ''; -``` - -Right after this line, add: - -```js - if (s.mcp_servers && s.mcp_servers.length > 0) { - s.mcp_servers.forEach(function(m) { - html += '' + escHtml(m) + ''; - }); - } - if (s.skills && s.skills.length > 0) { - s.skills.forEach(function(sk) { - html += '' + escHtml(sk) + ''; - }); - } -``` - -- [ ] **Step 2: Add badges to renderListCard()** - -In `renderListCard()`, find line ~524: - -```js - html += '' + escHtml(s.tool) + ''; -``` - -Right after this line, add the same badge code: - -```js - if (s.mcp_servers && s.mcp_servers.length > 0) { - s.mcp_servers.forEach(function(m) { - html += '' + escHtml(m) + ''; - }); - } - if (s.skills && s.skills.length > 0) { - s.skills.forEach(function(sk) { - html += '' + escHtml(sk) + ''; - }); - } -``` - -- [ ] **Step 3: Verify in browser** - -Reload the dashboard. Session cards that used MCP/Skills should show orange/purple badges next to the tool badge (e.g., `CLAUDE` `CHROME-DEVTOOLS` `FIGMA-USE`). - -- [ ] **Step 4: Commit** - -```bash -git add src/frontend/app.js -git commit -m "feat: render MCP/Skill badges on session cards" -``` - ---- - -### Task 5: Frontend — Badges in conversation view - -**Files:** -- Modify: `src/frontend/app.js:1128-1135` (showDetail message rendering) - -- [ ] **Step 1: Add tool badges to assistant messages in conversation view** - -In the `showDetail()` function, find the message rendering loop (line ~1128-1135): - -```js - data.messages.forEach(function(m) { - var roleClass = m.role === 'user' ? 'msg-user' : 'msg-assistant'; - var roleLabel = m.role === 'user' ? 'You' : 'Assistant'; - msgsHtml += '
'; - msgsHtml += '
' + roleLabel + '
'; - msgsHtml += '
' + escHtml(m.content) + '
'; - msgsHtml += '
'; - }); -``` - -Replace with: - -```js - data.messages.forEach(function(m) { - var roleClass = m.role === 'user' ? 'msg-user' : 'msg-assistant'; - var roleLabel = m.role === 'user' ? 'You' : 'Assistant'; - msgsHtml += '
'; - msgsHtml += '
' + roleLabel + '
'; - if (m.tools && m.tools.length > 0) { - msgsHtml += '
'; - m.tools.forEach(function(t) { - if (t.type === 'mcp') { - msgsHtml += '' + escHtml(t.tool) + ''; - } else if (t.type === 'skill') { - msgsHtml += '' + escHtml(t.skill) + ''; - } - }); - msgsHtml += '
'; - } - msgsHtml += '
' + escHtml(m.content) + '
'; - msgsHtml += '
'; - }); -``` - -- [ ] **Step 2: Verify in browser** - -Click on a session that used MCP/Skill tools. In the conversation view, assistant messages should show tool badges between the role label and message content. - -- [ ] **Step 3: Commit** - -```bash -git add src/frontend/app.js -git commit -m "feat: render MCP/Skill badges in conversation view" -``` diff --git a/docs/superpowers/specs/2026-04-06-card-footer-settings-design.md b/docs/superpowers/specs/2026-04-06-card-footer-settings-design.md deleted file mode 100644 index 8f052de..0000000 --- a/docs/superpowers/specs/2026-04-06-card-footer-settings-design.md +++ /dev/null @@ -1,69 +0,0 @@ -# Card Footer for MCP/Skills + Settings View - -## Card redesign — MCP/Skills footer - -Currently MCP/Skill badges render in `card-top` alongside the tool badge, project, time, and cost. This makes the header row cluttered. - -### New card structure - -``` -card-top: [CLAUDE] [project] [time] [~$cost] [star] -card-body: message text -card-footer: [msgs] [size] [date] [id] [tags] [expand] -card-tools: [LIGHTPANDA] [SUPERPOWERS] ← NEW, only if badges exist -``` - -`card-tools` is a new div below `card-footer` with `border-top`. It only renders when `s.mcp_servers.length > 0 || s.skills.length > 0`. Sessions without MCP/Skills look unchanged. - -### Changes - -**app.js — renderCard():** -- Remove MCP/Skill badge rendering from after `tool-badge` line -- Add `card-tools` div after `card-footer`, before `card-preview-area` - -**app.js — renderListCard():** -- Remove MCP/Skill badge rendering from after `tool-badge` line -- Add badges after the main list content (inline, same row or wrap) - -**styles.css:** -```css -.card-tools { - padding: 8px 16px; - border-top: 1px solid var(--border); - display: flex; - gap: 6px; - flex-wrap: wrap; -} -``` - -## Settings view - -Move Theme and Terminal selectors from sidebar bottom to a new Settings page. - -### Sidebar changes - -**index.html:** -- Remove `sidebar-settings` div (label + select for Theme and Terminal) -- Add new sidebar-item with gear icon and `data-view="settings"` after Changelog - -**app.js — render():** -- Add `settings` case that renders the settings page into `#content` -- Settings page contains: - - Theme selector (3 buttons: Dark / Light / System) — same functionality as current select - - Terminal selector (select dropdown) — same as current - - Version info at bottom - -**styles.css:** -- `.settings-page` container with padding -- `.settings-group` for each setting block (label + control) -- `.theme-btn` toggle buttons (active state matches current theme) - -### Data flow - -No backend changes. Settings are stored in localStorage (same keys: `codedash-theme`, `codedash-terminal`). The Settings view reads and writes to the same localStorage keys. Theme changes apply immediately via `setTheme()`. Terminal changes save via `saveTerminalPref()`. - -## Scope - -- No new API endpoints -- No new dependencies -- Frontend-only changes (app.js, styles.css, index.html) diff --git a/docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md b/docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md deleted file mode 100644 index 151cd0a..0000000 --- a/docs/superpowers/specs/2026-04-06-mcp-skill-badges-design.md +++ /dev/null @@ -1,85 +0,0 @@ -# MCP & Skill Badges - -Display MCP servers and Skills used in sessions as colored badges — on session cards (list view) and on individual messages (conversation view). - -## Data sources - -Session JSONL files already contain `tool_use` content blocks in assistant messages: - -- **MCP**: `{ "type": "tool_use", "name": "mcp____", "input": {...} }` -- **Skill**: `{ "type": "tool_use", "name": "Skill", "input": { "skill": "" } }` - -Both `loadSessions()` and `loadSessionDetail()` already read these JSONL files — the tool_use blocks are just currently discarded by `extractContent()`. - -## Backend changes (data.js) - -### loadSessions() — session enrichment (~line 284) - -Where `detail_messages` is counted by iterating JSONL lines, additionally collect: - -- `s.mcp_servers: string[]` — unique MCP server names extracted from tool names matching `mcp____` (take `` part) -- `s.skills: string[]` — unique skill names from `Skill` tool_use blocks via `input.skill` - -Both arrays default to `[]`. - -### loadSessionDetail() — message-level tools - -When parsing assistant messages, for each message additionally collect a `tools` array: - -```js -{ - role: 'assistant', - content: '...', - tools: [ - { type: 'mcp', server: 'chrome-devtools-mcp', tool: 'take_screenshot' }, - { type: 'skill', skill: 'figma:figma-use' } - ] -} -``` - -Only include tools where `name.startsWith('mcp__')` or `name === 'Skill'`. Deduplicate within a single message (same tool name = one entry). - -## Frontend changes (app.js) - -### Session cards — card-top badges - -After the existing `tool-badge tool-` span, render all MCP servers and skills: - -```html -chrome-devtools -figma-use -``` - -Show all badges, no limit. - -### Conversation view — message badges - -On each assistant message that has `tools.length > 0`, render a badge row under `msg-role`: - -```html -
- take_screenshot - navigate_page - figma-use -
-``` - -Card-level badges show server name (compact). Message-level badges show tool/skill name (detailed). - -## CSS (styles.css) - -Two new badge variants using existing `tool-badge` base class: - -```css -.badge-mcp { background: rgba(251, 146, 60, 0.2); color: #fb923c; border-color: #fb923c; } -.badge-skill { background: rgba(139, 92, 246, 0.2); color: #8b5cf6; border-color: #8b5cf6; } -.msg-tools { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } -``` - -Orange for MCP, purple for Skills. Both follow the existing `tool-badge` pattern (small, rounded, border). - -## Scope - -- Only MCP and Skill badges. No Agent, no built-in tools (Bash, Read, etc.). -- No new API endpoints — data added to existing responses. -- No new dependencies. diff --git a/docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md b/docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md deleted file mode 100644 index c70ab9d..0000000 --- a/docs/superpowers/specs/2026-04-06-preloader-sidebar-design.md +++ /dev/null @@ -1,37 +0,0 @@ -# Skeleton Preloader & Sidebar Font Fix - -## Sidebar — Unified font size - -Currently 3 different font sizes in sidebar items: 14px (nav), 12px (.small — Install Agents, Changelog), 10px (section headers). - -**Fix:** All sidebar items → 13px. Remove `.small` class usage. Section headers (Agents, Install Agents) stay 10px uppercase — they are labels, not navigation items. - -### Changes - -- `styles.css`: Change `.sidebar-item` font-size from 14px to 13px -- `styles.css`: Remove `.sidebar-item.small` rules (font-size: 12px, padding: 6px, small svg) -- `index.html`: Remove `class="small"` from Install Agents items and Changelog item - -## Skeleton Preloader - -When dashboard first loads, the content area is empty while `loadSessions()` fetches data. Show skeleton placeholder cards with shimmer animation. - -### Implementation - -- `styles.css`: Add `@keyframes skeleton-shimmer` animation, `.skeleton-card` styles -- `app.js`: In `init()`, render 6 skeleton cards into `#content` before `loadSessions()`. After `loadSessions()` completes, `render()` replaces them with real content (already happens — `applyFilters()` calls `render()` which sets `content.innerHTML`). - -### Skeleton card structure - -Mimics real card layout: -- Top row: badge placeholder + project placeholder + time placeholder -- Body: 2 text line placeholders -- Footer: 3 small metadata placeholders - -Shimmer: `linear-gradient(90deg, transparent 25%, rgba(255,255,255,0.06) 50%, transparent 75%)` animated left-to-right, 1.5s infinite. - -## Scope - -- No new API endpoints -- No new dependencies -- Pure CSS + minimal JS changes From 76ccb3cb826824be9590438d4d3f248cc94cd349 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 04:04:34 +0500 Subject: [PATCH 22/29] chore: add docs/superpowers/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2e086c2..4392d8a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ .DS_Store *.log .superpowers/ +docs/superpowers/ From 7a356b9d58535c9d14584e0014930b712c1add11 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 04:17:15 +0500 Subject: [PATCH 23/29] fix: use tab separator for sqlite3 queries to handle pipes in data --- src/data.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data.js b/src/data.js index d8b3516..c3d4068 100644 --- a/src/data.js +++ b/src/data.js @@ -21,14 +21,14 @@ function scanOpenCodeSessions() { try { // Use sqlite3 CLI to avoid Node version dependency const rows = execSync( - `sqlite3 "${OPENCODE_DB}" "SELECT s.id, s.title, s.directory, s.time_created, s.time_updated, COUNT(m.id) as msg_count FROM session s LEFT JOIN message m ON m.session_id = s.id GROUP BY s.id ORDER BY s.time_updated DESC"`, + `sqlite3 -separator $'\\t' "${OPENCODE_DB}" "SELECT s.id, s.title, s.directory, s.time_created, s.time_updated, COUNT(m.id) as msg_count FROM session s LEFT JOIN message m ON m.session_id = s.id GROUP BY s.id ORDER BY s.time_updated DESC"`, { encoding: 'utf8', timeout: 5000 } ).trim(); if (!rows) return sessions; for (const row of rows.split('\n')) { - const parts = row.split('|'); + const parts = row.split('\t'); if (parts.length < 6) continue; const [id, title, directory, timeCreated, timeUpdated, msgCount] = parts; @@ -128,14 +128,14 @@ function scanKiroSessions() { try { const rows = execSync( - `sqlite3 "${KIRO_DB}" "SELECT key, conversation_id, created_at, updated_at, substr(value, 1, 500) FROM conversations_v2 ORDER BY updated_at DESC"`, + `sqlite3 -separator $'\\t' "${KIRO_DB}" "SELECT key, conversation_id, created_at, updated_at, substr(value, 1, 500) FROM conversations_v2 ORDER BY updated_at DESC"`, { encoding: 'utf8', timeout: 5000 } ).trim(); if (!rows) return sessions; for (const row of rows.split('\n')) { - const parts = row.split('|'); + const parts = row.split('\t'); if (parts.length < 5) continue; const [directory, convId, createdAt, updatedAt, valuePeek] = parts; From 3113301c84670f667ea52634c83c25935545d0b4 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 04:23:34 +0500 Subject: [PATCH 24/29] perf: compute session cost during enrichment, eliminate duplicate file reads for analytics --- src/data.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/data.js b/src/data.js index c3d4068..bbfa218 100644 --- a/src/data.js +++ b/src/data.js @@ -393,13 +393,32 @@ function loadSessions() { let msgCount = 0; const mcpSet = new Set(); const skillSet = new Set(); + let costTotal = 0, costInput = 0, costOutput = 0, costModel = ''; const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean); for (const sl of sLines) { try { const entry = JSON.parse(sl); if (entry.type === 'user' || entry.type === 'assistant') msgCount++; if (entry.type === 'assistant') { - const content = (entry.message || {}).content; + const msg = entry.message || {}; + // Cost calculation + if (!costModel && msg.model) costModel = msg.model; + const u = msg.usage; + if (u) { + const pricing = getModelPricing(msg.model || costModel); + const inp = u.input_tokens || 0; + const cacheCreate = u.cache_creation_input_tokens || 0; + const cacheRead = u.cache_read_input_tokens || 0; + const out = u.output_tokens || 0; + costInput += inp + cacheCreate + cacheRead; + costOutput += out; + costTotal += inp * pricing.input + + cacheCreate * pricing.cache_create + + cacheRead * pricing.cache_read + + out * pricing.output; + } + // MCP/Skills extraction + const content = msg.content; if (Array.isArray(content)) { for (const block of content) { if (block.type !== 'tool_use') continue; @@ -419,7 +438,8 @@ function loadSessions() { s.detail_messages = msgCount; s.mcp_servers = Array.from(mcpSet); s.skills = Array.from(skillSet); - } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; } + s.cost = { cost: costTotal, inputTokens: costInput, outputTokens: costOutput, model: costModel }; + } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; s.cost = null; } } else { s.has_detail = false; s.file_size = 0; @@ -1040,7 +1060,7 @@ function getCostAnalytics(sessions) { const sessionCosts = []; for (const s of sessions) { - const costData = computeSessionCost(s.id, s.project); + const costData = s.cost || computeSessionCost(s.id, s.project); const cost = costData.cost; const tokens = costData.inputTokens + costData.outputTokens; if (cost === 0 && tokens === 0) continue; From 380b0da20e0a7471f431b2762369b443984e0237 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 04:36:32 +0500 Subject: [PATCH 25/29] perf: cache analytics result for 2min, skip large files (>10MB) in bulk cost calc --- src/data.js | 53 ++++++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/data.js b/src/data.js index bbfa218..2624f9a 100644 --- a/src/data.js +++ b/src/data.js @@ -393,32 +393,13 @@ function loadSessions() { let msgCount = 0; const mcpSet = new Set(); const skillSet = new Set(); - let costTotal = 0, costInput = 0, costOutput = 0, costModel = ''; const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean); for (const sl of sLines) { try { const entry = JSON.parse(sl); if (entry.type === 'user' || entry.type === 'assistant') msgCount++; if (entry.type === 'assistant') { - const msg = entry.message || {}; - // Cost calculation - if (!costModel && msg.model) costModel = msg.model; - const u = msg.usage; - if (u) { - const pricing = getModelPricing(msg.model || costModel); - const inp = u.input_tokens || 0; - const cacheCreate = u.cache_creation_input_tokens || 0; - const cacheRead = u.cache_read_input_tokens || 0; - const out = u.output_tokens || 0; - costInput += inp + cacheCreate + cacheRead; - costOutput += out; - costTotal += inp * pricing.input - + cacheCreate * pricing.cache_create - + cacheRead * pricing.cache_read - + out * pricing.output; - } - // MCP/Skills extraction - const content = msg.content; + const content = (entry.message || {}).content; if (Array.isArray(content)) { for (const block of content) { if (block.type !== 'tool_use') continue; @@ -438,8 +419,7 @@ function loadSessions() { s.detail_messages = msgCount; s.mcp_servers = Array.from(mcpSet); s.skills = Array.from(skillSet); - s.cost = { cost: costTotal, inputTokens: costInput, outputTokens: costOutput, model: costModel }; - } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; s.cost = null; } + } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; } } else { s.has_detail = false; s.file_size = 0; @@ -996,10 +976,25 @@ function getModelPricing(model) { // ── Compute real cost from session file token usage ──────── -function computeSessionCost(sessionId, project) { +function computeSessionCost(sessionId, project, maxFileSize) { const found = findSessionFile(sessionId, project); if (!found) return { cost: 0, inputTokens: 0, outputTokens: 0, model: '' }; + // Skip very large files in bulk operations (analytics) + if (maxFileSize) { + try { + const stat = fs.statSync(found.file); + if (stat.size > maxFileSize) { + // Estimate from file size instead + const tokens = stat.size / 4; + const pricing = getModelPricing(''); + const inp = Math.round(tokens * 0.3); + const out = Math.round(tokens * 0.7); + return { cost: inp * pricing.input + out * pricing.output, inputTokens: inp, outputTokens: out, model: 'estimated' }; + } + } catch {} + } + let totalCost = 0; let totalInput = 0; let totalOutput = 0; @@ -1051,7 +1046,13 @@ function computeSessionCost(sessionId, project) { // ── Cost analytics ──────────────────────────────────────── +let analyticsCache = null; +let analyticsCacheAt = 0; +const ANALYTICS_TTL = 120000; // 2 minutes + function getCostAnalytics(sessions) { + const now = Date.now(); + if (analyticsCache && (now - analyticsCacheAt) < ANALYTICS_TTL) return analyticsCache; const byDay = {}; const byProject = {}; const byWeek = {}; @@ -1060,7 +1061,7 @@ function getCostAnalytics(sessions) { const sessionCosts = []; for (const s of sessions) { - const costData = s.cost || computeSessionCost(s.id, s.project); + const costData = computeSessionCost(s.id, s.project, 10 * 1024 * 1024); const cost = costData.cost; const tokens = costData.inputTokens + costData.outputTokens; if (cost === 0 && tokens === 0) continue; @@ -1098,7 +1099,7 @@ function getCostAnalytics(sessions) { // Sort top sessions by cost sessionCosts.sort((a, b) => b.cost - a.cost); - return { + analyticsCache = { totalCost, totalTokens, totalSessions: sessions.length, @@ -1107,6 +1108,8 @@ function getCostAnalytics(sessions) { byProject, topSessions: sessionCosts.slice(0, 10), }; + analyticsCacheAt = Date.now(); + return analyticsCache; } // ── Active sessions detection ───────────────────────────── From 2767b4d7b8939d3f3be1affa638de2513d9f28e9 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Mon, 6 Apr 2026 04:45:31 +0500 Subject: [PATCH 26/29] feat: cache loadSessions() for 30s, add search clear button --- src/data.js | 11 ++++++++++- src/frontend/app.js | 16 +++++++++++++--- src/frontend/index.html | 5 ++++- src/frontend/styles.css | 22 +++++++++++++++++++++- src/server.js | 3 ++- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/data.js b/src/data.js index 2624f9a..c50eaef 100644 --- a/src/data.js +++ b/src/data.js @@ -318,7 +318,14 @@ function scanCodexSessions() { // ── Public API ───────────────────────────────────────────── -function loadSessions() { +let sessionsCache = null; +let sessionsCacheAt = 0; +const SESSIONS_TTL = 30000; // 30 seconds + +function loadSessions(forceRefresh) { + if (!forceRefresh && sessionsCache && (Date.now() - sessionsCacheAt) < SESSIONS_TTL) { + return sessionsCache; + } const sessions = {}; // Load Claude Code sessions @@ -444,6 +451,8 @@ function loadSessions() { s.date = dt.getFullYear() + '-' + String(dt.getMonth()+1).padStart(2,'0') + '-' + String(dt.getDate()).padStart(2,'0'); } + sessionsCache = result; + sessionsCacheAt = Date.now(); return result; } diff --git a/src/frontend/app.js b/src/frontend/app.js index b453e4d..c75e8a4 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -160,9 +160,11 @@ function toggleStar(id) { // ── Data loading ─────────────────────────────────────────────── -async function loadSessions() { +async function loadSessions(refresh) { try { - var resp = await fetch('/api/sessions'); + var url = '/api/sessions'; + if (refresh) url += '?refresh=1'; + var resp = await fetch(url); allSessions = await resp.json(); applyFilters(); } catch (e) { @@ -171,10 +173,16 @@ async function loadSessions() { } function refreshData() { - loadSessions(); + loadSessions(true); showToast('Refreshed'); } +function clearSearch() { + var box = document.getElementById('searchBox'); + if (box) box.value = ''; + onSearch(''); +} + async function loadTerminals() { try { var resp = await fetch('/api/terminals'); @@ -378,6 +386,8 @@ function applyFilters() { function onSearch(val) { searchQuery = val; + var clearBtn = document.getElementById('searchClear'); + if (clearBtn) clearBtn.style.display = val ? 'block' : 'none'; applyFilters(); // Trigger deep search after debounce diff --git a/src/frontend/index.html b/src/frontend/index.html index 5f3ae03..5a4331d 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -120,7 +120,10 @@
- +
+ + +