ui: improve subagent window titles and readability

- Extract better titles from task description (first line/sentence)
- Make subagent window text white for better readability
- Expand monitor panel to 700px when subagents tab is active
- Add smooth width transition when switching tabs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-25 17:40:28 +01:00
co-authored by Claude Opus 4.5
parent ad24020cdc
commit cec9608b66
7 changed files with 610 additions and 55 deletions
+23 -2
View File
@@ -17,7 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
**Version**: 0.1352
**Version**: 0.1353
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
@@ -222,6 +222,7 @@ claudeman reset # Reset all state
| `src/spawn-orchestrator.ts` | Full agent lifecycle: spawn, monitor, budget, queue, cleanup |
| `src/spawn-claude-md.ts` | Generates CLAUDE.md for spawned agent sessions |
| `src/mcp-server.ts` | MCP server binary (`claudeman-mcp`) exposing spawn tools to Claude Code |
| `src/subagent-watcher.ts` | Monitors Claude Code background agents in `~/.claude/projects/*/subagents/*.jsonl` |
| `src/tui/DirectAttach.ts` | Full-screen console attach with tab switching between sessions |
| `scripts/claudeman-web.service` | Systemd user service for `claudeman web --https` (Restart=always) |
@@ -245,6 +246,23 @@ Spawned agents are full-power Claude sessions in their own screen sessions, mana
See `docs/spawn-protocol.md` for the full protocol flow, directory structure, resource governance, and MCP configuration.
### Subagent Watcher (Claude Code Background Agents)
Monitors Claude Code's internal background agents (the `Task` tool) in real-time. Watches `~/.claude/projects/{project}/{session}/subagents/agent-{id}.jsonl` files and emits structured events.
**Events**: `subagent:discovered`, `subagent:tool_call`, `subagent:progress`, `subagent:message`, `subagent:completed`
**API**:
- `GET /api/subagents` - List all known subagents (optional `?minutes=60` for recent only)
- `GET /api/subagents/:agentId` - Get subagent info
- `GET /api/subagents/:agentId/transcript` - Get transcript (`?limit=N`, `?format=formatted`)
- `DELETE /api/subagents/:agentId` - Kill subagent process
- `GET /api/sessions/:id/subagents` - Get subagents for session's working directory
**Status lifecycle**: `active` → `idle` (30s no activity) → `completed` (process exited or file stale)
Implementation: `src/subagent-watcher.ts` - singleton `subagentWatcher` started on server boot.
### Session Modes
Sessions have a `mode` property (`SessionMode` type):
@@ -400,7 +418,7 @@ Tab switch/new session fix: clear xterm → write buffer → resize PTY → Ctrl
All events broadcast to `/api/events` with format: `{ type: string, sessionId?: string, data: any }`.
Event prefixes: `session:`, `task:`, `respawn:`, `spawn:`, `hook:`, `scheduled:`, `case:`, `screen:`, `init`.
Event prefixes: `session:`, `task:`, `respawn:`, `spawn:`, `subagent:`, `hook:`, `scheduled:`, `case:`, `screen:`, `init`.
Key events (see `app.js:handleSSEEvent()`):
- `session:idle`, `session:working` - Status indicators
@@ -408,6 +426,7 @@ Key events (see `app.js:handleSSEEvent()`):
- `session:ralphLoopUpdate`, `session:ralphTodoUpdate`, `session:ralphCompletionDetected` - Ralph tracking
- `respawn:detectionUpdate` - Idle detection status
- `spawn:queued`, `spawn:started`, `spawn:completed`, `spawn:failed` - Agent lifecycle
- `subagent:discovered`, `subagent:tool_call`, `subagent:progress`, `subagent:message`, `subagent:completed` - Claude Code background agents
- `hook:idle_prompt`, `hook:permission_prompt`, `hook:elicitation_dialog`, `hook:stop` - Claude Code hooks
### Frontend (app.js)
@@ -501,6 +520,8 @@ All routes defined in `server.ts:buildServer()`. Key endpoint groups:
- `/api/quick-start` - Create case + start session (`{mode?: 'claude'|'shell'}`)
- `/api/cases`, `/api/screens` - Case and screen management
- `/api/spawn/*` - Agent lifecycle (list, status, result, messages, cancel, trigger)
- `/api/subagents` - List/get/kill Claude Code background agents, get transcripts
- `/api/sessions/:id/subagents` - Get subagents for a specific session's working directory
- `/api/hook-event` - Claude Code hook callbacks (`{event, sessionId, data?}`)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1352",
"version": "0.1353",
"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",
+188
View File
@@ -11,6 +11,7 @@ import { createReadStream } from 'fs';
import { createInterface } from 'readline';
import { homedir } from 'os';
import { join, basename } from 'path';
import { execSync } from 'child_process';
// ========== Types ==========
@@ -25,6 +26,7 @@ export interface SubagentInfo {
toolCallCount: number;
entryCount: number;
fileSize: number;
description?: string; // Task description from first user message
}
export interface SubagentToolCall {
@@ -88,6 +90,7 @@ export interface SubagentEvents {
const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude/projects');
const IDLE_TIMEOUT_MS = 30000; // Consider agent idle after 30s of no activity
const POLL_INTERVAL_MS = 1000; // Check for new files every second
const LIVENESS_CHECK_MS = 10000; // Check if subagent processes are still alive every 10s
// ========== SubagentWatcher Class ==========
@@ -98,6 +101,7 @@ export class SubagentWatcher extends EventEmitter {
private agentInfo = new Map<string, SubagentInfo>();
private idleTimers = new Map<string, NodeJS.Timeout>();
private pollInterval: NodeJS.Timeout | null = null;
private livenessInterval: NodeJS.Timeout | null = null;
private isRunning = false;
private knownSubagentDirs = new Set<string>();
@@ -121,6 +125,56 @@ export class SubagentWatcher extends EventEmitter {
this.pollInterval = setInterval(() => {
this.scanForSubagents();
}, POLL_INTERVAL_MS);
// Periodic liveness check for active subagents
this.startLivenessChecker();
}
/**
* Start periodic liveness checker
* Detects when subagent processes have exited but status is still active/idle
*/
private startLivenessChecker(): void {
if (this.livenessInterval) return;
this.livenessInterval = setInterval(async () => {
for (const [agentId, info] of this.agentInfo) {
if (info.status === 'active' || info.status === 'idle') {
const alive = await this.checkSubagentAlive(agentId);
if (!alive) {
info.status = 'completed';
this.emit('subagent:completed', info);
}
}
}
}, LIVENESS_CHECK_MS);
}
/**
* Check if a subagent process is still running
*/
private async checkSubagentAlive(agentId: string): Promise<boolean> {
const info = this.agentInfo.get(agentId);
if (!info) return false;
// Method 1: Check if the process is still running
const pid = await this.findSubagentProcess(info.sessionId);
if (pid !== null) return true;
// Method 2: Check if the transcript file was recently modified
// (within the last 60 seconds - gives some buffer for slow operations)
try {
const stat = statSync(info.filePath);
const mtime = stat.mtime.getTime();
const now = Date.now();
if (now - mtime < 60000) {
return true;
}
} catch {
// File doesn't exist or can't be read
}
return false;
}
/**
@@ -134,6 +188,11 @@ export class SubagentWatcher extends EventEmitter {
this.pollInterval = null;
}
if (this.livenessInterval) {
clearInterval(this.livenessInterval);
this.livenessInterval = null;
}
for (const watcher of this.fileWatchers.values()) {
watcher.close();
}
@@ -187,6 +246,88 @@ export class SubagentWatcher extends EventEmitter {
);
}
/**
* Kill a subagent by its agent ID
* Finds the Claude process and sends SIGTERM
*/
async killSubagent(agentId: string): Promise<boolean> {
const info = this.agentInfo.get(agentId);
if (!info) return false;
// Already completed, nothing to kill
if (info.status === 'completed') return false;
try {
// Find Claude process with matching session ID
const pid = await this.findSubagentProcess(info.sessionId);
if (pid) {
process.kill(pid, 'SIGTERM');
info.status = 'completed';
this.emit('subagent:completed', info);
return true;
}
} catch {
// Process may have already exited
}
// Mark as completed even if we couldn't find the process
info.status = 'completed';
this.emit('subagent:completed', info);
return true;
}
/**
* Kill all subagents for a specific Claudeman session working directory
*/
async killSubagentsForSession(workingDir: string): Promise<void> {
const subagents = this.getSubagentsForSession(workingDir);
for (const agent of subagents) {
if (agent.status === 'active' || agent.status === 'idle') {
await this.killSubagent(agent.agentId);
}
}
}
/**
* Find the process ID of a Claude subagent by its session ID
* Searches /proc for claude processes with matching session ID in environment
*/
private async findSubagentProcess(sessionId: string): Promise<number | null> {
try {
// Find all claude processes
const pgrepOutput = execSync('pgrep -f "claude"', { encoding: 'utf8' });
const pids = pgrepOutput.trim().split('\n').filter(Boolean);
for (const pidStr of pids) {
const pid = parseInt(pidStr, 10);
if (isNaN(pid)) continue;
try {
// Check /proc/{pid}/environ for session ID
const environ = readFileSync(`/proc/${pid}/environ`, 'utf8');
if (environ.includes(sessionId)) {
return pid;
}
} catch {
// Can't read this process's environ - skip
}
try {
// Also check /proc/{pid}/cmdline for session ID
const cmdline = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
if (cmdline.includes(sessionId)) {
return pid;
}
} catch {
// Can't read this process's cmdline - skip
}
}
} catch {
// pgrep returns non-zero if no matches
}
return null;
}
/**
* Get transcript for a subagent (optionally limited to last N entries)
*/
@@ -351,6 +492,52 @@ export class SubagentWatcher extends EventEmitter {
// Initial info
const stat = statSync(filePath);
// Extract description from first user message in the JSONL
// The Task tool passes a "description" (short title) and "prompt" (full task)
// We want the short description, which may be in the first line or sentence
let description: string | undefined;
try {
const content = readFileSync(filePath, 'utf8');
const lines = content.split('\n').filter((l) => l.trim());
// Try each line until we find a user message with text
for (const line of lines.slice(0, 5)) {
try {
const entry = JSON.parse(line) as SubagentTranscriptEntry;
if (entry.type === 'user' && entry.message?.content) {
const firstContent = entry.message.content[0];
if (firstContent?.type === 'text' && firstContent.text) {
const text = firstContent.text.trim();
// Extract a useful title: first line, first sentence, or first 80 chars
// Split on newline first
const firstLine = text.split('\n')[0].trim();
// If still long, try to find a sentence boundary
let title = firstLine;
if (title.length > 80) {
// Look for sentence end (. ! ?) within first 100 chars
const sentenceEnd = title.substring(0, 100).search(/[.!?]/);
if (sentenceEnd > 10) {
title = title.substring(0, sentenceEnd + 1);
} else {
// Truncate at word boundary
const truncated = title.substring(0, 80);
const lastSpace = truncated.lastIndexOf(' ');
title = lastSpace > 40 ? truncated.substring(0, lastSpace) + '...' : truncated + '...';
}
}
description = title;
break;
}
}
} catch {
// Skip malformed lines
}
}
} catch {
// Failed to read description, continue without it
}
const info: SubagentInfo = {
agentId,
sessionId,
@@ -362,6 +549,7 @@ export class SubagentWatcher extends EventEmitter {
toolCallCount: 0,
entryCount: 0,
fileSize: stat.size,
description,
};
this.agentInfo.set(agentId, info);
+115 -23
View File
@@ -3184,6 +3184,7 @@ class ClaudemanApp {
this.closeAppSettings();
this.cancelCloseSession();
document.getElementById('monitorPanel').classList.remove('open');
this.closeSubagentsPanel();
}
// ========== Monitor Panel (combined Screen Sessions + Background Tasks) ==========
@@ -3803,33 +3804,94 @@ class ClaudemanApp {
// ========== Subagent Panel (Claude Code Background Agents) ==========
toggleSubagentPanel() {
this.subagentPanelVisible = !this.subagentPanelVisible;
const panel = this.$('subagentPanel');
if (panel) {
panel.style.display = this.subagentPanelVisible ? 'block' : 'none';
}
if (this.subagentPanelVisible) {
// Switch between Monitor and Subagents tabs in the monitor panel
switchMonitorTab(tabName) {
const monitorTab = document.querySelector('.monitor-tab[data-tab="monitor"]');
const subagentsTab = document.querySelector('.monitor-tab[data-tab="subagents"]');
const monitorContent = this.$('monitorTabContent');
const subagentsContent = this.$('subagentsTabContent');
const panel = this.$('monitorPanel');
if (tabName === 'monitor') {
monitorTab?.classList.add('active');
subagentsTab?.classList.remove('active');
if (monitorContent) {
monitorContent.style.display = 'flex';
monitorContent.classList.add('active');
}
if (subagentsContent) {
subagentsContent.style.display = 'none';
subagentsContent.classList.remove('active');
}
// Shrink panel back to normal width
panel?.classList.remove('subagents-active');
this.subagentPanelVisible = false;
} else if (tabName === 'subagents') {
monitorTab?.classList.remove('active');
subagentsTab?.classList.add('active');
if (monitorContent) {
monitorContent.style.display = 'none';
monitorContent.classList.remove('active');
}
if (subagentsContent) {
subagentsContent.style.display = 'flex';
subagentsContent.classList.add('active');
}
// Expand panel for subagents view
panel?.classList.add('subagents-active');
this.subagentPanelVisible = true;
this.renderSubagentPanel();
}
}
renderSubagentPanel() {
const panel = this.$('subagentPanel');
const list = this.$('subagentList');
const badge = this.$('subagentBadge');
toggleSubagentsPanel() {
// Open monitor panel and switch to subagents tab
const panel = this.$('monitorPanel');
if (panel && !panel.classList.contains('open')) {
panel.classList.add('open');
}
this.switchMonitorTab('subagents');
this.subagentPanelVisible = true;
}
if (!list) return;
closeSubagentsPanel() {
// Just switch back to monitor tab
this.switchMonitorTab('monitor');
this.subagentPanelVisible = false;
}
// Update badge count
const activeCount = Array.from(this.subagents.values()).filter(s => s.status === 'active').length;
if (badge) {
badge.textContent = activeCount > 0 ? activeCount : '';
badge.style.display = activeCount > 0 ? 'inline-block' : 'none';
// Legacy alias
toggleSubagentPanel() {
this.toggleSubagentsPanel();
}
updateSubagentTabButton() {
const tabBtn = this.$('subagentsTabBtn');
const badge = this.$('subagentCountBadge');
const activeCount = Array.from(this.subagents.values()).filter(s => s.status === 'active' || s.status === 'idle').length;
const totalCount = this.subagents.size;
// Show tab if there are any subagents
if (tabBtn) {
tabBtn.style.display = totalCount > 0 ? 'inline-flex' : 'none';
}
// If panel is not visible and there are active subagents, show indicator
if (!this.subagentPanelVisible && activeCount === 0) {
// Update badge with active count
if (badge) {
badge.textContent = activeCount > 0 ? activeCount : '';
}
}
renderSubagentPanel() {
const list = this.$('subagentList');
if (!list) return;
// Always update toolbar button visibility
this.updateSubagentTabButton();
// If panel is not visible, don't render content
if (!this.subagentPanelVisible) {
return;
}
@@ -3854,14 +3916,17 @@ class ClaudemanApp {
const lastActivity = activity[activity.length - 1];
const lastTool = lastActivity?.type === 'tool' ? lastActivity.tool : null;
const hasWindow = this.subagentWindows.has(agent.agentId);
const canKill = agent.status === 'active' || agent.status === 'idle';
const displayName = agent.description || agent.agentId.substring(0, 7);
html.push(`
<div class="subagent-item ${statusClass} ${isActive ? 'selected' : ''}"
onclick="app.selectSubagent('${agent.agentId}')">
<div class="subagent-header">
<span class="subagent-icon">🤖</span>
<span class="subagent-id">${agent.agentId.substring(0, 7)}</span>
<span class="subagent-id" title="${this.escapeHtml(agent.description || agent.agentId)}">${this.escapeHtml(displayName.length > 40 ? displayName.substring(0, 40) + '...' : displayName)}</span>
<span class="subagent-status ${statusClass}">${agent.status}</span>
${canKill ? `<button class="subagent-kill-btn" onclick="event.stopPropagation(); app.killSubagent('${agent.agentId}')" title="Kill agent">&#x2715;</button>` : ''}
<button class="subagent-window-btn" onclick="event.stopPropagation(); app.${hasWindow ? 'closeSubagentWindow' : 'openSubagentWindow'}('${agent.agentId}')" title="${hasWindow ? 'Close window' : 'Open in window'}">
${hasWindow ? '✕' : '⧉'}
</button>
@@ -3927,9 +3992,10 @@ class ClaudemanApp {
return '';
}).join('');
const detailTitle = agent.description || `Agent ${agent.agentId}`;
detail.innerHTML = `
<div class="subagent-detail-header">
<span class="subagent-id">Agent ${agent.agentId}</span>
<span class="subagent-id" title="${this.escapeHtml(agent.description || agent.agentId)}">${this.escapeHtml(detailTitle.length > 60 ? detailTitle.substring(0, 60) + '...' : detailTitle)}</span>
<span class="subagent-status ${agent.status}">${agent.status}</span>
<button class="subagent-transcript-btn" onclick="app.viewSubagentTranscript('${agent.agentId}')">
View Full Transcript
@@ -3976,6 +4042,30 @@ class ClaudemanApp {
return '';
}
async killSubagent(agentId) {
try {
const res = await fetch(`/api/subagents/${agentId}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
// Update local state
const agent = this.subagents.get(agentId);
if (agent) {
agent.status = 'completed';
this.subagents.set(agentId, agent);
}
this.renderSubagentPanel();
this.renderSubagentDetail();
this.updateSubagentWindows();
this.showToast(`Subagent ${agentId.substring(0, 7)} killed`, 'success');
} else {
this.showToast(data.error || 'Failed to kill subagent', 'error');
}
} catch (err) {
console.error('Failed to kill subagent:', err);
this.showToast('Failed to kill subagent: ' + err.message, 'error');
}
}
async viewSubagentTranscript(agentId) {
try {
const res = await fetch(`/api/subagents/${agentId}/transcript?format=formatted`);
@@ -4036,11 +4126,13 @@ class ClaudemanApp {
win.style.top = `${offsetY}px`;
win.style.zIndex = ++this.subagentWindowZIndex;
const windowTitle = agent.description || agentId.substring(0, 7);
const truncatedTitle = windowTitle.length > 50 ? windowTitle.substring(0, 50) + '...' : windowTitle;
win.innerHTML = `
<div class="subagent-window-header">
<div class="subagent-window-title">
<div class="subagent-window-title" title="${this.escapeHtml(agent.description || agentId)}">
<span class="icon">🤖</span>
<span class="id">${agentId.substring(0, 7)}</span>
<span class="id">${this.escapeHtml(truncatedTitle)}</span>
<span class="status ${agent.status}">${agent.status}</span>
</div>
<div class="subagent-window-actions">
+17 -17
View File
@@ -214,7 +214,6 @@
</div>
<div class="toolbar-right">
<!-- Buttons removed - use keyboard shortcuts instead -->
</div>
</footer>
@@ -242,10 +241,15 @@
</div>
</div>
<!-- Monitor Panel (combined Process Monitor + Background Tasks) -->
<!-- Monitor Panel (combined Process Monitor + Background Tasks + Subagents) -->
<div class="monitor-panel" id="monitorPanel">
<div class="monitor-panel-header" id="monitorPanelHeader">
<span>Monitor</span>
<div class="monitor-tabs">
<button class="monitor-tab active" data-tab="monitor" onclick="app.switchMonitorTab('monitor')">Monitor</button>
<button class="monitor-tab" data-tab="subagents" onclick="app.switchMonitorTab('subagents')" id="subagentsTabBtn" style="display: none;">
Subagents <span id="subagentCountBadge" class="monitor-tab-badge"></span>
</button>
</div>
<div class="monitor-panel-actions">
<button class="btn-toolbar btn-sm btn-danger" onclick="app.killAllSessions()" title="Kill all sessions and their screen processes">Kill All</button>
<button class="btn-icon-sm" onclick="app.reconcileScreens()" title="Refresh screens">&#x21BB;</button>
@@ -255,7 +259,8 @@
</div>
</div>
<div class="monitor-resize-handle" id="monitorResizeHandle"></div>
<div class="monitor-panel-body">
<!-- Monitor Tab Content -->
<div class="monitor-panel-body monitor-tab-content active" id="monitorTabContent">
<div class="monitor-section">
<div class="monitor-section-header">
<span>Screen Sessions</span>
@@ -272,20 +277,15 @@
<div class="monitor-section-body" id="backgroundTasksBody">
</div>
</div>
<div class="monitor-section" id="subagentSection">
<div class="monitor-section-header">
<span>Subagents</span>
<span class="subagent-badge" id="subagentBadge" style="display: none;"></span>
</div>
<!-- Subagents Tab Content -->
<div class="monitor-panel-body monitor-tab-content" id="subagentsTabContent" style="display: none;">
<div class="subagent-container">
<div class="subagent-list" id="subagentList">
<div class="subagent-empty">No background agents detected</div>
</div>
<div class="monitor-section-body">
<div class="subagent-container">
<div class="subagent-list" id="subagentList">
<div class="subagent-empty">No background agents detected</div>
</div>
<div class="subagent-detail" id="subagentDetail">
<div class="subagent-empty">Select an agent to view details</div>
</div>
</div>
<div class="subagent-detail" id="subagentDetail">
<div class="subagent-empty">Select an agent to view details</div>
</div>
</div>
</div>
+242 -12
View File
@@ -2850,7 +2850,7 @@ kbd {
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
z-index: 100;
transform: translateY(100%);
transition: transform 0.2s ease;
transition: transform 0.2s ease, width 0.2s ease;
display: flex;
flex-direction: column;
}
@@ -2859,6 +2859,15 @@ kbd {
transform: translateY(0);
}
/* Wider panel when subagents tab is active */
.monitor-panel.subagents-active {
width: 700px;
}
.monitor-panel.detached.subagents-active {
width: 800px;
}
/* Detached floating state */
.monitor-panel.detached {
bottom: auto;
@@ -2896,13 +2905,62 @@ kbd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1rem;
padding: 0.4rem 1rem;
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
font-weight: 500;
flex-shrink: 0;
}
.monitor-tabs {
display: flex;
gap: 0.25rem;
}
.monitor-tab {
background: transparent;
border: none;
color: var(--text-muted);
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
}
.monitor-tab:hover {
color: var(--text);
background: var(--bg-hover);
}
.monitor-tab.active {
color: white;
background: var(--accent);
}
.monitor-tab-badge {
display: inline-block;
background: var(--success);
color: white;
font-size: 0.6rem;
padding: 0.1rem 0.35rem;
border-radius: 8px;
margin-left: 0.25rem;
font-weight: 600;
}
.monitor-tab-badge:empty {
display: none;
}
.monitor-tab-content {
display: none;
}
.monitor-tab-content.active {
display: flex;
}
.monitor-panel-actions {
display: flex;
align-items: center;
@@ -3000,10 +3058,162 @@ kbd {
/* ========== Subagent Panel (Claude Code Background Agents) ========== */
/* Toolbar button for subagents tab */
.toolbar-btn-subagents {
display: flex;
align-items: center;
gap: 0.4rem;
background: var(--bg-hover);
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 0.75rem;
padding: 0.4rem 0.75rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.toolbar-btn-subagents:hover {
background: var(--bg-card);
border-color: var(--accent);
color: var(--text);
}
.toolbar-btn-subagents.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.toolbar-badge {
background: var(--accent);
color: white;
font-size: 0.65rem;
padding: 0.1rem 0.4rem;
border-radius: 8px;
min-width: 1.2em;
text-align: center;
}
.toolbar-badge:empty {
display: none;
}
/* Main subagents panel - positioned on left side */
.subagents-panel {
position: fixed;
bottom: var(--toolbar-height);
left: 0.5rem;
width: 500px;
max-width: calc(100vw - 1rem);
max-height: 60vh;
background: var(--bg-card);
border: 1px solid var(--border);
border-bottom: none;
border-radius: 8px 8px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
z-index: 100;
transform: translateY(100%);
transition: transform 0.2s ease;
display: flex;
flex-direction: column;
}
.subagents-panel.open {
transform: translateY(0);
}
/* Detached floating state */
.subagents-panel.detached {
bottom: auto;
left: auto;
top: 100px;
left: 50px;
width: 600px;
height: 400px;
max-height: none;
border-radius: 8px;
border-bottom: 1px solid var(--border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
transform: none;
resize: both;
overflow: hidden;
min-width: 400px;
min-height: 250px;
}
.subagents-panel.detached .subagents-panel-header {
cursor: move;
background: linear-gradient(180deg, var(--bg-hover) 0%, var(--bg-card) 100%);
user-select: none;
}
.subagents-panel.detached .subagents-resize-handle {
display: block;
}
.subagents-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1rem;
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
font-weight: 500;
flex-shrink: 0;
}
.subagents-panel-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.subagents-panel-body {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
/* Resize handle for detached panel */
.subagents-resize-handle {
display: none;
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, var(--border-light) 50%);
border-radius: 0 0 8px 0;
}
.subagents-resize-handle::before {
content: '';
position: absolute;
bottom: 3px;
right: 3px;
width: 8px;
height: 8px;
border-right: 2px solid var(--text-muted);
border-bottom: 2px solid var(--text-muted);
}
@media (max-width: 600px) {
.subagents-panel {
width: 100%;
max-width: 100%;
left: 0;
border-radius: 8px 8px 0 0;
}
}
/* Subagent container (list + detail) */
.subagent-container {
display: flex;
gap: 0.5rem;
height: 200px;
height: 100%;
min-height: 280px;
}
.subagent-list {
@@ -3020,7 +3230,7 @@ kbd {
}
.subagent-empty {
color: var(--text-muted);
color: var(--text-dim);
font-size: 0.75rem;
text-align: center;
padding: 1rem;
@@ -3038,7 +3248,7 @@ kbd {
.subagent-item {
padding: 0.5rem;
border-radius: 6px;
background: var(--bg-elevated);
background: var(--bg-input);
margin-bottom: 0.4rem;
cursor: pointer;
transition: background 0.15s;
@@ -3067,7 +3277,7 @@ kbd {
.subagent-id {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text);
color: white;
}
.subagent-status {
@@ -3106,6 +3316,22 @@ kbd {
color: var(--accent);
}
.subagent-kill-btn {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0.1rem 0.3rem;
font-size: 0.7rem;
opacity: 0.6;
transition: opacity 0.15s, color 0.15s;
}
.subagent-kill-btn:hover {
color: var(--red);
opacity: 1;
}
.subagent-meta {
display: flex;
gap: 0.5rem;
@@ -3145,7 +3371,7 @@ kbd {
}
.subagent-transcript-btn:hover {
background: var(--bg-elevated);
background: var(--bg-hover);
}
.subagent-detail-stats {
@@ -3210,7 +3436,9 @@ kbd {
position: fixed;
width: 500px;
height: 400px;
background: var(--bg-surface);
min-width: 300px;
min-height: 200px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
@@ -3218,13 +3446,14 @@ kbd {
display: flex;
flex-direction: column;
overflow: hidden;
resize: both;
}
.subagent-window-header {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--bg-elevated);
background: var(--bg-input);
border-bottom: 1px solid var(--border);
cursor: move;
}
@@ -3243,6 +3472,7 @@ kbd {
.subagent-window-title .id {
font-family: var(--font-mono);
font-size: 0.8rem;
color: white;
}
.subagent-window-title .status {
@@ -3289,7 +3519,7 @@ kbd {
}
.subagent-window-body .activity-line .time {
color: var(--text-muted);
color: var(--text-dim);
flex: 0 0 70px;
}
@@ -3303,13 +3533,13 @@ kbd {
}
.subagent-window-body .activity-line .tool-detail {
color: var(--text-dim);
color: white;
flex: 1;
}
.subagent-window-body .message-line {
padding: 0.3rem 0;
color: var(--text);
color: white;
}
.subagent-window-body .progress-line {
+24
View File
@@ -1635,6 +1635,21 @@ export class WebServer extends EventEmitter {
return { success: true, data: transcript };
});
// Kill a subagent
this.app.delete('/api/subagents/:agentId', async (req) => {
const { agentId } = req.params as { agentId: string };
const info = subagentWatcher.getSubagent(agentId);
if (!info) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Subagent not found');
}
const killed = await subagentWatcher.killSubagent(agentId);
if (killed) {
return { success: true, data: { agentId, status: 'killed' } };
}
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Subagent not found or already completed');
});
// ========== Hook Events ==========
this.app.post('/api/hook-event', async (req) => {
@@ -1767,6 +1782,15 @@ export class WebServer extends EventEmitter {
private async _doCleanupSession(sessionId: string, killScreen: boolean): Promise<void> {
const session = this.sessions.get(sessionId);
// Kill all subagents spawned by this session
if (session && killScreen) {
try {
await subagentWatcher.killSubagentsForSession(session.workingDir);
} catch (err) {
console.error(`[Server] Failed to kill subagents for session ${sessionId}:`, err);
}
}
// Stop and remove respawn controller - but save config first for restart recovery
const controller = this.respawnControllers.get(sessionId);
if (controller) {