Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 10 additions & 3 deletions src/frontend/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ async function renderAnalytics(container) {
coverageparts.push('<span class="coverage-ok">Claude Extension \u2713</span>');
if (byAgent['codex'] && byAgent['codex'].sessions > 0)
coverageparts.push('<span class="coverage-est">Codex ~est.</span>');
if (byAgent['qwen'] && byAgent['qwen'].sessions > 0) {
coverageparts.push(byAgent['qwen'].unavailable
? '<span class="coverage-est">Qwen tokens only</span>'
: '<span class="coverage-ok">Qwen Code \u2713</span>');
}
if (byAgent['opencode'] && byAgent['opencode'].sessions > 0)
coverageparts.push(byAgent['opencode'].estimated
? '<span class="coverage-est">OpenCode ~est.</span>'
Expand All @@ -115,8 +120,10 @@ async function renderAnalytics(container) {
agentEntriesOv.forEach(function(entry) {
var name = entry[0]; var info = entry[1];
var pct = maxAgentCostOv > 0 ? (info.cost / maxAgentCostOv * 100) : 0;
var label = { 'claude': 'Claude Code', 'claude-ext': 'Claude Ext', 'codex': 'Codex', 'opencode': 'OpenCode', 'cursor': 'Cursor', 'kiro': 'Kiro' }[name] || name;
var estMark = info.estimated ? ' <span style="font-size:10px;opacity:0.6">~est.</span>' : '';
var label = getToolLabel(name);
var estMark = info.unavailable
? ' <span style="font-size:10px;opacity:0.6">tokens only</span>'
: (info.estimated ? ' <span style="font-size:10px;opacity:0.6">~est.</span>' : '');
html += '<div class="hbar-row">';
html += '<span class="hbar-name">' + label + estMark + '</span>';
html += '<div class="hbar-track"><div class="hbar-fill" style="width:' + pct + '%"></div></div>';
Expand All @@ -143,7 +150,7 @@ async function renderAnalytics(container) {
html += '<div class="token-type-card token-cache-read"><span class="token-type-val">' + formatTokens(data.totalCacheReadTokens) + '</span><span class="token-type-label">Cache read</span><span class="token-type-pct">' + pctOf(data.totalCacheReadTokens) + '%</span></div>';
html += '<div class="token-type-card token-cache-create"><span class="token-type-val">' + formatTokens(data.totalCacheCreateTokens) + '</span><span class="token-type-label">Cache write</span><span class="token-type-pct">' + pctOf(data.totalCacheCreateTokens) + '%</span></div>';
if (data.avgContextPct > 0) {
html += '<div class="token-type-card token-context"><span class="token-type-val">' + data.avgContextPct + '%</span><span class="token-type-label">Avg context used</span><span class="token-type-pct">of 200K</span></div>';
html += '<div class="token-type-card token-context"><span class="token-type-val">' + data.avgContextPct + '%</span><span class="token-type-label">Avg context used</span><span class="token-type-pct">window avg</span></div>';
}
html += '</div>';

Expand Down
56 changes: 47 additions & 9 deletions src/frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,33 @@ function getSessionDisplayName(session) {
|| '';
}

var TOOL_META = {
claude: { label: 'Claude Code', shortLabel: 'claude', color: '#60a5fa' },
'claude-ext': { label: 'Claude Ext', shortLabel: 'claude ext', color: '#60a5fa' },
codex: { label: 'Codex', shortLabel: 'codex', color: '#22d3ee' },
qwen: { label: 'Qwen Code', shortLabel: 'qwen', color: '#fbbf24' },
cursor: { label: 'Cursor', shortLabel: 'cursor', color: '#4a9eff' },
opencode: { label: 'OpenCode', shortLabel: 'opencode', color: '#c084fc' },
kiro: { label: 'Kiro', shortLabel: 'kiro', color: '#fb923c' }
};

function getToolLabel(tool, shortLabel) {
var meta = TOOL_META[tool] || { label: tool || 'unknown', shortLabel: tool || 'unknown' };
return shortLabel ? meta.shortLabel : meta.label;
}

function getResumeCommand(tool, sessionId, project) {
if (tool === 'codex') return 'codex resume ' + sessionId;
if (tool === 'qwen') return 'qwen -r ' + sessionId;
if (tool === 'cursor') return 'cursor ' + (project ? '"' + project + '"' : '.');
return 'claude --resume ' + sessionId;
}

function getConvertTargets(tool) {
if (tool !== 'claude' && tool !== 'codex' && tool !== 'qwen') return [];
return ['claude', 'codex', 'qwen'].filter(function(target) { return target !== tool; });
}

// ── Utilities ──────────────────────────────────────────────────

function timeAgo(dateStr) {
Expand Down Expand Up @@ -185,6 +212,11 @@ function estimateCost(fileSize) {
return tokens * 0.3 * (3.0 / 1e6) + tokens * 0.7 * (15.0 / 1e6);
}

function getEstimatedSessionCost(session) {
if (!session || session.tool === 'qwen') return 0;
return estimateCost(session.file_size);
}

// ── Subscription service plans (pricing as of 2025) ─────────────
var SERVICE_PLANS = {
'Claude': { label: 'Claude (Anthropic)', plans: [
Expand Down Expand Up @@ -714,12 +746,12 @@ function renderCard(s, idx) {
var isSelected = selectedIds.has(s.id);
var isFocused = focusedIndex === idx;
var sessionTags = tags[s.id] || [];
var cost = estimateCost(s.file_size);
var cost = getEstimatedSessionCost(s);
var costStr = cost > 0 ? '~$' + cost.toFixed(2) : '';
var projName = getProjectName(s.project);
var projColor = getProjectColor(projName);
var toolClass = 'tool-' + s.tool;
var toolLabel = s.tool === 'claude-ext' ? 'claude ext' : s.tool;
var toolLabel = getToolLabel(s.tool, true);

var classes = 'card';
if (isSelected) classes += ' selected';
Expand Down Expand Up @@ -820,7 +852,7 @@ function renderListCard(s, idx) {
if (isFocused) classes += ' focused';

var html = '<div class="' + classes + '" data-id="' + s.id + '" onclick="onCardClick(\'' + s.id + '\', event)">';
var listToolLabel = s.tool === 'claude-ext' ? 'claude ext' : s.tool;
var listToolLabel = getToolLabel(s.tool, true);
html += '<span class="tool-badge tool-' + s.tool + '">' + escHtml(listToolLabel) + '</span>';
if (showBadges && s.mcp_servers && s.mcp_servers.length > 0) {
s.mcp_servers.forEach(function(m) {
Expand Down Expand Up @@ -1152,9 +1184,9 @@ function renderTimeline(container, sessions) {

function renderQACard(s, idx) {
var isStarred = stars.indexOf(s.id) >= 0;
var toolLabel = s.tool === 'claude-ext' ? 'claude ext' : s.tool;
var toolLabel = getToolLabel(s.tool, true);
var toolClass = 'tool-' + s.tool;
var cost = estimateCost(s.file_size);
var cost = getEstimatedSessionCost(s);
var costStr = cost > 0 ? '~$' + cost.toFixed(2) : '';
var classes = 'qa-item' + (selectedIds.has(s.id) ? ' selected' : '');

Expand Down Expand Up @@ -1199,7 +1231,7 @@ function renderProjects(container, sessions) {
var list = entry[1].slice().sort(function(a, b) { return b.last_ts - a.last_ts; });
var color = getProjectColor(name);
var totalMsgs = list.reduce(function(s, e) { return s + (e.messages || 0); }, 0);
var totalCost = list.reduce(function(s, e) { return s + estimateCost(e.file_size); }, 0);
var totalCost = list.reduce(function(s, e) { return s + getEstimatedSessionCost(e); }, 0);
var costLabel = totalCost > 0 ? ' · ~$' + totalCost.toFixed(2) : '';

html += '<div class="git-project-group">';
Expand Down Expand Up @@ -1523,7 +1555,7 @@ function renderRunningCard(a, s) {
html += '<div class="running-card-header">';
html += '<span class="live-badge live-' + a.status + '">' + (a.status === 'waiting' ? 'WAITING' : 'LIVE') + '</span>';
html += '<span class="running-project" style="color:' + projColor + '">' + escHtml(projName) + '</span>';
html += '<span class="running-tool">' + escHtml(a.entrypoint || a.kind || 'claude') + '</span>';
html += '<span class="running-tool">' + escHtml(getToolLabel(a.entrypoint || a.kind || 'claude')) + '</span>';
html += '</div>';
html += '<div class="running-stats">';
html += '<div class="running-stat"><span class="running-stat-val">' + a.cpu.toFixed(1) + '%</span><span class="running-stat-label">CPU</span></div>';
Expand All @@ -1550,7 +1582,7 @@ function renderDoneCard(s) {
html += '<div class="running-card-header">';
html += '<span class="live-badge live-done">DONE</span>';
html += '<span class="running-project" style="color:' + projColor + '">' + escHtml(projName) + '</span>';
html += '<span class="running-tool tool-' + (s.tool || 'claude') + '">' + escHtml(s.tool || 'claude') + '</span>';
html += '<span class="running-tool tool-' + (s.tool || 'claude') + '">' + escHtml(getToolLabel(s.tool || 'claude', true)) + '</span>';
html += '</div>';
var displayName = getSessionDisplayName(s);
if (displayName) html += '<div class="running-msg">' + escHtml(displayName.slice(0, 120)) + '</div>';
Expand All @@ -1575,7 +1607,7 @@ function renderRunning(container, sessions) {
}).slice(0, 8);

if (allActiveIds.length === 0 && done.length === 0) {
container.innerHTML = '<div class="empty-state">No running sessions detected.<br><span style="font-size:12px;color:var(--text-muted)">Start a Claude Code or Codex session and it will appear here.</span></div>';
container.innerHTML = '<div class="empty-state">No running sessions detected.<br><span style="font-size:12px;color:var(--text-muted)">Start a supported agent session and it will appear here.</span></div>';
return;
}

Expand Down Expand Up @@ -1851,6 +1883,12 @@ var AGENT_INSTALL = {
alt: 'brew install --cask codex',
url: 'https://github.com/openai/codex',
},
qwen: {
name: 'Qwen Code',
cmd: 'npm i -g @qwen-code/qwen-code',
alt: null,
url: 'https://github.com/QwenLM/qwen-code',
},
kiro: {
name: 'Kiro CLI',
cmd: 'curl -fsSL https://cli.kiro.dev/install | bash',
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ function setView(view) {
} else if (view === 'codex-only') {
toolFilter = toolFilter === 'codex' ? null : 'codex';
currentView = 'sessions';
} else if (view === 'qwen-only') {
toolFilter = toolFilter === 'qwen' ? null : 'qwen';
currentView = 'sessions';
} else if (view === 'cursor-only') {
toolFilter = toolFilter === 'cursor' ? null : 'cursor';
currentView = 'sessions';
Expand Down
29 changes: 13 additions & 16 deletions src/frontend/detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async function openDetail(s) {

title.textContent = escHtml(getProjectName(s.project)) + ' / ' + s.id.slice(0, 12);

var cost = estimateCost(s.file_size);
var cost = getEstimatedSessionCost(s);
var costStr = cost > 0 ? '~$' + cost.toFixed(2) : '';
var isStarred = stars.indexOf(s.id) >= 0;
var sessionTags = tags[s.id] || [];
Expand All @@ -26,7 +26,7 @@ async function openDetail(s) {
} else if (s.has_detail) {
infoHtml += '<div class="detail-row"><span class="detail-label">Name</span><button class="toolbar-btn" style="font-size:11px;padding:2px 8px" onclick="generateTitle(\'' + s.id + '\',\'' + escProject + '\')">Generate AI Name</button></div>';
}
var detailToolLabel = s.tool === 'claude-ext' ? 'claude ext' : s.tool;
var detailToolLabel = getToolLabel(s.tool);
infoHtml += '<div class="detail-row"><span class="detail-label">Tool</span><span class="tool-badge tool-' + s.tool + '">' + escHtml(detailToolLabel) + '</span></div>';
infoHtml += '<div class="detail-row"><span class="detail-label">Project</span><span>' + escHtml(s.project_short || s.project || '') + '</span></div>';
infoHtml += '<div class="detail-git-info" id="detail-git-info"></div>';
Expand Down Expand Up @@ -81,8 +81,9 @@ async function openDetail(s) {
if (s.has_detail) {
infoHtml += '<button class="launch-btn btn-secondary" onclick="closeDetail();openReplay(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Replay</button>';
infoHtml += '<button class="launch-btn btn-secondary" onclick="exportMd(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Export MD</button>';
var convertTarget = s.tool === 'codex' ? 'claude' : 'codex';
infoHtml += '<button class="launch-btn btn-secondary" onclick="convertTo(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\',\'' + convertTarget + '\')">Convert to ' + convertTarget + '</button>';
getConvertTargets(s.tool).forEach(function(target) {
infoHtml += '<button class="launch-btn btn-secondary" onclick="convertTo(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\',\'' + target + '\')">Convert to ' + getToolLabel(target) + '</button>';
});
infoHtml += '<button class="launch-btn btn-secondary" onclick="downloadHandoff(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Handoff</button>';
}
infoHtml += '<button class="star-btn detail-star' + (isStarred ? ' active' : '') + '" onclick="toggleStar(\'' + s.id + '\')">&#9733; ' + (isStarred ? 'Starred' : 'Star') + '</button>';
Expand Down Expand Up @@ -115,15 +116,20 @@ async function openDetail(s) {

// Load real cost
loadRealCost(s.id, s.project || '').then(function(costData) {
if (!costData || !costData.cost) return;
if (!costData) return;
var totalTokens = (costData.inputTokens || 0) + (costData.outputTokens || 0) + (costData.cacheReadTokens || 0) + (costData.cacheCreateTokens || 0);
if (!costData.cost && !totalTokens) return;
var row = document.getElementById('detail-real-cost');
if (row) {
row.style.display = '';
var cacheStr = '';
if ((costData.cacheReadTokens || 0) + (costData.cacheCreateTokens || 0) > 0)
cacheStr = ' / ' + formatTokens((costData.cacheReadTokens||0) + (costData.cacheCreateTokens||0)) + ' cache';
var valueHtml = costData.unavailable
? '<span class="cost-badge" style="background:rgba(251,191,36,0.2);color:#fbbf24">pricing unavailable</span>'
: '<span class="cost-badge" style="background:rgba(74,222,128,0.2);color:var(--accent-green)">$' + costData.cost.toFixed(2) + '</span>';
row.querySelector('span:last-child').innerHTML =
'<span class="cost-badge" style="background:rgba(74,222,128,0.2);color:var(--accent-green)">$' + costData.cost.toFixed(2) + '</span>' +
valueHtml +
' <span style="font-size:11px;color:var(--text-muted)">' +
formatTokens(costData.inputTokens) + ' in / ' + formatTokens(costData.outputTokens) + ' out' + cacheStr +
(costData.model ? ' (' + costData.model + ')' : '') + '</span>';
Expand Down Expand Up @@ -298,16 +304,7 @@ function launchSession(sessionId, tool, project, flags) {

function copyResume(sessionId, tool) {
var s = allSessions.find(function(x) { return x.id === sessionId; });
var cmd;
if (tool === 'codex') {
cmd = 'codex resume ' + sessionId;
} else if (tool === 'kilo') {
cmd = 'kilo resume ' + sessionId;
} else if (tool === 'cursor') {
cmd = 'cursor ' + (s && s.project ? '"' + s.project + '"' : '.');
} else {
cmd = 'claude --resume ' + sessionId;
}
var cmd = getResumeCommand(tool, sessionId, s && s.project ? s.project : '');
copyText(cmd, 'Copied: ' + cmd);
}

Expand Down
13 changes: 11 additions & 2 deletions src/frontend/heatmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,22 @@ function renderHeatmap(container) {
// Per-tool breakdown
var toolTotals = {};
allSessions.forEach(function(s) { if (s.date >= yearStart) { toolTotals[s.tool] = (toolTotals[s.tool] || 0) + 1; } });
var toolColors = { claude: '#60a5fa', codex: '#22d3ee', opencode: '#c084fc', kiro: '#fb923c', kilo: '#34d399' };
var toolColors = {
claude: '#60a5fa',
'claude-ext': '#60a5fa',
codex: '#22d3ee',
qwen: '#fbbf24',
cursor: '#4a9eff',
opencode: '#c084fc',
kiro: '#fb923c',
kilo: '#34d399'
};
html += '<div class="gh-tools">';
Object.keys(toolTotals).sort(function(a,b) { return toolTotals[b] - toolTotals[a]; }).forEach(function(tool) {
var pct = (toolTotals[tool] / Math.max(totalThisYear, 1) * 100).toFixed(0);
var color = toolColors[tool] || '#6b7280';
html += '<div class="gh-tool-row">';
html += '<span class="gh-tool-name" style="color:' + color + '">' + tool + '</span>';
html += '<span class="gh-tool-name" style="color:' + color + '">' + escHtml(getToolLabel(tool)) + '</span>';
html += '<div class="gh-tool-bar"><div class="gh-tool-fill" style="width:' + pct + '%;background:' + color + '"></div></div>';
html += '<span class="gh-tool-val">' + toolTotals[tool] + ' (' + pct + '%)</span>';
html += '</div>';
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
Codex
</div>
<div class="sidebar-item" data-view="qwen-only">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l7 4v8l-7 8-7-8V6l7-4z"/><path d="M9 10h6"/><path d="M9 14h6"/></svg>
Qwen Code
</div>
<div class="sidebar-item" data-view="kiro-only">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
Kiro
Expand Down Expand Up @@ -90,6 +94,10 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
Codex CLI
</div>
<div class="sidebar-item" onclick="installAgent('qwen')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
Qwen Code
</div>
<div class="sidebar-item" onclick="installAgent('kiro')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
Kiro CLI
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/leaderboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function renderGlobalBoard() {
var agents = Object.entries(u.stats?.agents || {}).sort(function(a,b){return b[1]-a[1]}).slice(0,3);
if (agents.length) {
html += '<div class="lb-global-agents">';
agents.forEach(function(a) { html += '<span class="lb-agent-mini tool-' + a[0] + '">' + a[0] + '</span>'; });
agents.forEach(function(a) { html += '<span class="lb-agent-mini tool-' + a[0] + '">' + escHtml(getToolLabel(a[0], true)) + '</span>'; });
html += '</div>';
}
html += '</div>';
Expand Down Expand Up @@ -236,7 +236,7 @@ async function renderLeaderboard(container) {
agentEntries.forEach(function(e) {
var pct = data.totals.sessions > 0 ? Math.round(e[1] / data.totals.sessions * 100) : 0;
html += '<div class="lb-agent-row">';
html += '<span class="tool-badge tool-' + e[0] + '">' + escHtml(e[0]) + '</span>';
html += '<span class="tool-badge tool-' + e[0] + '">' + escHtml(getToolLabel(e[0], true)) + '</span>';
html += '<div class="lb-agent-bar"><div class="lb-agent-bar-fill" style="width:' + pct + '%"></div></div>';
html += '<span class="lb-agent-count">' + e[1] + ' (' + pct + '%)</span>';
html += '</div>';
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ body {
.tool-chip:hover { color: var(--text-primary); }
.tool-chip.active-claude { background: rgba(96, 165, 250, 0.2); border-color: var(--accent-blue); color: var(--accent-blue); }
.tool-chip.active-codex { background: rgba(34, 211, 238, 0.2); border-color: var(--accent-cyan); color: var(--accent-cyan); }
.tool-chip.active-qwen { background: rgba(251, 191, 36, 0.2); border-color: #fbbf24; color: #fbbf24; }

.stats { color: var(--text-muted); font-size: 13px; white-space: nowrap; }

Expand Down Expand Up @@ -466,6 +467,7 @@ body {
}
.badge-claude { background: rgba(96, 165, 250, 0.15); color: var(--accent-blue); }
.badge-codex { background: rgba(34, 211, 238, 0.15); color: var(--accent-cyan); }
.badge-qwen { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }

[data-theme="light"] .badge { background: rgba(0,0,0,0.05); }

Expand Down Expand Up @@ -1202,6 +1204,11 @@ body {
color: var(--accent-cyan);
}

.tool-qwen {
background: rgba(251, 191, 36, 0.15);
color: #fbbf24;
}

.tool-opencode {
background: rgba(192, 132, 252, 0.15);
color: var(--accent-purple);
Expand Down