Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1dff8c1
Add design spec for MCP & Skill badges in session cards and conversat…
izzzzzi Apr 5, 2026
0e88dae
Add implementation plan for MCP & Skill badges
izzzzzi Apr 5, 2026
61be24f
feat: extract MCP servers and Skills from session JSONL in loadSessio…
izzzzzi Apr 5, 2026
4f0fa9f
feat: extract per-message MCP/Skill tools in loadSessionDetail()
izzzzzi Apr 5, 2026
a87c6ec
feat: add CSS for MCP and Skill badge styles
izzzzzi Apr 5, 2026
e8ff457
feat: render MCP/Skill badges on session cards
izzzzzi Apr 5, 2026
d47b4d7
feat: render MCP/Skill badges in conversation view
izzzzzi Apr 5, 2026
d82d6b9
fix: ensure mcp_servers/skills defaults for non-Claude sessions
izzzzzi Apr 5, 2026
4aae637
fix: strip plugin prefix from skill names (superpowers:writing-plans …
izzzzzi Apr 5, 2026
c505cb5
fix: show plugin name instead of skill name (superpowers:writing-plan…
izzzzzi Apr 5, 2026
51e18f2
feat: extract MCP/Skill badges for Codex sessions
izzzzzi Apr 5, 2026
cc5abaa
Add design spec for skeleton preloader and sidebar font fix
izzzzzi Apr 5, 2026
baf158e
feat: add skeleton preloader + unify sidebar font sizes to 13px
izzzzzi Apr 5, 2026
1247980
Add design spec for card MCP/Skills footer and Settings view
izzzzzi Apr 5, 2026
12adc12
feat: move MCP/Skills badges to card footer, clean up card header
izzzzzi Apr 5, 2026
ab47bc7
feat: add Settings view, move theme/terminal from sidebar to dedicate…
izzzzzi Apr 5, 2026
1b67228
feat: single-column detail info with MCP/Skills rows, message tool fo…
izzzzzi Apr 5, 2026
35cc300
feat: custom calendar date picker with range selection and presets
izzzzzi Apr 5, 2026
bbfee8e
refactor: replace inline SVGs with SVG sprite sheet (18 icons)
izzzzzi Apr 5, 2026
3859382
chore: add .superpowers/ to .gitignore
izzzzzi Apr 5, 2026
230f70e
chore: remove superpowers specs/plans from repo
izzzzzi Apr 5, 2026
76ccb3c
chore: add docs/superpowers/ to .gitignore
izzzzzi Apr 5, 2026
7a356b9
fix: use tab separator for sqlite3 queries to handle pipes in data
izzzzzi Apr 5, 2026
3113301
perf: compute session cost during enrichment, eliminate duplicate fil…
izzzzzi Apr 5, 2026
380b0da
perf: cache analytics result for 2min, skip large files (>10MB) in bu…
izzzzzi Apr 5, 2026
2767b4d
feat: cache loadSessions() for 30s, add search clear button
izzzzzi Apr 5, 2026
2943e78
fix: use parsed.searchParams instead of undefined params
izzzzzi Apr 5, 2026
7f4f5da
feat: grid/list layout toggle for Projects and Starred views
izzzzzi Apr 5, 2026
f4ee9de
feat: grid/list layout for Running view (recently inactive section)
izzzzzi Apr 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules/
.DS_Store
*.log
.superpowers/
docs/superpowers/
164 changes: 154 additions & 10 deletions src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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),
});
}
}
Expand All @@ -294,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
Expand Down Expand Up @@ -367,22 +398,50 @@ 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.includes(':') ? skill.split(':')[0] : 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 = [];
}
}

// 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) {
Expand All @@ -392,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;
}

Expand Down Expand Up @@ -420,23 +481,57 @@ 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 {
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);
if (content && !isSystemMessage(content)) {
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) };
}

Expand Down Expand Up @@ -617,6 +712,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.includes(':') ? skill.split(':')[0] : skill });
}
}
}
return tools;
}

function extractContent(raw) {
if (!raw) return '';
if (typeof raw === 'string') return raw;
Expand Down Expand Up @@ -864,10 +985,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;
Expand Down Expand Up @@ -919,7 +1055,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 = {};
Expand All @@ -928,7 +1070,7 @@ function getCostAnalytics(sessions) {
const sessionCosts = [];

for (const s of sessions) {
const costData = 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;
Expand Down Expand Up @@ -966,7 +1108,7 @@ function getCostAnalytics(sessions) {
// Sort top sessions by cost
sessionCosts.sort((a, b) => b.cost - a.cost);

return {
analyticsCache = {
totalCost,
totalTokens,
totalSessions: sessions.length,
Expand All @@ -975,6 +1117,8 @@ function getCostAnalytics(sessions) {
byProject,
topSessions: sessionCosts.slice(0, 10),
};
analyticsCacheAt = Date.now();
return analyticsCache;
}

// ── Active sessions detection ─────────────────────────────
Expand Down
Loading