diff --git a/CLAUDE.md b/CLAUDE.md index 8aa7752f..f82e3fa4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`) 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -**Version**: 0.1618 (must match `package.json`) +**Version**: 0.1619 (must match `package.json`) ## Project Overview diff --git a/package.json b/package.json index cb518489..51df94ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1618", + "version": "0.1619", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/web/public/app.js b/src/web/public/app.js index 95a5dc01..c663477a 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -16057,22 +16057,76 @@ class ClaudemanApp { let html = ''; for (const muxSession of this.muxSessions) { const stats = muxSession.stats || { memoryMB: 0, cpuPercent: 0, childCount: 0 }; - const modeClass = muxSession.mode === 'shell' ? 'shell' : ''; + + // Look up rich session data by sessionId + const session = this.sessions.get(muxSession.sessionId); + const status = session ? session.status : 'unknown'; + const isWorking = session ? session.isWorking : false; + + // Status badge + let statusLabel, statusClass; + if (status === 'idle' && !isWorking) { + statusLabel = 'IDLE'; + statusClass = 'status-idle'; + } else if (status === 'busy' || isWorking) { + statusLabel = 'WORKING'; + statusClass = 'status-working'; + } else if (status === 'stopped') { + statusLabel = 'STOPPED'; + statusClass = 'status-stopped'; + } else { + statusLabel = status.toUpperCase(); + statusClass = ''; + } + + // Token and cost info + const tokens = session && session.tokens ? session.tokens : null; + const totalCost = session ? session.totalCost : 0; + const model = session ? (session.cliModel || '') : ''; + const modelShort = model.includes('opus') ? 'opus' : model.includes('sonnet') ? 'sonnet' : model.includes('haiku') ? 'haiku' : ''; + + // Ralph/Todo progress + const todoStats = session ? session.ralphTodoStats : null; + let todoHtml = ''; + if (todoStats && todoStats.total > 0) { + const pct = Math.round((todoStats.completed / todoStats.total) * 100); + todoHtml = `${todoStats.completed}/${todoStats.total} (${pct}%)`; + } + + // Format tokens + let tokenHtml = ''; + if (tokens && tokens.total > 0) { + const totalK = (tokens.total / 1000).toFixed(1); + tokenHtml = `${totalK}k tok`; + } + + // Format cost + let costHtml = ''; + if (totalCost > 0) { + costHtml = `$${totalCost.toFixed(2)}`; + } + + // Model badge + let modelHtml = ''; + if (modelShort) { + modelHtml = `${modelShort}`; + } html += `
- ${muxSession.mode} + ${statusLabel}
-
${this.escapeHtml(muxSession.name || muxSession.muxName)}
+
${modelHtml} ${this.escapeHtml(muxSession.name || muxSession.muxName)}
+ ${tokenHtml} + ${costHtml} + ${todoHtml} ${stats.memoryMB}MB ${stats.cpuPercent}% - ${stats.childCount} children - PID: ${muxSession.pid}
- +
`; @@ -16128,13 +16182,16 @@ class ClaudemanApp { if (!confirm('Kill this mux session?')) return; try { - await fetch(`/api/mux-sessions/${sessionId}`, { method: 'DELETE' }); - this.muxSessions = this.muxSessions.filter(s => s.sessionId !== sessionId); - this.renderMuxSessions(); - this.showToast('Tmux session killed', 'success'); + // Use closeSession to properly clean up both the session tab and tmux process + // (closeSession handles its own toast messaging) + await this.closeSession(sessionId, true); } catch (err) { - this.showToast('Failed to kill tmux session', 'error'); + // Fallback: kill mux directly if session cleanup fails + try { await fetch(`/api/mux-sessions/${sessionId}`, { method: 'DELETE' }); } catch (_ignored) {} + this.showToast('Tmux session killed', 'success'); } + this.muxSessions = this.muxSessions.filter(s => s.sessionId !== sessionId); + this.renderMuxSessions(); } async reconcileMuxSessions() { diff --git a/src/web/public/index.html b/src/web/public/index.html index 4f5f210d..03db1829 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -8,8 +8,8 @@ Claudeman - - + + @@ -1560,6 +1560,6 @@ - + diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 5e42ceba..fb4e4788 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -4291,6 +4291,53 @@ kbd { color: var(--yellow); } +/* Monitor status badges */ +.monitor-status-badge { + font-size: 0.55rem; + font-weight: 700; + padding: 2px 6px; + border-radius: 3px; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; + min-width: 52px; + text-align: center; +} +.monitor-status-badge.status-idle { + background: rgba(234, 179, 8, 0.2); + color: var(--yellow); +} +.monitor-status-badge.status-working { + background: rgba(74, 222, 128, 0.2); + color: var(--green); + animation: pulse-working 2s ease-in-out infinite; +} +.monitor-status-badge.status-stopped { + background: rgba(239, 68, 68, 0.2); + color: var(--red, #ef4444); +} +@keyframes pulse-working { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* Monitor model badges */ +.monitor-model-badge { + font-size: 0.55rem; + padding: 1px 4px; + border-radius: 2px; + font-weight: 600; + text-transform: uppercase; +} +.monitor-model-badge.opus { background: rgba(168, 85, 247, 0.2); color: #c084fc; } +.monitor-model-badge.sonnet { background: rgba(59, 130, 246, 0.2); color: #60a5fa; } +.monitor-model-badge.haiku { background: rgba(74, 222, 128, 0.2); color: #4ade80; } + +/* Monitor stat colors */ +.process-stat.tokens { color: var(--accent-hover); } +.process-stat.cost { color: #f59e0b; } +.process-stat.todo-progress { color: #c084fc; } + /* Combined Monitor Panel (Tmux Sessions + Background Tasks) */ .monitor-panel { position: fixed;