mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
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:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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">✕</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
@@ -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">↻</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
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user