diff --git a/src/frontend/analytics.js b/src/frontend/analytics.js index ddc81d5..0d7dc99 100644 --- a/src/frontend/analytics.js +++ b/src/frontend/analytics.js @@ -90,6 +90,11 @@ async function renderAnalytics(container) { coverageparts.push('Claude Extension \u2713'); if (byAgent['codex'] && byAgent['codex'].sessions > 0) coverageparts.push('Codex ~est.'); + if (byAgent['qwen'] && byAgent['qwen'].sessions > 0) { + coverageparts.push(byAgent['qwen'].unavailable + ? 'Qwen tokens only' + : 'Qwen Code \u2713'); + } if (byAgent['opencode'] && byAgent['opencode'].sessions > 0) coverageparts.push(byAgent['opencode'].estimated ? 'OpenCode ~est.' @@ -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 ? ' ~est.' : ''; + var label = getToolLabel(name); + var estMark = info.unavailable + ? ' tokens only' + : (info.estimated ? ' ~est.' : ''); html += '
'; html += '' + label + estMark + ''; html += '
'; @@ -143,7 +150,7 @@ async function renderAnalytics(container) { html += '
' + formatTokens(data.totalCacheReadTokens) + 'Cache read' + pctOf(data.totalCacheReadTokens) + '%
'; html += '
' + formatTokens(data.totalCacheCreateTokens) + 'Cache write' + pctOf(data.totalCacheCreateTokens) + '%
'; if (data.avgContextPct > 0) { - html += '
' + data.avgContextPct + '%Avg context usedof 200K
'; + html += '
' + data.avgContextPct + '%Avg context usedwindow avg
'; } html += '
'; diff --git a/src/frontend/app.js b/src/frontend/app.js index 3c6c318..844cee7 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -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) { @@ -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: [ @@ -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'; @@ -820,7 +852,7 @@ function renderListCard(s, idx) { if (isFocused) classes += ' focused'; var html = '
'; - var listToolLabel = s.tool === 'claude-ext' ? 'claude ext' : s.tool; + var listToolLabel = getToolLabel(s.tool, true); html += '' + escHtml(listToolLabel) + ''; if (showBadges && s.mcp_servers && s.mcp_servers.length > 0) { s.mcp_servers.forEach(function(m) { @@ -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' : ''); @@ -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 += '
'; @@ -1523,7 +1555,7 @@ function renderRunningCard(a, s) { html += '
'; html += '' + (a.status === 'waiting' ? 'WAITING' : 'LIVE') + ''; html += '' + escHtml(projName) + ''; - html += '' + escHtml(a.entrypoint || a.kind || 'claude') + ''; + html += '' + escHtml(getToolLabel(a.entrypoint || a.kind || 'claude')) + ''; html += '
'; html += '
'; html += '
' + a.cpu.toFixed(1) + '%CPU
'; @@ -1550,7 +1582,7 @@ function renderDoneCard(s) { html += '
'; html += 'DONE'; html += '' + escHtml(projName) + ''; - html += '' + escHtml(s.tool || 'claude') + ''; + html += '' + escHtml(getToolLabel(s.tool || 'claude', true)) + ''; html += '
'; var displayName = getSessionDisplayName(s); if (displayName) html += '
' + escHtml(displayName.slice(0, 120)) + '
'; @@ -1575,7 +1607,7 @@ function renderRunning(container, sessions) { }).slice(0, 8); if (allActiveIds.length === 0 && done.length === 0) { - container.innerHTML = '
No running sessions detected.
Start a Claude Code or Codex session and it will appear here.
'; + container.innerHTML = '
No running sessions detected.
Start a supported agent session and it will appear here.
'; return; } @@ -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', diff --git a/src/frontend/calendar.js b/src/frontend/calendar.js index 648f994..e8000d6 100644 --- a/src/frontend/calendar.js +++ b/src/frontend/calendar.js @@ -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'; diff --git a/src/frontend/detail.js b/src/frontend/detail.js index 0f2ef85..0a61864 100644 --- a/src/frontend/detail.js +++ b/src/frontend/detail.js @@ -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] || []; @@ -26,7 +26,7 @@ async function openDetail(s) { } else if (s.has_detail) { infoHtml += '
Name
'; } - var detailToolLabel = s.tool === 'claude-ext' ? 'claude ext' : s.tool; + var detailToolLabel = getToolLabel(s.tool); infoHtml += '
Tool' + escHtml(detailToolLabel) + '
'; infoHtml += '
Project' + escHtml(s.project_short || s.project || '') + '
'; infoHtml += '
'; @@ -81,8 +81,9 @@ async function openDetail(s) { if (s.has_detail) { infoHtml += ''; infoHtml += ''; - var convertTarget = s.tool === 'codex' ? 'claude' : 'codex'; - infoHtml += ''; + getConvertTargets(s.tool).forEach(function(target) { + infoHtml += ''; + }); infoHtml += ''; } infoHtml += ''; @@ -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 + ? 'pricing unavailable' + : '$' + costData.cost.toFixed(2) + ''; row.querySelector('span:last-child').innerHTML = - '$' + costData.cost.toFixed(2) + '' + + valueHtml + ' ' + formatTokens(costData.inputTokens) + ' in / ' + formatTokens(costData.outputTokens) + ' out' + cacheStr + (costData.model ? ' (' + costData.model + ')' : '') + ''; @@ -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); } diff --git a/src/frontend/heatmap.js b/src/frontend/heatmap.js index 94a35ae..0f6f8a3 100644 --- a/src/frontend/heatmap.js +++ b/src/frontend/heatmap.js @@ -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 += '
'; 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 += '
'; - html += '' + tool + ''; + html += '' + escHtml(getToolLabel(tool)) + ''; html += '
'; html += '' + toolTotals[tool] + ' (' + pct + '%)'; html += '
'; diff --git a/src/frontend/index.html b/src/frontend/index.html index 34a3c16..605e803 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -60,6 +60,10 @@ Codex
+ + '; @@ -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 += '
'; - html += '' + escHtml(e[0]) + ''; + html += '' + escHtml(getToolLabel(e[0], true)) + ''; html += '
'; html += '' + e[1] + ' (' + pct + '%)'; html += '
'; diff --git a/src/frontend/styles.css b/src/frontend/styles.css index d810a4a..d1132c7 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -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; } @@ -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); } @@ -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);